Голанг додає предмет до фрагмента


79

Чому зріз aзалишається незмінним? Чи append()генерує новий фрагмент?

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)
}

Вихід:

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

Цей код пояснить, що відбувається: https://play.golang.org/p/eJYq65jeqwn . func Test (slice [] int), отримує копію значення зрізу a. І це вказує на той самий масив, що і вказівний.
гіханчанука

фрагмент - це structзначення, яке передається за значенням, а не за посиланням чи покажчиком. Знак рівності просто розриває ланцюжок цих slices в Test.
Ізана

Відповіді:


59

У вашому прикладі sliceаргумент Testфункції отримує копію змінної aв області виклику.

Оскільки змінна зрізу містить "дескриптор зрізу", який просто посилається на базовий масив, у вашій Testфункції ви sliceкілька разів поспіль модифікуєте дескриптор зрізу, що міститься у змінній, але це не впливає на абонента та його aзмінну.

Усередині Testфункції перший appendперерозподіляє масив підкладки під sliceзмінною, копіює її оригінальний вміст, додає 100до нього, і це те, що ви спостерігаєте. Після виходу із Test, sliceзмінна виходить за межі області дії, а також (новий) базовий масив, що нарізає посилання. ( Джефф Лі правильно говорить про те, що насправді трапляється не те, тож насправді відбувається оновлена ​​версія; як він правильно стверджує, ця відповідь є правильною, якщо, можливо, трохи стисла.)

Поза Testфункцією виділяється зріз довжиною 7 та місткістю 8, а його 7 елементів заповнені.
Усередині Testфункції перший appendбачить, що ємність зрізу все ще на один елемент більша за його довжину - іншими словами, є місце для додавання ще одного елемента без перерозподілу. Отже, він «з’їдає» той елемент, що залишився, і розміщує 100на ньому, після чого регулює довжину в копії дескриптора зрізу, щоб стати рівною капітатичності зрізу. Це не впливає на дескриптор фрагмента в області дії абонента.

І це те, що ви спостерігаєте. Після виходу з Test, sliceзмінна виходить за межі області дії, а також (новий) базовий масив, що нарізає посилання.

Якщо ви хочете змусити Testсебе вести себе так append, вам доведеться повернути з нього новий зріз - так само, як appendце робить - і вимагати від абонентів, Testщоб використовували його так само, як і вони append:

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

    fmt.Println(slice)

    return slice
}

a = Test(a)

Будь ласка, уважно прочитайте цю статтю, оскільки вона в основному показує вам, як реалізувати appendвручну, після пояснення того, як фрагменти працюють усередині. Тоді прочитайте це .


6
Я насправді вважаю, що цей опис є хибним і неточним. Відповідь @ doun нижче є насправді більш правильним відображенням того, що відбувається внутрішньо: appendin Testне перерозподіляє жодну пам’ять, оскільки вихідний розподіл зрізу резервної копії масиву aвсе ще може вмістити один додатковий елемент. Іншими словами, як написана ця програма, повернене значення Test(a)та aє різними заголовками зрізів з різною довжиною, але вони вказують на точно такий самий базовий масив. Друк fmt.Println(a[:cap(a)]як останній рядок mainфункції робить це зрозумілим.
Джефф Лі,

Це твердження неправильне; "У вашому прикладі аргумент зрізу функції Test отримує копію змінної a в області виклику". Як зазначалося у використанні Go slice , func отримує покажчик. Спробуйте змінити slice = append(slice, 100)-> slice[1] = 13. Вас надрукують [0 13 2 3 4 5 6]двічі. @kostix ти можеш це пояснити ?. Довідка
гіханчанука

@gihanchanuka, у випадку func Test(slice []int), функція не отримує "вказівник". У Go все і завжди передається за значенням; лише деякі типи випадково мають представлення покажчика або містять покажчики. Зрізи в Go мають останню різновид: будь-яке значення зрізу є структурою з трьох полів, одне з яких, справді, є вказівником на блок пам'яті, що містить елементи зрізу.
kostix 02

@gihanchanuka, тепер вбудована appendфункція приймає значення зрізу і повертає значення зрізу. В обох випадках це структура з трьома полями, яка копіюється на вході та на виході (у appendфрейм стека, а потім поза ним). Тепер, якщо appendдовелося перерозподілити пам'ять зрізу, щоб звільнити місце для додавання даних, повернене значення зрізу містить вказівник, відмінний від значення у вхідному значенні зрізу. І це трапляється лише в тому випадку, якщо appendдовелося перерозподілити, і не трапляється інакше (базовий масив мав невикористаний простір). Це суть "проблеми".
kostix 02

@kostix Я отримав "+1", дякую! Цей код пояснить, що відбувається: https://play.golang.org/p/zJT7CW-pfp8 . func Test(slice []int), отримує копію значення зрізу a. І це вказує на той самий масив, що і aвказівний. Я не можу редагувати мій вищезазначений коментар, і його видалення призведе до плутанини в цій розмові.
гіханчанука 02

36

Типове appendвикористання

a = append(a, x)

оскільки appendможе або змінити свій аргумент на місці, або повернути копію аргументу з додатковим записом, залежно від розміру та потужності його вводу. Використання фрагмента, до якого раніше було додано, може дати несподівані результати, наприклад

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

може друкувати

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

1
Дякую, larsmans, я змінив деякий код. "make" дасть достатню потужність. Результат той самий, і я підвіконня розгублений.
Pole_Zhang

1
Це потрібно _ = append(a[:3], 5)скомпілювати зараз
y3sh

append(a[:3], 5)те саме, що і a[3] = 5в наступному рубанні. Думаю, це приклад, коли трапляється несподіваний сюрприз.
Ізана

8

Для того, щоб ваш код працював без необхідності повертати фрагмент з тесту, ви можете передати такий вказівник:

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

ПРИМІТКА, що додається, генерує новий фрагмент, якщо обмеження недостатньо. Відповідь @ kostix правильна, або ви можете передати аргумент зрізу за вказівником!


1
Ви маєте рацію щодо вказівників, але я рішуче про них не згадував, оскільки зрізи були придумані здебільшого для того, щоб звільнити програмістів від роботи з вказівниками на масиви. У еталонній реалізації (від Go) змінна зрізу містить покажчик і два цілих числа, тому копіювати її дешево, і тому slice = append(slice, a, b, c)ідіоматично, не передаючи змінну зрізу за вказівником і модифікуючи її "на місці", щоб абонент бачив змінити.
kostix

2
@kostix Ви маєте рацію, призначення кодів має бути явним. Але я думаю, що вся історія стосується лише передачі значення, що зберігає вказівник, і передачі вказівника, що вказує на вказівник. Якщо ми модифікуємо посилання, обидва можуть працювати, але якщо ми замінимо посилання, перше втрачає ефекти. Програміст повинен знати, що він робить.
Гізак

4

Спробуйте це, що, на мою думку, дає зрозуміти. базовий масив змінено, але наш фрагмент - ні, printпросто друкує len()символи, іншим фрагментом до cap(), ви можете побачити змінений масив:

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]
}

тож "a" та "a [: cap (a)]" - це різний фрагмент?
Pole_Zhang

2
Так, якщо ви запустите код, ви це дізнаєтесь. тому що шапка (а) змінена під час виклику тесту (а)
doun

3

Пояснення (читайте вбудовані коментарі):


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
}

Вихід:

[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

Посилання 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

Го підходить до цього більш м’яко і ліниво. Він продовжує модифікувати той самий базовий масив, поки не буде досягнута ємність зрізу.

Посилання: http://criticalindirection.com/2016/02/17/slice-with-a-pinch-of-salt/

Висновок прикладу за посиланням пояснює поведінку фрагментів у Go.

Створення зрізу a.

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

Зріз b відноситься до показників 2, 3, 4 у зрізі a. Отже, ємність дорівнює 5 (= 7-2).

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

Модифікуючи зріз b, також змінює a, оскільки вони вказують на той самий базовий масив.

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]

Додавання 1 до зрізу b. Замінює a.

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

Додавання 2 до зрізу b. Замінює 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]

Додавання 3 до зрізу b. Тут робиться нова копія, оскільки ємність перевантажена.

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]

Перевірка зрізів a та b вказують на різні базові масиви після перевантаження ємності на попередньому кроці.

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]

введіть тут опис зображення

введіть тут опис зображення

Детальніше див. Https://allegro.tech/2017/07/golang-slices-gotcha.html


1

Я думаю, що оригінальна відповідь не зовсім правильна. append()змінив як зрізи, так і базовий масив, хоча базовий масив змінено, але все одно є спільним для обох зрізів.

Як зазначено в Go Doc:

Зріз не зберігає жодних даних, він просто описує розділ базового масиву. (Посилання)

Зрізи - це просто обгорткові значення навколо масивів, тобто вони містять інформацію про те, як вони нарізають базовий масив, який вони використовують для зберігання набору даних. Тому за замовчуванням зріз, переданий іншому методу, фактично передається за значенням, а не за посиланням / покажчиком, хоча вони все одно будуть використовувати той самий базовий масив. Зазвичай масиви також передаються за значенням, тому я припускаю, що зріз вказує на базовий масив, а не зберігає його як значення. Щодо вашого запитання, під час запуску ваш фрагмент передається такій функції:

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

ви насправді передали копію свого фрагмента разом із покажчиком на той самий базовий масив. Це означає, що зміни, які ви зробили slice, не вплинули на зміну mainфункції. Саме фрагмент зберігає інформацію про те, скільки масиву він нарізає та виставляє на загальний огляд. Отже, коли ви запускали append(slice, 1000), розширюючи базовий масив, ви також змінювали інформацію про нарізування slice, яка залишалася приватною у вашій Test()функції.

Однак, якщо ви змінили свій код наступним чином, це могло б спрацювати:

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

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

Причина в тому, що ви розширили a, сказавши a[:cap(a)]над його зміненим базовим масивом, зміненим Test()функцією. Як зазначено тут:

Ви можете збільшити довжину зрізу, повторно нарізавши його, за умови, що він має достатню ємність. (Посилання)


0

Ось гарна реалізація додатка для фрагментів. Я думаю, це схоже на те, що відбувається під капотом:

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
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.