Golang nối một vật phẩm vào một lát cắt


79

Tại sao lát cắt avẫn giữ nguyên? Có append()tạo ra một lát mới không?

package main

import (
    "fmt"
)

var a = make([]int, 7, 8)

func Test(slice []int) {
    slice = append(slice, 100)
    fmt.Println(slice)
}

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(a)
    fmt.Println(a)
}

Đầu ra:

[0 1 2 3 4 5 6 100]
[0 1 2 3 4 5 6]

Mã này sẽ giải thích những gì đang xảy ra: https://play.golang.org/p/eJYq65jeqwn . Kiểm tra func (Slice [] int), nhận một bản sao của giá trị slice của a. Và nó đang trỏ đến cùng một mảng như một trỏ.
gihanchanuka

lát cắt là một structgiá trị được truyền, không phải bằng tham chiếu hoặc con trỏ. Dấu bằng chỉ phá vỡ chuỗi các slices trong đó Test.
Izana,

Câu trả lời:


59

Trong ví dụ của bạn, sliceđối số của Testhàm nhận một bản sao của biến atrong phạm vi của người gọi.

Vì một biến lát cắt chứa một "bộ mô tả lát cắt" chỉ tham chiếu đến một mảng bên dưới, trong Testhàm của bạn, bạn sửa đổi bộ mô tả lát cắt được giữ trong slicebiến vài lần liên tiếp, nhưng điều này không ảnh hưởng đến trình gọi và abiến của nó .

Bên trong Testhàm, hàm đầu tiên appendphân bổ lại mảng hỗ trợ bên dưới slicebiến, sao chép nội dung ban đầu của nó lên, gắn 100vào nó và đó là những gì bạn đang quan sát. Khi thoát khỏi Test, slicebiến sẽ vượt ra khỏi phạm vi và mảng cơ bản (mới) cắt các tham chiếu đó cũng vậy. ( Jeff Lee đã đúng về việc đó không phải là những gì thực sự xảy ra, vì vậy phiên bản cập nhật sẽ theo sau; như anh ấy nói một cách chính xác, câu trả lời này là đúng, nếu có thể hơi ngắn gọn.)

Bên ngoài Testhàm, một lát có độ dài 7 và dung lượng 8 được phân bổ, và 7 phần tử của nó được lấp đầy.
Bên trong Testhàm, đầu tiên appendthấy rằng dung lượng của lát cắt vẫn lớn hơn một phần tử so với chiều dài của nó - nói cách khác, có chỗ cho một phần tử nữa để thêm vào mà không cần phân bổ lại. Vì vậy, nó "ăn" phần tử còn lại đó và đặt 100vào đó, sau đó nó điều chỉnh độ dài trong bản sao của bộ mô tả lát cắt để trở nên bằng với giới hạn của lát cắt. Điều này không ảnh hưởng đến bộ mô tả lát cắt trong phạm vi của trình gọi.

Và đó là những gì bạn đang quan sát. Khi thoát khỏi Test, slicebiến sẽ vượt ra khỏi phạm vi và mảng cơ bản (mới) cắt các tham chiếu đó cũng vậy.

Nếu bạn muốn thực hiện Testhành vi như thế nào append, bạn phải trả lại lát mới từ nó - giống như appendhiện tại - và yêu cầu người gọi Testsử dụng nó theo cách họ sẽ sử dụng append:

func Test(slice []int) []int {
    slice = append(slice, 100)

    fmt.Println(slice)

    return slice
}

a = Test(a)

Vui lòng đọc kỹ bài viết này vì về cơ bản nó chỉ cho bạn cách triển khai appendbằng tay, sau khi giải thích cách các lát cắt hoạt động nội bộ. Sau đó, đọc này .


6
Tôi thực sự nghĩ rằng mô tả này không chính xác theo những cách tinh tế. Câu trả lời của @ doun dưới đây thực sự là một đại diện chính xác hơn về những gì đang diễn ra bên trong: appendin Testkhông phân bổ lại bất kỳ bộ nhớ nào, vì phân bổ ban đầu của lát sao lưu mảng avẫn có thể phù hợp với một mục bổ sung. Nói cách khác, khi chương trình này được viết, giá trị trả về của Test(a)alà các tiêu đề lát cắt khác nhau có độ dài khác nhau, nhưng chúng trỏ đến cùng một mảng cơ bản. Việc in fmt.Println(a[:cap(a)]ở dòng cuối cùng của mainhàm làm rõ điều này.
Jeff Lee

Tuyên bố này là SAI; "Trong ví dụ của bạn, đối số lát cắt của hàm Kiểm tra nhận một bản sao của biến a trong phạm vi của người gọi". Như đã đề cập trong cách sử dụng lát Go , func nhận một con trỏ. Thử thay đổi slice = append(slice, 100)-> slice[1] = 13. Bạn sẽ được in [0 13 2 3 4 5 6]hai lần. @kostix bạn có thể giải thích điều đó không ?. Tham khảo - play.golang.org/p/QKRnl5CTcM1
gihanchanuka

@gihanchanuka, trong trường hợp của func Test(slice []int), hàm không nhận "con trỏ". Trong cờ vây, mọi thứ, từ trước đến nay, đều được vượt qua bởi giá trị; chỉ một số kiểu tình cờ có biểu diễn con trỏ hoặc chứa con trỏ. Slices trong Go thuộc loại thứ hai đó: bất kỳ giá trị lát cắt nào cũng là một cấu trúc gồm ba trường, một trong số đó thực sự là một con trỏ tới khối bộ nhớ chứa các phần tử của lát cắt.
kostix

@gihanchanuka, bây giờ hàm tích hợp sẵn appendnhận giá trị lát cắt và trả về giá trị lát cắt. Trong cả hai trường hợp, đó là cấu trúc ba trường, được sao chép ở đầu vào và đầu ra (vào appendkhung ngăn xếp của ', rồi bên ngoài nó). Bây giờ nếu appendphải phân bổ lại bộ nhớ của lát cắt để nhường chỗ cho dữ liệu được thêm vào, giá trị lát cắt mà nó trả về chứa một con trỏ khác với con trỏ trong giá trị lát đầu vào. Và điều này chỉ xảy ra nếu appendphải phân bổ lại, và không xảy ra theo cách khác (mảng bên dưới có không gian chưa sử dụng). Đó là ý chính của "vấn đề".
kostix

@kostix Tôi đã hiểu "+1", Cảm ơn! Mã này sẽ giải thích những gì đang xảy ra: https://play.golang.org/p/zJT7CW-pfp8 . func Test(slice []int), nhận một bản sao của giá trị lát cắt của a. Và nó đang trỏ đến cùng một mảng như atrỏ. Tôi không thể chỉnh sửa nhận xét ở trên của mình và việc xóa nó sẽ gây nhầm lẫn cho cuộc trò chuyện này.
gihanchanuka

36

appendCách sử dụng điển hình là

a = append(a, x)

bởi vì appendcó thể sửa đổi đối số của nó tại chỗ hoặc trả về một bản sao của đối số của nó với một mục nhập bổ sung, tùy thuộc vào kích thước và dung lượng của đầu vào. Sử dụng một phần trước đó đã được thêm vào có thể cho kết quả không mong muốn, ví dụ:

a := []int{1,2,3}
a = append(a, 4)
fmt.Println(a)
append(a[:3], 5)
fmt.Println(a)

có thể in

[1 2 3 4]
[1 2 3 5]

1
Cảm ơn, larsmans, tôi đã sửa đổi một số mã. "make" sẽ cung cấp đủ công suất. Kết quả là như nhau và tôi đang bối rối.
Pole_Zhang

1
Điều này cần phải được _ = append(a[:3], 5)biên dịch ngay bây giờ
y3sh

append(a[:3], 5)giống như a[3] = 5trong vụ hack sau đây. Tôi đoán đây là một ví dụ mà bất ngờ bất ngờ xảy ra.
Izana

8

Để làm cho mã của bạn hoạt động mà không cần phải trả lại lát từ Kiểm tra, bạn có thể chuyển một con trỏ như sau:

package main

import (
    "fmt"
)

var a = make([]int, 7, 8)

func Test(slice *[]int) {
    *slice = append(*slice, 100)

    fmt.Println(*slice)
}

func main() {

    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(&a)

    fmt.Println(a)
}

6

LƯU Ý rằng append tạo ra một lát mới nếu không đủ giới hạn . Câu trả lời của @ kostix là đúng, hoặc bạn có thể chuyển đối số lát cắt bằng con trỏ!


1
Bạn nói đúng về con trỏ nhưng tôi đã quyết định không đề cập đến chúng bởi vì các lát cắt được phát minh ra chủ yếu để giải phóng các lập trình viên xử lý các con trỏ đến mảng. Trong một triển khai tham chiếu (từ Go), một biến lát cắt chứa một con trỏ và hai số nguyên, vì vậy việc sao chép nó rất rẻ và đó là lý do tại sao slice = append(slice, a, b, c)là thành ngữ, không chuyển một biến lát cắt bằng con trỏ và sửa đổi nó "tại chỗ" để người gọi thấy thay đổi.
kostix

2
@kostix Bạn nói đúng, mục đích của mã phải rõ ràng. Nhưng tôi nghĩ toàn bộ câu chuyện chỉ là chuyển một giá trị lưu trữ một con trỏ và chuyển một con trỏ trỏ tới một con trỏ. Nếu chúng ta sửa đổi tham chiếu, cả hai đều có thể hoạt động, nhưng nếu chúng ta thay thế tham chiếu, tham chiếu đầu tiên sẽ mất tác dụng. Lập trình viên nên biết những gì anh ta đang làm.
Gizak

4

Hãy thử điều này, mà tôi nghĩ làm cho nó rõ ràng. mảng bên dưới được thay đổi nhưng lát cắt của chúng tôi thì không, printchỉ in các len()ký tự, bằng một lát cắt khác vào cap(), bạn có thể thấy mảng đã thay đổi:

func main() {

  for i := 0; i < 7; i++ {
      a[i] = i
  }

  Test(a)

  fmt.Println(a) // prints [0..6]
  fmt.Println(a[:cap(a)] // prints [0..6,100]
}

vậy 'a' và 'a [: cap (a)]' có khác lát cắt không?
Pole_Zhang

2
Có, nếu bạn chạy mã, bạn sẽ tìm ra điều đó. vì nắp (a) được thay đổi trong thời gian thử nghiệm (a) gọi
doun

3

Giải thích (đọc bình luận nội tuyến):


package main

import (
    "fmt"
)

var a = make([]int, 7, 8)
// A slice is a descriptor of an array segment. 
// It consists of a pointer to the array, the length of the segment, and its capacity (the maximum length of the segment).
// The length is the number of elements referred to by the slice.
// The capacity is the number of elements in the underlying array (beginning at the element referred to by the slice pointer).
// |-> Refer to: https://blog.golang.org/go-slices-usage-and-internals -> "Slice internals" section

func Test(slice []int) {
    // slice receives a copy of slice `a` which point to the same array as slice `a`
    slice[6] = 10
    slice = append(slice, 100)
    // since `slice` capacity is 8 & length is 7, it can add 100 and make the length 8
    fmt.Println(slice, len(slice), cap(slice), " << Test 1")
    slice = append(slice, 200)
    // since `slice` capacity is 8 & length also 8, slice has to make a new slice 
    // - with double of size with point to new array (see Reference 1 below).
    // (I'm also confused, why not (n+1)*2=20). But make a new slice of 16 capacity).
    slice[6] = 13 // make sure, it's a new slice :)
    fmt.Println(slice, len(slice), cap(slice), " << Test 2")
}

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    fmt.Println(a, len(a), cap(a))
    Test(a)
    fmt.Println(a, len(a), cap(a))
    fmt.Println(a[:cap(a)], len(a), cap(a))
    // fmt.Println(a[:cap(a)+1], len(a), cap(a)) -> this'll not work
}

Đầu ra:

[0 1 2 3 4 5 6] 7 8
[0 1 2 3 4 5 10 100] 8 8  << Test 1
[0 1 2 3 4 5 13 100 200] 9 16  << Test 2
[0 1 2 3 4 5 10] 7 8
[0 1 2 3 4 5 10 100] 7 8

Tham khảo 1: https://blog.golang.org/go-slices-usage-and-internals

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // if necessary, reallocate
        // allocate double what's needed, for future growth.
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

2

Go có cách tiếp cận nhẹ nhàng và lười biếng hơn trong việc này. Nó tiếp tục sửa đổi cùng một mảng cơ bản cho đến khi đạt đến dung lượng của một lát cắt.

Tham khảo: http://criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/

Đầu ra của ví dụ từ liên kết giải thích hành vi của các lát trong Go.

Tạo lát cắt a.

Slice a len=7 cap=7 [0 0 0 0 0 0 0]

Lát b đề cập đến các chỉ số 2, 3, 4 trong lát a. Do đó, công suất là 5 (= 7-2).

b := a[2:5]
Slice b len=3 cap=5 [0 0 0]

Sửa đổi lát b, cũng sửa đổi a, vì chúng đang trỏ đến cùng một mảng cơ bản.

b[0] = 9
Slice a len=7 cap=7 [0 0 9 0 0 0 0]
Slice b len=3 cap=5 [9 0 0]

Thêm 1 vào lát cắt b. Ghi đè a.

Slice a len=7 cap=7 [0 0 9 0 0 1 0]
Slice b len=4 cap=5 [9 0 0 1]

Nối 2 thành lát b. Ghi đè a.

Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=5 cap=5 [9 0 0 1 2]

Nối 3 thành lát b. Tại đây, một bản sao mới được tạo ra khi dung lượng bị quá tải.

Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 0 0 1 2 3]

Việc xác minh các lát a và b trỏ đến các mảng bên dưới khác nhau sau khi quá tải dung lượng ở bước trước.

b[1] = 8
Slice a len=7 cap=7 [0 0 9 0 0 1 2]
Slice b len=6 cap=12 [9 8 0 1 2 3]

2
package main

import (
    "fmt"
)

func a() {
    x := []int{}
    x = append(x, 0)
    x = append(x, 1)  // commonTags := labelsToTags(app.Labels)
    y := append(x, 2) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    z := append(x, 3) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    fmt.Println(y, z)
}

func b() {
    x := []int{}
    x = append(x, 0)
    x = append(x, 1)
    x = append(x, 2)  // commonTags := labelsToTags(app.Labels)
    y := append(x, 3) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    z := append(x, 4) // Tags: append(commonTags, labelsToTags(d.Labels)...)
    fmt.Println(y, z)
}

func main() {
    a()
    b()
}

First guess could be

[0, 1, 2] [0, 1, 3]
[0, 1, 2, 3] [0, 1, 2, 4]

but in fact it results in

[0, 1, 2] [0, 1, 3]
[0, 1, 2, 4] [0, 1, 2, 4]

nhập mô tả hình ảnh ở đây

nhập mô tả hình ảnh ở đây

Thông tin chi tiết xem tại https://allegro.tech/2017/07/golang-slices-gotcha.html


1

Tôi nghĩ câu trả lời ban đầu là không chính xác. append()đã thay đổi cả lát và mảng bên dưới mặc dù mảng bên dưới bị thay đổi nhưng vẫn được chia sẻ bởi cả hai lát.

Theo quy định của Go Doc:

Một lát cắt không lưu trữ bất kỳ dữ liệu nào, nó chỉ mô tả một phần của một mảng bên dưới. (Liên kết)

Slices chỉ là các giá trị bao bọc xung quanh mảng, có nghĩa là chúng chứa thông tin về cách chúng cắt một mảng bên dưới mà chúng sử dụng để lưu trữ một tập dữ liệu. Do đó, theo mặc định, một lát cắt, khi được truyền cho một phương thức khác, thực sự được truyền theo giá trị, thay vì tham chiếu / con trỏ mặc dù chúng vẫn sẽ sử dụng cùng một mảng cơ bản. Thông thường, các mảng cũng được truyền theo giá trị, vì vậy tôi giả sử một lát cắt trỏ đến một mảng bên dưới thay vì lưu trữ nó dưới dạng giá trị. Về câu hỏi của bạn, khi bạn chạy đã chuyển lát cắt của mình cho hàm sau:

func Test(slice []int) {
    slice = append(slice, 100)
    fmt.Println(slice)
}

bạn thực sự đã chuyển một bản sao của lát cắt của mình cùng với một con trỏ đến cùng một mảng bên dưới. Điều đó có nghĩa là những thay đổi bạn đã thực hiện đối với slicenó không ảnh hưởng đến một trong các mainhàm. Bản thân lát cắt lưu trữ thông tin về số lượng mảng mà nó cắt lát và hiển thị cho công chúng. Do đó, khi bạn chạy append(slice, 1000), trong khi mở rộng mảng bên dưới, bạn cũng thay đổi thông tin cắt lát, thông tin slicenày được giữ kín trongTest() hàm .

Tuy nhiên, nếu bạn đã thay đổi mã của mình như sau, nó có thể đã hoạt động:

func main() {
    for i := 0; i < 7; i++ {
        a[i] = i
    }

    Test(a)
    fmt.Println(a[:cap(a)])
}

Lý do là bạn đã mở rộng abằng cách nói a[:cap(a)]trên mảng cơ bản đã thay đổi của nó, thay đổi theo Test()chức năng. Như được chỉ định ở đây:

Bạn có thể kéo dài chiều dài của lát bằng cách cắt lại, miễn là nó có đủ dung lượng. (Liên kết)


0

Đây là một triển khai tốt của append cho các lát. Tôi đoán nó tương tự như những gì đang diễn ra dưới mui xe:

package main

import "fmt"

func main() {
    slice1 := []int{0, 1, 2, 3, 4}
    slice2 := []int{55, 66, 77}
    fmt.Println(slice1)
    slice1 = Append(slice1, slice2...) // The '...' is essential!
    fmt.Println(slice1)
}

// Append ...
func Append(slice []int, items ...int) []int {
    for _, item := range items {
        slice = Extend(slice, item)
    }
    return slice
}

// Extend ...
func Extend(slice []int, element int) []int {
    n := len(slice)
    if n == cap(slice) {
        // Slice is full; must grow.
        // We double its size and add 1, so if the size is zero we still grow.
        newSlice := make([]int, len(slice), 2*len(slice)+1)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0 : n+1]
    slice[n] = element
    return slice
}
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.