Slicy (řezy) jsou základním konceptem v Rustu, který umožňuje odkazovat na části kolekcí bez převzetí
vlastnictví (ownership). Poskytují způsob, jak efektivně pracovat s daty, a zároveň zachovávají garance
bezpečnosti paměti, které Rust nabízí.
Co jsou to Slicy?
Slice je specifický typ reference, který ukazuje na část nebo sekvenci kolekce. Stejně jako je pizza
kousek části celé pizzy, slice v Rustu představuje fragment větší kolekce. Běžné příklady zahrnují:
• Řetězcové slicy (&str): Reference na sekvenci znaků v
řetězci
• Slicy polí: Reference na sekvenci prvků v poli
Důležité je, že slice nepřebírají vlastnictví dat, na která odkazují — pouze si je „půjčují“ v souladu
s pravidly vlastnictví v Rustu.
Řetězcové Slicy v praxi
Řetězcové slicy patří mezi nejčastěji používané typy sliců v Rustu. Podívejme se, jak fungují na
různých příkladech:
Vytváření řetězcových sliců z heapově alokovaných řetězců
let action_star = String::from("Keanu Reeves");
// Získání slicu pro křestní jméno
let first_name = &action_star[0..5]; // "Keanu"
// Získání slicu pro příjmení
let last_name = &action_star[6..12]; // "Reeves"
println!("{}", first_name); // Vytiskne: Keanu
println!("{}", last_name); // Vytiskne: Reeves
V tomto příkladu first_name a last_name jsou řetězcové slicy (&str), které odkazují na části
původního řetězce action_star. Syntaxe slice používá hranaté závorky
a rozsah k určení, které bajty mají být zahrnuty.
Řetězcové Slicy z řetězcových literálů
Řetězcové literály (text v uvozovkách) jsou už samy o sobě řetězcové slicy:
let action_star = "Keanu Reeves"; // Typ je &str
let first_name = &action_star[0..5]; // "Keanu"
let last_name = &action_star[6..12]; // "Reeves"
Zde je action_star přímo řetězcový slice, který ukazuje na text
zabudovaný v binárce programu. Když vytvoříte další slicy jako first_name a last_name,
vytváříte nové nezávislé reference na stejné místo v paměti.
Zkratky pro slice syntax
Rust nabízí několik zkratek pro běžné situace při slicování:
Slicování od začátku
Pokud slice začíná na pozici 0, lze začátek vynechat:
let first_name = &action_star[..5]; // To samé jako &action_star[0..5]
Slicování do konce
Pokud chcete slice až do konce kolekce, můžete vynechat konec:
let last_name = &action_star[6..]; // To samé jako &action_star[6..12]
Celý slice
Pro vytvoření slicu celé kolekce použijte pouze dvě tečky:
let full_name = &action_star[..]; // To samé jako &action_star[0..12]
Důležité poznámky
Indexování podle bajtů
Při vytváření řetězcových sliců je důležité si uvědomit, že rozsahy určují bajtové pozice, ne znakové. Toto je důležité u ne-ASCII znaků:
let food = "🍕"; // Emoji pizzy
println!("{}", food.len()); // Vytiskne: 4 (bajty)
// Tohle vyhodí chybu - index 3 není hranicí znaku
// let pizza_slice = &food[0..3];
// Tohle funguje - zahrnuje všechny bajty znaku
let pizza_slice = &food[0..4];
Unicode znaky jako emoji mohou zabírat více bajtů, a Rust vyžaduje, aby slicy byly pouze na platných hranicích znaků, jinak by mohly vzniknout neplatné UTF-8 sekvence.
String Slicy vs. String Reference jako parametry funkcí
Při definování funkcí, které přijímají řetězcová data, je obvykle flexibilnější použít řetězcový slice (&str) namísto reference na String (&String):
// Tato funkce akceptuje jak &String, tak &str
fn do_hero_stuff(hero_name: &str) {
println!("{} zachraňuje den", hero_name);
}
let hero1 = String::from("Keanu Reeves");
let hero2 = "Tom Cruise";
do_hero_stuff(&hero1); // Funguje s &String
do_hero_stuff(hero2); // Funguje s &str
Díky mechanismu deref koerce Rust automaticky převede &String na &str, pokud funkce přijímá &str. Naopak to ale neplatí – funkce, která vyžaduje &String, nemůže přijmout &str.
Proč používat Slicy?
Slicy mají několik výhod:
• Efektivita: Umožňují odkazovat na části dat bez kopírování
• Flexibilita: Fungují s různými typy kolekcí a lze s nimi pracovat s částmi i celými kolekcemi
• Bezpečnost: Systém vlastnictví v Rustu zajišťuje, že slice nemůže přežít data, na která odkazuje
Řezy polí
Řez pole v Rustu je reference na část nebo úsek pole. Podobně jako řezy řetězců, řezy polí používají operátor výpůjčky (&) následovaný hranatými závorkami pro určení rozsahu prvků, které chcete zacílit:
let values = [4, 8, 15, 16, 23, 42];
let my_slice = &values[0..3]; // Řez obsahující prvky na indexech 0, 1 a 2
Čísla uvnitř hranatých závorek představují počáteční a koncové pozice indexů (přičemž koncový index je exkluzivní). Ve výše uvedeném příkladu my_slice obsahuje první tři prvky pole values: 4, 8 a 15.
Syntaxe řezů a zkratky
Rust poskytuje několik zkratek pro vytváření řezů:
• Vynechání počátečního indexu (&values[..3]) znamená "začít od začátku"
• Vynechání koncového indexu (&values[2..]) znamená "jít až do konce"
• Vynechání obou indexů (&values[..]) vytvoří řez celého pole
Zde jsou některé příklady:
let values = [4, 8, 15, 16, 23, 42];
// Různé způsoby vytváření řezů
let slice1 = &values[0..3]; // Prvky na indexech 0, 1, 2
let slice2 = &values[..3]; // Stejné jako výše
let slice3 = &values[2..4]; // Prvky na indexech 2, 3
let slice4 = &values[2..]; // Prvky od indexu 2 do konce
let slice5 = &values[..]; // Všechny prvky
Řezy vs. plné reference
Existuje důležitý rozdíl mezi řezem a plnou referencí na pole:
let values = [4, 8, 15, 16, 23, 42];
let array_slice = &values[..]; // Typ: &[i32]
let array_reference = &values; // Typ: &[i32; 6]
Všimněte si rozdílu v typech:
• &[i32] je řez pole - reference na nějakou část pole s dynamickou délkou
• &[i32; 6] je plná reference na 6prvkové pole, přičemž délka je součástí typu
Tento rozdíl se stává klíčovým při definování funkcí, které přijímají parametry polí.
Parametry funkcí a deref koerce
Při definování funkcí, které přijímají pole, poskytuje použití řezů polí jako parametrů větší flexibilitu:
// Tato funkce přijímá pouze reference na 6prvková pole
fn strict_function(reference: &[i32; 6]) {
println!("Délka: {}", reference.len());
}
// Tato funkce přijímá jakýkoliv řez pole, bez ohledu na délku
fn flexible_function(reference: &[i32]) {
println!("Délka: {}", reference.len());
}
Druhá funkce je všestrannější, protože může přijímat:
• Řezy polí jakékoliv délky
• Plné reference na pole (prostřednictvím deref koerce)
To je podobné tomu, jak parametry řezu řetězce (&str) mohou přijímat jak řezy řetězce, tak plné reference řetězce (&String).
Měnitelné řezy polí
Na rozdíl od řezů řetězců, které mohou být pouze neměnné, Rust umožňuje měnitelné řezy polí:
let mut my_array = [10, 15, 20, 25, 30];
let my_slice = &mut my_array[2..4]; // Měnitelný řez prvků na indexech 2 a 3
// Úprava řezu
my_slice[0] = 100;
// Původní pole je také upraveno
println!("{:?}", my_array); // Výstup: [10, 15, 100, 25, 30]
Když vytvoříte měnitelný řez a upravíte prvky v něm, tyto změny ovlivní původní pole. K tomu dochází, protože řez je reference na část pole, nikoliv kopie.