Aktywne Wpisy
konradpra +686
symetrysta +288
Tym czasem: 98% mężczyzn pracuje mimo przeziębienia, lub grypy, lub każdej innej choroby, bo albo nie dostanie zwolnienia od lekarza, albo po prostu go nie potrzebuje. I potem jest płacz, że jak to ktoś woli zatrudnić/awansować mężczyznę, tylko dlatego, że jest mężczyzną.
#pieklomezczyzn #logikarozowychpaskow
#pieklomezczyzn #logikarozowychpaskow
Część 2. Ale, że jak bez Gówno Czujki?
W poprzedniej części dałem krótki opis czym Rust wyróżnia się na tle innych języków. Dzisiaj postaram się bardziej szczegółowo omówić system typów oraz powiązany z nim lifetime.
Rozdział 1. Typy podstawowe
Rust posiada parę typów, które są wbudowane i podstawowe (co chyba oczywiste):
+ typy całkowite ze znakiem:
i8
,i16
,i32
ii64
+ typy całkowite bez znaku:
u8
,u16
,u32
iu64
+ typy zmiennoprzecinkowe:
f32
,f64
+ ciąg znaków (jest on tzw. slice, o czym później):
&str
Co się rzuca w oczy to brak typy przechowującego znaki (
char
). Jest to związane z tym, że Rust uznaje wszystkie ciągi znaków za kodowane w UTF-8 [1], więc "typ znakowy" nie miał by sensu, jako, że znaki w UTF-8 mogą mieć różną długość. Zamiast tego, jako "surowy ciąg znaków" traktowany jest typu8
.Oprócz tego istnieje jeszcze "placeholder", który mówi kompilatorowi, że sami nie wiemy co tam ma się znaleźć i sam ma się domyślić, przykładowo:
let a: = 10;
\ to to samo co
let a = 10;
przydaje on się czasem gdy chcemy częściowo kreślić typ zwracanej wartości, przykładowo:
// to jest paskudne i niepolecane
let vec = vec![1,2,3].intoiter().map(|x| x * x).collect::>();
// zamiast można napisać
let vec: Vec<> = vec![1,2,3].intoiter().map(|x| x * x).collect();
Rozdział 2. Referencje
Zaczniemy od rozwiązania poprzedniej zagadki. Przypomnę tylko treść:
Czy dany kod w C++ jest poprawny:
#include
#include
#include
using namespace std;
int main() {
vector vec;
vec.pushback("Lol");
string& ref = vec[0];
vec.pushback("really?");
cout << ref;
return 0;
}
Jak można zobaczyć na Ideone dostajemy błąd wykonania. Co? Jak? Gdzie? Czemu? Ano z tego powodu, że
std::vector
to tak naprawdę otoczka wokół zwykłej dynamicznej tablicy. W momencie gdy dodajemy nowy element mogą się zdarzyć 2 rzeczy (zgodnie ze standardem):+ biblioteka standardowa "przewidziała" dodatkowe miejsce na nowe elementy, więc jedyne co trzeba zrobić to wpisać element na kolejnej pozycji i zwiększyć wartość
size
+ tablica jest za mała, co wymaga utworzenia nowej tablicy, przepisania wartości z poprzedniej tablicy i usunięcia starej tablicy z pamięci
W związku z 2. przypadkiem standard mówi, że wszystkie referencje do elementów wektora, w momencie zmiany jego wielkości, powinny zostać uznane za nieprawidłowe (mimo iż w teorii to nie musi być prawda, patrz pkt. 1).
Spójrzmy na identyczny przykład w Ruscie:
fn main() {
let mut vec = Vec::new();
vec.push("Lol");
let ref reference = vec[0];
vec.push("really?");
println!("{}", reference);
}
przy próbie kompilacji otrzymamy:
test.rs:8:5: 8:8 error: cannot borrow
vec
as mutable because it is also borrowed as immutabletest.rs:8 vec.push("really?");
^~~
test.rs:6:25: 6:28 note: previous borrow of
vec
occurs here; the immutable borrow prevents subsequent moves or mutable borrows ofvec
until the borrow endstest.rs:6 let ref reference = vec[0];
^~~
test.rs:11:2: 11:2 note: previous borrow ends here
test.rs:1 fn main() {
...
test.rs:11 }
^
error: aborting due to previous error
Jak widać kompilator poinformuje nas, że próbujemy zrobić coś dziwnego. Wszystko dzięki systemowi borrowing, który określa, czy możemy modyfikować daną zmienną.
By uzmysłowić sobie jak to działa wyobraźmy sobie zmienną jako naszą książkę. Możemy z takową książką zrobić 3 rzeczy:
+ używać jej sami
+ pożyczyć komuś (przekazać referencję)
+ oddać komuś (przenieść własność)
Wszystko to ma na celu zapobiegnięciu powstawania "zwisających" wskaźników.
Kolejny przykład:
fn main() {
// początek lifetime 'a
let mut a = &10;
println!("a = {}", a);
{
// początek lifetime 'b
let b = 20;
println!("a = {}\nb = {}", a, b);
// a = &b // nie możemy "pożyczyć" książki, której właściciel będzie żył krócej niż my
// koniec lifetime 'b
}
println!("a = {}", a);
// koniec lifetime 'a
}
Tutaj mamy zobrazowane jak kompilator rozpoznaje czas życia poszczególnych zmiennych i jak decyduje, czy dana zmienna może odnosić się do do innej.
Część 3. Przenoszenie własności
Dodatkowym systemem jest zabezpieczenie przed podwójnym usunięciem elementów jest system przenoszenia własności. By zobrazować jak to może być uciążliwe spójrzmy na przykład (C++):
class Foo {
int* ptr;
public:
Foo() {
ptr = new int;
}
Foo(Foo&) = default;
~Foo() {
delete ptr;
}
}
Widać tutaj błąd? Jak nie, to zastanów się, co się stanie jak przekażemy tą klasę przez wartość.
Rust przeciwdziała takowemu działaniu poprzez system "własności" (ownership). Przykładowo:
let a = "lol".tostring(); // "owned" string
let b = a;
println!("{}", a); // błąd,
a
nie jest już właścicielem tego ciągu znakówTo samo w przypadku funkcji:
fn foo(string: String) {
println!("{}", string);
// wewnętrzna reprezentacja
string
jest tutaj niszczona}
let a = "lol".tostring();
foo(a); // przenosimy własność
println!("{}", a); // nie możemy już użyć zmiennej
a
jako, że nie jest właścicielem wartościBy zachować własność, musimy "zwrócić" wartość, którą otrzymaliśmy (i tym samym przedłużyć czas jej życia):
fn foo(string: String) -> String { println!("{}", string); string }
let a = "lol".tostring();
let a = foo(a);
println!("{}", a);
Oczywiście nie wszystkie typy wymagają takiego bezpieczeństwa, specjalnie dla nich są zaimplementowane 2
trait
sy:+
Clone
- implementuje metodęClone::clone(&self)
, która pozwala na utworzenie głębokiej kopii obiektu+
Copy
- typ, który można skopiować poprzez użyciememcpy
Oprócz tego mamy jeszcze parę struktur, które ułatwiają pracę z pamięcią:
+
Box
- owned pointer, odpowiednik "czystych wskaźników" z C/C++, użyteczne np. przy implementacji list+
String
- owned string+
Rc
- reference counted pointer (trochę kłamałem, że nie ma GC :P), prosty wskaźnik zarządzany przez GC zliczające referencje+
Arc
- j.w. z tym wyjątkiem, że zliczanie jest atomowe, co jest przydatne przy wskaźnikach przekazywanych między wątkami+
Cell
- wskaźnik na mutowalny obszar pamięci+ i jeszcze parę innych
Trochę tego jest, ale większość ma swoje ścisłe zastosowania i nie używa się ich za często. W celu zapoznania się z różnicami zalecam przejrzeć dokumentację.
Część 4. Tablice, slices [2] i iteratory
Tablice w Ruscie są praktycznie idealnym odpowiednikiem tablic z C z tą różnicą, że są one zawsze inicjalizowane wartością domyślną (jak wszystkie zmienne w Ruscie) oraz w czasie wykonania jest sprawdzany zakres (nie da się odwołać do wartości spoza tablicy). Dodatkowo Rust sprawdza rozmiar tablic przekazywanych jako argument do funkcji, i. e.:
fn foo(arr: [u32; 8]) { … }
foo([1,2,3,4,5,6,7,8]);
// foo([1,2,3]); // zwróci błąd - niewłaściwy typ
Slices z kolei mogą być traktowane jako wskaźniki na pierwszy element tablicy. Slices pozwalają nam na przekazywanie tablic o nieznanej w czasie kompilacji długości lub fragmentów większych tablic.
Iteratory są raczej znanym konceptem z innych języków, więc nie będę się rozpisywał do czego służą a skupię się na ich stosowaniu.
Po pierwsze w Ruscie (tak jak, np. w Pythonie czy Ruby) jest tylko pętla
for … in …
, której zastosowanie wygląda tak:let vec = vec![0, 1, 2, 3, 4];
for i in vec {
println!("{}", i);
}
i znaczy tyle samo co:
for i in vec.intoiter() { … }
Do iterowania po zakresie służy operator zakresu (zerżnięty z Rubiego):
for i in 1..10 { … } // IMHO dość czytelnie
W ogólnym założeniu wektor ma 2 różne iteratory:
-
iter
-
into_iter
Pierwszy iterator iteruje po elementach wektora i zwraca referencję do poszczególnych elementów, natomiast
into_iter
jest iteratorem "konsumującym", który przekazuje elementy wektora przez wartość i "niszczy" nasz wcześniejszy wektor. Jak zwykle, więcej info i przykłady w dokumentacji (link wyżej).Oprócz oczywistej rzeczy - iteracji, iteratory implementują niektóre z high-order functions jak
map
czyfold
(przez traitIteratorExt
). Więcej o funkcyjności dam w innej części cyklu (jak ktoś chce przykład już teraz, to jest on na początku tej części).Epilog
To by były podstawy systemu typów w Ruscie. Nie jest on prosty, ale po przyzwyczajeniu się i ogarnięciu podstawowych zasad nim rządzących staje się on w miarę prosty i zrozumiały. Dodatkowo kompilator stara nam się wytłumaczyć co i dla czego w dość przystępny sposób, więc to nie jest zbytni problem.
Happy hacking!
PS
Ktoś stworzył całkiem przystępną formę określania co jest czym. Dla zainteresowanych: The Periodic Table of Rust Types
[1] - Jak na razie. Jest RFC, którego celem jest ustandaryzowanie tego zachowania między platformami.
[2] - Ma ktoś pomysł jak to można przetłumaczyć?