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.


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.


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

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.


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:""}

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.