Програмски језик Раст

Базирано на верзији 1.65.0. У изради.

Проф. др Игор Дејановић (igord at uns ac rs)

Креирано 2023-01-16 Mon 18:17, притисни ESC за мапу, "м" за мени, Ctrl+Shift+F за претрагу

1. Увод

  • Језик опште намене, компајлиран и статички типизиран са инференцом типова
  • Системско програмирање али са особинама вишег нивоа апстракције као што су функционално програмирање
  • 2010, Graydon Hoare, Mozilla Research
  • Перформансе и сигурност
  • Не користи garbage collector али обезбеђује меморијску сигурност кроз borrow checker
  • Синтаксно сличан C++. Утицај и OCaml-а, Haskell-а и Erlang-а.
  • Користи се у великим фирмама: Amazon, Facebook, Google, Microsoft…
  • Више година за редом на SO упитнику први у категорији "most loved programming languages"

2. Инсталација и подешавање

2.1. Инсталација

  • Раст стиже са алатом за управљање ланцем алата (енг. toolchain) – rustup
$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
  • Провера инсталације:
~> rustc --version
rustc 1.65.0 (897e37553 2022-11-02)

2.2. Преглед инсталације

~> rustup show
Default host: x86_64-unknown-linux-gnu
rustup home:  /home/igor/.rustup

installed toolchains
--------------------

stable-x86_64-unknown-linux-gnu (default)
nightly-x86_64-unknown-linux-gnu

active toolchain
----------------

stable-x86_64-unknown-linux-gnu (default)
rustc 1.58.1 (db9d1b20b 2022-01-20)

2.3. Ажурирање

За ажурирање инсталације на најновију верзију:

rustup update

2.4. Документација

Раст стиже са веома добром документацијом и књигама које су доступне директно из инсталације:

rustup doc

3. Почетак

3.1. Hello, World!

$ mkdir hello_world
$ cd hello_world

File main.rs:

fn main() {
    println!("Hello, world!");
}

3.2. Компајлирање и покретање

$ rustc main.rs
$ ./main
Hello, world!

3.3. Hello, Cargo!

  • Алат за разрешавање зависности и управљање пројектом.
~> cargo --version
cargo 1.65.0 (4bc8f24d3 2022-10-20)
  • Креирање пројекта са cargo алатом:
$ cargo new hello_cargo
$ cd hello_cargo

3.4. Садржај креираног пројекта

Фајл cargo.toml чува конфигурацију пројекта. Формат је TOML (Tom’s Obvious, Minimal Language):

[package]
name = "hello_cargo"
version = "0.1.0"
edition = "2021"

[dependencies]

Fajl src/main.rs:

fn main() {
    println!("Hello, world!");
}

3.5. Покретање

$ cargo build
   Compiling hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 2.85 secs

Извршни фајл се може наћи на локацији target/debug/hello_cargo:

$ ./target/debug/hello_cargo
Hello, world!

Али cargo омогућава и једноставнији начин покретања:

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
     Running `target/debug/hello_cargo`
Hello, world!

3.6. Провера

Такође је могуће брзо проверити да ли се код компајлира:

$ cargo check
   Checking hello_cargo v0.1.0 (file:///projects/hello_cargo)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32 secs

3.7. Изградња финалне верзије

  • У току развоја користимо изградњу за дебаговање која се брже заврши али генерисани извршни код није оптималан.
  • За финалну верзију је потребно изградњу обавити на следећи начин:

        cargo build --release
    
  • Ово ће обавити додатне оптимизације које ће дуже трајати али ће крајњи код бити оптимизован.

3.8. cargo као конвенција

Практично сви Раст пројекти користе cargo тако да је унификован начин изградње пројеката. Углавном се своди на:

$ git clone example.org/someproject
$ cd someproject
$ cargo build

4. Игра погађања бројева

4.1. Подешавање пројекта

$ cargo new guessing_game
$ cd guessing_game

Фајл Cargo.toml:

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

[dependencies]

Фајл src/main.rs:

fn main() {
    println!("Hello, world!");
}
$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Hello, world!

4.2. Преузимање броја са стандардног улаза

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

4.3. Променљивост (mutability)

  • Варијабле су подразумевано непромењиве (immutable).
  • Уколико желимо варијаблу чија вредност може да се мења користимо кључну реч mut.

      let apples = 5; // immutable
      let mut bananas = 5; // mutable
    
  • Исто важи и за параметре функција:

      io::stdin()
          .read_line(&mut guess)
    

4.4. Обрада могућих грешака употребом Result типа

read_line може да заврши неуспешно. Зато враћа io::Result тип који представља тип енумерације (enum) и има две могуће вредности: Ok и Err.

io::stdin()
    .read_line(&mut guess)
    .expect("Failed to read line");

expect имплементиран на Ok варијанти ће вратити вредност која је садржана унутар варијанте док имплементација над Err варијанти прекида извршавање програма.

4.5. Покретање

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

4.6. Генерисање случајног броја

Користимо rand пакет (сандук - crate у терминологији Cargo-a).

Секција [dependencies] у фајлу Cargo.toml:

[dependencies]
rand = "0.8.3"
$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.3
  Downloaded libc v0.2.86
  ...
   Compiling rand_chacha v0.3.0
   Compiling rand v0.8.3
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

Cargo користи Semantic Versioning. Раст пакети се преузимају са сајта crates.io и кеширају локално.

4.7. Поновљивост изградње - Cargo.lock

  • Свако следеће покретање изградње користи исте верзије.
  • Први пут када се покрене cargo build креира се фајл Cargo.lock са информацијама о верзијама свих сандука који су инсталирани.
  • Cargo.lock је потребно чувати у систему контроле верзија (нпр. git) да би се осигурала поновљивост.

4.8. Ажурирање сандука

  • Ажурирање на нове верзије сандука се обавља са:

      $ cargo update
        Updating crates.io index
        Updating rand v0.8.3 -> v0.8.4
    
  • Поштује се семантичко верзионирање тј. аутоматски се ажурира на следећу верзију која је мања од наредне главне (major) верзије.
  • Ако прелазимо на нову главну верзију то морамо урадити изменом верзије у Cargo.toml фајлу.

4.9. Генерисање случајног броја

Фајл src/main.rs:

use std::io;
use rand::Rng;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..101);

    println!("The secret number is: {}", secret_number);

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    println!("You guessed: {}", guess);
}

У gen_range користимо израз опсега (range expression). Интервал је затворен на доњој граници и отворен на горњој. За интервал затворен и од горе можемо писати 1..=100.

4.10. Документација за локалне сандуке

Да би знали које методе и функције су нам доступне можемо користити уграђену документацију за сандуке пројекта. Документацију добијамо са:

cargo doc --open

4.11. Покретање програма

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

4.12. Поређење тајног броја са задатим

Фајл src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    // --snip--

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}
  • Користимо std::cmp::Ordering енумерисани тип који има варијанте Less, Greater и Equal
  • match израз пореди задату вредност са вредностима задатим у телу и извршава грану која се подудара. Гране match израза се у Раст терминологији зову "руке" (arms).

4.13. Поправка типова

Код са претходног слајда није исправан:

$ cargo build
   Compiling libc v0.2.86
   ...
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

error[E0283]: type annotations needed for `{integer}`
   --> src/main.rs:8:44
    |
8   |     let secret_number = rand::thread_rng().gen_range(1..101);
    |         -------------                      ^^^^^^^^^ cannot infer type for type `{integer}`
    |         |
    |         consider giving `secret_number` a type
    |
    = note: multiple `impl`s satisfying `{integer}: SampleUniform` found in the `rand` crate:
            - impl SampleUniform for i128;
            - impl SampleUniform for i16;
            - impl SampleUniform for i32;
            - impl SampleUniform for i64;
            and 8 more
...

Основа грешке је неслагање типова. Са улаза прихватамо String док нам је тајни број integer.

4.14. Конверзија стринга у број

Проблем решавамо конверзијом стринга са улаза у бројни тип.

// --snip--

    let mut guess = String::new();

    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = guess.trim().parse().expect("Please type a number!");

    println!("You guessed: {}", guess);

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }

Сада се програм компајлира.

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

4.15. Омогућавање вишеструког погађања - употреба петље

Фајл src/main.rs:

// --snip--

    println!("The secret number is: {}", secret_number);

    loop {
        println!("Please input your guess.");

        // --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

4.16. Прекид рада

Проблем је како прекинути програм када корисник погоди број?

// --snip--

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

4.17. Руковање неисправним улазом

// --snip--

        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {}", guess);

        // --snip--

Потребно је још обрисати линију која приказује генерисани број.

4.18. Финални код

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");
    let secret_number = rand::thread_rng().gen_range(1..101);

    loop {
        println!("Please input your guess.");
        let mut guess = String::new();
        io::stdin()
            .read_line(&mut guess)
            .expect("Failed to read line");
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };
        println!("You guessed: {}", guess);
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

5. Основни програмски концепти

5.1. Варијабле и променљивост (mutability)

5.1.1. Варијабле и променљивост (mutability)

  • Варијабле су подразумевано непромењиве.
  • Једном када добију вредност (binding) та вредност се не може променити

        fn main() {
            let x = 5;
            println!("The value of x is: {}", x);
            x = 6;     // greška
            println!("The value of x is: {}", x);
        }
    
    $ 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: consider making this binding mutable: `mut x`
    3 |     println!("The value of x is: {}", 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

5.1.2. mut кључна реч

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

5.2. Константе - const

  • Слично као непромењиве варијабле са следећим разликама:
    • Увек су непромењиве
    • Могу се иницијализовати само константним изразом (познатим у време компајлирања)
    • Валидне за целокупно време извршавања програма у опсегу важења где су дефинисане (scope)
    • Мора се експлицитно дефинисати тип
  • Компајлер ће константе "убацити" на месту употребе
  • По конвенцији имена константи се пишу великим словима

        const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;
    

5.3. Типови података

5.3.1. Типови података

  • Свака вредност у Расту има тип. Типови морају бити познати у време компајлирања (статички типизиран језик).
  • Компајлер ће пробати да одреди типове (type inference). Ако није могуће захтева се да дефинишемо тип експлицитно.

    Нпр:

        let guess: u32 = "42".parse().expect("Not a number!");
    

    Овде није могуће одредити тип јер str::parse функција може вратити различите бројне типове (функција је генеричка) а не постоји начин да се тип аутоматски одреди.

    Сигнатура је:

        pub fn parse<F>(&self) -> Result<F, <F as FromStr>::Err>
        where
            F: FromStr
    

5.3.2. Integer типови

Дужина Signed Unsigned
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

5.3.3. Литерали бројева

Литерали Примери
Decimal 98_222
Hex 0xff
Octal 0o77
Binary 0b1111_0000
Byte (u8 only) b'A'

5.3.4. Floating-Point типови

fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

IEEE-754 стандард

5.3.5. Операције над бројевима

fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let difference = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let floored = 2 / 3; // Results in 0

    // remainder
    let remainder = 43 % 5;
}

5.3.6. Boolean тип

fn main() {
    let t = true;

    let f: bool = false; // with explicit type annotation
}

5.3.7. Тип карактера

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let heart_eyed_cat = '😻';
}

5.3.8. Торке (tuples)

fn main() {
    let tup: (i32, f64, u8) = (500, 6.4, 1);
}
  • Распакивање торки (destructuring):
fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

    println!("The value of y is: {}", y);
}

Приступ елементима торке:

fn main() {
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let five_hundred = x.0;

    let six_point_four = x.1;

    let one = x.2;
}

5.3.9. Низовни тип

  • Сваки елемент низовног типа мора имати исти тип
  • Димензија низа је непромењива (алоциран је на стеку)

        fn main() {
            let a = [1, 2, 3, 4, 5];
        }
    
  • Тип се може експлицитно дефинисати на следећи начин (низ дужине 5 типа i32):

        let a: [i32; 5] = [1, 2, 3, 4, 5];
    
  • Иницијализација свих елемената на исту вредност се обавља на следећи начин:

      let a = [3; 5];
    

    Где је вредност сваког елемента 3 а дужина низа 5.

Индексни приступ:

fn main() {
    let a = [1, 2, 3, 4, 5];

    let first = a[0];
    let second = a[1];
}

5.4. Функције

5.4.1. Функције

fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function() {
    println!("Another function.");
}
  • За именовање функција као и варијабли користи се snake_case.

5.4.2. Funkcije - parametri

fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {}{}", value, unit_label);
}

5.4.3. Искази и изрази

  • Рaст је језик базиран на изразима (expression-based)
  • Искази (statements) су језичке конструкције које немају повратну вредност. Изрази (expressions) се евалуирају у одређени резултат, тј. имају вредност.
  • Пример: let је исказ тј. нема повратну вредност. Ово можете писати:

      let y = 6;
    

    Ali ovo ne:

      let x = (let y = 6);
    

Блок кода је такође израз. Шта је вредност у коју се евалуира?

{
    let x = 3;
    x + 1
}

Вредност блока је вредност последњег израза, тј. x+1. Приметите да ту не користимо ; јер терминација овим карактером претвара израз у исказ.

Због овога је сасвим легално да пишемо:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

5.4.4. Повратне вредности функција

Вредност функције је вредност блока који представља тело функције, дакле последњег израза унутар тела функције.

fn five() -> i32 {
    5
}

fn main() {
    let x = five();

    println!("The value of x is: {}", x);
}

Или на пример:

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Уколико израз x+1 терминирамо са ; код се неће компајлирати јер функција декларише да враћа тип i32 док сада враћа () (тзв. unit type) односно нема повратну вредност јер је последња инструкција исказ.

fn main() {
    let x = plus_one(5);

    println!("The value of x is: {}", x);
}

fn plus_one(x: i32) -> i32 {
    x + 1;
}
error[E0308]: mismatched types
 --> src/main.rs:8:24
  |
8 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
9 |     x + 1;
  |          - help: consider removing this semicolon

For more information about this error, try `rustc --explain E0308`.
error: could not compile `cargo4UyKF0` due to previous error

5.5. Коментари

Коментари се пишу после // или у форми блок коментара /*.... */ као и у C++-у. Ово би били валидни коментари:

// So we’re doing something complicated here, long enough that we need
// multiple lines of comments to do it! Whew! Hopefully, this comment will
// explain what’s going on.

fn main() {
    let lucky_number = 7; // I’m feeling lucky today
}
...
fn main() {
    // I’m feeling lucky today
    let lucky_number = 7;
    /* this is block comment
       which can span multiple lines.
       Nesting is allowed.
     */
}

Постоје и коментари за документацију који представљају посебну синтаксу за коментаре који су део API документације. Они се пишу после ///. На пример:

/// Adds one to the number given.
///
/// # Examples
///
/// ```
/// let arg = 5;
/// let answer = my_crate::add_one(arg);
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
    x + 1
}

5.6. Контрола тока

5.6.1. if изрази

fn main() {
    let number = 3;

    if number < 5 {
        println!("condition was true");
    } else {
        println!("condition was false");
    }
}

5.6.2. if-else

fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

5.6.3. if у let исказима

if је израз па се може користити где год можемо писати и било који други израз.

fn main() {
    let condition = true;
    let number = if condition { 5 } else { 6 };

    println!("The value of number is: {}", number);
}

Али се гране морају слагати по типу. Ово је погрешно јер је прва грана типа i32 док је else грана типа str.

fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

    println!("The value of number is: {}", number);
}

5.6.4. Петље

Рaст има три типа петље:

  • loop - за бесконачне петље
  • while - условна петља
  • for - петља за итерацију кроз елементе итерабилних типова

5.6.5. loop

fn main() {
    loop {
        println!("again!");
    }
}

5.6.6. loop лабеле

Уколико имамо угњеждене loop исказе можемо користити лабеле приликом изласка са break инструкцијом.

fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {}", count);
        let mut remaining = 10;

        loop {
            println!("remaining = {}", remaining);
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {}", count);
}

5.6.7. loop као израз

loop може имати повратну вредност. Повратна вредност се дефинише као параметар break инструкције.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is {}", result);
}

5.6.8. while

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

5.6.9. for

Итерацију кроз уређену колекцију, као што је низ, можемо обавити са while петљом.

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

Али је за ту намену природније и сигурније користити for петљу. Такође ће се програм брже извршавати.

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a {
        println!("the value is: {}", element);
    }
}

for петље су најчешћи облик петљи у употреби у Расту. Користе се нпр. и у ситуацији када је потребно извршити петљу одређени број пута.

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

6. Власништво и позајмљивање (Ownership and borrowing)

6.1. Власништво

6.1.1. Власништво

  • Један од најважнијих концепата језика.
  • Скуп правила који омогућавају управљање меморијом.
  • Гаранције за меморијску сигурност без употребе garbage collector
  • Све провере се обављају у време компајлирања - резултује одличним перформансама у време извршавања.

6.1.2. Стек и хип (Stack and Heap)

  • Стек - алокација простора за податке чија је величина позната у време компајлирања.
    • Бржа алокација и деалокација - једноставан механизам, LIFO структура.
    • Бржи приступ - локалне варијабле, кеширање приступа.
  • Хип - слободна алокација у време извршавања.
    • Спорија алокација и деалокација.
    • Спорији приступ - произвољна локација.

6.1.3. Правила власништва

  1. Свака вредност у Расту има варијаблу која се назива власником (owner).
  2. У сваком тренутку постоји само један власник.
  3. Када власник изађе из опсега важења (scope) вредност се деалоцира (drop).

6.1.4. Опсег важења варијабле (Variable Scope)

{                      // s није валидно овде јер још није декларисано
    let s = "hello";   // s је валидно од ове позиције

    // користимо s
}   // овде опсег престаје да важи и s више није валидно

6.1.5. String тип

  • Демонстрација власништва над типом који се алоцира на хипу.

      let mut s = String::from("hello");
      s.push_str(", world!"); // push_str() додаје литерал на стринг
      println!("{}", s); // hello, world!`
    
  • Меморија се алоцира са хипа у време извршавања.
  • Морамо вратити меморију алокатору када нам више није потребна.
  • Алокација стринга се обавља на линији:

      let mut s = String::from("hello");
    
  • Али деалокација је тежа:
    • Garbage collector
    • Memory waste
    • Double-free
  • Раст компајлер ће додати код који ради деалокацију када власник напусти опсег важења.
  • Позива се функција drop над типом и ова функција је задужена да обави деалокацију.

      {
            let s = String::from("hello"); // s постаје валидно
    
            // користимо s
      }  // <- s излази из опсега и позива се "drop"
    

6.1.6. Додела вредности

  • Копирање вредности x у y. Обе варијабле сада имају вредност 5.

      let x = 5;
      let y = x;
    
  • Али са String типом дешава се нешто друго.

      let s1 = String::from("hello");
    

string-type.png

  • Ако би се копирао само садржај са стека без имали бисмо следећу ситуацију (тзв. shallow copy).

      let s1 = String::from("hello");
      let s2 = s1;
    

string-type-2.png

Што је проблематично јер када и s1 и s2 напусте опсег покушаће се двострука деалокација исте меморије на хипу (double free).

Ако би се и хип меморија копирала (tzv. deep copy) имали бисмо валидну ситуацију али би таква операција била веома "скупа".

string-type-3.png

6.1.7. Премештање (Move)

Ако пробамо да компајлирамо следећи код:

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

Добићемо грешку:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

Оно што нам компајлер каже је да се у исказу:

let s2 = s1;

променио власник овог стринга. Нови власник је сада s2 док је варијабла s1 постала невалидна и није је више могуће користити.

Кажемо да се обавило "премештање" (move) вредности из s1 у s2.

string-type-4.png

Чиме се испуњава прво правило власништва и спречава double free грешка.

6.1.8. Клонирање

Последица претходног је да Раст никада неће аутоматски обавити дубоко копирање варијабле јер би то могло да изазове лоше перформансе.

Дубоко копирање (стек+хип) радимо са clone методом:

let s1 = String::from("hello");
let s2 = s1.clone();

println!("s1 = {}, s2 = {}", s1, s2);

Сада је лако пронаћи у коду сва места где се обавља потенцијално "скупа" операција дубоког копирања.

6.1.9. Copy типови

Како онда ради пример који смо видели претходно? Зашто не долази до премештања и инвалидације y варијабле?

let x = 5;
let y = x;

println!("x = {}, y = {}", x, y);
  • Код простих типова чија је величина позната у време компајлирања и који могу у целости стати на стек нема разлике између дубоког и плитког копирања.
  • Овакви типови су анотирани са Copy трејтом (Trait). Типови који су на овај начин анотирани не обављају премештање већ увек копирање.
  • Импликација је да се варијабла са десне стране доделе може користити и након доделе.
  • Раст ће спречити Copy анотацију ако тип имплементира и Drop јер то значи да ради неку специјалну алокацију па му је потребна и посебна деалокација што значи да мора да се ради премештање.

6.1.10. Власништво и функције

  • Семантика преноса параметара код позива функција је слична семантици доделе.
fn main() {
    let s = String::from("hello");  // s постаје валидно

    takes_ownership(s);             // s вредност се премешта у функцију...
                                    // ... тако да s није валидно од ове позиције

    let x = 5;                      // x постаје валидно

    makes_copy(x);                  // x би се преместило у функцију,
                                    // али i32 је Copy, тако да је ok
                                    // да се x користи и после

} // x излази из опсега, затим s. Али пошто је s премештено ништа посебно се
  // не дешава.

fn takes_ownership(some_string: String) { // some_string улази у опсег
    println!("{}", some_string);
} // some_string излази из опсега и позива се `drop`. Меморија са хипа се
  // ослобађа.

fn makes_copy(some_integer: i32) { // some_integer улази у опсег
    println!("{}", some_integer);
} // some_integer излази из опсега. Пошто није 'Drop', ништа посебно се не
  // догађа (осим "скидања" са стека наравно)

6.1.11. Повратне вредности и опсези

Приликом враћања вредности из функције такође може доћи до премештања власништва.

fn main() {
    let s1 = gives_ownership();         // gives_ownership премешта повратну
                                        // вредност у s1

    let s2 = String::from("hello");     // s2 постаје валидно

    let s3 = takes_and_gives_back(s2);  // s2 се премешта у функцију
                                        // takes_and_gives_back, која премешта
                                        // повратну вредност у s3
} // s3 излази из опсега и позива се `drop`. s2 је премештена па се ништа
  // не дешава. s1 такође излази из опсега и деалоцира се.

fn gives_ownership() -> String {             // gives_ownership ће преместити
                                             // своју повратну вредност у функцију
                                             // која је позива

    let some_string = String::from("yours"); // some_string постаје валидно

    some_string                              // some_string се премешта
                                             // у функцију позиваоца
}

// Ова функција узима власништво над стрингом и враћа га назад
fn takes_and_gives_back(a_string: String) -> String { // a_string постаје валидно

    a_string  // a_string се премешта у функцију позиваоца
}
  • Често нам је потребно да варијаблу користимо и после слања у функцију.
  • Могли би је стало враћати заједно са резултатом функције на пример употребом торки.
fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len();

    (s, length)
}

Али је то напорно. Постоји концепт у Раст који је намењен оваквим ситуацијама и базиран је на референцама и позајмљивању вредности.

6.2. Референце и позајмљивање (References and Borrowing)

6.2.1. Референце и позајмљивање (References and Borrowing)

  • Референца је попут поинтера, садржи адресу вредности коју поседује нека друга варијабла.
  • За разлику од поинтера, референце у Расту су гарантовано валидне.
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{}' is {}.", s1, len);
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

reference.png

6.2.2. Референцирање

  • Примена оператора & над варијаблом називамо референцирање.

      let s1 = String::from("hello");
      let len = calculate_length(&s1);
    
  • Синтакса &s1 нам омогућава да креирамо референцу на вредност чији власник је s1 без узимања власништва.
  • Пошто референца није власник не долази до деалокације приликом изласка из опсега.
  • Операција обрнута референцирању назива се дереференцирање и врши се * оператором над референцом (нпр. *s2 је вредност на коју референцира s2).
  • Операцију референцирања називамо позајмљивањем (borrowing).
  • Такође, параметар функције може бити референца.
fn calculate_length(s: &String) -> usize { // s је референца на String
    s.len()
} // s излази из опсега али пошто нема власништво над вредношћу
  // ништа се не дешава.

6.2.3. Промена позајмљене вредности

Шта се дешава уколико покушамо да модификујемо позајмљену вредност?

fn main() {
    let s = String::from("hello");
    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers
  to cannot be borrowed as mutable

For more information about this error, try `rustc --explain E0596`.
error: could not compile `ownership` due to previous error

6.2.4. Промењиве референце

Као и варијабле, и референце су подразумевано непромењива (immutable). Морамо бити експлицитни уколико нам треба промењива референца.

fn main() {
    let mut s = String::from("hello");
    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

6.2.5. Више промењивих референци над истом вредношћу

Раст не дозвољава да исти податак у једном тренутку има више промењивих референци.

let mut s = String::from("hello");

let r1 = &mut s;
let r2 = &mut s;

println!("{}, {}", r1, r2);
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

For more information about this error, try `rustc --explain E0499`.
error: could not compile `ownership` due to previous error

Овим ограничењем Раст, у време компајлирања, спречава класу грешака које доводе до недефинисаног понашања и које зовемо data races. Ове грешке се веома тешко откривају и отклањају и могу настати уколико су задовољени следећи услови:

  • Два или више поинтера приступају истим подацима у исто време,
  • Бар један поинтер се користи за измену податка,
  • Не постоји механизам за синхронизацију приступа.

Више промењивих референци можемо имати али не у истом опсегу:

let mut s = String::from("hello");

{
    let r1 = &mut s;
} // r1 овде излази из опсега тако да можемо краирати нове референце

let r2 = &mut s;

6.2.6. Комбинација промењивих и непромењивих референци

Слично правило постоји и уколико имамо комбинацију промењивих и непромењивих референци:

let mut s = String::from("hello");

let r1 = &s; // ovo je OK
let r2 = &s; // ovo je OK
let r3 = &mut s; // GREŠKA!

println!("{}, {}, and {}", r1, r2, r3);
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error
  • Не можемо истовремено имати непромењиве и промењиве референце јер корисници непромењивих референци не очекују да се подаци мењају.
  • Можемо имати више непромењивих референци јер нико не може да мења податке и тиме утиче на друге.

6.2.7. Опсег важења референце

Опсег важења референце почиње од места где је уведена па до њене последње употребе. На пример, ово је валидно:

let mut s = String::from("hello");

let r1 = &s; // OK
let r2 = &s; // OK
println!("{} and {}", r1, r2);
// варијабле r1 и r2 се не користе у наставку па њихов опсег
// престаје да важи.

let r3 = &mut s; // зато је ово OK
println!("{}", r3);

Ова особина референци се назива Non-Lexical Lifetimes (NLL).

6.2.8. "Висеће" референце

  • У језицима са поинтерима лако је креирати поинтер на део меморије који је деалоциран.
  • Раст гарантовано спречава овакве грешке. Референце су увек валидне у Расту.
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value
  for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                ~~~~~~~~

For more information about this error, try `rustc --explain E0106`.
error: could not compile `ownership` due to previous error
  • Механизам који ово спречава назива се "време живота" (lifetime) и биће детаљније обрађено у наставку.
  • У преводу Раст нам поручује:

    Тип повратне вредности ове функције је позајмљена вредност, али вредност која је позајмљена после повратка више не постоји.

6.2.9. Шта се тачно десило?

fn dangle() -> &String { // dangle враћа референцу на String

    let s = String::from("hello"); // s је нови String

    &s // Враћамо референцу на String s
} // s излази из опсега и позива се `drop`. Меморија се ослобађа.
  // ОПАСНОСТ! Враћена референца ће бити "висећа".

Како се може решити. Вратићемо поседовану (owned) вредност, тј урадићемо премештање вредности у функцију позиваоца.

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

6.2.10. Правила референцирања

  1. У сваком тренутку можемо имати или једну промењиву или произвољан број непромењивих референци.
  2. Референце увек морају бити валидне.

6.3. Исечци (Slice тип)

6.3.1. Slice

  • Исечци (slices) омогућавају референцирање континуалне секвенце унутар колекције уместо целе колекције

6.3.2. Пример

Функција која за задати стринг враћа прву реч.

fn first_word(s: &String) -> usize {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return i;
        }
    }

    s.len()
}
  • Дакле, враћамо индекс краја речи. Позиваоц сада има потребне информације да дође до тражене речи.
  • Проблем: два податка која су у вези али морамо ту везу ручно да одржавамо јер немамо гаранцију да ће број који је враћен бити валидан и у будућности (нпр. стринг може да се промени или да изађе из опсега).
fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s); // word ће добити вредност 5

    s.clear(); // стринг s постаје ""

    // word је и даље 5 иако то више није исправно
}

Проблем постаје још озбиљнији ако нпр. напишемо функцију second_word која враћа другу реч задатог стринга. По аналогији са претходним, требали би да вратимо почетак и крај друге речи.

fn second_word(s: &String) -> (usize, usize) {

Сада имамо три податка о којима морамо да водимо рачуна.

6.3.3. Стринг исечак

Проблем решавамо употребом исечака.

let s = String::from("hello world");

let hello = &s[0..5];
let world = &s[6..11];

Синтакса s[x..y] креира исечак над секвенцом s. С обзиром да овај тип ([T]) нема познату величину у време компајлирања, не можемо га алоцирати на стеку, односно не можемо га доделити локалним варијаблама или прослеђивати као параметар функције. Зато се у пракси користи референца на исечак (&[T]) (често се зове и fat pointer).

Дакле, у пракси много чешће срећемо синтаксу &s[x..y].

slice.png

6.3.4. Исечци и синтакса опсега (range)

let s = String::from("hello");

let slice = &s[0..2];
let slice = &s[..2];
let s = String::from("hello");

let len = s.len();

let slice = &s[3..len];
let slice = &s[3..];
let s = String::from("hello");

let len = s.len();

let slice = &s[0..len];
let slice = &s[..];

6.3.5. Модификација примера да користи стринг исечке

fn first_word(s: &String) -> &str {
    let bytes = s.as_bytes();

    for (i, &item) in bytes.iter().enumerate() {
        if item == b' ' {
            return &s[0..i];
        }
    }

    &s[..]
}

Исти API и за second_word.

fn second_word(s: &String) -> &str {

Сада нам компајлер осигурава да увек имамо валидну референцу на реч.

fn main() {
    let mut s = String::from("hello world");

    let word = first_word(&s);

    s.clear(); // ГРЕШКА!

    println!("the first word is: {}", word);
}
$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
  --> src/main.rs:18:5
   |
16 |     let word = first_word(&s);
   |                           -- immutable borrow occurs here
17 |
18 |     s.clear(); // error!
   |     ^^^^^^^^^ mutable borrow occurs here
19 |
20 |     println!("the first word is: {}", word);
   |                                       ---- immutable borrow later used here

For more information about this error, try `rustc --explain E0502`.
error: could not compile `ownership` due to previous error

Не само да је API лакши за употребу већ је читава класа грешака које се тешко откривају елиминисана у време компајлирања.

6.3.6. Стринг литерали су референце на исечке

let s = "Hello, world!";

Тип од s је &str. str је стринг исечак.

6.3.7. Стринг исечци као параметри функција

fn first_word(s: &String) -> &str {

Али ће искусни Раст програмери писати:

fn first_word(s: &str) -> &str {

Јер ће ова функција моћи да се користи и за &str и за &String. Овде се употребљава тзв. Deref Coercion односно могућности типова да се дереференцирају у други тип. Нпр. &String ће се аутоматски дереференцирати у &str уколико је то потребно. Операција је ефикасна.

fn main() {
    let my_string = String::from("hello world");

    // `first_word` works on slices of `String`s, whether partial or whole
    let word = first_word(&my_string[0..6]);
    let word = first_word(&my_string[..]);
    // `first_word` also works on references to `String`s, which are equivalent
    // to whole slices of `String`s
    let word = first_word(&my_string);

    let my_string_literal = "hello world";

    // `first_word` works on slices of string literals, whether partial or whole
    let word = first_word(&my_string_literal[0..6]);
    let word = first_word(&my_string_literal[..]);

    // Because string literals *are* string slices already,
    // this works too, without the slice syntax!
    let word = first_word(my_string_literal);
}

6.3.8. Употреба исечака над другим структурама

let a = [1, 2, 3, 4, 5];

let slice = &a[1..3]; // референца на исечак типа &[i32]

assert_eq!(slice, &[2, 3]);

7. Структуре

8. Енумерације и подударање образаца (Pattern Matching)

8.1. Enum тип

8.1.1. Enum тип

  • Дефинисање типа навођењем свих могућих варијанти
  • Варијанта може да садржи додатне податке
  • Слично са алгебарским типовима података (algebraic data types) у функционалним језицима, нпр. F#, OCaml и Haskell-у. Ова врста алгебарског типа је позната и под називом sum type.

8.1.2. Пример - IP адреса

  • IP адреса представља идентификацију мрежних интерфејса у уређајима који комуницирају посредством Интернет протокола (Internet Protocol - IP).
  • Тренутно имамо у употреби стару верзију 4 и нову верзију 6 која ће временом заменити верзију 4.
  • Верзија 4 је дужине 32 бита и наводи се као четири октета у облику нпр. 192.0.2.1
  • Верзија 6 је дужине 128 бита и наводи се као 8 група од по 4 хекса цифре раздвојене са :, нпр. 2001:db8:0:1234:0:567:8:1

У Расту можемо писати следеће:

enum IpAddrKind {
    V4,
    V6,
}

Док вредности можемо креирати са:

let four = IpAddrKind::V4;
let six = IpAddrKind::V6;

8.1.3. Пример - IP адреса - функција

Сада можемо писати функцију која прима овај тип чиме је могуће проследити било коју варијанту као аргумент.

fn route(ip_kind: IpAddrKind) {}

...

route(IpAddrKind::V4);
route(IpAddrKind::V6);

8.1.4. Пример - IP адреса - вредност

  • Али како да дефинишемо конкретну вредност IP адресе?
  • Прва идеја би могла бити да користимо структуру.
enum IpAddrKind {
    V4,
    V6,
}

struct IpAddr {
    kind: IpAddrKind,
    address: String,
}

let home = IpAddr {
    kind: IpAddrKind::V4,
    address: String::from("127.0.0.1"),
};

let loopback = IpAddr {
    kind: IpAddrKind::V6,
    address: String::from("::1"),
};

Али, са enum типом можемо то урадити и боље. Enum варијанте могу садржати додатне податке.

enum IpAddr {
    V4(String),
    V6(String),
}

let home = IpAddr::V4(String::from("127.0.0.1"));

let loopback = IpAddr::V6(String::from("::1"));

Вредност садржана у варијанти не мора бити иста за све варијанте. На пример, IPv4 се састоји од 4 октета и можда желимо да вредност наводимо и чувамо у том облику.

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);

let loopback = IpAddr::V6(String::from("::1"));

8.1.5. Пример - IP адреса - std библиотека

У стандардној библиотеци можемо пронаћи тип IpAddr. Дефинисан је на следећи начин.

struct Ipv4Addr {
    // --snip--
}

struct Ipv6Addr {
    // --snip--
}

enum IpAddr {
    V4(Ipv4Addr),
    V6(Ipv6Addr),
}

8.1.6. Пример - Message

enum Message {
    Quit,
    Move { x: i32, y: i32 },
    Write(String),
    ChangeColor(i32, i32, i32),
}

Следеће структуре садрже исте податке као претходни enum тип.

struct QuitMessage; // unit struct
struct MoveMessage {
    x: i32,
    y: i32,
}
struct WriteMessage(String); // tuple struct
struct ChangeColorMessage(i32, i32, i32); // tuple struct

Али, предност enum типа је што је то јединствен тип па можемо нпр. направити функцију која прима било коју вредност/варијанту овог типа.

Над enum типом, као и другим типовима, можемо имплементирати методе употребом imlp кључне речи.

impl Message {
    fn call(&self) {
        // тело методе се овде дефинише
    }
}

let m = Message::Write(String::from("hello"));
m.call();

8.2. Option енумерација

8.2.1. Option енумерација

  • Чест случај да вредност може бити нешто или ништа.
  • Нпр. ако функција враћа први елемент из листе која није празна добићемо први елемент, међутим ако је листа празна повратна вредност је ништа.
  • Овај концепт се у различитим језицима различито имплементира. Често се користи специјална вредност null (или nil, none и сл.) и све варијабле могу бити null или non-null. Проблем је што програмер не дефинише могућност ове вредности кроз тип па самим тим компајлер нема могућност да провери да ли код исправно обрађује ову могућност.

У презентацији из 2009 године под називом Null References: The Billion Dollar Mistake Tony Hoare, који је први увео null вредност као концепт је написао следеће:

I call it my billion-dollar mistake. At that time, I was designing the first comprehensive type system for references in an object-oriented language. My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn’t resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

Option је генерички enum тип у Расту.

enum Option<T> {
    None,
    Some(T),
}

Примери употребе:

let some_number = Some(5);           // тип је Option<i32>
let some_string = Some("a string");  // тип је Option<&str>

let absent_number: Option<i32> = None;    // немогућа инференца

8.2.2. Зашто је Option<T> бољи од null?

let x: i8 = 5;
let y: Option<i8> = Some(5);

let sum = x + y;
$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
 --> src/main.rs:5:17
  |
5 |     let sum = x + y;
  |                 ^ no implementation for `i8 + Option<i8>`
  |
  = help: the trait `Add<Option<i8>>` is not implemented for `i8`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` due to previous error

8.2.3. Како обрађивати Option<T> податке?

  • Морамо експлицитно обрадити вредност Option<T> и могућност да вредност буде None.
  • Option<T> тип има богат API који је потребно знати јер је овај тип врло често у употреби.
let x = Some(2);
let y = None;
assert_eq!(x.or(y), Some(2));

let x = None;
let y = Some(100);
assert_eq!(x.or(y), Some(100));
  • Често користимо језичке конструкције које омогућавају обраду обе варијанте.

8.3. match конструкција за контролу тока

8.3.1. match израз

  • Моћан израз контроле тока базиран на подударању образаца (Pattern Matching).
  • Образац може бити литерал, назив варијабле, џокер (wildcards) итд.
  • match израз ће извршити подударање уз исцрпљивање свих могућност. Уколико нека могућност није обрађена компајлер ће пријавити грешку.

8.3.2. match као машина за сортирање новчића

За почетак можемо match израз посматрати као аутомат за сортирање новчића.

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter,
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}
  • match почиње са изразом произвољног типа.
  • У телу се налазе "руке" (arms) где свака рука има леву страну која представља образац за подударање и десну страну (после =>) која представља код који се евалуира у случају подударања. Руке су раздвојене зарезима.
  • Руке се подударају у редоследу навођења.
  • Повратна вредност целог match израза биће вредност евалуираног кода руке чије је подударање успело

Можемо на десној страни користити произвољан израз па и блок кода.

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => {
            println!("Lucky penny!");
            1
        }
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter => 25,
    }
}

8.3.3. Повезивање имена при подударању (binding)

#[derive(Debug)]
enum UsState {
    Alabama,
    Alaska,
    Arizona,
    // --snip--
}

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(UsState),
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("State quarter from {:?}!", state);
            25
        }
    }
}

8.3.4. match је исцрпан

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(UsState::Alabama) | Coin::Quarter(UsState::Alaska) => 25
    }
}
$ cargo run
   Compiling match_test v0.1.0 (/home/igor/NTP/match_test)
error[E0004]: non-exhaustive patterns: `Quarter(Arizona)` not covered
  --> src/main.rs:21:11
   |
13 | / enum Coin {
14 | |     Penny,
15 | |     Nickel,
16 | |     Dime,
17 | |     Quarter(UsState),
   | |     ------- not covered
18 | | }
   | |_- `Coin` defined here
...
21 |       match coin {
   |             ^^^^ pattern `Quarter(Arizona)` not covered
   |
   = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
   = note: the matched value is of type `Coin`

8.3.5. Подударање са Option<T> типом

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

let five = Some(5);
let six = plus_one(five);
let none = plus_one(None);

Пошто је match исцрпан не можемо заборавити да обрадимо None случај.

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        Some(i) => Some(i + 1),
    }
}
$ cargo run
   Compiling enums v0.1.0 (file:///projects/enums)
error[E0004]: non-exhaustive patterns: `None` not covered
   --> src/main.rs:3:15
    |
3   |         match x {
    |               ^ pattern `None` not covered
    |
    = help: ensure that all possible cases are being handled, possibly by adding wildcards or more match arms
    = note: the matched value is of type `Option<i32>`

For more information about this error, try `rustc --explain E0004`.
error: could not compile `enums` due to previous error

8.3.6. Подразумевана обрада преосталих случајева

  • Желимо на специфичан начин обрадимо само неколико случајева док за све остале радимо подразумевану обраду.
let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    other => move_player(other),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn move_player(num_spaces: u8) {}
  • Раст ће нас упозорити ако додамо руку после оне која обрађује све случајеве јер та се рука никада неће употребити.

Ако желимо да обрадимо све случајеве али нас вредност не интересује можемо користити _.

let dice_roll = 9;
match dice_roll {
    3 => add_fancy_hat(),
    7 => remove_fancy_hat(),
    _ => reroll(),
}

fn add_fancy_hat() {}
fn remove_fancy_hat() {}
fn reroll() {}

8.4. if let kontrola toka

if let нам омогућава комбинацију if и let исказе у мање опширан исказ за обраду само једног случаја поклапања и игнорисање осталих.

let config_max = Some(3u8);
match config_max {
    Some(max) => println!("The maximum is configured to be {}", max),
    _ => (),
}

Ово можемо концизније исказати са:

let config_max = Some(3u8);
if let Some(max) = config_max {
    println!("The maximum is configured to be {}", max);
}
  • Губимо исцрпност match израза.
  • Синтаксни шећер у ситуацијама када желимо да игноришемо остале могућности.

Обрасци за подударање могу имати две форме: поништив (refutable) и непоништив (irrefutable). Образац који увек мора успети (нпр. let x = 5;) је непоништив. Образац који не мора да успе увек је поништив (нпр. if let Some(x) = a_value неће успети за a_value==None).

Параметри функција, let искази и for петље прихватају само непоништиве обрасце јер програм не може урадити ништа смислено уколико подударање не успе.

if let и while let прихватају и поништиве и непоништиве обрасце али ће нас компајлер упозорити уколико користимо непоништиве.

Можемо користити и else грану. Следеће је еквивалентно.

let mut count = 0;
match coin {
    Coin::Quarter(state) => println!("State quarter from {:?}!", state),
    _ => count += 1,
}
let mut count = 0;
if let Coin::Quarter(state) = coin {
    println!("State quarter from {:?}!", state);
} else {
    count += 1;
}

9. Модули

10. Колекције

11. Обрада грешака

12. Генерички типови, особине (Traits) и животни век (Lifetimes)

12.1. Генерички типови (generics)

12.1.1. Генерички типови (generics)

  • Механизам за елиминацију дуплирања кода.
  • Генерички типови су апстрактне замене за конкретне типове у друге особине у време извршавања.
  • Омогућавају нам да на апстрактан начин искажемо особине типова и њихове везе са другим типовима без знања о томе који ће се конкретни типови наћи на њиховом месту у време компајлирања и извршавања кода.
  • На пример функције могу примити генеричке параметре. Такође, сложени типови могу бити параметризовани генеричким типовима (пример је Option<T>).

12.1.2. Уклањање дуплог кода употребом функција

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
}
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let mut largest = number_list[0];

    for number in number_list {
        if number > largest {
            largest = number;
        }
    }

    println!("The largest number is {}", largest);
}
fn largest(list: &[i32]) -> i32 {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let number_list = vec![102, 34, 6000, 89, 54, 2, 43, 8];

    let result = largest(&number_list);
    println!("The largest number is {}", result);
}

Поступак елиминације дуплог кода је подразумевао следеће:

  1. Идентификација дуплог кода.
  2. Екстракција кода у функцију, дефинисање параметара и повратне вредности.
  3. Замена инстанци дуплог кода са позивом функције.

12.1.3. Уклањање дуплог кода употребом генеричких функција

Имамо функцију за проналажење највећег елемента у листи са различитим типовима.

fn largest_i32(list: &[i32]) -> i32 {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
fn largest_char(list: &[char]) -> char {
    let mut largest = list[0];
    for &item in list {
        if item > largest {
            largest = item;
        }
    }
    largest
}
fn main() {
    let number_list = vec![34, 50, 25, 100, 65];
    let result = largest_i32(&number_list);
    println!("The largest number is {}", result);
    let char_list = vec!['y', 'm', 'a', 'q'];
    let result = largest_char(&char_list);
    println!("The largest char is {}", result);
}

Функције су готово идентичне. Разлика је само у типу. Пишемо генеричку функцију тако што дефинишемо генерички тип унутар <> после назива функције:

fn largest<T>(list: &[T]) -> T {

T је генерички тип и у време компајлирања биће замењен са конкретним типом.

Сада је наш код следећи:

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Али се не компајлира.

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

Тип T може бити било који тип па и тип који не дефинише операцију > која се користи у коду. Дакле, морамо ограничити који типови су могући.

Видећемо како се ово ради у наставку у причи о особинама (Traits).

12.1.4. Генерички типови у структурама

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

Типови оба поља морају бити исти.

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}
$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` due to previous error

Ако желимо да поља имају различите типове онда морамо имати различите генеричке типове.

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

12.1.5. Генерички типови у енумерацијама

enum Option<T> {
    Some(T),
    None,
}
enum Result<T, E> {
    Ok(T),
    Err(E),
}

12.1.6. Дефиниције метода

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

Пошто смо навели <T> иза кључне речи impl Раст даље зна да је тип T генерички а не конкретни тип тако да је метода x за Point<T> дефинисана над свим типовима T.

Могли смо нпр. методу дефинисати само за одређени Point тип.

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

Овај impl блок дефинише методу distance_from_origin али само за Point<f32> док остали Point типови неће имати ову методу.

12.1.7. Генерички типови у методама и impl блоковима

Генерички типови у структурама и методама не морају бити исти. На пример, можемо креирати методу mixup која узима две инстанце Point и враћа нови Point тип где ће прво поље имати исти тип као прва тачка а друго поље као друга тачка.

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };
    let p3 = p1.mixup(p2);
    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}
  • Генерички типови X2 и Y2 су наведени само у методи јер су релевантни само у њеном контексту.

12.1.8. Перформансе

Употреба генеричког кода не доводи до деградације перформанси. Раст користи технику мономорфизације (Monomorphization) код које ће компајлер инстанцирати конкретан код за сваки тип посебно.

То би значило следеће. Ако имамо код:

let integer = Some(5);
let float = Some(5.0)

Компајлер ће произвести код еквивалентан следећем:

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

12.2. Особине (Traits) - дефинисање заједничког понашања

12.2.1. Traits

  • Trait дефинише функционалност коју тип може делити са другим типовима.
  • У другим језицима сличан концепт назива се интерфејс.
  • Можемо користити ограничења кроз особине (Trait bounds) да дефинишемо да генерички тип мора задовољити одређена ограничења.

12.2.2. Дефинисање особина

  • Пример дефинисања медиа агрегатор библиотеке која може да приказе сажетак података који се може чувати у NewsArticle или Tweet инстанци.
  • Дефинишемо Summary особину да опишемо ову функционалност.
pub trait Summary {
    fn summarize(&self) -> String;
}

12.2.3. Имплементација особина

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Методе особина се позивају као и обичне методе. Једина разлика је у томе што методе особина морају бити доступне у опсегу (укључити их са use).

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Ограничење у употреби особина је да можемо имплементирати особину над типом једино ако је бар једно од њих локално за наш сандук. Ово правило је део скупа правила који се називају coherence. Конкретно ово правило зове се orphan rule и спречава некомпатибилне имплементације особина над типовима од стране више сандука.

Без овог правила могло би се десити да два различита сандука имплементирају исту особину над истим типом на различите начине и компајлер не би знао коју верзију да користи.

12.2.4. Подразумевана имплементација

Особина може имати подразумевану имплементацију метода.

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

Да би користили подразумевану имплементацију можемо навести празно тело у impl блоку:

impl Summary for NewsArticle {}

И затим можемо позвати summarize методу:

let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
  • Подразумевана имплементација може бити редефинисана приликом имплементације.
  • Такође, подразумеване методе могу позивати друге методе.
pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

Сада је потребно и довољно да приликом имплементације дефинишемо summarize_author методу:

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

12.2.5. Osobine kao parametri

  • Сада можемо користити особине да дефинишемо функције које раде над параметрима различитог типа.
pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Сада функција notify ради над свим типовима који имплементирају Summary

Претходна употреба impl код параметра је синтаксни шећер за општи облик навођења ограничења кроз особине.

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Ограничења кроз особине (Trait bounds) стављамо после двотачке код навођења генеричког типа. Компајлер ће верификовати да све што користимо над вредностима овог типа унутар функције је заиста дефинисано особинама наведеним у заглављу.

impl Trait синтаксе је концизнија код једноставних примера али је пуна синтакса боља код сложенијих примера.

На пример:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

би у пуној синтакси било:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

12.2.6. Вишеструке особине код ограничења

Можемо дефинисати и више ограничења употребом + синтаксе. На пример, ако параметар мора да имплементира Summary и Display:

pub fn notify(item: &(impl Summary + Display)) {

или у пуној синтакси:

pub fn notify<T: Summary + Display>(item: &T) {

12.2.7. where клаузула у ограничењима

  • Уколико имамо више ограничења основна синтакса може да смањи читкост.
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
  • where клаузула измешта дефинисање ограничења после заглавља функције чиме се постиже боља читкост:
fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

12.2.8. impl Trait као повратна вредност функције

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

Посебно значајно у контексту затворења (closures) и итератора где је конкретан тип превише сложен за писање или је познат само компајлеру.

impl Trait синтакса за повратне вредности је могућа само ако функција враћа један тип који имплементира дату особину. На пример, ова функција неће радити ако функција може вратити NewsArticle или Tweet.

12.2.9. Поправка largest генеричке функције

На претходним сладовима имали смо функцију largest. Сада можемо да завршимо њену дефиницију.

Грешка је била следећа:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` due to previous error

Дакле, морамо ограничити типове на оне који подржавају поређње. Ова особина се у стандардној библиотеци зове std::cmp::PartialOrd, па ћемо преправити заглавље функције на следећи начин:

fn largest<T: PartialOrd>(list: &[T]) -> T {

Међутим, сада имамо још једну грешку:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `item`

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10` due to 2 previous errors

Уводимо додатно ограничење да генерички тип мора бити и Copy. Тако да је пуно решење следеће:

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

12.2.10. Употреба особина ограничења за условну имплементацију метода

Ако желимо да имплементирамо методу само над типовима који имплементирају одређене особине.

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

12.3. Валидација референци кроз животни век (Lifetimes)

12.3.1. Животни век (Lifetime)

  • Свака референца има животни век, опсег у коме је референца валидна.
  • У доста ситуација компајлер аутоматски може да закључи који је животни век референце. Када то није у стању морамо да урадимо ручну анотацију.
  • Анотацијама животног века доводимо у везу животни век различитих референци.

12.3.2. Спречавање "висећих" референци

{
        let r;

        {
            let x = 5;
            r = &x;
        }

        println!("r: {}", r);
    }

 cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0597]: `x` does not live long enough
  --> src/main.rs:7:17
   |
7  |             r = &x;
   |                 ^^ borrowed value does not live long enough
8  |         }
   |         - `x` dropped here while still borrowed
9  |
10 |         println!("r: {}", r);
   |                           - borrow later used here

For more information about this error, try `rustc --explain E0597`.
error: could not compile `chapter10` due to previous error

12.3.3. Borrow Checker

  • Раст компајлер има компоненту borrow checker која проверава да ли су све позајмице валидне.
{
    let r;                // ---------+-- 'a
                          //          |
    {                     //          |
        let x = 5;        // -+-- 'b  |
        r = &x;           //  |       |
    }                     // -+       |
                          //          |
    println!("r: {}", r); //          |
}                         // ---------+
  • Програм је одбачен јер r референцира вредност чији животни век ('b) је краћи од животног века варијабле r ('a). Уколико би се ово дозволило, r би у једном делу референцирала невалидну вредност.

Варијабле имају животни век који је једнак синстаксном опсегу у коме је варијабла дефинисана. Животни век имају и референце које позајмљују вредност и смештају се у варијабле (нпр. r=&x из претходног примера).

Једно од правила је да референца на варијаблу (тј. вредност) не може да живи дуже од саме варијабле. Односно животни век варијабле мора да садржи животни век референце позајмљене од те варијабле.

{
    let r;
    {
        let x = 5;
        r = &x;           // -+- &x не може да живи дуже од
    }                     // -+  овог животног века
    println!("r: {}", r);
}

Ако сместимо референцу у варијаблу, референца мора бити исправна за цео животни век варијабле у коју је смештена.

Кажемо да животни век референце мора да садржи животни век варијабле у коју је смештена.

{
    let r;
    {
        let x = 5;
        r = &x;           // -+- животни век било чега што
    }                     //  |  се смести у r мора да живи
    println!("r: {}", r); // -+  бар оволико
}

Видимо да ово правило није задовољено. &x не живи довољно дуго јер x не живи довољно дуго.

Ако претходна два правила објединимо можемо рећи да:

Животни век варијабле мора да садржи животни век свих варијабли које позајмљују вредност од посматране варијабле.

Односно, варијабла може отићи из опсега тек када не постоји више ни једна варијабла која чува позајмљену вредност.

Следећи код је прихваћен. Референца r не живи дуже од податка који референцира (x).

{
    let x = 5;            // ----------+-- 'b
                          //           |
    let r = &x;           // --+-- 'a  |
                          //   |       |
    println!("r: {}", r); //   |       |
                          // --+       |
}                         // ----------+

12.3.4. Генерички животни векови у функцијама

  • Креирамо функцију longest која враћа дужи од два прослеђена стринга. Прослеђујемо референцу на стринг исечак &str.
fn main() {
    let string1 = String::from("abcd");
    let string2 = "xyz";

    let result = longest(string1.as_str(), string2);
    println!("The longest string is {}", result);
}

Овај програм није прихваћен.

fn longest(x: &str, y: &str) -> &str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0106]: missing lifetime specifier
 --> src/main.rs:9:33
  |
9 | fn longest(x: &str, y: &str) -> &str {
  |               ----     ----     ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say
  whether it is borrowed from `x` or `y`
help: consider introducing a named lifetime parameter
  |
9 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
  |           ++++     ++          ++          ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `chapter10` due to previous error
  • Зашто програм није прихваћен?
  • Borrow checker није у стању да одреди у којој релацији су референце x и y са повратном вредношћу која је такође референца.
  • Тј. повратна вредност мора бити позајмљена од некуд, али компајлер не може да одреди да ли је позамљена од x или y или од неке глобалне вредности.
  • Ова информација је потребна да би се обавила калкулација и провера животног века.

12.3.5. Синтакса за анотацију животног века

  • Да би помогли компајлеру референце означавамо са животним веком и тиме доводимо у везу различите референце (нпр. параметре и повратне вредности функције).
  • Анотација је облика 'a. Иза апострофа се пише име анотације које је најчешће једно мало слово са почетка алфабета ('a, 'b,...).
  • Важно: анотације животног века не мењају животни век референце већ само помажу компајлеру у провери.
&i32        // референца
&'a i32     // референца са експлицитним животним веком
&'a mut i32 // промењива референца са експлицитним животним веком
  • Једна анотација нема пуно смисла. Функцију имају тек када анотирамо више референци јер се доводе у везу ако имају исто име.
  • Уведимо функцију lt која враћа животни век варијабле/вредности и релацију између животних векова 'a≥'b односно 'b≤'a која значи да 'a обухвата 'b односно 'b је садржано у 'a.

12.3.6. Анотација животног века у сигнатурама функција

  • Следећа анотација за функцију longest је исправна јер референца која се враћа може бити x или y у зависности од дужине стринга. Стога је потребно да имамо строжији услов да је животни век повратне вредности у вези са животним веком оба параметра.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() {
        x
    } else {
        y
    }
}
  • Овим кажемо компајлеру да за неки генерички животни век 'a, функција прима две референце чији животни век вредности обухвата 'a и враћа референцу која мора бити валидна бар колико и животни век 'a тј. животни век повратне референце мора да обувати 'a. Уколико такав животни век постоји код се прихвата.
  • Сигнатура доводи у везу обе улазне референце са излазном.

Посматрајмо код који позива функцију longest:

fn main() {
    let string1 = String::from("abcd");
    {
        let string2 = "xyz";
        let result = longest(string1.as_str(), string2);
    }
    println!("The longest string is {}", result);
}

За код мора да постоји 'a тако да важи:

lt(string1) ≥ 'a     # животни век вредности првог параметара садржи 'a
lt(string2) ≥ 'a     # животни век вредности другог параметара садржи 'a
'a ≥ lt(result)       # 'a мора да садржи животни веку ресулт варијабле

Што не може бити испуњено истовремено, тј. не постоји 'a које испуњава ова ограничења.

Други начин размишљања је да повратна вредност функције позајмљује од вредности улазних параметара означених истим именом животног века.

fn main() {
    let string1 = String::from("abcd");
    {
        let string2 = "xyz";
        let result = longest(string1.as_str(), string2);
    }
    println!("The longest string is {}", result);
}

Што значи да result позајмљује од string1 и string2 и та позајмица траје све време живота варијабле result што не може бити валидно јер варијабла живи дуже од вредности string2.

Иако, можемо видети да ће, због дужине стринга, враћена вредност бити string1 и програм је валидан, компајлер ово не може да закључи.

13. Литература