Ćwiczenie — eksplorowanie wycinków

Ukończone

Omówiliśmy tablice w poprzedniej sekcji i dowiedzieliśmy się, że tablice są podstawą wycinków i map. Zrozumiesz, dlaczego za chwilę. Podobnie jak tablice, wycinek jest typem danych w języku Go, który reprezentuje sekwencję elementów tego samego typu. Jednak bardziej znaczącą różnicą w tablicach jest to, że rozmiar wycinka jest dynamiczny, a nie stały.

Wycinek to struktura danych na podstawie tablicy lub innego wycinka. Jako macierz bazową odwołujemy się do tablicy źródłowej lub wycinka. Za pomocą wycinka można uzyskać dostęp do całej podstawowej tablicy lub tylko podsekwencja elementów.

Wycinek ma tylko trzy składniki:

  • Wskaźnik do pierwszego dostępnego elementu podstawowej tablicy. Ten element nie musi być pierwszym elementem tablicy. array[0]
  • Długość wycinka. Liczba elementów w wycinku.
  • Pojemność wycinka. Liczba elementów między rozpoczęciem wycinka a końcem bazowej tablicy.

Na poniższej ilustracji przedstawiono fragment:

Diagram przedstawiający wygląd wycinków w języku Go.

Zwróć uwagę, że wycinek jest tylko podzbiorem podstawowej tablicy. Zobaczmy, jak można przedstawić powyższy obraz w kodzie.

Deklarowanie i inicjowanie wycinka

Aby zadeklarować wycinek, należy to zrobić w taki sam sposób, jak zadeklarowanie tablicy. Na przykład poniższy kod reprezentuje to, co pokazano na obrazie wycinka:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    fmt.Println(months)
    fmt.Println("Length:", len(months))
    fmt.Println("Capacity:", cap(months))
}

Po uruchomieniu kodu zobaczysz następujące dane wyjściowe:

[January February March April May June July August September October November December]
Length: 12
Capacity: 12

Zwróć uwagę, że w tej chwili wycinek nie różni się zbytnio od tablicy. Deklarujesz je w taki sam sposób. Aby uzyskać informacje z wycinka, możesz użyć wbudowanych funkcji len() i cap(). Będziemy nadal używać tych funkcji, aby potwierdzić, że wycinek może mieć podsekwencję elementów z podstawowej tablicy.

Fragmentuj elementy

Funkcja Go obsługuje operator s[i:p]fragmentowania , gdzie:

  • s reprezentuje tablicę.
  • i reprezentuje wskaźnik do pierwszego elementu podstawowej tablicy (lub innego wycinka), który ma zostać dodany do nowego wycinka. Zmienna i odpowiada elementowi w lokalizacji i indeksu w tablicy . array[i] Pamiętaj, że ten element nie musi być pierwszym elementem podstawowej tablicy. array[0]
  • p reprezentuje liczbę elementów w tablicy bazowej do użycia podczas tworzenia nowego wycinka, a także położenie elementu. Zmienna p odpowiada ostatniemu elementowi w podstawowej tablicy, który może być używany w nowym wycinku. Element znajdujący się na pozycji p w podstawowej tablicy znajduje się w lokalizacji array[i+1]. Zwróć uwagę, że ten element nie musi być ostatnim elementem macierzy bazowej. array[len(array)-1]

W związku z tym wycinek może odwoływać się tylko do podzestawu elementów.

Załóżmy, że chcesz, aby cztery zmienne reprezentowały każdy kwartał roku i masz fragment z months 12 elementami. Na poniższej ilustracji pokazano, jak podzielić months na cztery nowe quarter wycinki:

Diagram przedstawiający wygląd wielu wycinków w języku Go.

Aby przedstawić w kodzie, który został wyświetlony na powyższym obrazie, możesz użyć następującego kodu:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter1 := months[0:3]
    quarter2 := months[3:6]
    quarter3 := months[6:9]
    quarter4 := months[9:12]
    fmt.Println(quarter1, len(quarter1), cap(quarter1))
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter3, len(quarter3), cap(quarter3))
    fmt.Println(quarter4, len(quarter4), cap(quarter4))
}

Po uruchomieniu kodu uzyskasz następujące dane wyjściowe:

[January February March] 3 12
[April May June] 3 9
[July August September] 3 6
[October November December] 3 3

Zwróć uwagę, że długość wycinków jest taka sama, ale pojemność jest inna. Przyjrzyjmy się fragmentowi quarter2 . Po zadeklarowaniu tego wycinka mówi się, że chcesz, aby wycinek zaczynał się od pozycji numer trzy, a ostatni element znajduje się na pozycji numer sześć. Długość wycinka to trzy elementy, ale pojemność wynosi dziewięć, ponieważ tablica bazowa ma więcej dostępnych elementów lub pozycji, ale nie jest widoczna dla wycinka. Jeśli na przykład spróbujesz wydrukować coś takiego jak fmt.Println(quarter2[3]), zostanie wyświetlony następujący błąd: panic: runtime error: index out of range [3] with length 3.

Pojemność wycinka informuje tylko o tym, ile można rozszerzyć wycinek. Z tego powodu można utworzyć rozszerzony wycinek z quarter2obiektu , jak w tym przykładzie:

package main

import "fmt"

func main() {
    months := []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
    quarter2 := months[3:6]
    quarter2Extended := quarter2[:4]
    fmt.Println(quarter2, len(quarter2), cap(quarter2))
    fmt.Println(quarter2Extended, len(quarter2Extended), cap(quarter2Extended))
}

Po uruchomieniu poprzedniego kodu uzyskasz następujące dane wyjściowe:

[April May June] 3 9
[April May June July] 4 9

Zwróć uwagę, że podczas deklarowania zmiennej quarter2Extended nie trzeba określać pozycji początkowej ([:4]). Gdy to zrobisz, program Go zakłada, że chcesz umieścić pierwszą pozycję wycinka. Możesz to zrobić dla ostatniej pozycji ([1:]). W tym miejscu przyjęto założenie, że chcesz odwołać się do wszystkich elementów do ostatniej pozycji wycinka (len()-1).

Dołączanie elementów

Dowiesz się, jak działają wycinki i jak są one podobne do tablic. Teraz odkryjmy, jak różnią się one od tablic. Pierwsza różnica polega na tym, że rozmiar wycinka nie jest stały, jest dynamiczny. Po utworzeniu wycinka możesz dodać do niego więcej elementów, a wycinek zostanie rozszerzony. Zobaczysz za chwilę, co się stanie z macierzą bazową.

Aby dodać element do wycinka, funkcja Go oferuje wbudowaną append(slice, element) funkcję. Wycinek należy przekazać, aby zmodyfikować element i dołączyć go jako wartości do funkcji. Następnie append funkcja zwraca nowy wycinek, który jest przechowywany w zmiennej. Może to być ta sama zmienna dla zmienianych fragmentów.

Zobaczmy, jak wygląda proces dołączania w kodzie:

package main

import "fmt"

func main() {
    var numbers []int
    for i := 0; i < 10; i++ {
        numbers = append(numbers, i)
        fmt.Printf("%d\tcap=%d\t%v\n", i, cap(numbers), numbers)
    }
}

Po uruchomieniu poprzedniego kodu powinny zostać wyświetlone następujące dane wyjściowe:

0       cap=1   [0]
1       cap=2   [0 1]
2       cap=4   [0 1 2]
3       cap=4   [0 1 2 3]
4       cap=8   [0 1 2 3 4]
5       cap=8   [0 1 2 3 4 5]
6       cap=8   [0 1 2 3 4 5 6]
7       cap=8   [0 1 2 3 4 5 6 7]
8       cap=16  [0 1 2 3 4 5 6 7 8]
9       cap=16  [0 1 2 3 4 5 6 7 8 9]

Te dane wyjściowe są interesujące. Szczególnie w przypadku zwracanego wywołania cap() funkcji. Wszystko wygląda normalnie do trzeciej iteracji, gdzie pojemność zmienia się na 4, a w wycinku znajdują się tylko trzy elementy. W piątej iteracji pojemność zmienia się ponownie do 8, a w dziewiątym do 16.

Czy widzisz wzorzec z danych wyjściowych pojemności? Gdy wycinek nie ma wystarczającej pojemności, aby pomieścić więcej elementów, funkcja Go podwoi jego pojemność. Tworzy nową macierz bazową z nową pojemnością. Nie musisz nic robić, aby ten wzrost pojemności się wydarzył. Go robi to automatycznie. Musisz być ostrożny. W pewnym momencie wycinek może mieć znacznie większą pojemność niż jest potrzebna, a ty marnujesz pamięć.

Usuń elementy

Być może zastanawiasz się, co z usuwaniem elementów? Cóż, język Go nie ma wbudowanej funkcji usuwania elementów z wycinka. Możesz użyć omówionego wcześniej operatora s[i:p] wycinka, aby utworzyć nowy wycinek tylko z potrzebnymi elementami.

Na przykład następujący kod usuwa element z wycinka:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    remove := 2

	if remove < len(letters) {

		fmt.Println("Before", letters, "Remove ", letters[remove])

		letters = append(letters[:remove], letters[remove+1:]...)

		fmt.Println("After", letters)
	}

}

Po uruchomieniu poprzedniego kodu uzyskasz następujące dane wyjściowe:

Before [A B C D E] Remove  C
After [A B D E]

Ten kod usuwa element z wycinka. Zastępuje element do usunięcia z następnym elementem w wycinku lub żaden, jeśli usuwasz ostatni element.

Innym podejściem jest utworzenie nowej kopii wycinka. Dowiesz się, jak tworzyć kopie wycinków w następnej sekcji.

Tworzenie kopii wycinków

Funkcja Go ma wbudowaną copy(dst, src []Type) funkcję do tworzenia kopii wycinka. Wycinek docelowy i wycinek źródłowy są wysyłane. Na przykład można utworzyć kopię wycinka, jak pokazano w tym przykładzie:

slice2 := make([]string, 3)
copy(slice2, letters[1:4])

Dlaczego warto tworzyć kopie? Cóż, po zmianie elementu z wycinka zmieniasz też podstawową tablicę. Dotyczy to wszystkich innych wycinków odwołujących się do tej samej tablicy bazowej. Zobaczmy ten proces w kodzie, a następnie naprawimy go, tworząc kopię wycinka.

Użyj poniższego kodu, aby potwierdzić, że wycinek wskazuje tablicę, a każda zmiana wprowadzana we fragmentatorze ma wpływ na tablicę bazową.

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]
    slice2 := letters[1:4]

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Po uruchomieniu poprzedniego kodu zobaczysz następujące dane wyjściowe:

Before [A B C D E]
After [A Z C D E]
Slice2 [Z C D]

Zwróć uwagę, jak zmiana miała wpływ na slice1 tablicę letters i slice2. W danych wyjściowych widać, że litera B została zastąpiona przez Z i dotyczy wszystkich osób wskazujących tablicę letters .

Aby rozwiązać ten problem, należy utworzyć kopię wycinka, która pod maską tworzy nową macierz bazową. Możesz użyć następującego kodu:

package main

import "fmt"

func main() {
    letters := []string{"A", "B", "C", "D", "E"}
    fmt.Println("Before", letters)

    slice1 := letters[0:2]

    slice2 := make([]string, 3)
    copy(slice2, letters[1:4])

    slice1[1] = "Z"

    fmt.Println("After", letters)
    fmt.Println("Slice2", slice2)
}

Po uruchomieniu poprzedniego kodu zobaczysz następujące dane wyjściowe:

Before [A B C D E]
After [A Z C D E]
Slice2 [B C D]

Zwróć uwagę, że zmiana w slice1 tablicy bazowej miała wpływ, ale nie miała wpływu na nową slice2.