Wszystko o slicesach
Wycinki to typ, który wart jest głębszego omówienia, bo jest szalenie wygodny i użyteczny, im wcześniej się z nim zaprzyjaźnisz tym lepiej.
Czym są slicesy
Slice to przykrywka na tablice, które są toporne w użyciu. Dla każdej zmiennej
tablicowej trzeba przewidzieć rozmiar (bo przecież [2]int
to nie to samo co
[1]int
), trzeba pamiętać, by w wywołaniach funkcji przekazywać wskaźniki do
tablic, a nie wartości tablic (bo wartości argumentów funkcji są kopiowane).
Generalnie operowanie na zwykłych tablicach nastręcza trudności.
Dlatego twórcy Go zaimplementowali dużo lżejszy w użyciu i elastyczniejszy typ indeksowanej kolekcji, który w podstawowym działaniu przypomina tablice, to jest:
- jego elementy mogą być tylko jednego określonego typu
- dostęp do poszczególnych elementów odbywa się tak jak w przypadku tablic za
pomocą operatora
[]
- iterowalny
- o wyliczalnej ilości elementów (wbudowana funkcja
len()
)
Dodali mu jednak kilka innych uelastyczniających cech:
- nie posiada z góry określonej ilości elementów - jest zmienna
- jest typem referencyjnym - wartości tego typu są referencjami, zatem nie trzeba się martwić o kopiowanie dużych ilości danych przy wywołaniach funkcji
- można w łatwy sposób zwiększać pojemność takiej wartości (
append
)
Są trzy sposoby tworzenia wycinków:
make([]T,len,cap)
, alokacja nowego wycinka o zadanych parametrach (szczegóły w artykule o wartościach)- literał (szczegóły w artykule o literałach)
- za pomocą operatora wycinania z tablic lub innych wartości
slice
Operator wcinania
Jedną z podstawowych sytuacji w których powinniśmy użyć slice
sa zachodzi, gdy
chcemy przekazać część tablicy (lub stringa czy innego slice) np. w wywołaniu
funkcji, bez kopiowania jej. Wtedy możemy uciec się do operacji wycinania za
pomocą operatora [od : do]
, gdzie od
to indeks od którego zaczynamy wycinanie
a do
to indeks końca wycinania (element o tym indeksie nie wchodzi do wycinka).
Powyższy fragment kodu tworzy zmienną sliceInt
, która będzie wskazywała na
wartości 2 i 3. Indeksowanie slicesów rozpoczyna się od zera więc sliceInt[0]
będzie wskazywało 2 a sliceInt[1]
będzie wskazywało 3.
Indeksy od
i do
są opcjonalne, brak indeksu od
oznacza "od początku" a
brak indeksu do
oznacza "do końca".
s1 := arr[:] //wycinek całości sekwencji <=> arr[0:len(arr)]
s2 := arr[:4]//wycinek z pierwszych 4 elementów <=> arr[0:4]
s3 := arr[4:]//wycinek od 4 indeksu do końca <=> arr[4:len(arr)]
Funkcje pomocnicze
Mamy wbudowane cztery funkcje, które pomogą nam przeprowadzać operacje na wycinkach.
len
Funkcja len
zwróci liczbę jak wiele elementów zawiera wycinek
cap
Funkcja cap
zwraca ilość elementów, które wycinek może zawierać bez tworzenia
nowej tablicy bazowej (więcej o tym w ostatniej sekcji tego artykułu).
append
Powiększanie wycinka o dodatkowe elementy możemy zrealizować dzięki funkcji
append
, jako argumenty przyjmuje ona rozszerzany slice i dodatkowe elementy
(nieokreśloną ilość) o typie zgodnym z elementami wycinka i zwraca nowego slice.
Ale uwaga, gdy wycinek uzyskany jest z tablicy to mogą dziać się dziwne rzeczy np.:
Niejawnie tablica s
zostanie zmodyfikowana: na pozycjach 3 i 4 wartości
zastąpią zera, podczas gdy q := append(p, 0, 0, 0, 0), nie zmodyfikuje tablicy,
wynika to z konstrukcji slicesów, co omówię później w tym artykule. Aby uniknąć
takich wpadek polecam dla pewności zrobić copy
na slicesach niewiadomego
pochodzenia, przed dodawaniem mu elementów.
copy
Kopiowanie przeprowadzamy za pomocą funkcji wbudowanej copy
, która przyjmuje
jako argumenty cel i źródło a zwraca ilość skopiowanych elementów. Oba argumenty
muszą mieć elementy tego samego typu. Co do zasady, cel i źródło, to muszą być
slicesy, choć zrobiono wyjątek dla źródła będącego typu string
i celem będącego
[]byte
.
Jeśli wielkość wycinków nie jest równa to zostanie skopiowana mniejsza ilość elementów:
Slicesy od podszewki
We wstępie napisałem, że slice
to abstrakcja na tablice i tak rzeczywiście
jest. Każdy slice
ma odpowiadającą mu tablicę zapisaną gdzieś tam w pamięci,
a on sam przechowuje jedynie referencję do niej. Dzięki temu cokolwiek zmienimy
w wycinku, zostanie zmienione w owej tablicy.
Oprócz referencji do tablicy wycinki trzymają informację o ilości elementów
tablicy bazowej które udostępniają (nazywamy to długością len(s)
), oraz o
ilości elementów, które wycinek może jeszcze reprezentować, bez alokowania
(i kopiowania danych do) nowej tablicy (nazywamy to pojemnością cap(s)).
Dla przykładu:
To oznacza, że możemy do wycinka s
dodać cap(s) - len(s)
(5) elementów bez
kosztowo. Gdybyśmy jednak chcieli dodać do s więcej niż 5 elementów zostałaby
utworzona nowa tablica, ze wszystkimi elementami które slice udostępniał
powiększona o bliżej nieokreśloną ilość elementów (choć odnoszę wrażenie, że
nowe tablice są zawsze dokładnie dwa razy większe).
Aby była jasność: dodanie elementu do wycinka to modyfikacja tablicy bazowej. Jeśli w tablicy bazowej nie ma miejsca na nowe elementy, to się jej pozbywamy i tworzymy większą.
PROTIP: Jeśli używasz wycinków aby trzymać jakieś iterowalne dane i wiesz, że będziesz do nich dodawał wiele elementów stwórz odrazu większy wycinek. Zyskasz na wydajności, bo nie trzeba będzie wielokrotnie kopiować tablic bazowych.
myGrowingSlice := make([]int, 0, 1200)
Zagrożenia
Należy pamiętać, że Go jest językiem z garbage collectorem, który głównie
zajmuje się liczeniem referencji do wartości. Jeśli stworzymy ogromną tablicę,
do której utworzymy slice
, to tablica będzie tak długo rezydowała w pamięci
aż pozbędziemy się tego wycinka (czyli referencji do niej).