Zmień wartości podczas iteracji

153

Załóżmy, że mam te typy:

type Attribute struct {
    Key, Val string
}
type Node struct {
    Attr []Attribute
}

i że chcę iterować na atrybutach mojego węzła, aby je zmienić.

Bardzo chciałbym móc:

for _, attr := range n.Attr {
    if attr.Key == "href" {
        attr.Val = "something"
    }
}

ale ponieważ attrnie jest wskaźnikiem, to nie zadziała i muszę zrobić:

for i, attr := range n.Attr {
    if attr.Key == "href" {
        n.Attr[i].Val = "something"
    }
}

Czy jest prostszy lub szybszy sposób? Czy można bezpośrednio uzyskać wskazówki range?

Oczywiście nie chcę zmieniać struktur tylko dla iteracji, a bardziej szczegółowe rozwiązania nie są rozwiązaniami.

Denys Séguret
źródło
2
Więc chcesz coś Array.prototype.forEachw JavaScript?
Florian Margaine
To ciekawy pomysł i mogłoby to być rozwiązanie, ale wywołanie funkcji, która z kolei wywoła funkcję w każdej iteracji, wygląda ciężko i źle w języku po stronie serwera. A brak leków generycznych sprawiłby, że byłoby to jeszcze cięższe.
Denys Séguret
Szczerze, nie sądzę, żeby to było takie ciężkie. Wywołanie jednej lub dwóch funkcji jest bardzo tanie, zwykle to właśnie kompilatory optymalizują najbardziej. Spróbowałbym i przetestowałbym to, aby sprawdzić, czy pasuje do rachunku.
Florian Margaine
Ponieważ w Go brakuje forEachtypów ogólnych, obawiam się, że funkcja przekazana do musi zaczynać się od asercji typu. To nie jest lepsze niż attr := &n.Attr[i].
Denys Séguret

Odpowiedzi:

152

Nie, żądany skrót nie jest możliwy.

Powodem tego jest to, że rangekopiuje wartości z wycinka, po którym iterujesz. Specyfikacja o zasięgu mówi:

Range expression                          1st value             2nd value (if 2nd variable is present)
array or slice  a   [n]E, *[n]E, or []E   index    i  int       a[i]       E

Tak więc zakres jest używany a[i]jako druga wartość dla tablic / plasterków, co skutecznie oznacza, że ​​wartość jest kopiowana, co sprawia, że ​​oryginalna wartość jest nietykalna.

To zachowanie jest przedstawione w następującym kodzie :

x := make([]int, 3)

x[0], x[1], x[2] = 1, 2, 3

for i, val := range x {
    println(&x[i], "vs.", &val)
}

Kod drukuje całkowicie różne lokalizacje pamięci dla wartości z zakresu i rzeczywistej wartości w wycinku:

0xf84000f010 vs. 0x7f095ed0bf68
0xf84000f014 vs. 0x7f095ed0bf68
0xf84000f018 vs. 0x7f095ed0bf68

Więc jedyne, co możesz zrobić, to użyć wskaźników lub indeksu, co zostało już zaproponowane przez jnml i peterSO.

nemo
źródło
16
Można o tym pomyśleć, że przypisanie wartości powoduje skopiowanie. Gdybyś zobaczył val: = x [1], nie byłoby całkowicie zaskakujące, że val byłby kopią x [1]. Zamiast myśleć o zakresie jako robiącym coś specjalnego, pamiętaj, że każda iteracja zakresu zaczyna się od przypisania zmiennych indeksu i wartości i że to przypisanie, a nie zakres, powoduje kopiowanie.
Andy Davis
Przepraszam, nadal jestem trochę zdezorientowany. Jeśli drugą wartością pętli for jest [i], to czym różni a[i]się pętla z pętli for od pętli w chwili, a[i]gdy piszemy? Wygląda na to samo, ale tak nie jest, prawda?
Tiến Nguyễn Hoàng
1
@ TiếnNguyễnHoàng rangezwraca a[i]jako drugą wartość zwracaną. Ta operacja, val = a[i]tak jak została wykonana przez, rangetworzy kopię wartości, więc każda operacja zapisu valjest stosowana do kopii.
nemo
37

Wydaje się, że prosisz o coś równoważnego z tym:

package main

import "fmt"

type Attribute struct {
    Key, Val string
}
type Node struct {
    Attr []Attribute
}

func main() {

    n := Node{
        []Attribute{
            {"key", "value"},
            {"href", "http://www.google.com"},
        },
    }
    fmt.Println(n)

    for i := 0; i < len(n.Attr); i++ {
        attr := &n.Attr[i]
        if attr.Key == "href" {
            attr.Val = "something"
        }
    }

    fmt.Println(n)
}

Wynik:

{[{key value} {href http://www.google.com}]}
{[{key value} {href something}]}

Pozwala to uniknąć tworzenia - być może dużej - kopii Attributewartości typu kosztem sprawdzania granic wycinków. W twoim przykładzie typ Attributejest stosunkowo mały, stringodwołania do dwóch wycinków: 2 * 3 * 8 = 48 bajtów na komputerze o architekturze 64-bitowej.

Możesz też po prostu napisać:

for i := 0; i < len(n.Attr); i++ {
    if n.Attr[i].Key == "href" {
        n.Attr[i].Val = "something"
    }
}

Ale sposobem na uzyskanie równoważnego wyniku z rangeklauzulą, która tworzy kopię, ale minimalizuje sprawdzanie granic wycinków, jest:

for i, attr := range n.Attr {
    if attr.Key == "href" {
        n.Attr[i].Val = "something"
    }
}
peterSO
źródło
2
Szkoda, że value := &someMap[key]nie zadziała, jeśli someMapjestmap
warvariuc
peterSO w swoim pierwszym fragmencie kodu, czy nie musisz szanować atr, aby coś do niego przypisać? tj.*attr.Val = "something"
Homam Bahrani
25

Dostosowałbym twoją ostatnią sugestię i użył tylko indeksowanej wersji zakresu.

for i := range n.Attr {
    if n.Attr[i].Key == "href" {
        n.Attr[i].Val = "something"
    }
}

Wydaje mi się, że prostsze jest odwoływanie się n.Attr[i]bezpośrednio zarówno do wiersza, który testuje, jak Keyi do wiersza, który ustawia Val, zamiast używać attrdla jednego i n.Attr[i]drugiego.

Paul Hankin
źródło
15

Na przykład:

package main

import "fmt"

type Attribute struct {
        Key, Val string
}

type Node struct {
        Attr []*Attribute
}

func main() {
        n := Node{[]*Attribute{
                &Attribute{"foo", ""},
                &Attribute{"href", ""},
                &Attribute{"bar", ""},
        }}

        for _, attr := range n.Attr {
                if attr.Key == "href" {
                        attr.Val = "something"
                }
        }

        for _, v := range n.Attr {
                fmt.Printf("%#v\n", *v)
        }
}

Plac zabaw


Wynik

main.Attribute{Key:"foo", Val:""}
main.Attribute{Key:"href", Val:"something"}
main.Attribute{Key:"bar", Val:""}

Alternatywne podejście:

package main

import "fmt"

type Attribute struct {
        Key, Val string
}

type Node struct {
        Attr []Attribute
}

func main() {
        n := Node{[]Attribute{
            {"foo", ""},
            {"href", ""},
            {"bar", ""},
        }}

        for i := range n.Attr {
                attr := &n.Attr[i]
                if attr.Key == "href" {
                        attr.Val = "something"
                }
        }

        for _, v := range n.Attr {
                fmt.Printf("%#v\n", v)
        }
}

Plac zabaw


Wynik:

main.Attribute{Key:"foo", Val:""}
main.Attribute{Key:"href", Val:"something"}
main.Attribute{Key:"bar", Val:""}
zzzz
źródło
Myślałem, że to oczywiste, ale nie chcę zmieniać struktur, które otrzymuję (pochodzą z go.net/htmlpaczki)
Denys Séguret
1
@dystroy: Drugie podejście powyżej nie zmienia typów („struktur”) w OP.
zzzz
Tak, wiem, ale to nic nie przynosi. Spodziewałem się pomysłu, który mógłbym przegapić. Jestem przekonany, że nie ma prostszego rozwiązania, to byłaby odpowiedź.
Denys Séguret
1
@dystroy: To ma przynieść coś, to nie kopiować tutaj iz powrotem cały atrybut. I tak, jestem przekonany, że przyjęcie adresu elementu plasterka w celu uniknięcia podwójnej kopii (r + w) aktualizacji elementu jest optymalnym rozwiązaniem.
zzzz