Zmienne i ich modyfikowalność
Tak jak wspomniano w rozdziale „Storing Values with Variables” , zmienne są domyślnie niemodyfikowalne (niemutowalne, ang. immutable). To jeden z wielu prztyczków, którymi Rust zachęca cię do tworzenia kodu w pełni wykorzystującego mechanizmy bezpieczeństwa i prostoty współbieżności, które oferuje ten język programowania. Jednakże nadal możesz uczynić zmienne modyfikowalnymi. Przyjrzyjmy się bliżej temu, jak i dlaczego Rust zachęca cię do preferowania niemodyfikowalności zmiennych oraz czemu czasem możesz chcieć zrezygnować z tej właściwości.
Gdy zmienna jest niemodyfikowalna, po przypisaniu wartości do danej nazwy, nie można później zmienić tej wartości. Aby to zobrazować, utworzymy nowy projekt o nazwie variables w folderze projects korzystając z komendy
cargo new --bin variables
.
Następnie w nowo utworzonym folderze variables, odnajdź i otwórz src/main.rs, zmień kod w tym pliku na poniższy, który jednak jeszcze nie skompiluje się poprawnie:
Plik: src/main.rs
fn main() {
let x = 5;
println!("Wartość x wynosi: {x}");
x = 6;
println!("Wartość x wynosi: {x}");
}
Zapiszmy zmiany i uruchommy program, używając cargo run
. Powinniśmy otrzymać następujący komunikat o błędzie związanym z niemutowalnością:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3 | println!("Wartość x wynosi: {x}");
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
For more information about this error, try `rustc --explain E0384`.
error: could not compile `variables` due to previous error
Ten przykład pokazuje, jak kompilator pomaga ci odnajdywać błędy w twoich programach. Mimo że błędy kompilacji mogą być denerwujące, świadczą jedynie o tym, że twój program jeszcze nie działa prawidłowo. Nie wykonuje w bezpieczny sposób tego, co chcesz, by robił. Tw błędy nie oznaczają jednak, że nie jesteś dobrym programistą! Nawet doświadczeni Rustowcy nadal napotykają błędy podczas kompilacji.
Otrzymany komunikat błędu `` cannot assign twice to immutable variable x``` oznacza, że nie można dwukrotnie przypisać wartości do niemodyfikowalnej zmiennej
x`.
Pierwotnie nadanej wartości nie można zmienić.
To ważne, że napotykamy błędy w trakcie kompilacji, gdy próbujemy zmienić wartość, którą wcześniej określiliśmy jako niemodyfikowalną, gdyż takie działanie może prowadzić do podatności i błędów w programie. Jeżeli pierwsza część kodu opiera się na założeniu, że dana wartość nigdy nie ulegnie zmianie, a inna część kodu zmienia tę wartość, pierwsza część kodu może przestać wykonywać swoje zadanie poprawnie. Przyczyna tego rodzaju błędów może być trudna do zidentyfikowania po wystąpieniu, szczególnie gdy druga część kodu zmienia daną wartość tylko czasami.
W Ruście, kompilator gwarantuje, że jeżeli ustawimy wartość na niemodyfikowalną, to naprawdę nigdy nie ulegnie zmianie. Oznacza to, że czytając i pisząc kod, nie musisz ciągle sprawdzać gdzie i jak wartość może się zmienić. W związku z tym tworzony przez ciebie kod staje się łatwiejszy do zrozumienia.
Jednak modyfikowalność może być też bardzo użyteczna. Zmienne są tylko domyślnie niemodyfikowalne. Można uczynić
je modyfikowalnymi, dodając mut
przed nazwą zmiennej. Poza tym, że dzięki dodaniu mut
możliwa jest modyfikacja wartości zmiennej, jest ono też wyraźnym sygnałem dla osób, które będą czytały kod w przyszłości. Informuje, że inne
części kodu będą modyfikować wartość danej zmiennej.
Na przykład, zmieńmy kod w src/main.rs na poniższy:
Plik: src/main.rs
fn main() { let mut x = 5; println!("Wartość x wynosi: {x}"); x = 6; println!("Wartość x wynosi: {x}"); }
Gdy teraz uruchomimy program, otrzymamy:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
Wartość x wynosi: 5
Wartość x wynosi: 6
Możemy zmienić wartość, do której odwołuje się x
z 5
na 6
, dzięki wykorzystaniu mut
. W niektórych przypadkach będziesz chciał uczynić zmienną modyfikowalną, ponieważ sprawi to, że pisanie kodu stanie się wygodniejsze niż
gdyby tworzono go tylko z użyciem niemodyfikowalnych zmiennych.
Stałe
Brak możliwości modyfikacji wartości zmiennej może przypominać ci inne rozwiązanie programistyczne, które wykorzystuje wiele języków programowania: stałe (constants). Podobnie jak zmienne niemodyfikowalne, stałe to wartości, których nie można zmienić, przypisane do nazw, ale występuje też kilka różnic między stałymi i zmiennymi.
Po pierwsze, nie możesz używać mut
do stałych. Stałe są nie tylko domyślnie niemodyfikowalne. One są zawsze niemodyfikowalne.
Do deklaracji stałej wykorzystujemy słowo kluczowe const
zamiast let
i zawsze musimy określić typ wartości. Typy danych i ich adnotacje omówimy już niedługo, w następnym podrozdziale "Typy Danych",
więc nie przejmuj się na razie szczegółami. Po prostu zapamiętaj, że zawsze musisz nadać stałej typ danych.
Stałe mogą być deklarowane w każdym zasięgu, włączając w to zasięg globalny, dzięki czemu są bardzo użyteczne w przypadku wartości, z których korzysta wiele części kodu.
Ostatnia różnica to, że stałym można nadać wartości tylko za pomocą stałych wyrażeń, a nie takich obliczanych dopiero trakcie działania programu.
Oto przykład deklaracji stałej:
#![allow(unused)] fn main() { const TRZY_GODZINY_W_SEKUNDACH: u32 = 60 * 60 * 3; }
Nazwą stałej jest TRZY_GODZINY_W_SEKUNDACH
, zaś jej wartością jest iloczyn: 60 (liczba sekund w minucie), kolejnej 60 (liczba minut w godzienie) i 3 (liczba godzin którą chcemy odliczyć w programie). Konwencja nazewnicza Rusta dla stałych
zobowiązuje do wykorzystywanie tylko dużych liter z podkreśleniami między słowami.
The compiler is able to evaluate a limited set of operations at compile time, which lets us choose to write out this value in a way that’s easier to understand and verify, rather than setting this constant to the value 10,800. See the Rust Reference’s section on constant evaluation for more information on what operations can be used when declaring constants.
Stałe są dostępne przez cały okres działania programu w zasięgu, w którym zostały zadeklarowane, stają się tym samym dobrym wyborem dla wartości w twojej domenie aplikacji, które mogą być wykorzystywane przez różne elementy programu, takich jak maksymalna liczba punktów, które może uzyskać gracz, czy też prędkość światła.
Nazywanie predefiniowanych wartości używanych przez twój program stałymi jest użyteczne w przekazywaniu znaczenia wykorzystywanych wartości dla przyszłych współtwórców kodu. Pomaga to w utrzymaniu predefiniowanych wartości w jednym miejscu i ułatwia ich późniejsze uaktualnianie.
Przesłanianie
Jak widzieliśmy w poradniku do gry zgadywanki w rozdziale 2, można zadeklarować nową zmienną o takiej samej nazwie, jak miała dawna zmienna, i ta nowa zmienna przesłania dawną zmienną. Rustowcy mówią, że pierwsza zmienna jest przesłoniona przez drugą. I właśnie tą nową zmienną użyje kompilator w miejscach wystąpienia jej nazwy, aż do czasu gdy i ona nie zostanie przesłonięta, albo nie skończy się zasięg jej życia.
Możemy przesłonić zmienną poprzez wykorzystanie tej
samej nazwy zmiennej i ponowne użycie słowa kluczowego let
, tak jak poniżej:
Plik: src/main.rs
fn main() { let x = 5; let x = x + 1; { let x = x * 2; println!("Wartość x w wewnętrznym bloku kodu wynosi: {x}"); } println!("Wartość x wynosi: {x}"); }
Ten program najpierw deklaruje zmienną x
o wartość 5
. Następnie tworzy nową zmienną x
powtarzając let x =
, pobiera oryginalną wartość zmiennej i dodaje do niej 1
w wyniku czego wartość x
to obecnie 6
. Użycie deklaracji let
po raz trzeci również przesłania x
i tworzy kolejną zmienną, której wartość jest ustalona poprzez przemnożenie poprzedniej wartość przez 2
, czyli na 12
. Gdy
uruchomimy ten program, otrzymamy:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.31s
Running `target/debug/variables`
Wartość x w wewnętrznym bloku kodu wynosi: 12
Wartość x wynosi: 6
To nie to samo, co nadanie mut
zmiennej, gdyż jeżeli przypadkowo spróbujemy ponownie przypisać wartość do zmiennej, nie wykorzystując słowa kluczowego let
otrzymamy błąd kompilacji. Dzięki użyciu let
, możemy przeprowadzić
kilka transformacji na wartości, pozostawiając przy tym zmienną niemodyfikowalną.
Inna różnica między mut
i przesłanianiem to, że za każdym razem, gdy używamy słowa kluczowego let
, tworzymy nową zmienną, co oznacza, że możemy wybrać inny typ danych, ale ponownie użyć tej samej nazwy zmiennej. Na przykład, powiedzmy, że nasz program prosi użytkownika o pokazanie ilości spacji, jaka ma zostać umieszczona między jakimś tekstem, poprzez wpisanie tych spacji, ale my tak naprawdę chcemy przechowywać tę wartość jako liczbę:
fn main() { let spaces = " "; let spaces = spaces.len(); }
Powyższa konstrukcja jest dozwolona, gdyż pierwsza zmienna spaces
typu string, to zupełnie inna zmienna niż druga zmienna spaces
typu numerycznego. Dzięki przesłanianiu nie musimy wykorzystywać dwóch różnych nazw np. spaces_str
i spaces_num
. Zamiast tego, ponownie korzystamy z prostszej nazwy spaces
. Jednak, jeżeli spróbowalibyśmy użyć
mut
dla tej zmiennej otrzymalibyśmy błąd kompilacji:
fn main() {
let mut spaces = " ";
spaces = spaces.len();
}
Błąd mówi o tym, że nie możemy zmodyfikować typu zmiennej:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.
error: could not compile `variables` due to previous error
Teraz gdy poznaliśmy już działanie zmiennych, przyjrzyjmy się bliżej typom danych, jakich mogą być zmienne.