Cvičení – prozkoumání řezů

Dokončeno

Prozkoumali jsme pole v předchozí části a zjistili jsme, že pole jsou základem pro řezy a mapy. Pochopíš proč za chvilku. Podobně jako pole je řez datovým typem v Go, který představuje posloupnost prvků stejného typu. Významnější rozdíl u polí je ale to, že velikost řezu je dynamická, nikoli pevná.

Řez je datová struktura nad polem nebo jiným řezem. Označujeme původní matici nebo řez jako podkladovou matici. Pomocí řezu můžete získat přístup k celému podkladovému poli nebo pouze k dílčí sekvenci prvků.

Řez má pouze tři komponenty:

  • Ukazatel na první dosažitelný prvek podkladového pole. Tento prvek není nutně prvním prvkem pole. array[0]
  • Délka řezu Počet prvků v řezu
  • Kapacita řezu Počet prvků mezi začátkem řezu a koncem podkladového pole.

Následující obrázek znázorňuje, co je řez:

Diagram znázorňující, jak řezy vypadají v Go

Všimněte si, že řez je pouze podmnožinou podkladového pole. Pojďme se podívat, jak můžete znázorňovat předchozí obrázek v kódu.

Deklarace a inicializace řezu

Pokud chcete deklarovat řez, uděláte to stejným způsobem, jakým deklarujete pole. Například následující kód představuje, co jste viděli na obrázku řezu:

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

Při spuštění kódu se zobrazí následující výstup:

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

Všimněte si, že v tuto chvíli se řez moc neliší od pole. Deklarujete je stejným způsobem. K získání informací z řezu můžete použít předdefinované funkce len() a cap(). Tyto funkce budeme dál používat k potvrzení, že řez může obsahovat dílčí sekvenci prvků z podkladového pole.

Položky řezu

Go podporuje operátor s[i:p]řezu, kde:

  • s představuje pole.
  • i představuje ukazatel na první prvek podkladové matice (nebo jiného řezu), který se má přidat do nového řezu. Proměnná i odpovídá prvku v umístění i indexu v poli, array[i]. Pamatujte, že tento prvek nemusí nutně být prvním prvkem podkladového pole. array[0]
  • p představuje počet prvků v podkladovém poli, které se mají použít při vytváření nového řezu, a také umístění prvku. Proměnná p odpovídá poslednímu prvku v podkladovém poli, který lze použít v novém řezu. Prvek na pozici p v podkladové matici je nalezen v umístění array[i+1]. Všimněte si, že tento prvek nemusí nutně obsahovat poslední prvek podkladového pole . array[len(array)-1]

Řez by proto mohl odkazovat pouze na podmnožinu prvků.

Řekněme, že chcete, aby čtyři proměnné představovaly každé čtvrtletí roku a máte řez months s 12 prvky. Následující obrázek znázorňuje, jak rozdělit months na čtyři nové quarter řezy:

Diagram znázorňující, jak více řezů vypadá v Go

Pokud chcete znázorňovat kód, který jste viděli na předchozím obrázku, můžete použít následující kód:

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

Při spuštění kódu získáte následující výstup:

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

Všimněte si, jak je délka řezů stejná, ale kapacita se liší. Pojďme se podívat na quarter2 řez. Když deklarujete tento řez, říkáte, že chcete, aby řez začínal číslem tři a poslední prvek se nachází na pozici šest. Délka řezu je tři prvky, ale kapacita je devět, protože podkladové pole má k dispozici více prvků nebo pozic, ale není viditelné pro řez. Pokud se například pokusíte něco vytisknout fmt.Println(quarter2[3]), zobrazí se následující chyba: panic: runtime error: index out of range [3] with length 3.

Kapacita řezu vám jenom říká, kolik můžete rozšířit řez. Z tohoto důvodu můžete vytvořit rozšířený řez z quarter2tohoto příkladu:

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

Při spuštění předchozího kódu získáte následující výstup:

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

Všimněte si, že když deklarujete proměnnou quarter2Extended , nemusíte zadávat počáteční pozici ([:4]). Když to uděláte, Go předpokládá, že chcete první pozici řezu. To samé můžete udělat pro poslední pozici ([1:]). Go předpokládá, že chcete odkazovat na všechny prvky až do poslední pozice řezu (len()-1).

Připojit položky

Prozkoumali jsme, jak řezy fungují a jak se podobají polím. Teď se podíváme, jak se liší od polí. Prvním rozdílem je, že velikost řezu není pevná, je dynamická. Po vytvoření řezu do něj můžete přidat další prvky a řez se rozšíří. Uvidíte za chvíli, co se stane s podkladovým polem.

Pokud chcete přidat prvek do řezu, Go nabízí append(slice, element) integrovanou funkci. Předáte řez, který chcete upravit, a prvek, který se má připojit jako hodnoty do funkce. Funkce append pak vrátí nový řez, který uložíte do proměnné. Může to být stejná proměnná pro řez, který měníte.

Pojďme se podívat, jak proces připojení vypadá v kódu:

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

Při spuštění předchozího kódu by se měl zobrazit následující výstup:

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]

Tento výstup je zajímavý. Zvláště pro to, co volání cap() funkce vrací. Všechno vypadá normálně až do třetí iterace, kde se kapacita změní na 4 a v řezu jsou pouze tři prvky. V páté iteraci se kapacita opět liší na 8 a v deváté až 16.

Všimli jste si vzoru z výstupu kapacity? Pokud řez nemá dostatečnou kapacitu pro uložení více prvků, Go zdvojnásobí svou kapacitu. Vytvoří nové základní pole s novou kapacitou. Pro toto zvýšení kapacity nemusíte nic dělat. Go to udělá automaticky. Musíš být opatrný. V určitém okamžiku může mít řez větší kapacitu, než potřebuje, a budete ztrácet paměť.

Odebrání položek

Možná vás zajímá, co když odeberete prvky? Go nemá integrovanou funkci pro odebrání prvků z řezu. K vytvoření nového řezu s potřebnými prvky můžete použít operátor s[i:p] řezu, který jsme probrali.

Například následující kód odebere prvek z řezu:

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

}

Při spuštění předchozího kódu získáte následující výstup:

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

Tento kód odebere prvek z řezu. Nahradí prvek, který se má odebrat dalším prvkem v řezu, nebo žádný, pokud odebíráte poslední prvek.

Dalším přístupem je vytvoření nové kopie řezu. V další části se dozvíte, jak vytvářet kopie řezů.

Vytváření kopií řezů

Go má integrovanou copy(dst, src []Type) funkci pro vytváření kopií řezu. Cílový řez a zdrojový řez odešlete. Můžete například vytvořit kopii řezu jako v tomto příkladu:

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

Proč byste se starali o vytváření kopií? Když změníte prvek z řezu, změníte i podkladové pole. Všechny ostatní řezy, které odkazují na stejné podkladové pole, budou ovlivněny. Pojďme se podívat na tento proces v kódu a pak ho opravíme vytvořením kopie řezu.

Pomocí následujícího kódu potvrďte, že řez odkazuje na matici a každá změna, kterou v řezu provedete, ovlivní podkladovou matici.

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

Při spuštění předchozího kódu se zobrazí následující výstup:

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

Všimněte si, jak jsme provedli změnu u slice1letters pole a slice2. Ve výstupu vidíte, že písmeno B bylo nahrazeno písmenem Z a ovlivňuje každého, kdo na pole ukazuje letters .

Pokud chcete tento problém vyřešit, musíte vytvořit kopii řezu, která pod kapotou vytvoří nové podkladové pole. Můžete použít následující kód:

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

Při spuštění předchozího kódu se zobrazí následující výstup:

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

Všimněte si, že změna v slice1 podkladovém poli ovlivnila, ale nová změna nebyla ovlivněna slice2.