V jazyku Rust nalezneme kromě referencí, jejichž použití je kontrolované překladačem, i ukazatele (pointer). S nimi lze realizovat některé „triky“ známé z jazyků C a C++, ale některé operace jsou potenciálně nebezpečné.
V jazyku Rust nalezneme kromě referencí, jejichž použití je kontrolované překladačem, i ukazatele (pointer). S nimi lze realizovat některé „triky“ známé z jazyků C a C++, ale některé operace jsou potenciálně nebezpečné.
1. Ukazatele v Rustu aneb temná strana Síly
2. Deklarace a inicializace proměnné typu ukazatel
3. První demonstrační příklad – deklarace a inicializace ukazatele
4. Přístup k hodnotám proměnných přes ukazatel – blok unsafe
5. Kontrola překladačem na použití bloku unsafe
6. Měnitelné versus neměnitelné ukazatele, měnitelné versus neměnitelné proměnné
7. Ukazatele pro čtení či pro zápis hodnot do referencovaných proměnných
8. Demonstrační příklad – zápis do proměnné přes ukazatel
9. Kontrola, zda je možné do proměnné skutečně zapisovat
10. Krátká rekapitulace – deklarace a inicializace ukazatelů různých typů
11. Čtení či zápis do proměnné mimo oblast její životnosti?
12. Ukazatel na datovou strukturu
13. Přístup k prvkům struktury přes ukazatel
14. Repositář s demonstračními příklady
V programovacím jazyku Rust je možné a v některých případech dokonce i nutné používat ukazatele reprezentované primitivním typem pointer, které se však v některých ohledech liší od „klasických“ ukazatelů známých z jazyků C či C++. Na základě mnohaletých zkušeností vývojářů Rustu se zmíněnými programovacími jazyky C/C++ totiž byly ukazatele v Rustu navrženy takovým způsobem, aby práce s nimi byla poněkud bezpečnější (pokud je to vůbec možné) a aby všechny potenciálně nebezpečné operace byly umístěny do speciálního bloku nazvaného unsafe. Do určité míry se zjednodušila i ukazatelová aritmetika (pointer arithmetic/pointer math) díky existenci funkce pointer.offset(). I tak je však při práci s ukazateli nutné dbát zvýšené opatrnosti, protože překladač například nehlídá to, jestli ukazatel obsahuje adresu stále živé proměnné atd.
V praxi se většinou snažíme se použití ukazatelů zcela vyhnout, samozřejmě za předpokladu, že je to možné. Mnoho programů vytvořených v Rustu ukazatele vůbec nepoužívá.
Poznámka: v minulosti existovalo v Rustu několik typů ukazatelů, navíc je slovo pointer dodnes používáno v několika kontextech. Ukazatele popisované dnes spadají do kategorie „raw pointers“.
V programovacím jazyku Rust rozeznáváme dva typy „raw“ ukazatelů – ukazatele používané pro čtení konstantní i měnitelné hodnoty a ukazatele na hodnoty, které lze přes ukazatel změnit (mutable). Nejdříve se seznámíme s ukazateli používanými jen pro čtení hodnoty (read only). Jejich typ je vždy *const T, kde T je prakticky libovolný datový typ Rustu. Pokud například budeme chtít nadefinovat proměnnou nazvanou pointer, která je ukazatelem na konstantní či měnitelnou 32bitovou celou hodnotu se znaménkem, bude deklarace vypadat následovně:
let pointer: *const i32;
Přiřazení adresy do ukazatele se do značné míry podobá zápisu používaném v céčku nebo i v Rustu při získávání reference:
let value: i32 = 42; pointer = &value;
Deklaraci ukazatele můžeme spojit s jeho inicializací:
let value: i32 = 42; let pointer: *const i32 = &value;
Poznámka: v tomto případě ovšem vždy musíte explicitně uvést datový typ ukazatele, jinak by se vytvořila „pouze“ reference na hodnotu (což již známe). Následující kód má tedy odlišný význam:
let value: i32 = 42; let reference = &value;
Popř. explicitně:
let value: i32 = 42; let reference: &i32 = &value;
Poznámka: interně se reference a ukazatele vlastně neliší, protože obsahují stejnou hodnotu = adresu proměnné. Práce s nimi je však odlišná, stejně jako kontroly, které provádí překladač.
V dnešním prvním demonstračním příkladu vytvoříme neměnitelnou (immutable) proměnnou nazvanou value, k ní získáme referenci a taktéž ukazatel na ni. Následně je na standardní výstup vypsána hodnota proměnné, hodnota téže proměnné, ale přes referenci a konečně též obsah samotného ukazatele. Ovšem ukazatel obsahuje adresu proměnné, takže třetí řádek nevypíše 42, ale hexadecimální adresu value umístěné na zásobníkovém rámci funkce main (u reference se naproti tomu automaticky provede dereference a tudíž přečtení hodnoty proměnné). Povšimněte si, že získání reference na proměnnou a adresy proměnné se zapisuje stejným znakem &, jediný rozdíl spočívá v odlišném datovém typu:
fn main() { let value: i32 = 42; let reference: &i32 = &value; let pointer: *const i32 = &value; println!("{}", value); println!("{}", reference); println!("{:?}", pointer); }
Po spuštění může tento příklad vypsat například následující hodnoty. Adresa proměnné value (tedy hodnota ukazatele) se samozřejmě může ve vašem případě lišit:
42 42 0x7fffc7039394
K hodnotám proměnných lze samozřejmě přistupovat i přes ukazatel (jinak by ostatně nemělo smysl s ukazateli pracovat). Zápis je v tomto případě prakticky stejný, jak ho známe z céčka či z C++:
*ukazatel
Velký rozdíl mezi Rustem na jedné straně a jazyky C/C++ na straně druhé však spočívá v tom, že přístup přes ukazatel je považován za potenciálně nebezpečnou operaci a musí být za všech okolností uzavřen do speciálního bloku unsafe:
unsafe { ... *ukazatel ... }
Podívejme se nyní na druhý demonstrační příklad, který na posledním řádku vypíše hodnotu proměnné získané nepřímo přes ukazatel:
fn main() { let value: i32 = 42; let reference: &i32 = &value; let pointer: *const i32 = &value; println!("{}", value); println!("{}", reference); println!("{:?}", pointer); unsafe { println!("{}", *pointer); } }
Příklad výstupu tohoto demonstračního příkladu (třetí řádek se opět pravděpodobně bude na vašem počítači lišit):
42 42 0x7fff56270904 42
Alternativně je možné příklad přepsat takto (vložením bloku unsafe dovnitř makra println!):
fn main() { let value: i32 = 42; let reference: &i32 = &value; let pointer: *const i32 = &value; println!("{}", value); println!("{}", reference); println!("{:?}", pointer); println!("{}", unsafe {*pointer}); }
Ve chvíli, kdy blok unsafe nepoužijeme, bude se na nás překladač zlobit. Ostatně si to můžeme velmi snadno vyzkoušet na nepatrně upraveném příkladu, tentokrát ovšem bez unsafe bloku:
fn main() { let value: i32 = 42; let reference: &i32 = &value; let pointer: *const i32 = &value; println!("{}", value); println!("{}", reference); println!("{:?}", pointer); println!("{}", *pointer); }
Překladač skutečně při pokusu o vytvoření binárního spustitelného souboru vypíše chybové hlášení:
--> pointers02_no_unsafe.rs:11:20 | 11 | println!("{}", *pointer); | ^^^^^^^^ unsafe call requires unsafe function or block <std macros>:2:27: 2:58 note: in this expansion of format_args! <std macros>:3:1: 3:54 note: in this expansion of print! (defined in <std macros>) pointers02_no_unsafe.rs:11:5: 11:30 note: in this expansion of println! (defined in <std macros>) error: aborting due to previous error
Podobně je tomu i u dalších potenciálně nebezpečných operací, tj. u většiny operací s ukazateli.
Vzhledem k tomu, že ukazatele jsou primitivním datovým typem, rozeznává Rust měnitelné a neměnitelné ukazatele. Ovšem je rozdíl například mezi ukazatelem na měnitelnou hodnotu a měnitelným ukazatelem (a aby toho nebylo málo, rozeznáváme ještě typy *const a *mut popsané v navazujících kapitolách). Podívejme se nyní na jednotlivé varianty.
Neměnitelný ukazatel na neměnitelnou hodnotu – ten již známe z předchozích kapitol. Samotnou hodnotu nelze změnit, ovšem ani ukazatel nelze „přesměrovat“ na jinou proměnnou. Jediná povolená operace s ukazatelem je přečtení hodnoty proměnné přes její adresu (tj. dereferencování) v bloku unsafe:
fn main() { let value: i32 = 42; let pointer: *const i32 = &value; println!("{}", value); println!("{:?}", pointer); }
Neměnitelný ukazatel na měnitelnou hodnotu. Samotný ukazatel vždy ukazuje na jedinou proměnnou, jejíž hodnota se však může změnit. Tento ukazatel lze ovšem použít pouze pro čtení hodnoty proměnné, nikoli pro zápis (což je velmi rozumné, jinými slovy kde to jde používejte *const a nikoli dále popsaný *mut):
fn main() { let mut value: i32 = 42; let pointer: *const i32 = &value; println!("{}", value); println!("{:?}", pointer); unsafe { println!("{}", *pointer); } value = 100; println!("{}", value); println!("{:?}", pointer); unsafe { println!("{}", *pointer); } }
Měnitelný ukazatel na neměnitelnou (konstantní) hodnotu. Nyní jedna proměnná pointer v první části programu obsahuje adresu proměnné value1, v další části pak adresu proměnné value2. Jinými slovy adresa uložena v proměnné pointer se sice může změnit, nikoli však hodnoty uložené na této adrese:
fn main() { let value1: i32 = 42; let value2: i32 = 100; let mut pointer: *const i32; println!("{}", value1); pointer = &value1; println!("{:?}", pointer); println!("{}", unsafe {*pointer}); pointer = &value2; println!("{:?}", pointer); println!("{}", unsafe {*pointer}); }
Měnitelný je ukazatel i samotná hodnota. Kombinace předchozích dvou příkladů (stále však platí, že přes ukazatel je možné proměnnou pouze číst, protože typ ukazatele je *const i32):
fn main() { let mut value1: i32 = 1; let mut value2: i32 = 3; let mut pointer: *const i32; pointer = &value1; println!("{}", unsafe {*pointer}); value1 = 2; println!("{}", unsafe {*pointer}); pointer = &value2; println!("{}", unsafe {*pointer}); value2 = 4; println!("{}", unsafe {*pointer}); }
Aby toho nebylo málo, má klíčové slovo mut ještě jeden význam. Odlišujeme jím totiž ukazatele používané pouze pro čtení a ukazatele, přes něž můžeme měnit (mutovat) proměnnou, na níž ukazují. Prakticky to znamená, že se striktně rozlišuje mezi těmito typy:
let pointer1: *const i32 = &value; let pointer2: *mut i32 = &mut value;
Pokud se použije druhý zápis, musí být i samotná proměnná měnitelná! Tím je zajištěno, že se nikdo nebude snažit modifikovat immutable hodnotu (tedy snažit se může, ale překladač mu to nedovolí).
Rozdíl spočívá v tom, že ve druhém případě můžeme do proměnné zapisovat přes ukazatel. Podívejme se nyní na demonstrační příklad, z něhož je patrné, že i zápis přes ukazatel je – pochopitelně – potenciálně nebezpečná operace, která musí být uzavřena do bloku unsafe:
fn main() { let mut value: i32 = 42; let pointer: *mut i32 = &mut value; println!("{}", value); unsafe { println!("{}", *pointer); } value = 1; println!("{}", value); unsafe { println!("{}", *pointer); } unsafe { *pointer = 20; } println!("{}", value); unsafe { println!("{}", *pointer); } }
Po překladu a spuštění tohoto příkladu dostaneme tento výstup:
42 42 1 1 20 20
Opět platí, že si překladač hlídá, zda se ukazatel typu *mut skutečně vytváří pro měnitelnou proměnnou. Zkusme na první modifikátor mut zapomenout:
fn main() { letmutvalue: i32 = 42; let pointer: *mut i32 = &mut value; println!("{}", value); unsafe { println!("{}", *pointer); } value = 1; println!("{}", value); unsafe { println!("{}", *pointer); } unsafe { *pointer = 20; } println!("{}", value); unsafe { println!("{}", *pointer); } }
Překladač v takovém případě vypíše chybové hlášení, jehož druhou část již známe:
error: cannot borrow immutable local variable `value` as mutable --> pointers04_immut_var.rs:4:34 | 2 | let value: i32 = 42; | ----- use `mut value` here to make mutable 3 | 4 | let pointer: *mut i32 = &mut value; | ^^^^^ cannot borrow mutably error[E0384]: re-assignment of immutable variable `value` --> pointers04_immut_var.rs:11:5 | 2 | let value: i32 = 42; | ----- first assignment to `value` ... 11 | value = 1; | ^^^^^^^^^ re-assignment of immutable variable error: aborting due to 2 previous errors
Prozatím jsme se setkali se dvěma způsoby deklarace proměnných – immutable (výchozí volba) a mutable. U ukazatelů taktéž existují varianty, přičemž klíčovými slovy const/mut se určuje, zda se přes ukazatel může do proměnné zapisovat či nikoli. Další varianty určují, zda i samotný ukazatel je immutable či mutable, tj. zde se může ukazatel změnit. Povšimněte si, že dvě možnosti jsou chybné – pokus o vytvoření ukazatele typu *mut na neměnitelnou proměnnou:
Proměnná | Ukazatel | Čtení proměnné | Zápis do proměnné | Čtení přes ukazatel | Zápis přes ukazatel | Ukazatel lze změnit |
---|---|---|---|---|---|---|
let v: i32; | let p: *const i32; | ano | ne | ano | ne | ne |
let v: i32; | let mut p: *const i32; | ano | ne | ano | ne | ano |
let v: i32; | let p: *mut i32; | ano | ne | chyba | chyba | chyba |
let v: i32; | let mut p: *mut i32; | ano | ne | chyba | chyba | chyba |
let mut v: i32; | let p: *const i32; | ano | ano | ano | ne | ne |
let mut v: i32; | let mut p: *const i32; | ano | ano | ano | ne | ano |
let mut v: i32; | let p: *mut i32; | ano | ano | ano | ano | ne |
let mut v: i32; | let mut p: *mut i32; | ano | ano | ano | ano | ano |
Programovací jazyk Rust (resp. přesněji řečeno jeho překladač) sice hlídá mnoho operací prováděných s ukazateli, ovšem nedokáže (a pravděpodobně to ani není prakticky možné) určit, zda ukazatel ještě obsahuje adresu živé proměnné či zda se již proměnná nachází mimo oblast své platnosti. Proto je možné přeložit následující program, který může (ale také nemusí) fungovat, protože v poslední části přistupujeme přes ukazatel k proměnné mimo oblast její viditelnosti:
fn main() { let pointer: *mut i32; { let mut value: i32 = 42; pointer = &mut value; println!("{}", value); unsafe { println!("{}", *pointer); } value = 1; println!("{}", value); unsafe { println!("{}", *pointer); } unsafe { *pointer = 20; } println!("{}", value); unsafe { println!("{}", *pointer); } } // !!! unsafe { *pointer = 99; println!("{}", *pointer); } }
Poznámka: toto je asi největší praktický rozdíl mezi referencemi a ukazateli.
Ukazatel samozřejmě nemusí obsahovat jen adresu primitivní proměnné, ale i adresu datové struktury. Opět se na chvíli vraťme k naší struktuře s komplexními čísly, jejíž nejjednodušší varianta vypadá takto:
#[derive(Debug)] struct Complex { real: f32, imag: f32, } impl Complex { fn new(real: f32, imag: f32) -> Complex { println!("Constructing complex number: {:}+{:}i", real, imag); Complex{real:real, imag:imag} } }
Nyní si můžeme vytvořit novou proměnnou typu Complex, deklarovat ukazatel na tuto datovou strukturu a naplnit ho. Pro zajímavost bude struktura měnitelná a tudíž si můžeme dovolit vytvořit i ukazatel typu *mut:
let mut value: Complex = Complex::new(1.0, 2.0); let pointer: *mut Complex; pointer = &mut value;
Změna složek datové struktury přímo (přes příslušnou proměnnou) je triviální:
value.real = 10.0; value.imag = 20.0;
Totéž lze provést nepřímo přes ukazatel. Závorky pro dereferenci jsou nutné:
unsafe { (*pointer).real = 20.0; (*pointer).imag = 40.0; }
Poznámka: kupodivu zde nelze použít operátor ->, což je škoda, protože mi připadá mnohem čitelnější.
Celý příklad, v němž se použije ukazatel na strukturu typu Complex, může vypadat takto:
#[derive(Debug)] struct Complex { real: f32, imag: f32, } impl Complex { fn new(real: f32, imag: f32) -> Complex { println!("Constructing complex number: {:}+{:}i", real, imag); Complex{real:real, imag:imag} } } fn main() { let mut value: Complex = Complex::new(1.0, 2.0); let pointer: *mut Complex; pointer = &mut value; println!("{:?}", value); unsafe { println!("{:?}", *pointer); } value.real = 10.0; value.imag = 20.0; println!("{:?}", value); unsafe { println!("{:?}", *pointer); } unsafe { (*pointer).real = 20.0; (*pointer).imag = 40.0; } println!("{:?}", value); unsafe { println!("{:?}", *pointer); } }
Všechny dnes popisované demonstrační příklady byly, ostatně podobně jako ve všech předchozích částech tohoto seriálu, uloženy do Git repositáře dostupného na adrese https://github.com/tisnik/presentations. Příklady si můžete v případě potřeby stáhnout i jednotlivě bez nutnosti klonovat celý repositář:
Příklad | Adresa |
---|---|
pointers01.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers01.rs |
pointers02.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers02.rs |
pointers02_inner_unsafe.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers02_inner_unsafe.rs |
pointers02_no_unsafe.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers02_no_unsafe.rs |
pointers03.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers03.rs |
pointers03_mut_all.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers03_mut_all.rs |
pointers03_mut_pointer.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers03_mut_pointer.rs |
pointers04.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers04.rs |
pointers04_immut_var.rs | https://github.com/tisnik/presentations/blob/master/rust/pointers/pointers04_immut_var.rs |
Další příklady byly připraveny pro navazující část tohoto seriálu, takže jen pro úplnost:
Pavel Tišnovský vystudoval VUT FIT a v současné době pracuje ve společnosti Red Hat, kde vyvíjí nástroje pro OpenShift.io.
Informace nejen ze světa Linuxu. ISSN 1212-8309
Copyright © 1998 – 2018 Internet Info, s.r.o. Všechna práva vyhrazena. Powered by Linux.
Při poskytování služeb nám pomáhají cookies. Používáním webu s tím vyjadřujete souhlas.