Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Rust на Прикладах

Rust — це сучасна системна мова програмування, яка фокусується на безпеці, швидкодії та паралелізмі. Вона досягає цих цілей, будучи безпечною щодо пам’яті без використання збирача сміття.

Rust на Прикладах (RBE) — це збірка виконуваних прикладів, які ілюструють різні концепції Rust та стандартну бібліотеку. Щоб отримати ще більше від цих прикладів, не забудьте встановити Rust локально та переглянути офіційну документацію. Також для допитливих можна переглянути вихідний код цього сайту.

Почнімо!

  • Привіт, світ — Почніть із традиційної програми Hello World.

  • Примітиви — Дізнайтеся про знакові цілі числа, беззнакові цілі числа та інші примітиви.

  • Користувацькі типиstruct та enum.

  • Прив’язки змінних — змінні прив’язки, область видимості, затінення.

  • Типи — Дізнайтеся про зміну та визначення типів.

  • Перетворення — Перетворення між різними типами, такими як рядки, цілі числа та числа з плаваючою комою.

  • Вирази — Дізнайтеся про вирази та як їх використовувати.

  • Потік керуванняif/else, for та інші.

  • Функції — Дізнайтеся про методи, замикання та функції вищого порядку.

  • Модулі — Організація коду за допомогою модулів.

  • Крейти — Крейт — це одиниця компіляції в Rust. Навчіться створювати бібліотеку.

  • Cargo — Ознайомтеся з базовими можливостями офіційного інструменту керування пакетами Rust.

  • Атрибути — Атрибут — це метадані, застосовані до модуля, крейту або елемента.

  • Узагальнені типи — Навчіться писати функцію або тип даних, який може працювати з кількома типами аргументів.

  • Правила області видимості — Області видимості відіграють важливу роль у володінні, запозиченні та часах життя.

  • Трейти — Трейт — це набір методів, визначених для невідомого типу: Self.

  • Макроси — Макроси — це спосіб написання коду, який пише інший код, що відомо як метапрограмування.

  • Обробка помилок — Навчіться обробляти помилки у стилі Rust.

  • Типи стандартної бібліотеки — Дізнайтеся про деякі спеціальні типи, надані бібліотекою std.

  • Різне стандартної бібліотеки — Більше спеціальних типів для роботи з файлами, потоками.

  • Тестування — Усі види тестування в Rust.

  • Небезпечні операції — Дізнайтеся про вхід у блок небезпечних операцій.

  • Сумісність — Робота з еволюцією Rust та потенційними проблемами сумісності.

  • Метадані — Документація, бенчмаркінг.

Hello World

Це вихідний код традиційної програми Hello World.

// Це коментар, і він ігнорується компілятором.
// Ви можете протестувати цей код, натиснувши кнопку "Run" он там ->
// або, якщо ви віддаєте перевагу клавіатурі, ви можете скористатися
// комбінацією клавіш "Ctrl + Enter".

// цей код можна редагувати, сміливо змінюйте його!
// Ви завжди можете повернутися до початкового коду, натиснувши кнопку "Reset" ->

// Це головна функція.
fn main() {
    // Statements here are executed when the compiled binary is called.

    // Print text to the console.
    println!("Hello World!");
}

println! — це macro, який виводить текст у консоль.

Бінарний файл можна згенерувати за допомогою компілятора Rust: rustc.

$ rustc hello.rs

rustc створить бінарний файл hello, який можна виконати.

$ ./hello
Hello World!

Діяльність

Натисніть ‘Run’ вище, щоб побачити очікуваний вивід. Далі додайте новий рядок із другим макросом println!, щоб вивід показував:

Hello World!
I'm a растацеанці (Rustaceans)!

Коментарі

Кожна програма потребує коментарів, і Rust підтримує кілька різних різновидів:

Звичайні коментарі

Вони ігноруються компілятором:

  • Покоментарні рядки: Починаються з // і тривають до кінця рядка
  • Блокові коментарі: Укладені в /* ... */ і можуть охоплювати кілька рядків

Документаційні коментарі (Doc Comments), які розбираються в HTML library documentation:

  • /// - Генерує документацію для елемента, що слідує за ним
  • //! - Генерує документацію для елемента, що охоплює його (зазвичай використовується на початку файлу або модуля)

fn main() {
    // Покоментарні рядки починаються з двох слешів.
    // Усе після слешів ігнорується компілятором.

    // Приклад: цей рядок не виконається
    // println!("Hello, world!");

    // Спробуйте видалити слеші вище й запустити код знову.

    /*
     * Блокові коментарі корисні для тимчасового вимкнення коду.
     * Їх також можна вкладати: /* like this */ що робить простим
     * швидке коментування великих фрагментів.
     */

    /*
    Примітка: колонка зірочок ліворуч — це лише для стилю - 
    вона не є обов'язковою для мови.
    */

    // Блокові коментарі роблять простим вмикання/вимикання коду шляхом додавання
    // або видалення лише одного слеша:

    /* <- Додайте тут '/' , щоб розкоментувати весь блок нижче

    println!("Now");
    println!("everything");
    println!("executes!");
    // Покоментарні рядки всередині залишаються без змін

    // */

    // Блокові коментарі також можна використовувати всередині виразів:
    let x = 5 + /* 90 + */ 5;
    println!("Is `x` 10 or 100? x = {}", x);
}

Дивіться також:

Документація бібліотеки

Форматований друк

Друк обробляється серією макросів, визначених у std::fmt, деякі з яких:

  • format!: записує форматований текст у String
  • print!: те саме, що й format!, але текст друкується в консоль (io::stdout).
  • println!: те саме, що й print!, але додається новий рядок.
  • eprint!: те саме, що й print!, але текст друкується у стандартний потік помилок (io::stderr).
  • eprintln!: те саме, що й eprint!, але додається новий рядок.

Усі вони розбирають текст однаковим способом. Крім того, Rust перевіряє коректність форматування під час компіляції.

fn main() {
    // In general, the `{}` will be automatically replaced with any
    // arguments. These will be stringified.
    println!("{} days", 31);

    // Positional arguments can be used. Specifying an integer inside `{}`
    // determines which additional argument will be replaced. Arguments start
    // at 0 immediately after the format string.
    println!("{0}, this is {1}. {1}, this is {0}", "Alice", "Bob");

    // As can named arguments.
    println!("{subject} {verb} {object}",
             object="the lazy dog",
             subject="the quick brown fox",
             verb="jumps over");

    // Different formatting can be invoked by specifying the format character
    // after a `:`.
    println!("Base 10:               {}",   69420); // 69420
    println!("Base 2 (binary):       {:b}", 69420); // 10000111100101100
    println!("Base 8 (octal):        {:o}", 69420); // 207454
    println!("Base 16 (hexadecimal): {:x}", 69420); // 10f2c

    // You can right-justify text with a specified width. This will
    // output "    1". (Four white spaces and a "1", for a total width of 5.)
    println!("{number:>5}", number=1);

    // You can pad numbers with extra zeroes,
    println!("{number:0>5}", number=1); // 00001
    // and left-adjust by flipping the sign. This will output "10000".
    println!("{number:0<5}", number=1); // 10000

    // You can use named arguments in the format specifier by appending a `$`.
    println!("{number:0>width$}", number=1, width=5);

    // Rust even checks to make sure the correct number of arguments are used.
    println!("My name is {0}, {1} {0}", "Bond");
    // FIXME ^ Add the missing argument: "James"

    // Only types that implement fmt::Display can be formatted with `{}`. User-
    // defined types do not implement fmt::Display by default.

    #[allow(dead_code)] // disable `dead_code` which warn against unused module
    struct Structure(i32);

    // This will not compile because `Structure` does not implement
    // fmt::Display.
    // println!("This struct `{}` won't print...", Structure(3));
    // TODO ^ Try uncommenting this line

    // For Rust 1.58 and above, you can directly capture the argument from a
    // surrounding variable. Just like the above, this will output
    // "    1", 4 white spaces and a "1".
    let number: f64 = 1.0;
    let width: usize = 5;
    println!("{number:>width$}");
}

std::fmt містить багато traits, які визначають відображення тексту. Базова форма двох важливих із них наведена нижче:

  • fmt::Debug: використовує позначку {:?}. Форматує текст для цілей налагодження.
  • fmt::Display: використовує позначку {}. Форматує текст у більш елегантний, зручний для користувача спосіб.

Тут ми використали fmt::Display, тому що бібліотека std надає реалізації для цих типів. Щоб друкувати текст для власних типів, потрібні додаткові кроки.

Реалізація трейт-об’єкта fmt::Display автоматично реалізує ToString трейт, який дозволяє [перетворити] тип на String.

У рядку 43 #[allow(dead_code)] — це [атрибут], який застосовується лише до елемента після нього.

Дії

  • Виправте проблему у наведеному вище коді (див. FIXME), щоб він запускався без помилки.
  • Спробуйте розкоментувати рядок, який намагається форматувати структуру Structure (див. TODO)
  • Додайте виклик макроса println!, який виводить: Pi is roughly 3.142, контролюючи кількість показаних десяткових знаків. Для цього вправи використайте let pi = 3.141592 як оцінку pi. (Підказка: можливо, вам потрібно буде перевірити документацію std::fmt щодо встановлення кількості десяткових знаків для відображення)

Див. також:

std::fmt, macros, struct, traits, і dead_code

Debug

Усі типи, які хочуть використовувати форматувальні traits з std::fmt, потребують реалізації, щоб їх можна було виводити. Автоматичні реалізації надаються лише для типів, таких як у бібліотеці std. Усі інші мають бути реалізовані вручну якимось способом.

trait fmt::Debug робить це дуже простим. Усі типи можуть derive (автоматично створити) реалізацію fmt::Debug. Це не так для fmt::Display, який має бути реалізований вручну.

#![allow(unused)]
fn main() {
// This structure cannot be printed either with `fmt::Display` or
// with `fmt::Debug`.
struct UnPrintable(i32);

// The `derive` attribute automatically creates the implementation
// required to make this `struct` printable with `fmt::Debug`.
#[derive(Debug)]
struct DebugPrintable(i32);
}

Усі типи бібліотеки std також автоматично можна виводити за допомогою {:?}:

// Derive the `fmt::Debug` implementation for `Structure`. `Structure`
// is a structure which contains a single `i32`.
#[derive(Debug)]
struct Structure(i32);

// Put a `Structure` inside of the structure `Deep`. Make it printable
// also.
#[derive(Debug)]
struct Deep(Structure);

fn main() {
    // Printing with `{:?}` is similar to with `{}`.
    println!("{:?} months in a year.", 12);
    println!("{1:?} {0:?} is the {actor:?} name.",
             "Slater",
             "Christian",
             actor="actor's");

    // `Structure` is printable!
    println!("Now {:?} will print!", Structure(3));

    // The problem with `derive` is there is no control over how
    // the results look. What if I want this to just show a `7`?
    println!("Now {:?} will print!", Deep(Structure(7)));
}

Отже, fmt::Debug безумовно робить це вивідним, але жертвує певною елегантністю. Rust також надає “красиве виведення” за допомогою {:#?}.

#[derive(Debug)]
struct Person<'a> {
    name: &'a str,
    age: u8
}

fn main() {
    let name = "Peter";
    let age = 27;
    let peter = Person { name, age };

    // Pretty print
    println!("{:#?}", peter);
}

Можна вручну реалізувати fmt::Display, щоб керувати відображенням.

Дивіться також:

attributes, derive, std::fmt, та struct

Display

fmt::Debug навряд чи виглядає компактним і чистим, тому часто вигідно налаштувати вигляд виводу. Це робиться шляхом ручного виведення реалізації fmt::Display, який використовує позначку друку {}. Реалізація цього виглядає так:

#![allow(unused)]
fn main() {
// Import (via `use`) the `fmt` module to make it available.
use std::fmt;

// Define a structure for which `fmt::Display` will be implemented. This is
// a tuple struct named `Structure` that contains an `i32`.
struct Structure(i32);

// To use the `{}` marker, the trait `fmt::Display` must be implemented
// manually for the type.
impl fmt::Display for Structure {
    // This trait requires `fmt` with this exact signature.
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Write strictly the first element into the supplied output
        // stream: `f`. Returns `fmt::Result` which indicates whether the
        // operation succeeded or failed. Note that `write!` uses syntax which
        // is very similar to `println!`.
        write!(f, "{}", self.0)
    }
}
}

fmt::Display може бути чистішим за fmt::Debug, але це створює проблему для бібліотеки std. Як слід відображати неоднозначні типи? Наприклад, якщо б бібліотека std реалізувала один стиль для всіх Vec<T>, яким мав би бути цей стиль? Чи був би він одним із цих двох?

  • Vec<path>: /:/etc:/home/username:/bin (розділено за :)
  • Vec<number>: 1,2,3 (розділено за ,)

Ні, тому що не існує ідеального стилю для всіх типів, і бібліотека std не бере на себе сміливість нав’язувати один. fmt::Display не реалізовано для Vec<T> або для будь-яких інших узагальнених контейнерів. Тоді для цих узагальнених випадків слід використовувати fmt::Debug.

Втім, це не проблема, тому що для будь-якого нового типу контейнера, який не є узагальненим, можна реалізувати fmt::Display.

use std::fmt; // Import `fmt`

// A structure holding two numbers. `Debug` will be derived so the results can
// be contrasted with `Display`.
#[derive(Debug)]
struct MinMax(i64, i64);

// Implement `Display` for `MinMax`.
impl fmt::Display for MinMax {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Use `self.number` to refer to each positional data point.
        write!(f, "({}, {})", self.0, self.1)
    }
}

// Define a structure where the fields are nameable for comparison.
#[derive(Debug)]
struct Point2D {
    x: f64,
    y: f64,
}

// Similarly, implement `Display` for `Point2D`.
impl fmt::Display for Point2D {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Customize so only `x` and `y` are denoted.
        write!(f, "x: {}, y: {}", self.x, self.y)
    }
}

fn main() {
    let minmax = MinMax(0, 14);

    println!("Compare structures:");
    println!("Display: {}", minmax);
    println!("Debug: {:?}", minmax);

    let big_range =   MinMax(-300, 300);
    let small_range = MinMax(-3, 3);

    println!("The big range is {big} and the small is {small}",
             small = small_range,
             big = big_range);

    let point = Point2D { x: 3.3, y: 7.2 };

    println!("Compare points:");
    println!("Display: {}", point);
    println!("Debug: {:?}", point);

    // The following line would not compile: both `Debug` and `Display`
    // were implemented, but `{:b}` requires `fmt::Binary` to be
    // implemented, which it hasn't been for `Point2D`.
    // println!("What does Point2D look like in binary: {:b}?", point);
}

Отже, fmt::Display було реалізовано, але fmt::Binary — ні, і тому його не можна використовувати. std::fmt має багато таких traits, і кожен вимагає своєї реалізації. Це детальніше описано в std::fmt.

Activity

Після перевірки виводу наведеного вище прикладу, використайте структуру Point2D як зразок, щоб додати до прикладу структуру Complex. Коли її буде надруковано тим самим способом, вивід має бути таким:

Display: 3.3 +7.2i
Debug: Complex { real: 3.3, imag: 7.2 }

Display: 4.7 -2.3i
Debug: Complex { real: 4.7, imag: -2.3 }

Бонус: Додайте пробіл після знаків +/-.

Підказки на випадок, якщо ви застрягнете:

  • Перевірте документацію для Sign/#/0 у std::fmt.
  • Бонус: Перевірте розгалуження if-else і функцію abs.

See also:

derive, std::fmt, macros, struct, trait, and use

Testcase: List

Реалізація fmt::Display для структури, де кожен елемент потрібно обробляти послідовно, є непростою. Проблема в тому, що кожен write! створює fmt::Result. Правильна обробка цього вимагає роботи з усіма результатами. Rust надає оператор ? саме для цієї мети.

Використання ? на write! виглядає так:

// Try `write!` to see if it errors. If it errors, return
// the error. Otherwise continue.
write!(f, "{}", value)?;

З ? реалізувати fmt::Display для Vec просто:

use std::fmt; // Import the `fmt` module.

// Define a structure named `List` containing a `Vec`.
struct List(Vec<i32>);

impl fmt::Display for List {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        // Create a reference to the Vec<i32> stored in the List struct.
        let vec = &self.0;

        write!(f, "[")?;

        // Iterate over `v` in `vec` while enumerating the iteration
        // index in `index`.
        for (index, v) in vec.iter().enumerate() {
            // For every element except the first, add a comma.
            // Use the ? operator to return on errors.
            if index != 0 { write!(f, ", ")?; }
            write!(f, "{}", v)?;
        }

        // Close the opened bracket and return a fmt::Result value.
        write!(f, "]")
    }
}

fn main() {
    let v = List(vec![1, 2, 3]);
    println!("{}", v);
}

Дія

Спробуйте змінити програму так, щоб також виводився індекс кожного елемента у векторі. Новий вивід має виглядати так:

[0: 1, 1: 2, 2: 3]

Див. також:

for, ref, Result, struct, ?, and vec!

Форматування

Ми бачили, що форматування задається за допомогою рядка формату:

  • format!("{}", foo) -> "3735928559"
  • format!("0x{:X}", foo) -> "0xDEADBEEF"
  • format!("0o{:o}", foo) -> "0o33653337357"

Той самий змінна (foo) може бути відформатовано по-різному залежно від того, який тип аргументу використовується: X проти o проти не вказано.

Ця функціональність форматування реалізована через трейти, і для кожного типу аргументу існує окремий трейт. Найпоширенішим трейтом форматування є Display, який обробляє випадки, коли тип аргументу не вказано: {} наприклад.

use std::fmt::{self, Formatter, Display};

struct City {
    name: &'static str,
    // Latitude
    lat: f32,
    // Longitude
    lon: f32,
}

impl Display for City {
    // `f` is a buffer, and this method must write the formatted string into it.
    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
        let lat_c = if self.lat >= 0.0 { 'N' } else { 'S' };
        let lon_c = if self.lon >= 0.0 { 'E' } else { 'W' };

        // `write!` is like `format!`, but it will write the formatted string
        // into a buffer (the first argument).
        write!(f, "{}: {:.3}°{} {:.3}°{}",
               self.name, self.lat.abs(), lat_c, self.lon.abs(), lon_c)
    }
}

#[derive(Debug)]
struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

fn main() {
    for city in [
        City { name: "Dublin", lat: 53.347778, lon: -6.259722 },
        City { name: "Oslo", lat: 59.95, lon: 10.75 },
        City { name: "Vancouver", lat: 49.25, lon: -123.1 },
    ] {
        println!("{}", city);
    }
    for color in [
        Color { red: 128, green: 255, blue: 90 },
        Color { red: 0, green: 3, blue: 254 },
        Color { red: 0, green: 0, blue: 0 },
    ] {
        // Switch this to use {} once you've added an implementation
        // for fmt::Display.
        println!("{:?}", color);
    }
}

Ви можете переглянути повний список трейтів форматування та їхніх типів аргументів у документації std::fmt.

Завдання

Додайте реалізацію трейта fmt::Display для структури Color вище так, щоб вивід відображався так:

RGB (128, 255, 90) 0x80FF5A
RGB (0, 3, 254) 0x0003FE
RGB (0, 0, 0) 0x000000

Дві підказки, якщо ви застрягнете:

Бонус:

Див. також:

std::fmt

Примітиви

Rust надає доступ до широкого різноманіття primitives. До прикладів належать:

Скалярні типи

  • Знакові цілі числа: i8, i16, i32, i64, i128 і isize (розмір вказівника)
  • Беззнакові цілі числа: u8, u16, u32, u64, u128 і usize (розмір вказівника)
  • Числа з рухомою комою: f32, f64
  • char — значення Unicode-скалярів, як-от 'a', 'α' і '∞' (4 байти кожне)
  • bool — або true, або false
  • Тип одиниці (), єдиним можливим значенням якого є порожній кортеж: ()

Попри те, що значення типу одиниці є кортежем, його не вважають складеним типом, тому що він не містить кількох значень.

Складені типи

  • Масиви, як [1, 2, 3]
  • Кортежі, як (1, true)

Змінні завжди можуть бути типізовані явно. Числа додатково можуть бути типізовані за допомогою суфікса або за замовчуванням. Типом цілих чисел за замовчуванням є i32, а чисел з рухомою комою — f64. Зверніть увагу, що Rust також може виводити типи з контексту.

fn main() {
    // Variables can be type annotated.
    let logical: bool = true;

    let a_float: f64 = 1.0;  // Regular annotation
    let an_integer   = 5i32; // Suffix annotation

    // Or a default will be used.
    let default_float   = 3.0; // `f64`
    let default_integer = 7;   // `i32`

    // A type can also be inferred from context.
    let mut inferred_type = 12; // Type i64 is inferred from another line.
    inferred_type = 4294967296i64;

    // A mutable variable's value can be changed.
    let mut mutable = 12; // Mutable `i32`
    mutable = 21;

    // Error! The type of a variable can't be changed.
    mutable = true;

    // Variables can be overwritten with shadowing.
    let mutable = true;

    /* Compound types - Array and Tuple */

    // Array signature consists of Type T and length as [T; length].
    let my_array: [i32; 5] = [1, 2, 3, 4, 5];

    // Tuple is a collection of values of different types
    // and is constructed using parentheses ().
    let my_tuple = (5u32, 1u8, true, -5.04f32);
}

Також див.:

бібліотеку std, mut, inference і shadowing

Літерали та оператори

Цілі числа 1, числа з плаваючою комою 1.2, символи 'a', рядки "abc", логічні значення true і одиничний тип () можуть бути виражені за допомогою літералів.

Цілі числа, альтернативно, можуть бути виражені у шістнадцятковому, вісімковому або двійковому записі з використанням таких префіксів відповідно: 0x, 0o або 0b.

Підкреслення можна вставляти в числові літерали для покращення читабельності, напр. 1_000 — це те саме, що 1000, а 0.000_001 — це те саме, що 0.000001.

Rust також підтримує науковий E-запис, напр. 1e6, 7.6e-4. Відповідний тип — f64.

Нам потрібно повідомити компілятору тип літералів, які ми використовуємо. Поки що ми будемо використовувати суфікс u32, щоб позначити, що літерал — це беззнакове 32-бітне ціле число, і суфікс i32, щоб позначити, що це знакове 32-бітне ціле число.

Доступні оператори та їхня пріоритетність у Rust схожі на інші мови, схожі на C.

fn main() {
    // Додавання цілих чисел
    println!("1 + 2 = {}", 1u32 + 2);

    // Віднімання цілих чисел
    println!("1 - 2 = {}", 1i32 - 2);
    // TODO ^ Спробуйте змінити `1i32` на `1u32`, щоб побачити, чому тип важливий

    // Науковий запис
    println!("1e4 is {}, -2.5e-3 is {}", 1e4, -2.5e-3);

    // Булева логіка з коротким замиканням
    println!("true AND false is {}", true && false);
    println!("true OR false is {}", true || false);
    println!("NOT true is {}", !true);

    // Побітові операції
    println!("0011 AND 0101 is {:04b}", 0b0011u32 & 0b0101);
    println!("0011 OR 0101 is {:04b}", 0b0011u32 | 0b0101);
    println!("0011 XOR 0101 is {:04b}", 0b0011u32 ^ 0b0101);
    println!("1 << 5 is {}", 1u32 << 5);
    println!("0x80 >> 2 is 0x{:x}", 0x80u32 >> 2);

    // Використовуйте підкреслення, щоб покращити читабельність!
    println!("One million is written as {}", 1_000_000u32);
}

Кортежі

Кортеж — це набір значень різних типів. Кортежі створюються за допомогою дужок (), і сам кожен кортеж є значенням із сигнатурою типу (T1, T2, ...), де T1, T2 — це типи його членів. Функції можуть використовувати кортежі для повернення кількох значень, оскільки кортежі можуть містити будь-яку кількість значень.

// Tuples can be used as function arguments and as return values.
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    // `let` can be used to bind the members of a tuple to variables.
    let (int_param, bool_param) = pair;

    (bool_param, int_param)
}

// The following struct is for the activity.
#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);

fn main() {
    // A tuple with a bunch of different types.
    let long_tuple = (1u8, 2u16, 3u32, 4u64,
                      -1i8, -2i16, -3i32, -4i64,
                      0.1f32, 0.2f64,
                      'a', true);

    // Values can be extracted from the tuple using tuple indexing.
    println!("Long tuple first value: {}", long_tuple.0);
    println!("Long tuple second value: {}", long_tuple.1);

    // Tuples can be tuple members.
    let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);

    // Tuples are printable.
    println!("tuple of tuples: {:?}", tuple_of_tuples);

    // But long Tuples (more than 12 elements) cannot be printed.
    //let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
    //println!("Too long tuple: {:?}", too_long_tuple);
    // TODO ^ Uncomment the above 2 lines to see the compiler error

    let pair = (1, true);
    println!("Pair is {:?}", pair);

    println!("The reversed pair is {:?}", reverse(pair));

    // To create one element tuples, the comma is required to tell them apart
    // from a literal surrounded by parentheses.
    println!("One element tuple: {:?}", (5u32,));
    println!("Just an integer: {:?}", (5u32));

    // Tuples can be destructured to create bindings.
    let tuple = (1, "hello", 4.5, true);

    let (a, b, c, d) = tuple;
    println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);

    let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
    println!("{:?}", matrix);
}

Активність

  1. Повторення: Додайте трейт fmt::Display до структури Matrix у наведеному вище прикладі, щоб якщо ви переключитеся з виведення формату налагодження {:?} на формат відображення {}, ви побачили такий вивід:

    ( 1.1 1.2 )
    ( 2.1 2.2 )
    

    Ви можете захотіти повернутися до прикладу для відображення друку.

  2. Додайте функцію transpose, використовуючи функцію reverse як шаблон, яка приймає матрицю як аргумент і повертає матрицю, у якій два елементи були поміняні місцями. Наприклад:

    println!("Matrix:\n{}", matrix);
    println!("Transpose:\n{}", transpose(matrix));

    Дає такий вивід:

    Matrix:
    ( 1.1 1.2 )
    ( 2.1 2.2 )
    Transpose:
    ( 1.1 2.1 )
    ( 1.2 2.2 )
    

Масиви та зрізи

Масив — це колекція об’єктів одного типу T, що зберігаються в суміжній пам’яті. Масиви створюються за допомогою дужок [], а їхня довжина, яка відома під час компіляції, є частиною їхньої сигнатури типу [T; length].

Зрізи схожі на масиви, але їхня довжина невідома під час компіляції. Натомість зріз — це об’єкт із двох слів; перше слово — це вказівник на дані, друге слово — це довжина зрізу. Розмір слова такий самий, як і usize, визначається архітектурою процесора, наприклад 64 біти на x86-64. Зрізи можна використовувати, щоб запозичити частину масиву, і вони мають сигнатуру типу &[T].

use std::mem;

// This function borrows a slice.
fn analyze_slice(slice: &[i32]) {
    println!("First element of the slice: {}", slice[0]);
    println!("The slice has {} elements", slice.len());
}

fn main() {
    // Fixed-size array (type signature is superfluous).
    let xs: [i32; 5] = [1, 2, 3, 4, 5];

    // All elements can be initialized to the same value.
    let ys: [i32; 500] = [0; 500];

    // Indexing starts at 0.
    println!("First element of the array: {}", xs[0]);
    println!("Second element of the array: {}", xs[1]);

    // `len` returns the count of elements in the array.
    println!("Number of elements in array: {}", xs.len());

    // Arrays are stack allocated.
    println!("Array occupies {} bytes", mem::size_of_val(&xs));

    // Arrays can be automatically borrowed as slices.
    println!("Borrow the whole array as a slice.");
    analyze_slice(&xs);

    // Slices can point to a section of an array.
    // They are of the form [starting_index..ending_index].
    // `starting_index` is the first position in the slice.
    // `ending_index` is one more than the last position in the slice.
    println!("Borrow a section of the array as a slice.");
    analyze_slice(&ys[1 .. 4]);

    // Example of empty slice `&[]`:
    let empty_array: [u32; 0] = [];
    assert_eq!(&empty_array, &[]);
    assert_eq!(&empty_array, &[][..]); // Same but more verbose

    // Arrays can be safely accessed using `.get`, which returns an
    // `Option`. This can be matched as shown below, or used with
    // `.expect()` if you would like the program to exit with a nice
    // message instead of happily continue.
    for i in 0..xs.len() + 1 { // Oops, one element too far!
        match xs.get(i) {
            Some(xval) => println!("{}: {}", i, xval),
            None => println!("Slow down! {} is too far!", i),
        }
    }

    // Out of bound indexing on array with constant value causes compile time error.
    //println!("{}", xs[5]);
    // Out of bound indexing on slice causes runtime error.
    //println!("{}", xs[..][5]);
}

Користувацькі типи

Користувацькі типи даних Rust формуються головним чином за допомогою двох ключових слів:

  • struct: визначає структуру
  • enum: визначає перелік

Константи також можуть бути створені за допомогою ключових слів const і static.

Структури

Існують три типи структур (“structs”), які можна створити за допомогою ключового слова struct:

  • Кортежні структури, які, по суті, є іменованими кортежами.
  • Класичні C structs
  • Одиничні структури, які не мають полів, корисні для узагальнених типів.
// An attribute to hide warnings for unused code.
#![allow(dead_code)]

#[derive(Debug)]
struct Person {
    name: String,
    age: u8,
}

// A unit struct
struct Unit;

// A tuple struct
struct Pair(i32, f32);

// A struct with two fields
struct Point {
    x: f32,
    y: f32,
}

// Structs can be reused as fields of another struct
struct Rectangle {
    // A rectangle can be specified by where the top left and bottom right
    // corners are in space.
    top_left: Point,
    bottom_right: Point,
}

fn main() {
    // Create struct with field init shorthand
    let name = String::from("Peter");
    let age = 27;
    let peter = Person { name, age };

    // Print debug struct
    println!("{:?}", peter);

    // Instantiate a `Point`
    let point: Point = Point { x: 5.2, y: 0.4 };
    let another_point: Point = Point { x: 10.3, y: 0.2 };

    // Access the fields of the point
    println!("point coordinates: ({}, {})", point.x, point.y);

    // Make a new point by using struct update syntax to use the fields of our
    // other one
    let bottom_right = Point { x: 10.3, ..another_point };

    // `bottom_right.y` will be the same as `another_point.y` because we used that field
    // from `another_point`
    println!("second point: ({}, {})", bottom_right.x, bottom_right.y);

    // Destructure the point using a `let` binding
    let Point { x: left_edge, y: top_edge } = point;

    let _rectangle = Rectangle {
        // struct instantiation is an expression too
        top_left: Point { x: left_edge, y: top_edge },
        bottom_right: bottom_right,
    };

    // Instantiate a unit struct
    let _unit = Unit;

    // Instantiate a tuple struct
    let pair = Pair(1, 0.1);

    // Access the fields of a tuple struct
    println!("pair contains {:?} and {:?}", pair.0, pair.1);

    // Destructure a tuple struct
    let Pair(integer, decimal) = pair;

    println!("pair contains {:?} and {:?}", integer, decimal);
}

Діяльність

  1. Додайте функцію rect_area, яка обчислює площу Rectangle (спробуйте використати вкладене деструктурування).
  2. Додайте функцію square, яка приймає Point і f32 як аргументи, і повертає Rectangle з його верхнім лівим кутом у точці, а шириною та висотою, що відповідають f32.

Дивіться також

attributes, raw identifiers and destructuring

Переліки

Ключове слово enum дозволяє створення типу, який може бути одним із кількох різних варіантів. Будь-який варіант, який є дійсним як struct, також є дійсним в enum.

// Створіть `enum` для класифікації вебподії. Зверніть увагу, як імена
// та інформація про тип разом визначають варіант:
// `PageLoad != PageUnload` і `KeyPress(char) != Paste(String)`.
// Кожен із них є різним і незалежним.
enum WebEvent {
    // Варіант `enum` може бути або `unit-like`,
    PageLoad,
    PageUnload,
    // як кортежні структури,
    KeyPress(char),
    Paste(String),
    // або структури у стилі C.
    Click { x: i64, y: i64 },
}

// Функція, яка приймає `WebEvent` enum як аргумент і
// не повертає нічого.
fn inspect(event: WebEvent) {
    match event {
        WebEvent::PageLoad => println!("page loaded"),
        WebEvent::PageUnload => println!("page unloaded"),
        // Деструктуруйте `c` зсередини варіанта `enum`.
        WebEvent::KeyPress(c) => println!("pressed '{}'.", c),
        WebEvent::Paste(s) => println!("pasted \"{}\".", s),
        // Деструктуруйте `Click` на `x` і `y`.
        WebEvent::Click { x, y } => {
            println!("clicked at x={}, y={}.", x, y);
        },
    }
}

fn main() {
    let pressed = WebEvent::KeyPress('x');
    // `to_owned()` створює власний `String` із рядкового зрізу.
    let pasted  = WebEvent::Paste("my text".to_owned());
    let click   = WebEvent::Click { x: 20, y: 80 };
    let load    = WebEvent::PageLoad;
    let unload  = WebEvent::PageUnload;

    inspect(pressed);
    inspect(pasted);
    inspect(click);
    inspect(load);
    inspect(unload);
}

Псевдоніми типів

Якщо ви використовуєте псевдонім типу, ви можете звертатися до кожного варіанта enum через його псевдонім. Це може бути корисно, якщо назва enum занадто довга або занадто загальна, і ви хочете перейменувати його.

enum VeryVerboseEnumOfThingsToDoWithNumbers {
    Add,
    Subtract,
}

// Створює псевдонім типу
type Operations = VeryVerboseEnumOfThingsToDoWithNumbers;

fn main() {
    // Ми можемо звертатися до кожного варіанта через його псевдонім, а не через його довгу і незручну
    // назву.
    let x = Operations::Add;
}

Найпоширеніше місце, де ви це побачите, — це блоки impl, що використовують псевдонім Self.

enum VeryVerboseEnumOfThingsToDoWithNumbers {
    Add,
    Subtract,
}

impl VeryVerboseEnumOfThingsToDoWithNumbers {
    fn run(&self, x: i32, y: i32) -> i32 {
        match self {
            Self::Add => x + y,
            Self::Subtract => x - y,
        }
    }
}

Щоб дізнатися більше про переліки та псевдоніми типів, ви можете прочитати звіт про стабілізацію відтоді, коли ця можливість була стабілізована в Rust.

Див. також:

match, fn, and String, “Type alias enum variants” RFC

use

Декларацію use можна використовувати, щоб уникнути введення повного шляху до модуля для доступу до імені:

// An attribute to hide warnings for unused code.
#![allow(dead_code)]

enum Stage {
    Beginner,
    Advanced,
}

enum Role {
    Student,
    Teacher,
}

fn main() {
    // Explicitly `use` each name so they are available without
    // manual scoping.
    use Stage::{Beginner, Advanced};
    // Automatically `use` each name inside `Role`.
    use Role::*;

    // Equivalent to `Stage::Beginner`.
    let stage = Beginner;
    // Equivalent to `Role::Student`.
    let role = Student;

    match stage {
        // Note the lack of scoping because of the explicit `use` above.
        Beginner => println!("Beginners are starting their learning journey!"),
        Advanced => println!("Advanced learners are mastering their subjects..."),
    }

    match role {
        // Note again the lack of scoping.
        Student => println!("Students are acquiring knowledge!"),
        Teacher => println!("Teachers are spreading knowledge!"),
    }
}

Дивіться також:

match та use

C-like

enum також може використовуватися як C-подібні enum.

// An attribute to hide warnings for unused code.
#![allow(dead_code)]

// enum with implicit discriminator (starts at 0)
enum Number {
    Zero,
    One,
    Two,
}

// enum with explicit discriminator
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

fn main() {
    // `enums` can be cast as integers.
    println!("zero is {}", Number::Zero as i32);
    println!("one is {}", Number::One as i32);

    println!("roses are #{:06x}", Color::Red as u32);
    println!("violets are #{:06x}", Color::Blue as u32);
}

Див. також:

casting

Тестовий приклад: пов’язаний список

Поширений спосіб реалізувати пов’язаний список — через enums:

use crate::List::*;

enum List {
    // Cons: Tuple struct that wraps an element and a pointer to the next node
    Cons(u32, Box<List>),
    // Nil: A node that signifies the end of the linked list
    Nil,
}

// Methods can be attached to an enum
impl List {
    // Create an empty list
    fn new() -> List {
        // `Nil` has type `List`
        Nil
    }

    // Consume a list, and return the same list with a new element at its front
    fn prepend(self, elem: u32) -> List {
        // `Cons` also has type List
        Cons(elem, Box::new(self))
    }

    // Return the length of the list
    fn len(&self) -> u32 {
        // `self` has to be matched, because the behavior of this method
        // depends on the variant of `self`
        // `self` has type `&List`, and `*self` has type `List`, matching on a
        // concrete type `T` is preferred over a match on a reference `&T`
        // after Rust 2018 you can use self here and tail (with no ref) below as well,
        // rust will infer &s and ref tail.
        // See https://doc.rust-lang.org/edition-guide/rust-2018/ownership-and-lifetimes/default-match-bindings.html
        match *self {
            // Can't take ownership of the tail, because `self` is borrowed;
            // instead take a reference to the tail
            // And it's a non-tail recursive call which may cause stack overflow for long lists.
            Cons(_, ref tail) => 1 + tail.len(),
            // Base Case: An empty list has zero length
            Nil => 0
        }
    }

    // Return representation of the list as a (heap allocated) string
    fn stringify(&self) -> String {
        match *self {
            Cons(head, ref tail) => {
                // `format!` is similar to `print!`, but returns a heap
                // allocated string instead of printing to the console
                format!("{}, {}", head, tail.stringify())
            },
            Nil => {
                format!("Nil")
            },
        }
    }
}

fn main() {
    // Create an empty linked list
    let mut list = List::new();

    // Prepend some elements
    list = list.prepend(1);
    list = list.prepend(2);
    list = list.prepend(3);

    // Show the final state of the list
    println!("linked list has length: {}", list.len());
    println!("{}", list.stringify());
}

Дивіться також:

Box і методи

constants

У Rust є два різні типи констант, які можна оголошувати в будь-якій області видимості включно із глобальною. Обидва вимагають явного зазначення типу:

  • const: незмінне значення (загальний випадок).
  • static: можлива змінна з часом життя 'static. Час життя static виводиться і його не потрібно вказувати. Доступ до змінної static або її зміна є unsafe.
// Globals are declared outside all other scopes.
static LANGUAGE: &str = "Rust";
const THRESHOLD: i32 = 10;

fn is_big(n: i32) -> bool {
    // Access constant in some function
    n > THRESHOLD
}

fn main() {
    let n = 16;

    // Access constant in the main thread
    println!("This is {}", LANGUAGE);
    println!("The threshold is {}", THRESHOLD);
    println!("{} is {}", n, if is_big(n) { "big" } else { "small" });

    // Error! Cannot modify a `const`.
    THRESHOLD = 5;
    // FIXME ^ Comment out this line
}

Дивіться також:

The const/static RFC, 'static lifetime

Зв’язування змінних

Rust надає безпеку типів через статичну типізацію. Зв’язування змінних можна анотувати типом під час оголошення. Однак у більшості випадків компілятор зможе вивести тип змінної з контексту, значно зменшуючи обсяг анотування.

Значення (наприклад, літерали) можна прив’язувати до змінних, використовуючи let-зв’язування.

fn main() {
    let an_integer = 1u32;
    let a_boolean = true;
    let unit = ();

    // copy `an_integer` into `copied_integer`
    let copied_integer = an_integer;

    println!("An integer: {}", copied_integer);
    println!("A boolean: {}", a_boolean);
    println!("Meet the unit value: {:?}", unit);

    // The compiler warns about unused variable bindings; these warnings can
    // be silenced by prefixing the variable name with an underscore
    let _unused_variable = 3u32;

    let noisy_unused_variable = 2u32;
    // FIXME ^ Prefix with an underscore to suppress the warning
    // Please note that warnings may not be shown in a browser
}

Змінність (mutability)

Зв’язування змінних за замовчуванням є незмінними, але це можна перевизначити, використовуючи модифікатор mut.

fn main() {
    let _immutable_binding = 1;
    let mut mutable_binding = 1;

    println!("Before mutation: {}", mutable_binding);

    // Ok
    mutable_binding += 1;

    println!("After mutation: {}", mutable_binding);

    // Error! Cannot assign a new value to an immutable variable
    _immutable_binding += 1;
}

Компілятор видасть докладну діагностику про помилки змінності.

Область видимості та затінення

Прив’язки змінних мають область видимості та обмежені часом життя у блоці. Блок — це набір операторів, укладених у фігурні дужки {}.

fn main() {
    // This binding lives in the main function
    let long_lived_binding = 1;

    // This is a block, and has a smaller scope than the main function
    {
        // This binding only exists in this block
        let short_lived_binding = 2;

        println!("inner short: {}", short_lived_binding);
    }
    // End of the block

    // Error! `short_lived_binding` doesn't exist in this scope
    println!("outer short: {}", short_lived_binding);
    // FIXME ^ Comment out this line

    println!("outer long: {}", long_lived_binding);
}

Також дозволене затінення змінної.

fn main() {
    let shadowed_binding = 1;

    {
        println!("before being shadowed: {}", shadowed_binding);

        // This binding *shadows* the outer one
        let shadowed_binding = "abc";

        println!("shadowed in inner block: {}", shadowed_binding);
    }
    println!("outside inner block: {}", shadowed_binding);

    // This binding *shadows* the previous binding
    let shadowed_binding = 2;
    println!("shadowed in outer block: {}", shadowed_binding);
}

Спочатку оголошення

Можна спочатку оголосити прив’язки змінних, а ініціалізувати їх пізніше, але всі прив’язки змінних мають бути ініціалізовані до того, як їх буде використано: компілятор забороняє використання неініціалізованих прив’язок змінних, оскільки це призвело б до невизначеної поведінки.

Непоширено спочатку оголошувати прив’язку змінної, а потім ініціалізувати її пізніше у функції. Читачеві важче знайти ініціалізацію, коли ініціалізація відокремлена від оголошення. Звично оголошувати й ініціалізувати прив’язку змінної поблизу того місця, де змінну буде використано.

fn main() {
    // Declare a variable binding
    let a_binding;

    {
        let x = 2;

        // Initialize the binding
        a_binding = x * x;
    }

    println!("a binding: {}", a_binding);

    let another_binding;

    // Error! Use of uninitialized binding
    println!("another binding: {}", another_binding);
    // FIXME ^ Comment out this line

    another_binding = 1;

    println!("another binding: {}", another_binding);
}

Замороження

Коли дані зв’язані тим самим ім’ям незмінно, вони також заморожуються. Заморожені дані не можна змінювати, доки незмінне зв’язування не вийде з області видимості:

fn main() {
    let mut _mutable_integer = 7i32;

    {
        // Shadowing by immutable `_mutable_integer`
        let _mutable_integer = _mutable_integer;

        // Error! `_mutable_integer` is frozen in this scope
        _mutable_integer = 50;
        // FIXME ^ Comment out this line

        // `_mutable_integer` goes out of scope
    }

    // Ok! `_mutable_integer` is not frozen in this scope
    _mutable_integer = 3;
}

Типи

Rust надає кілька механізмів, щоб змінювати або визначати тип примітивних і користувацьких типів. У таких розділах розглядається:

Casting

Rust не надає неявного перетворення типів (coercion) між примітивними типами. Але явне перетворення типів (casting) можна виконати за допомогою ключового слова as.

Правила перетворення між цілочисельними типами загалом відповідають угодам C, за винятком випадків, де в C є невизначена поведінка. Поведінка всіх перетворень між цілочисельними типами в Rust чітко визначена.

// Suppress all errors from casts which overflow.
#![allow(overflowing_literals)]

fn main() {
    let decimal = 65.4321_f32;

    // Error! No implicit conversion
    let integer: u8 = decimal;
    // FIXME ^ Comment out this line

    // Explicit conversion
    let integer = decimal as u8;
    let character = integer as char;

    // Error! There are limitations in conversion rules.
    // A float cannot be directly converted to a char.
    let character = decimal as char;
    // FIXME ^ Comment out this line

    println!("Casting: {} -> {} -> {}", decimal, integer, character);

    // when casting any value to an unsigned type, T,
    // T::MAX + 1 is added or subtracted until the value
    // fits into the new type ONLY when the #![allow(overflowing_literals)]
    // lint is specified like above. Otherwise there will be a compiler error.

    // 1000 already fits in a u16
    println!("1000 as a u16 is: {}", 1000 as u16);

    // 1000 - 256 - 256 - 256 = 232
    // Under the hood, the first 8 least significant bits (LSB) are kept,
    // while the rest towards the most significant bit (MSB) get truncated.
    println!("1000 as a u8 is : {}", 1000 as u8);
    // -1 + 256 = 255
    println!("  -1 as a u8 is : {}", (-1i8) as u8);

    // For positive numbers, this is the same as the modulus
    println!("1000 mod 256 is : {}", 1000 % 256);

    // When casting to a signed type, the (bitwise) result is the same as
    // first casting to the corresponding unsigned type. If the most significant
    // bit of that value is 1, then the value is negative.

    // Unless it already fits, of course.
    println!(" 128 as a i16 is: {}", 128 as i16);

    // In boundary case 128 value in 8-bit two's complement representation is -128
    println!(" 128 as a i8 is : {}", 128 as i8);

    // repeating the example above
    // 1000 as u8 -> 232
    println!("1000 as a u8 is : {}", 1000 as u8);
    // and the value of 232 in 8-bit two's complement representation is -24
    println!(" 232 as a i8 is : {}", 232 as i8);

    // Since Rust 1.45, the `as` keyword performs a *saturating cast*
    // when casting from float to int. If the floating point value exceeds
    // the upper bound or is less than the lower bound, the returned value
    // will be equal to the bound crossed.

    // 300.0 as u8 is 255
    println!(" 300.0 as u8 is : {}", 300.0_f32 as u8);
    // -100.0 as u8 is 0
    println!("-100.0 as u8 is : {}", -100.0_f32 as u8);
    // nan as u8 is 0
    println!("   nan as u8 is : {}", f32::NAN as u8);

    // This behavior incurs a small runtime cost and can be avoided
    // with unsafe methods, however the results might overflow and
    // return **unsound values**. Use these methods wisely:
    unsafe {
        // 300.0 as u8 is 44
        println!(" 300.0 as u8 is : {}", 300.0_f32.to_int_unchecked::<u8>());
        // -100.0 as u8 is 156
        println!("-100.0 as u8 is : {}", (-100.0_f32).to_int_unchecked::<u8>());
        // nan as u8 is 0
        println!("   nan as u8 is : {}", f32::NAN.to_int_unchecked::<u8>());
    }
}

Літерали

Числові літерали можна анотувати типом, додавши тип як суфікс. Наприклад, щоб вказати, що літерал 42 має мати тип i32, напишіть 42i32.

Тип несуфіксованих числових літералів залежатиме від того, як їх використовують. Якщо немає жодного обмеження, компілятор використовуватиме i32 для цілих чисел і f64 для чисел із плаваючою комою.

fn main() {
    // Суфіксовані літерали, їхні типи відомі під час ініціалізації
    let x = 1u8;
    let y = 2u32;
    let z = 3f32;

    // Несуфіксовані літерали, їхні типи залежать від того, як їх використовують
    let i = 1;
    let f = 1.0;

    // `size_of_val` повертає розмір змінної в байтах
    println!("size of `x` in bytes: {}", std::mem::size_of_val(&x));
    println!("size of `y` in bytes: {}", std::mem::size_of_val(&y));
    println!("size of `z` in bytes: {}", std::mem::size_of_val(&z));
    println!("size of `i` in bytes: {}", std::mem::size_of_val(&i));
    println!("size of `f` in bytes: {}", std::mem::size_of_val(&f));
}

У попередньому коді використано кілька понять, які ще не було пояснено далі, ось коротке пояснення для нетерплячих читачів:

  • std::mem::size_of_val — це функція, але викликана за своїм повним шляхом. Код можна розділити на логічні одиниці, які називаються модулями. У цьому випадку функція size_of_val визначена в модулі mem, а модуль mem визначений у крейді std. Для докладнішої інформації дивіться модулі і крейти.

Інференція

Механізм виведення типів досить розумний. Він робить більше, ніж просто дивиться на тип виразу значення під час ініціалізації. Він також дивиться на те, як змінна використовується пізніше, щоб вивести її тип. Ось просунутий приклад виведення типів:

fn main() {
    // Because of the annotation, the compiler knows that `elem` has type u8.
    let elem = 5u8;

    // Create an empty vector (a growable array).
    let mut vec = Vec::new();
    // At this point the compiler doesn't know the exact type of `vec`, it
    // just knows that it's a vector of something (`Vec<_>`).

    // Insert `elem` in the vector.
    vec.push(elem);
    // Aha! Now the compiler knows that `vec` is a vector of `u8`s (`Vec<u8>`)
    // TODO ^ Try commenting out the `vec.push(elem)` line

    println!("{:?}", vec);
}

Жодної анотації типу для змінних не знадобилося, компілятор задоволений, і програміст також!

Псевдоніми

Використовуючи type можна дати нове ім’я існуючому типу. Зазвичай, типи повинні мати назви у стилі UpperCamelCase, інакше компілятор виведе попередження. Виключенням є лише примітивні типи: usize, f32 і т.д.

// `NanoSecond`, `Inch`, and `U64` are new names for `u64`.
type NanoSecond = u64;
type Inch = u64;
type U64 = u64;

fn main() {
    // `NanoSecond` = `Inch` = `U64` = `u64`.
    let nanoseconds: NanoSecond = 5 as u64;
    let inches: Inch = 2 as U64;

    // Note that type aliases *don't* provide any extra type safety, because
    // aliases are *not* new types
    println!("{} nanoseconds + {} inches = {} unit?",
             nanoseconds,
             inches,
             nanoseconds + inches);
}

The main use of aliases is to reduce boilerplate; for example the io::Result<T> type is an alias for the Result<T, io::Error> type.

Дивіться також:

Атрибути

Перетворення

Примітивні типи можуть бути перетворені один в один за допомогою casting.

Rust вирішує перетворення між користувацькими типами (тобто struct і enum) за допомогою traits. Узагальнені перетворення використовуватимуть трейт-и From і Into. Однак існують і більш спеціалізовані для найпоширеніших випадків, зокрема під час перетворення до і з Strings.

From і Into

Трейт-и From і Into невід’ємно пов’язані, і це фактично є частиною його реалізації. Якщо ви можете перетворити тип A з типу B, тоді легко повірити, що ми повинні мати змогу перетворити тип B у тип A.

From

Трейт From дозволяє типу визначити, як створити самого себе з іншого типу, тим самим надаючи дуже простий механізм для перетворення між кількома типами. У стандартній бібліотеці є численні реалізації цього трейт-а для перетворення примітивних і поширених типів.

Наприклад, ми можемо легко перетворити str у String

#![allow(unused)]
fn main() {
let my_str = "hello";
let my_string = String::from(my_str);
}

Ми можемо зробити щось подібне, щоб визначити перетворення для нашого власного типу.

use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let num = Number::from(30);
    println!("My number is {:?}", num);
}

Into

Трейт Into — це просто зворотний трейт до From. Він визначає, як перетворити один тип в інший тип.

Виклик into() зазвичай вимагає від нас указати тип результату, оскільки компілятор у більшості випадків не може визначити це сам.

use std::convert::Into;

#[derive(Debug)]
struct Number {
    value: i32,
}

impl Into<Number> for i32 {
    fn into(self) -> Number {
        Number { value: self }
    }
}

fn main() {
    let int = 5;
    // Try removing the type annotation
    let num: Number = int.into();
    println!("My number is {:?}", num);
}

From and Into are interchangeable

From і Into призначені бути взаємодоповнювальними. Нам не потрібно надавати реалізацію для обох трейтів. Якщо ви реалізували трейт From для вашого типу, Into викличе його за потреби. Зауважте, однак, що зворотне не є істинним: реалізація Into для вашого типу не надасть йому автоматично реалізацію From.

use std::convert::From;

#[derive(Debug)]
struct Number {
    value: i32,
}

// Define `From`
impl From<i32> for Number {
    fn from(item: i32) -> Self {
        Number { value: item }
    }
}

fn main() {
    let int = 5;
    // use `Into`
    let num: Number = int.into();
    println!("My number is {:?}", num);
}

TryFrom and TryInto

Подібно до From and Into, TryFrom and TryInto — це узагальнені трейт-и для перетворення між типами. На відміну від From/Into, трейт-и TryFrom/TryInto використовуються для перетворень, які можуть завершитися помилкою, і тому повертають Results.

use std::convert::TryFrom;
use std::convert::TryInto;

#[derive(Debug, PartialEq)]
struct EvenNumber(i32);

impl TryFrom<i32> for EvenNumber {
    type Error = ();

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        if value % 2 == 0 {
            Ok(EvenNumber(value))
        } else {
            Err(())
        }
    }
}

fn main() {
    // TryFrom

    assert_eq!(EvenNumber::try_from(8), Ok(EvenNumber(8)));
    assert_eq!(EvenNumber::try_from(5), Err(()));

    // TryInto

    let result: Result<EvenNumber, ()> = 8i32.try_into();
    assert_eq!(result, Ok(EvenNumber(8)));
    let result: Result<EvenNumber, ()> = 5i32.try_into();
    assert_eq!(result, Err(()));
}

До та з рядків (Strings)

Перетворення в String

Щоб перетворити будь-який тип на String, достатньо реалізувати трейт ToString для цього типу. Замість того щоб робити це напряму, слід реалізувати трейт fmt::Display, який автоматично надає ToString і також дозволяє виводити тип, як описано в розділі про print!.

use std::fmt;

struct Circle {
    radius: i32
}

impl fmt::Display for Circle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "Circle of radius {}", self.radius)
    }
}

fn main() {
    let circle = Circle { radius: 6 };
    println!("{}", circle.to_string());
}

Розбір рядка

Корисно перетворювати рядки на багато типів, але однією з найпоширеніших операцій над рядками є перетворення їх із рядка на число. Ідіоматичний підхід до цього — використовувати функцію parse і або покластися на виведення типу, або вказати тип, у який треба виконати розбір, використовуючи синтаксис turbofish. Обидва варіанти показано в наведеному нижче прикладі.

Це перетворить рядок на вказаний тип, якщо для цього типу реалізовано трейт FromStr. Це реалізовано для багатьох типів у стандартній бібліотеці.

fn main() {
    let parsed: i32 = "5".parse().unwrap();
    let turbo_parsed = "10".parse::<i32>().unwrap();

    let sum = parsed + turbo_parsed;
    println!("Sum: {:?}", sum);
}

Щоб отримати цю функціональність для типу, визначеного користувачем, просто реалізуйте трейт FromStr для цього типу.

use std::num::ParseIntError;
use std::str::FromStr;

#[derive(Debug)]
struct Circle {
    radius: i32,
}

impl FromStr for Circle {
    type Err = ParseIntError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.trim().parse() {
            Ok(num) => Ok(Circle{ radius: num }),
            Err(e) => Err(e),
        }
    }
}

fn main() {
    let radius = "    3 ";
    let circle: Circle = radius.parse().unwrap();
    println!("{:?}", circle);
}

Вирази

Програма Rust складається (переважно) з послідовності операторів:

fn main() {
    // statement
    // statement
    // statement
}

У Rust є кілька видів операторів. Найпоширеніші два — це оголошення зв’язування змінної та використання ; із виразом:

fn main() {
    // variable binding
    let x = 5;

    // expression;
    x;
    x + 1;
    15;
}

Блоки також є виразами, тож їх можна використовувати як значення в присвоєннях. Останній вираз у блоці буде присвоєно місцю-виразу, такому як локальна змінна. Однак якщо останній вираз блоку закінчується крапкою з комою, повернене значення буде ().

fn main() {
    let x = 5u32;

    let y = {
        let x_squared = x * x;
        let x_cube = x_squared * x;

        // This expression will be assigned to `y`
        x_cube + x_squared + x
    };

    let z = {
        // The semicolon suppresses this expression and `()` is assigned to `z`
        2 * x;
    };

    println!("x is {:?}", x);
    println!("y is {:?}", y);
    println!("z is {:?}", z);
}

Потік керування

Невід’ємною частиною будь-якої мови програмування є способи змінювати потік керування: if/else, for та інші. Давайте поговоримо про них у Rust.

if/else

Розгалуження з if-else подібне до інших мов. На відміну від багатьох із них, булева умова не потребує дужок, і за кожною умовою слідує блок. Умовні конструкції if-else є виразами, і всі гілки мають повертати той самий тип.

fn main() {
    let n = 5;

    if n < 0 {
        print!("{} is negative", n);
    } else if n > 0 {
        print!("{} is positive", n);
    } else {
        print!("{} is zero", n);
    }

    let big_n =
        if n < 10 && n > -10 {
            println!(", and is a small number, increase ten-fold");

            // This expression returns an `i32`.
            10 * n
        } else {
            println!(", and is a big number, halve the number");

            // This expression must return an `i32` as well.
            n / 2
            // TODO ^ Try suppressing this expression with a semicolon.
        };
    //   ^ Don't forget to put a semicolon here! All `let` bindings need it.

    println!("{} -> {}", n, big_n);
}

loop

Rust надає ключове слово loop, щоб позначити нескінченний цикл.

Оператор break можна використати, щоб вийти з циклу будь-коли, тоді як оператор continue можна використати, щоб пропустити решту ітерації та почати нову.

fn main() {
    let mut count = 0u32;

    println!("Let's count until infinity!");

    // Infinite loop
    loop {
        count += 1;

        if count == 3 {
            println!("three");

            // Skip the rest of this iteration
            continue;
        }

        println!("{}", count);

        if count == 5 {
            println!("OK, that's enough");

            // Exit this loop
            break;
        }
    }
}

Вкладені цикли та мітки

Можливо break або continue зовнішніх циклів, коли йдеться про вкладені цикли. У цих випадках цикли мають бути позначені деякою 'label, а мітку потрібно передати в оператор break/continue.

#![allow(unreachable_code, unused_labels)]

fn main() {
    'outer: loop {
        println!("Entered the outer loop");

        'inner: loop {
            println!("Entered the inner loop");

            // This would break only the inner loop
            //break;

            // This breaks the outer loop
            break 'outer;
        }

        println!("This point will never be reached");
    }

    println!("Exited the outer loop");
}

Повернення з циклів

Одним із використань loop є повторна спроба виконання операції, доки вона не буде успішною. Якщо ж операція повертає значення, вам може знадобитися передати його решті коду: поставте його після break, і його буде повернено виразом loop.

fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

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

    assert_eq!(result, 20);
}

while

Ключове слово while можна використовувати для запуску циклу, поки умова є істинною.

Давайте напишемо сумнозвісний FizzBuzz, використовуючи цикл while.

fn main() {
    // A counter variable
    let mut n = 1;

    // Loop while `n` is less than 101
    while n < 101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }

        // Increment counter
        n += 1;
    }
}

цикли for

for і range

Конструкцію for in можна використовувати для ітерації через Iterator. Один із найпростіших способів створити ітератор — використати нотацію діапазону a..b. Це дає значення від a (включно) до b (виключно) з кроком один.

Давайте напишемо FizzBuzz, використовуючи for замість while.

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    for n in 1..101 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

Альтернативно, a..=b можна використати для діапазону, який є включним з обох кінців. Вище можна записати так:

fn main() {
    // `n` will take the values: 1, 2, ..., 100 in each iteration
    for n in 1..=100 {
        if n % 15 == 0 {
            println!("fizzbuzz");
        } else if n % 3 == 0 {
            println!("fizz");
        } else if n % 5 == 0 {
            println!("buzz");
        } else {
            println!("{}", n);
        }
    }
}

Просто пам’ятайте, що хоча ви можете скомпілювати код, коли a>b, цикл ніколи не виконується.

for i in 10..1{
println!("fizzbuzz");
}

Якщо ви хочете рахувати у зворотному напрямку, вам потрібно використати .rev() замість цього

for i in (1..10).rev(){
println!("fizzbuzz");
}

for і ітератори

Конструкція for in може взаємодіяти з Iterator кількома способами. Як обговорювалося в розділі про трейт Iterator, за замовчуванням цикл for застосує функцію into_iter до колекції. Однак це не єдиний спосіб перетворення колекцій на ітератори.

into_iter, iter і iter_mut усі виконують перетворення колекції на ітератор різними способами, надаючи різні подання даних усередині.

  • iter - Це запозичує кожен елемент колекції через кожну ітерацію. Таким чином, колекція залишається недоторканою і доступною для повторного використання після циклу.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter() {
        match name {
            &"Ferris" => println!("There is a rustacean among us!"),
            // TODO ^ Try deleting the & and matching just "Ferris"
            _ => println!("Hello {}", name),
        }
    }

    println!("names: {:?}", names);
}
  • into_iter - Це споживає колекцію, так що на кожній ітерації надаються точні дані. Щойно колекцію було спожито, вона більше не доступна для повторного використання, оскільки її було ‘переміщено’ всередині циклу.
fn main() {
    let names = vec!["Bob", "Frank", "Ferris"];

    for name in names.into_iter() {
        match name {
            "Ferris" => println!("There is a rustacean among us!"),
            _ => println!("Hello {}", name),
        }
    }

    // `names` has been 'moved' and can no longer be used.
    // Try uncommenting the line below to see the compiler error:
    // println!("names: {:?}", names);
}
  • iter_mut - Це змінно запозичує кожен елемент колекції, дозволяючи змінювати колекцію на місці.
fn main() {
    let mut names = vec!["Bob", "Frank", "Ferris"];

    for name in names.iter_mut() {
        *name = match name {
            &mut "Ferris" => "There is a rustacean among us!",
            _ => "Hello",
        }
    }

    println!("names: {:?}", names);
}

У наведених вище фрагментах зверніть увагу на тип гілки match, це ключова різниця в типах ітерації. Різниця в типі, звісно, передбачає різні дії, які можна виконати.

Дивіться також:

Iterator

зіставлення (match)

Rust надає зіставлення зі зразком через ключове слово match, яке можна використовувати як C switch. Обчислюється перша відповідна гілка, і мають бути охоплені всі можливі значення.

fn main() {
    let number = 13;
    // TODO ^ Try different values for `number`

    println!("Tell me about {}", number);
    match number {
        // Match a single value
        1 => println!("One!"),
        // Match several values
        2 | 3 | 5 | 7 | 11 => println!("This is a prime"),
        // TODO ^ Try adding 13 to the list of prime values
        // Match an inclusive range
        13..=19 => println!("A teen"),
        // Handle the rest of cases
        _ => println!("Ain't special"),
        // TODO ^ Try commenting out this catch-all arm
    }

    let boolean = true;
    // Match is an expression too
    let binary = match boolean {
        // The arms of a match must cover all the possible values
        false => 0,
        true => 1,
        // TODO ^ Try commenting out one of these arms
    };

    println!("{} -> {}", boolean, binary);
}

Деструктурування

Блок match може деструктурувати елементи різними способами.

Дивіться також:

The Rust Reference for Destructuring

кортежі

Кортежі можна деструктурувати в match так, як показано нижче:

fn main() {
    let triple = (0, -2, 3);
    // TODO ^ Try different values for `triple`

    println!("Tell me about {:?}", triple);
    // Match can be used to destructure a tuple
    match triple {
        // Destructure the second and third elements
        (0, y, z) => println!("First is `0`, `y` is {:?}, and `z` is {:?}", y, z),
        (1, ..)  => println!("First is `1` and the rest doesn't matter"),
        (.., 2)  => println!("last is `2` and the rest doesn't matter"),
        (3, .., 4)  => println!("First is `3`, last is `4`, and the rest doesn't matter"),
        // `..` can be used to ignore the rest of the tuple
        _      => println!("It doesn't matter what they are"),
        // `_` means don't bind the value to a variable
    }
}

Дивіться також:

Кортежі

arrays/slices

Як і кортежі, масиви та зрізи можна деструктурувати так:

fn main() {
    // Try changing the values in the array, or make it a slice!
    let array = [1, -2, 6];

    match array {
        // Binds the second and the third elements to the respective variables
        [0, second, third] =>
            println!("array[0] = 0, array[1] = {}, array[2] = {}", second, third),

        // Single values can be ignored with _
        [1, _, third] => println!(
            "array[0] = 1, array[2] = {} and array[1] was ignored",
            third
        ),

        // You can also bind some and ignore the rest
        [-1, second, ..] => println!(
            "array[0] = -1, array[1] = {} and all the other ones were ignored",
            second
        ),
        // The code below would not compile
        // [-1, second] => ...

        // Or store them in another array/slice (the type depends on
        // that of the value that is being matched against)
        [3, second, tail @ ..] => println!(
            "array[0] = 3, array[1] = {} and the other elements were {:?}",
            second, tail
        ),

        // Combining these patterns, we can, for example, bind the first and
        // last values, and store the rest of them in a single array
        [first, middle @ .., last] => println!(
            "array[0] = {}, middle = {:?}, array[2] = {}",
            first, middle, last
        ),
    }
}

Дивіться також:

Масиви та зрізи і зв’язування для @ sigil

перелічення

enum розбирається на складові подібним чином:

// `allow` required to silence warnings because only
// one variant is used.
#[allow(dead_code)]
enum Color {
    // These 3 are specified solely by their name.
    Red,
    Blue,
    Green,
    // These likewise tie `u32` tuples to different names: color models.
    RGB(u32, u32, u32),
    HSV(u32, u32, u32),
    HSL(u32, u32, u32),
    CMY(u32, u32, u32),
    CMYK(u32, u32, u32, u32),
}

fn main() {
    let color = Color::RGB(122, 17, 40);
    // TODO ^ Try different variants for `color`

    println!("What color is it?");
    // An `enum` can be destructured using a `match`.
    match color {
        Color::Red   => println!("The color is Red!"),
        Color::Blue  => println!("The color is Blue!"),
        Color::Green => println!("The color is Green!"),
        Color::RGB(r, g, b) =>
            println!("Red: {}, green: {}, and blue: {}!", r, g, b),
        Color::HSV(h, s, v) =>
            println!("Hue: {}, saturation: {}, value: {}!", h, s, v),
        Color::HSL(h, s, l) =>
            println!("Hue: {}, saturation: {}, lightness: {}!", h, s, l),
        Color::CMY(c, m, y) =>
            println!("Cyan: {}, magenta: {}, yellow: {}!", c, m, y),
        Color::CMYK(c, m, y, k) =>
            println!("Cyan: {}, magenta: {}, yellow: {}, key (black): {}!",
                c, m, y, k),
        // Don't need another arm because all variants have been examined
    }
}

Дивіться також:

#[allow(...)], color models and enum

pointers/ref

Для вказівників потрібно зробити розрізнення між деструктуризацією та розіменуванням, оскільки це різні концепції, які використовуються інакше, ніж у мовах на кшталт C/C++.

  • Розіменування використовує *
  • Деструктуризація використовує &, ref і ref mut
fn main() {
    // Assign a reference of type `i32`. The `&` signifies there
    // is a reference being assigned.
    let reference = &4;

    match reference {
        // If `reference` is pattern matched against `&val`, it results
        // in a comparison like:
        // `&i32`
        // `&val`
        // ^ We see that if the matching `&`s are dropped, then the `i32`
        // should be assigned to `val`.
        &val => println!("Got a value via destructuring: {:?}", val),
    }

    // To avoid the `&`, you dereference before matching.
    match *reference {
        val => println!("Got a value via dereferencing: {:?}", val),
    }

    // What if you don't start with a reference? `reference` was a `&`
    // because the right side was already a reference. This is not
    // a reference because the right side is not one.
    let _not_a_reference = 3;

    // Rust provides `ref` for exactly this purpose. It modifies the
    // assignment so that a reference is created for the element; this
    // reference is assigned.
    let ref _is_a_reference = 3;

    // Accordingly, by defining 2 values without references, references
    // can be retrieved via `ref` and `ref mut`.
    let value = 5;
    let mut mut_value = 6;

    // Use `ref` keyword to create a reference.
    match value {
        ref r => println!("Got a reference to a value: {:?}", r),
    }

    // Use `ref mut` similarly.
    match mut_value {
        ref mut m => {
            // Got a reference. Gotta dereference it before we can
            // add anything to it.
            *m += 10;
            println!("We added 10. `mut_value`: {:?}", m);
        },
    }
}

Дивіться також:

Патерн ref

структури

Аналогічно, struct можна деструктурувати, як показано:

fn main() {
    struct Foo {
        x: (u32, u32),
        y: u32,
    }

    // Try changing the values in the struct to see what happens
    let foo = Foo { x: (1, 2), y: 3 };

    match foo {
        Foo { x: (1, b), y } => println!("First of x is 1, b = {},  y = {} ", b, y),

        // you can destructure structs and rename the variables,
        // the order is not important
        Foo { y: 2, x: i } => println!("y is 2, i = {:?}", i),

        // and you can also ignore some variables:
        Foo { y, .. } => println!("y = {}, we don't care about x", y),
        // this will give an error: pattern does not mention field `x`
        //Foo { y } => println!("y = {}", y),
    }

    let faa = Foo { x: (1, 2), y: 3 };

    // You do not need a match block to destructure structs:
    let Foo { x : x0, y: y0 } = faa;
    println!("Outside: x0 = {x0:?}, y0 = {y0}");

    // Destructuring works with nested structs as well:
    struct Bar {
        foo: Foo,
    }

    let bar = Bar { foo: faa };
    let Bar { foo: Foo { x: nested_x, y: nested_y } } = bar;
    println!("Nested: nested_x = {nested_x:?}, nested_y = {nested_y:?}");
}

Див. також:

Структури

Охоронні умови

До match можна додати охоронну умову, щоб відфільтрувати гілку.

#[allow(dead_code)]
enum Temperature {
    Celsius(i32),
    Fahrenheit(i32),
}

fn main() {
    let temperature = Temperature::Celsius(35);
    // ^ TODO try different values for `temperature`

    match temperature {
        Temperature::Celsius(t) if t > 30 => println!("{}C is above 30 Celsius", t),
        // The `if condition` part ^ is a guard
        Temperature::Celsius(t) => println!("{}C is equal to or below 30 Celsius", t),

        Temperature::Fahrenheit(t) if t > 86 => println!("{}F is above 86 Fahrenheit", t),
        Temperature::Fahrenheit(t) => println!("{}F is equal to or below 86 Fahrenheit", t),
    }
}

Зверніть увагу, що компілятор не братиме до уваги умови охоронних умов, коли перевіряє, чи всі зразки охоплено виразом match.

fn main() {
    let number: u8 = 4;

    match number {
        i if i == 0 => println!("Zero"),
        i if i > 0 => println!("Greater than zero"),
        // _ => unreachable!("Should never happen."),
        // TODO ^ uncomment to fix compilation
    }
}

Див. також:

Кортежі Переліки

Binding

Непряме доступання до змінної унеможливлює розгалуження й використання цієї змінної без повторного зв’язування. match надає сигіл @ для зв’язування значень із іменами:

// A function `age` which returns a `u32`.
fn age() -> u32 {
    15
}

fn main() {
    println!("Tell me what type of person you are");

    match age() {
        0             => println!("I haven't celebrated my first birthday yet"),
        // Could `match` 1 ..= 12 directly but then what age
        // would the child be?
        // Could `match` n and use an `if` guard, but would
        // not contribute to exhaustiveness checks.
        // (Although in this case that would not matter since
        // a "catch-all" pattern is present at the bottom)
        // Instead, bind to `n` for the sequence of 1 ..= 12.
        // Now the age can be reported.
        n @ 1  ..= 12 => println!("I'm a child of age {:?}", n),
        n @ 13 ..= 19 => println!("I'm a teen of age {:?}", n),
        // A similar binding can be done when matching several values.
        n @ (1 | 7 | 15 | 13) => println!("I'm a teen of age {:?}", n),
        // Nothing bound. Return the result.
        n             => println!("I'm an old person of age {:?}", n),
    }
}

Ви також можете використовувати зв’язування, щоб “деструктурувати” варіанти enum, такі як Option:

fn some_number() -> Option<u32> {
    Some(42)
}

fn main() {
    match some_number() {
        // Got `Some` variant, match if its value, bound to `n`,
        // is equal to 42.
        // Could also use `Some(42)` and print `"The Answer: 42!"`
        // but that would require changing `42` in 2 spots should
        // you ever wish to change it.
        // Could also use `Some(n) if n == 42` and print `"The Answer: {n}!"`
        // but that would not contribute to exhaustiveness checks.
        // (Although in this case that would not matter since
        // the next arm is a "catch-all" pattern)
        Some(n @ 42) => println!("The Answer: {}!", n),
        // Match any other number.
        Some(n)      => println!("Not interesting... {}", n),
        // Match anything else (`None` variant).
        _            => (),
    }
}

Дивіться також:

functions, enums і Option

if let

Для деяких випадків використання, коли зіставляють переліки, match є незручним. Наприклад:

#![allow(unused)]
fn main() {
// Make `optional` of type `Option<i32>`
let optional = Some(7);

match optional {
    Some(i) => println!("This is a really long string and `{:?}`", i),
    _ => {},
    // ^ Required because `match` is exhaustive. Doesn't it seem
    // like wasted space?
};

}

if let є охайнішим для цього випадку використання і додатково дозволяє вказувати різні варіанти невдачі:

fn main() {
    // All have type `Option<i32>`
    let number = Some(7);
    let letter: Option<i32> = None;
    let emoticon: Option<i32> = None;

    // The `if let` construct reads: "if `let` destructures `number` into
    // `Some(i)`, evaluate the block (`{}`).
    if let Some(i) = number {
        println!("Matched {:?}!", i);
    }

    // If you need to specify a failure, use an else:
    if let Some(i) = letter {
        println!("Matched {:?}!", i);
    } else {
        // Destructure failed. Change to the failure case.
        println!("Didn't match a number. Let's go with a letter!");
    }

    // Provide an altered failing condition.
    let i_like_letters = false;

    if let Some(i) = emoticon {
        println!("Matched {:?}!", i);
    // Destructure failed. Evaluate an `else if` condition to see if the
    // alternate failure branch should be taken:
    } else if i_like_letters {
        println!("Didn't match a number. Let's go with a letter!");
    } else {
        // The condition evaluated false. This branch is the default:
        println!("I don't like letters. Let's go with an emoticon :)!");
    }
}

Таким самим чином if let можна використовувати, щоб зіставити будь-яке значення переліку:

// Our example enum
enum Foo {
    Bar,
    Baz,
    Qux(u32)
}

fn main() {
    // Create example variables
    let a = Foo::Bar;
    let b = Foo::Baz;
    let c = Foo::Qux(100);

    // Variable a matches Foo::Bar
    if let Foo::Bar = a {
        println!("a is foobar");
    }

    // Variable b does not match Foo::Bar
    // So this will print nothing
    if let Foo::Bar = b {
        println!("b is foobar");
    }

    // Variable c matches Foo::Qux which has a value
    // Similar to Some() in the previous example
    if let Foo::Qux(value) = c {
        println!("c is {}", value);
    }

    // Binding also works with `if let`
    if let Foo::Qux(value @ 100) = c {
        println!("c is one hundred");
    }
}

Ще одна перевага полягає в тому, що if let дозволяє нам зіставляти непараметризовані варіанти переліку. Це вірно навіть у випадках, коли перелік не реалізує і не виводить PartialEq. У таких випадках if Foo::Bar == a не зможе скомпілюватися, тому що екземпляри переліку не можна порівняти на рівність, однак if let продовжить працювати.

Хочете виклик? Виправте наступний приклад, щоб використати if let:

// This enum purposely neither implements nor derives PartialEq.
// That is why comparing Foo::Bar == a fails below.
enum Foo {Bar}

fn main() {
    let a = Foo::Bar;

    // Variable a matches Foo::Bar
    if Foo::Bar == a {
    // ^-- this causes a compile-time error. Use `if let` instead.
        println!("a is foobar");
    }
}

Див. також:

enum, Option, and the RFC

let-else

🛈 stable since: rust 1.65

🛈 ви можете цілитися у конкретне видання, скомпілювавши так rustc --edition=2021 main.rs

За допомогою let-else, спростовуваний шаблон може зіставитися та зв’язати змінні в навколишній області видимості як звичайний let, або ж розійтися (наприклад, break, return, panic!) коли шаблон не зіставляється.

use std::str::FromStr;

fn get_count_item(s: &str) -> (u64, &str) {
    let mut it = s.split(' ');
    let (Some(count_str), Some(item)) = (it.next(), it.next()) else {
        panic!("Can't segment count item pair: '{s}'");
    };
    let Ok(count) = u64::from_str(count_str) else {
        panic!("Can't parse integer: '{count_str}'");
    };
    (count, item)
}

fn main() {
    assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
}

Область видимості прив’язок імен — це головна річ, яка робить це відмінним від виразів match або if let-else. Раніше ви могли приблизно відтворити ці шаблони за допомогою невдалого дублювання та зовнішнього let:

#![allow(unused)]
fn main() {
use std::str::FromStr;

fn get_count_item(s: &str) -> (u64, &str) {
    let mut it = s.split(' ');
    let (count_str, item) = match (it.next(), it.next()) {
        (Some(count_str), Some(item)) => (count_str, item),
        _ => panic!("Can't segment count item pair: '{s}'"),
    };
    let count = if let Ok(count) = u64::from_str(count_str) {
        count
    } else {
        panic!("Can't parse integer: '{count_str}'");
    };
    (count, item)
}

assert_eq!(get_count_item("3 chairs"), (3, "chairs"));
}

Дивіться також:

option, match, if let та let-else RFC.

while let

Подібно до if let, while let може робити незручні послідовності match більш прийнятними. Розгляньте таку послідовність, яка збільшує i:

#![allow(unused)]
fn main() {
// Make `optional` of type `Option<i32>`
let mut optional = Some(0);

// Repeatedly try this test.
loop {
    match optional {
        // If `optional` destructures, evaluate the block.
        Some(i) => {
            if i > 9 {
                println!("Greater than 9, quit!");
                optional = None;
            } else {
                println!("`i` is `{:?}`. Try again.", i);
                optional = Some(i + 1);
            }
            // ^ Requires 3 indentations!
        },
        // Quit the loop when the destructure fails:
        _ => { break; }
        // ^ Why should this be required? There must be a better way!
    }
}
}

Використання while let робить цю послідовність набагато кращою:

fn main() {
    // Make `optional` of type `Option<i32>`
    let mut optional = Some(0);

    // This reads: "while `let` destructures `optional` into
    // `Some(i)`, evaluate the block (`{}`). Else `break`.
    while let Some(i) = optional {
        if i > 9 {
            println!("Greater than 9, quit!");
            optional = None;
        } else {
            println!("`i` is `{:?}`. Try again.", i);
            optional = Some(i + 1);
        }
        // ^ Less rightward drift and doesn't require
        // explicitly handling the failing case.
    }
    // ^ `if let` had additional optional `else`/`else if`
    // clauses. `while let` does not have these.
}

Дивіться також:

enum, Option, і RFC

Functions

Функції оголошуються за допомогою ключового слова fn. Їхні аргументи мають типізовані анотації, так само як і змінні, і, якщо функція повертає значення, тип результату має бути вказаний після стрілки ->.

Кінцевий вираз у функції буде використано як значення, що повертається. Як альтернатива, для повернення значення раніше зсередини функції можна використовувати оператор return, навіть ізсередини циклів або операторів if.

Давайте перепишемо FizzBuzz, використовуючи функції!

// Unlike C/C++, there's no restriction on the order of function definitions
fn main() {
    // We can use this function here, and define it somewhere later
    fizzbuzz_to(100);
}

// Function that returns a boolean value
fn is_divisible_by(lhs: u32, rhs: u32) -> bool {
    // Corner case, early return
    if rhs == 0 {
        return false;
    }

    // This is an expression, the `return` keyword is not necessary here
    lhs % rhs == 0
}

// Functions that "don't" return a value, actually return the unit type `()`
fn fizzbuzz(n: u32) -> () {
    if is_divisible_by(n, 15) {
        println!("fizzbuzz");
    } else if is_divisible_by(n, 3) {
        println!("fizz");
    } else if is_divisible_by(n, 5) {
        println!("buzz");
    } else {
        println!("{}", n);
    }
}

// When a function returns `()`, the return type can be omitted from the
// signature
fn fizzbuzz_to(n: u32) {
    for n in 1..=n {
        fizzbuzz(n);
    }
}

Асоційовані функції та методи

Деякі функції пов’язані з певним типом. Вони існують у двох формах: асоційовані функції та методи. Асоційовані функції — це функції, які визначені для типу загалом, тоді як методи — це асоційовані функції, які викликаються для певного екземпляра типу.

struct Point {
    x: f64,
    y: f64,
}

// Implementation block, all `Point` associated functions & methods go in here
impl Point {
    // This is an "associated function" because this function is associated with
    // a particular type, that is, Point.
    //
    // Associated functions don't need to be called with an instance.
    // These functions are generally used like constructors.
    fn origin() -> Point {
        Point { x: 0.0, y: 0.0 }
    }

    // Another associated function, taking two arguments:
    fn new(x: f64, y: f64) -> Point {
        Point { x: x, y: y }
    }
}

struct Rectangle {
    p1: Point,
    p2: Point,
}

impl Rectangle {
    // This is a method
    // `&self` is sugar for `self: &Self`, where `Self` is the type of the
    // caller object. In this case `Self` = `Rectangle`
    fn area(&self) -> f64 {
        // `self` gives access to the struct fields via the dot operator
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        // `abs` is a `f64` method that returns the absolute value of the
        // caller
        ((x1 - x2) * (y1 - y2)).abs()
    }

    fn perimeter(&self) -> f64 {
        let Point { x: x1, y: y1 } = self.p1;
        let Point { x: x2, y: y2 } = self.p2;

        2.0 * ((x1 - x2).abs() + (y1 - y2).abs())
    }

    // This method requires the caller object to be mutable
    // `&mut self` desugars to `self: &mut Self`
    fn translate(&mut self, x: f64, y: f64) {
        self.p1.x += x;
        self.p2.x += x;

        self.p1.y += y;
        self.p2.y += y;
    }
}

// `Pair` owns resources: two heap allocated integers
struct Pair(Box<i32>, Box<i32>);

impl Pair {
    // This method "consumes" the resources of the caller object
    // `self` desugars to `self: Self`
    fn destroy(self) {
        // Destructure `self`
        let Pair(first, second) = self;

        println!("Destroying Pair({}, {})", first, second);

        // `first` and `second` go out of scope and get freed
    }
}

fn main() {
    let rectangle = Rectangle {
        // Associated functions are called using double colons
        p1: Point::origin(),
        p2: Point::new(3.0, 4.0),
    };

    // Methods are called using the dot operator
    // Note that the first argument `&self` is implicitly passed, i.e.
    // `rectangle.perimeter()` === `Rectangle::perimeter(&rectangle)`
    println!("Rectangle perimeter: {}", rectangle.perimeter());
    println!("Rectangle area: {}", rectangle.area());

    let mut square = Rectangle {
        p1: Point::origin(),
        p2: Point::new(1.0, 1.0),
    };

    // Error! `rectangle` is immutable, but this method requires a mutable
    // object
    //rectangle.translate(1.0, 0.0);
    // TODO ^ Try uncommenting this line

    // Okay! Mutable objects can call mutable methods
    square.translate(1.0, 1.0);

    let pair = Pair(Box::new(1), Box::new(2));

    pair.destroy();

    // Error! Previous `destroy` call "consumed" `pair`
    //pair.destroy();
    // TODO ^ Try uncommenting this line
}

Замикання (Closures)

Замикання — це функції, які можуть захоплювати навколишнє середовище. Наприклад, замикання, що захоплює змінну x:

|val| val + x

Синтаксис і можливості замикань роблять їх дуже зручними для використання на льоту. Виклик замикання точно такий самий, як виклик функції. Однак і типи вхідних даних, і тип повернення можуть бути виведені, а імена вхідних змінних мають бути вказані.

Інші характеристики замикань включають:

  • використання || замість () навколо вхідних змінних.
  • необов’язкове обмеження тіла ({}) для однорядкового виразу (обов’язково в іншому разі).
  • можливість захоплювати змінні зовнішнього середовища.
fn main() {
    let outer_var = 42;

    // Звичайна функція не може посилатися на змінні в навколишньому середовищі
    //fn function(i: i32) -> i32 { i + outer_var }
    // TODO: uncomment the line above and see the compiler error. The compiler
    // suggests that we define a closure instead.

    // Замикання анонімні, тут ми прив'язуємо їх до посилань.
    // Анотація ідентична анотації функції, але є необов'язковою,
    // як і `{}` навколо тіла. Ці безіменні функції
    // присвоюються змінним із відповідними назвами.
    let closure_annotated = |i: i32| -> i32 { i + outer_var };
    let closure_inferred  = |i     |          i + outer_var  ;

    // Виклик замикань.
    println!("closure_annotated: {}", closure_annotated(1));
    println!("closure_inferred: {}", closure_inferred(1));
    // Після того як тип замикання було виведено, його не можна знову вивести з іншим типом.
    //println!("cannot reuse closure_inferred with another type: {}", closure_inferred(42i64));
    // TODO: uncomment the line above and see the compiler error.

    // Замикання без аргументів, яке повертає `i32`.
    // Тип повернення виводиться.
    let one = || 1;
    println!("closure returning one: {}", one());

}

Захоплення

Замикання є невід’ємно гнучкими і робитимуть те, що вимагає функціональність, щоб змусити замикання працювати без анотації. Це дозволяє захопленню гнучко пристосовуватися до випадку використання, інколи переміщуючи, а інколи запозичуючи. Замикання можуть захоплювати змінні:

  • за посиланням: &T
  • за змінним посиланням: &mut T
  • за значенням: T

Вони переважно захоплюють змінні за посиланням і спускаються нижче лише за потреби.

fn main() {
    use std::mem;

    let color = String::from("green");

    // Замикання для друку `color`, яке негайно запозичує (`&`) `color` і
    // зберігає запозичення та замикання у змінній `print`. Воно залишатиметься
    // запозиченим, доки `print` не буде використано востаннє.
    //
    // `println!` потребує лише аргументів за незмінним посиланням, тож воно не
    // накладає нічого більш обмежувального.
    let print = || println!("`color`: {}", color);

    // Виклик замикання з використанням запозичення.
    print();

    // `color` знову можна запозичити незмінно, бо замикання тримає лише
    // незмінне посилання на `color`.
    let _reborrow = &color;
    print();

    // Переміщення або повторне запозичення дозволено після фінального використання `print`
    let _color_moved = color;


    let mut count = 0;
    // Замикання для збільшення `count` могло б взяти або `&mut count`, або `count`
    // але `&mut count` менш обмежувальне, тож воно його й бере. Негайно
    // запозичує `count`.
    //
    // На `inc` потрібен `mut`, бо всередині зберігається `&mut`. Отже,
    // виклик замикання змінює `count`, що потребує `mut`.
    let mut inc = || {
        count += 1;
        println!("`count`: {}", count);
    };

    // Виклик замикання з використанням змінного запозичення.
    inc();

    // Замикання все ще змінно запозичує `count`, бо його буде викликано пізніше.
    // Спроба повторного запозичення призведе до помилки.
    // let _reborrow = &count;
    // ^ TODO: try uncommenting this line.
    inc();

    // Тепер замиканню більше не потрібно запозичувати `&mut count`. Тому
    // можливо повторно запозичити без помилки
    let _count_reborrowed = &mut count;


    // Тип без копіювання.
    let movable = Box::new(3);

    // `mem::drop` потребує `T`, тож це має брати за значенням. Тип із копіюванням
    // скопіювався б у замикання, залишивши оригінал недоторканим.
    // Тип без копіювання має переміститися, і тому `movable` негайно переміщується в
    // замикання.
    let consume = || {
        println!("`movable`: {:?}", movable);
        mem::drop(movable);
    };

    // `consume` споживає змінну, тож це можна викликати лише один раз.
    consume();
    // consume();
    // ^ TODO: Try uncommenting this line.
}

Використання move перед вертикальними рисками примушує замикання брати у володіння захоплені змінні:

fn main() {
    // `Vec` має семантику без копіювання.
    let haystack = vec![1, 2, 3];

    let contains = move |needle| haystack.contains(needle);

    println!("{}", contains(&1));
    println!("{}", contains(&4));

    // println!("There're {} elements in vec", haystack.len());
    // ^ Uncommenting above line will result in compile-time error
    // because borrow checker doesn't allow re-using variable after it
    // has been moved.

    // Removing `move` from closure's signature will cause closure
    // to borrow _haystack_ variable immutably, hence _haystack_ is still
    // available and uncommenting above line will not cause an error.
}

Дивіться також:

Box та std::mem::drop

Як вхідні параметри

Хоча Rust обирає, як захоплювати змінні на льоту, здебільшого без анотації типу, ця неоднозначність не допускається під час написання функцій. Коли замикання передається як вхідний параметр, його повний тип має бути анотований за допомогою одного з кількох traits, і вони визначаються тим, що замикання робить із захопленим значенням. У порядку зменшення обмежувальності вони такі:

  • Fn: замикання використовує захоплене значення через посилання (&T)
  • FnMut: замикання використовує захоплене значення через змінне посилання (&mut T)
  • FnOnce: замикання використовує захоплене значення за значенням (T)

Для кожної змінної окремо компілятор захоплюватиме змінні найменш обмежувальним способом, наскільки це можливо.

Наприклад, розгляньте параметр, анотований як FnOnce. Це визначає, що замикання може захоплювати через &T, &mut T або T, але компілятор зрештою обере залежно від того, як захоплені змінні використовуються в замиканні.

Це тому, що якщо можливе переміщення, то будь-який тип запозичення також має бути можливим. Зауважте, що зворотне не є правдою. Якщо параметр анотовано як Fn, тоді захоплення змінних через &mut T або T не дозволяється. Однак &T дозволяється.

У наведеному нижче прикладі спробуйте поміняти місцями використання Fn, FnMut і FnOnce, щоб побачити, що станеться:

// A function which takes a closure as an argument and calls it.
// <F> denotes that F is a "Generic type parameter"
fn apply<F>(f: F) where
    // The closure takes no input and returns nothing.
    F: FnOnce() {
    // ^ TODO: Try changing this to `Fn` or `FnMut`.

    f();
}

// A function which takes a closure and returns an `i32`.
fn apply_to_3<F>(f: F) -> i32 where
    // The closure takes an `i32` and returns an `i32`.
    F: Fn(i32) -> i32 {

    f(3)
}

fn main() {
    use std::mem;

    let greeting = "hello";
    // A non-copy type.
    // `to_owned` creates owned data from borrowed one
    let mut farewell = "goodbye".to_owned();

    // Capture 2 variables: `greeting` by reference and
    // `farewell` by value.
    let diary = || {
        // `greeting` is by reference: requires `Fn`.
        println!("I said {}.", greeting);

        // Mutation forces `farewell` to be captured by
        // mutable reference. Now requires `FnMut`.
        farewell.push_str("!!!");
        println!("Then I screamed {}.", farewell);
        println!("Now I can sleep. zzzzz");

        // Manually calling drop forces `farewell` to
        // be captured by value. Now requires `FnOnce`.
        mem::drop(farewell);
    };

    // Call the function which applies the closure.
    apply(diary);

    // `double` satisfies `apply_to_3`'s trait bound
    let double = |x| 2 * x;

    println!("3 doubled: {}", apply_to_3(double));
}

Див. також:

std::mem::drop, Fn, FnMut, Generics, where і FnOnce

Анонімність типу

Замикання стисло захоплюють змінні з охоплюючих областей видимості. Чи має це якісь наслідки? Безумовно, має. Зверніть увагу, що використання замикання як параметра функції потребує [узагальнень], що необхідно через те, як вони визначені:

#![allow(unused)]
fn main() {
// `F` must be generic.
fn apply<F>(f: F) where
    F: FnOnce() {
    f();
}
}

Коли замикання визначається, компілятор неявно створює нову анонімну структуру для зберігання захоплених змінних всередині, водночас реалізуючи функціональність через один із traits: Fn, FnMut або FnOnce для цього невідомого типу. Цей тип призначається змінній, яка зберігається до виклику.

Оскільки цей новий тип є невідомим типом, будь-яке використання у функції вимагатиме узагальнень. Однак необмежений параметр типу <T> усе одно був би неоднозначним і не був би дозволений. Отже, обмеження одним із traits: Fn, FnMut або FnOnce (який він реалізує) є достатнім, щоб визначити його тип.

// `F` must implement `Fn` for a closure which takes no
// inputs and returns nothing - exactly what is required
// for `print`.
fn apply<F>(f: F) where
    F: Fn() {
    f();
}

fn main() {
    let x = 7;

    // Capture `x` into an anonymous type and implement
    // `Fn` for it. Store it in `print`.
    let print = || println!("{}", x);

    apply(print);
}

Див. також:

Ґрунтовний аналіз, Fn, FnMut, та FnOnce

Input functions

Оскільки замикання можуть використовуватися як аргументи, ви можете замислитися, чи те саме можна сказати про функції. І справді, можна! Якщо ви оголосите функцію, яка приймає замикання як параметр, тоді будь-яка функція, що задовольняє обмеження трейтів цього замикання, може бути передана як параметр.

// Define a function which takes a generic `F` argument
// bounded by `Fn`, and calls it
fn call_me<F: Fn()>(f: F) {
    f();
}

// Define a wrapper function satisfying the `Fn` bound
fn function() {
    println!("I'm a function!");
}

fn main() {
    // Define a closure satisfying the `Fn` bound
    let closure = || println!("I'm a closure!");

    call_me(closure);
    call_me(function);
}

Як додаткове зауваження, Fn, FnMut, і FnOnce traits визначають, як замикання захоплює змінні з навколишньої області видимості.

See also:

Fn, FnMut, and FnOnce

Як вихідні параметри

Замикання як вхідні параметри можливі, тож повернення замикань як вихідних параметрів також має бути можливим. Однак анонімні типи замикань за означенням невідомі, тому для їхнього повернення ми маємо використовувати impl Trait.

Дійсні трейт-и для повернення замикання:

  • Fn
  • FnMut
  • FnOnce

Окрім цього, потрібно використовувати ключове слово move, яке сигналізує, що всі захоплення відбуваються за значенням. Це потрібно, тому що будь-які захоплення за посиланням були б звільнені щойно функція завершилася б, залишаючи недійсні посилання в замиканні.

fn create_fn() -> impl Fn() {
    let text = "Fn".to_owned();

    move || println!("This is a: {}", text)
}

fn create_fnmut() -> impl FnMut() {
    let text = "FnMut".to_owned();

    move || println!("This is a: {}", text)
}

fn create_fnonce() -> impl FnOnce() {
    let text = "FnOnce".to_owned();

    move || println!("This is a: {}", text)
}

fn main() {
    let fn_plain = create_fn();
    let mut fn_mut = create_fnmut();
    let fn_once = create_fnonce();

    fn_plain();
    fn_mut();
    fn_once();
}

Дивіться також:

Fn, FnMut, Узагальнені та impl Trait.

Приклади в std

Цей розділ містить кілька прикладів використання замикань із бібліотеки std.

Iterator::any

Iterator::any — це функція, яка, коли їй передають ітератор, повертає true, якщо будь-який елемент задовольняє предикат. Інакше — false. Її сигнатура:

pub trait Iterator {
    // The type being iterated over.
    type Item;

    // `any` takes `&mut self` meaning the caller may be borrowed
    // and modified, but not consumed.
    fn any<F>(&mut self, f: F) -> bool where
        // `FnMut` meaning any captured variable may at most be
        // modified, not consumed. `Self::Item` is the closure parameter type,
        // which is determined by the iterator (e.g., `&T` for `.iter()`,
        // `T` for `.into_iter()`).
        F: FnMut(Self::Item) -> bool;
}
fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    // `iter()` for vecs yields `&i32`. Destructure to `i32`.
    println!("2 in vec1: {}", vec1.iter()     .any(|&x| x == 2));
    // `into_iter()` for vecs yields `i32`. No destructuring required.
    println!("2 in vec2: {}", vec2.into_iter().any(|x| x == 2));

    // `iter()` only borrows `vec1` and its elements, so they can be used again
    println!("vec1 len: {}", vec1.len());
    println!("First element of vec1 is: {}", vec1[0]);
    // `into_iter()` does move `vec2` and its elements, so they cannot be used again
    // println!("First element of vec2 is: {}", vec2[0]);
    // println!("vec2 len: {}", vec2.len());
    // TODO: uncomment two lines above and see compiler errors.

    let array1 = [1, 2, 3];
    let array2 = [4, 5, 6];

    // `iter()` for arrays yields `&i32`.
    println!("2 in array1: {}", array1.iter()     .any(|&x| x == 2));
    // `into_iter()` for arrays yields `i32`.
    println!("2 in array2: {}", array2.into_iter().any(|x| x == 2));
}

Дивіться також:

std::iter::Iterator::any

Пошук через ітератори

Iterator::find — це функція, яка ітерує по ітератору та шукає перше значення, що задовольняє певну умову. Якщо жодне зі значень не задовольняє умову, вона повертає None. Її сигнатура:

pub trait Iterator {
    // The type being iterated over.
    type Item;

    // `find` takes `&mut self` meaning the caller may be borrowed
    // and modified, but not consumed.
    fn find<P>(&mut self, predicate: P) -> Option<Self::Item> where
        // `FnMut` meaning any captured variable may at most be
        // modified, not consumed. `&Self::Item` states it takes
        // arguments to the closure by reference.
        P: FnMut(&Self::Item) -> bool;
}
fn main() {
    let vec1 = vec![1, 2, 3];
    let vec2 = vec![4, 5, 6];

    // `vec1.iter()` yields `&i32`.
    let mut iter = vec1.iter();
    // `vec2.into_iter()` yields `i32`.
    let mut into_iter = vec2.into_iter();

    // `iter()` yields `&i32`, and `find` passes `&Item` to the predicate.
    // Since `Item = &i32`, the closure argument has type `&&i32`,
    // which we pattern-match to dereference down to `i32`.
    println!("Find 2 in vec1: {:?}", iter.find(|&&x| x == 2));
    
    // `into_iter()` yields `i32`, and `find` passes `&Item` to the predicate.
    // Since `Item = i32`, the closure argument has type `&i32`,
    // which we pattern-match to dereference down to `i32`.
    println!("Find 2 in vec2: {:?}", into_iter.find(|&x| x == 2));

    let array1 = [1, 2, 3];
    let array2 = [4, 5, 6];

    // `array1.iter()` yields `&i32`, and `find` passes `&Item` to the
    // predicate. Since `Item = &i32`, the closure argument has type `&&i32`.
    println!("Find 2 in array1: {:?}", array1.iter().find(|&&x| x == 2));
    // `array2.into_iter()` yields `i32` (since Rust 2021 edition), and
    // `find` passes `&Item` to the predicate. Since `Item = i32`, the
    // closure argument has type `&i32`.
    println!("Find 2 in array2: {:?}", array2.into_iter().find(|&x| x == 2));
}

Iterator::find дає вам посилання на елемент. Але якщо ви хочете індекс елемента, використовуйте Iterator::position.

fn main() {
    let vec = vec![1, 9, 3, 3, 13, 2];

    // `position` passes the iterator’s `Item` by value to the predicate.
    // `vec.iter()` yields `&i32`, so the predicate receives `&i32`,
    // which we pattern-match to dereference to `i32`.
    let index_of_first_even_number = vec.iter().position(|&x| x % 2 == 0);
    assert_eq!(index_of_first_even_number, Some(5));

    // `vec.into_iter()` yields `i32`, so the predicate receives `i32` directly.
    let index_of_first_negative_number = vec.into_iter().position(|x| x < 0);
    assert_eq!(index_of_first_negative_number, None);
}

Дивіться також:

std::iter::Iterator::find

std::iter::Iterator::find_map

std::iter::Iterator::position

std::iter::Iterator::rposition

Вищі функції

Rust надає Вищі функції (HOF). Це функції, які приймають одну або кілька функцій і/або створюють кориснішу функцію. HOFs і ледачі ітератори надають Rust його функціонального забарвлення.

fn is_odd(n: u32) -> bool {
    n % 2 == 1
}

fn main() {
    println!("Find the sum of all the numbers with odd squares under 1000");
    let upper = 1000;

    // Imperative approach
    // Declare accumulator variable
    let mut acc = 0;
    // Iterate: 0, 1, 2, ... to infinity
    for n in 0.. {
        // Square the number
        let n_squared = n * n;

        if n_squared >= upper {
            // Break loop if exceeded the upper limit
            break;
        } else if is_odd(n_squared) {
            // Accumulate value, if it's odd
            acc += n;
        }
    }
    println!("imperative style: {}", acc);

    // Functional approach
    let sum: u32 =
        (0..).take_while(|&n| n * n < upper) // Below upper limit
             .filter(|&n| is_odd(n * n))     // That are odd
             .sum();                         // Sum them
    println!("functional style: {}", sum);
}

Option та Iterator реалізують свою частку HOFs.

Diverging functions

Функції розбіжності ніколи не повертають. Їх позначають за допомогою !, який є порожнім типом.

#![allow(unused)]
fn main() {
fn foo() -> ! {
    panic!("This call never returns.");
}
}

На відміну від усіх інших типів, цей не можна створити, тому що множина всіх можливих значень, які цей тип може мати, порожня. Зауважте, що він відрізняється від типу (), який має рівно одне можливе значення.

Наприклад, ця функція повертає як звичайно, хоча в значенні, що повертається, немає інформації.

fn some_fn() {
    ()
}

fn main() {
    let _a: () = some_fn();
    println!("This function returns and you can see this line.");
}

На відміну від цієї функції, яка ніколи не поверне керування назад викликачеві.

#![feature(never_type)]

fn main() {
    let x: ! = panic!("This call never returns.");
    println!("You will never see this line!");
}

Хоча це може здаватися абстрактним поняттям, насправді воно дуже корисне і часто зручне. Основна перевага цього типу полягає в тому, що його можна привести до будь-якого іншого типу, роблячи його універсальним у ситуаціях, коли потрібен точний тип, наприклад у гілках match. Ця гнучкість дає змогу писати такий код:

fn main() {
    fn sum_odd_numbers(up_to: u32) -> u32 {
        let mut acc = 0;
        for i in 0..up_to {
            // Notice that the return type of this match expression must be u32
            // because of the type of the "addition" variable.
            let addition: u32 = match i%2 == 1 {
                // The "i" variable is of type u32, which is perfectly fine.
                true => i,
                // On the other hand, the "continue" expression does not return
                // u32, but it is still fine, because it never returns and therefore
                // does not violate the type requirements of the match expression.
                false => continue,
            };
            acc += addition;
        }
        acc
    }
    println!("Sum of odd numbers up to 9 (excluding): {}", sum_odd_numbers(9));
}

Це також є типом повернення функцій, які працюють у нескінченному циклі (наприклад, loop {}), як-от мережеві сервери або функції, що завершують процес (наприклад, exit()).

Модулі

Rust надає потужну систему модулів, яку можна використовувати для ієрархічного розбиття коду на логічні одиниці (модулі) та керування видимістю (публічна/приватна) між ними.

Модуль — це набір елементів: функції, структури, трейтів, блоків impl, і навіть інших модулів.

Видимість

За замовчуванням елементи в модулі мають приватну видимість, але це можна перевизначити за допомогою модифікатора pub. Лише публічні елементи модуля можна доступити ззовні області видимості модуля.

// A module named `my_mod`
mod my_mod {
    // Items in modules default to private visibility.
    fn private_function() {
        println!("called `my_mod::private_function()`");
    }

    // Use the `pub` modifier to override default visibility.
    pub fn function() {
        println!("called `my_mod::function()`");
    }

    // Items can access other items in the same module,
    // even when private.
    pub fn indirect_access() {
        print!("called `my_mod::indirect_access()`, that\n> ");
        private_function();
    }

    // Modules can also be nested
    pub mod nested {
        pub fn function() {
            println!("called `my_mod::nested::function()`");
        }

        #[allow(dead_code)]
        fn private_function() {
            println!("called `my_mod::nested::private_function()`");
        }

        // Functions declared using `pub(in path)` syntax are only visible
        // within the given path. `path` must be a parent or ancestor module
        pub(in crate::my_mod) fn public_function_in_my_mod() {
            print!("called `my_mod::nested::public_function_in_my_mod()`, that\n> ");
            public_function_in_nested();
        }

        // Functions declared using `pub(self)` syntax are only visible within
        // the current module, which is the same as leaving them private
        pub(self) fn public_function_in_nested() {
            println!("called `my_mod::nested::public_function_in_nested()`");
        }

        // Functions declared using `pub(super)` syntax are only visible within
        // the parent module
        pub(super) fn public_function_in_super_mod() {
            println!("called `my_mod::nested::public_function_in_super_mod()`");
        }
    }

    pub fn call_public_function_in_my_mod() {
        print!("called `my_mod::call_public_function_in_my_mod()`, that\n> ");
        nested::public_function_in_my_mod();
        print!("> ");
        nested::public_function_in_super_mod();
    }

    // pub(crate) makes functions visible only within the current crate
    pub(crate) fn public_function_in_crate() {
        println!("called `my_mod::public_function_in_crate()`");
    }

    // Nested modules follow the same rules for visibility
    mod private_nested {
        #[allow(dead_code)]
        pub fn function() {
            println!("called `my_mod::private_nested::function()`");
        }

        // Private parent items will still restrict the visibility of a child item,
        // even if it is declared as visible within a bigger scope.
        #[allow(dead_code)]
        pub(crate) fn restricted_function() {
            println!("called `my_mod::private_nested::restricted_function()`");
        }
    }
}

fn function() {
    println!("called `function()`");
}

fn main() {
    // Modules allow disambiguation between items that have the same name.
    function();
    my_mod::function();

    // Public items, including those inside nested modules, can be
    // accessed from outside the parent module.
    my_mod::indirect_access();
    my_mod::nested::function();
    my_mod::call_public_function_in_my_mod();

    // pub(crate) items can be called from anywhere in the same crate
    my_mod::public_function_in_crate();

    // pub(in path) items can only be called from within the module specified
    // Error! function `public_function_in_my_mod` is private
    //my_mod::nested::public_function_in_my_mod();
    // TODO ^ Try uncommenting this line

    // Private items of a module cannot be directly accessed, even if
    // nested in a public module:

    // Error! `private_function` is private
    //my_mod::private_function();
    // TODO ^ Try uncommenting this line

    // Error! `private_function` is private
    //my_mod::nested::private_function();
    // TODO ^ Try uncommenting this line

    // Error! `private_nested` is a private module
    //my_mod::private_nested::function();
    // TODO ^ Try uncommenting this line

    // Error! `private_nested` is a private module
    //my_mod::private_nested::restricted_function();
    // TODO ^ Try uncommenting this line
}

Видимість структури

Структури мають додатковий рівень видимості для своїх полів. Видимість за замовчуванням є приватною і може бути перевизначена за допомогою модифікатора pub. Ця видимість має значення лише тоді, коли до структури звертаються з-за меж модуля, де її визначено, і має на меті приховування інформації (інкапсуляцію).

mod my {
    // Публічна структура з публічним полем узагальненого типу `T`
    pub struct OpenBox<T> {
        pub contents: T,
    }

    // Публічна структура з приватним полем узагальненого типу `T`
    pub struct ClosedBox<T> {
        contents: T,
    }

    impl<T> ClosedBox<T> {
        // Публічний конструктор-метод
        pub fn new(contents: T) -> ClosedBox<T> {
            ClosedBox {
                contents: contents,
            }
        }
    }
}

fn main() {
    // Публічні структури з публічними полями можна створювати як зазвичай
    let open_box = my::OpenBox { contents: "public information" };

    // і до їхніх полів можна нормально звертатися.
    println!("The open box contains: {}", open_box.contents);

    // Публічні структури з приватними полями не можна створювати, використовуючи імена полів.
    // Помилка! `ClosedBox` має приватні поля
    //let closed_box = my::ClosedBox { contents: "classified information" };
    // TODO ^ Спробуйте розкоментувати цей рядок

    // Однак структури з приватними полями можна створювати, використовуючи
    // публічні конструктори
    let _closed_box = my::ClosedBox::new("classified information");

    // і до приватних полів публічної структури не можна звертатися.
    // Помилка! Поле `contents` є приватним
    //println!("The closed box contains: {}", _closed_box.contents);
    // TODO ^ Спробуйте розкоментувати цей рядок
}

Див. також:

узагальнені типи and методи

Декларація use

Декларацію use можна використовувати, щоб прив’язати повний шлях до нової назви для простішого доступу. Її часто використовують так:

use crate::deeply::nested::{
    my_first_function,
    my_second_function,
    AndATraitType
};

fn main() {
    my_first_function();
}

Ви можете використовувати ключове слово as, щоб прив’язати імпорти до іншої назви:

// Прив’язати шлях `deeply::nested::function` до `other_function`.
use deeply::nested::function as other_function;

fn function() {
    println!("called `function()`");
}

mod deeply {
    pub mod nested {
        pub fn function() {
            println!("called `deeply::nested::function()`");
        }
    }
}

fn main() {
    // Простіший доступ до `deeply::nested::function`
    other_function();

    println!("Entering block");
    {
        // Це еквівалентно `use deeply::nested::function as function`.
        // Ця `function()` затінить зовнішню.
        use crate::deeply::nested::function;

        // Прив’язки `use` мають локальну область видимості. У цьому випадку
        // затінення `function()` є лише в цьому блоці.
        function();

        println!("Leaving block");
    }

    function();
}

Ви також можете використовувати pub use, щоб перевизначити експорт елемента з модуля, щоб до нього можна було отримати доступ через публічний інтерфейс модуля:

mod deeply {
    pub mod nested {
        pub fn function() {
            println!("called `deeply::nested::function()`");
        }
    }
}

mod cool {
    pub use crate::deeply::nested::function;
}

fn main() {
    cool::function();
}

super and self

The super and self keywords can be used in the path to remove ambiguity when accessing items and to prevent unnecessary hardcoding of paths.

fn function() {
    println!("called `function()`");
}

mod cool {
    pub fn function() {
        println!("called `cool::function()`");
    }
}

mod my {
    fn function() {
        println!("called `my::function()`");
    }

    mod cool {
        pub fn function() {
            println!("called `my::cool::function()`");
        }
    }

    pub fn indirect_call() {
        // Let's access all the functions named `function` from this scope!
        print!("called `my::indirect_call()`, that\n> ");

        // The `self` keyword refers to the current module scope - in this case `my`.
        // Calling `self::function()` and calling `function()` directly both give
        // the same result, because they refer to the same function.
        self::function();
        function();

        // We can also use `self` to access another module inside `my`:
        self::cool::function();

        // The `super` keyword refers to the parent scope (outside the `my` module).
        super::function();

        // This will bind to the `cool::function` in the *crate* scope.
        // In this case the crate scope is the outermost scope.
        {
            use crate::cool::function as root_function;
            root_function();
        }
    }
}

fn main() {
    my::indirect_call();
}

Ієрархія файлів

Модулі можна зіставити з ієрархією файлів/каталогів. Розберімо приклад видимості у файлах:

$ tree .
.
├── my
│   ├── inaccessible.rs
│   └── nested.rs
├── my.rs
└── split.rs

У split.rs:

// This declaration will look for a file named `my.rs` and will
// insert its contents inside a module named `my` under this scope
mod my;

fn function() {
    println!("called `function()`");
}

fn main() {
    my::function();

    function();

    my::indirect_access();

    my::nested::function();
}

У my.rs:

// Similarly `mod inaccessible` and `mod nested` will locate the `nested.rs`
// and `inaccessible.rs` files and insert them here under their respective
// modules
mod inaccessible;
pub mod nested;

pub fn function() {
    println!("called `my::function()`");
}

fn private_function() {
    println!("called `my::private_function()`");
}

pub fn indirect_access() {
    print!("called `my::indirect_access()`, that\n> ");

    private_function();
}

У my/nested.rs:

pub fn function() {
    println!("called `my::nested::function()`");
}

#[allow(dead_code)]
fn private_function() {
    println!("called `my::nested::private_function()`");
}

У my/inaccessible.rs:

#[allow(dead_code)]
pub fn public_function() {
    println!("called `my::inaccessible::public_function()`");
}

Перевірмо, що все досі працює, як і раніше:

$ rustc split.rs && ./split
called `my::function()`
called `function()`
called `my::indirect_access()`, that
> called `my::private_function()`
called `my::nested::function()`

Крейт (Crates)

Крейт — це одиниця компіляції в Rust. Щоразу, коли викликається rustc some_file.rs, some_file.rs розглядається як файл крейту. Якщо some_file.rs має всередині оголошення mod, тоді вміст файлів модулів буде вставлено в місця, де в файлі крейту знайдено оголошення mod, перед тим, як компілятор обробить його. Іншими словами, модулі не компілюються окремо, компілюються лише крейти.

Крейт можна скомпілювати у бінарний файл або в бібліотеку. За замовчуванням, rustc створюватиме бінарний файл із крейту. Цю поведінку можна змінити, передавши прапорець --crate-type до lib.

Створення бібліотеки

Давайте створимо бібліотеку, а потім подивимося, як прив’язати її до іншого крейту.

У rary.rs:

pub fn public_function() {
    println!("called rary's `public_function()`");
}

fn private_function() {
    println!("called rary's `private_function()`");
}

pub fn indirect_access() {
    print!("called rary's `indirect_access()`, that\n> ");

    private_function();
}
$ rustc --crate-type=lib rary.rs
$ ls lib*
library.rlib

Бібліотеки отримують префікс “lib”, і за замовчуванням вони називаються за назвою свого файлу крейту, але цю назву за замовчуванням можна змінити, передавши параметр --crate-name до rustc або використавши crate_name атрибут.

Використання бібліотеки

Для зв’язування крейта з цією новою бібліотекою ви можете використати прапорець --extern rustc. Усі її елементи тоді буде імпортовано під модулем з назвою, такою самою, як у бібліотеки. Цей модуль загалом поводиться так само, як і будь-який інший модуль.

// extern crate rary; // May be required for Rust 2015 edition or earlier

fn main() {
    rary::public_function();

    // Error! `private_function` is private
    //rary::private_function();

    rary::indirect_access();
}
# Де library.rlib — це шлях до скомпільованої бібліотеки, припускаючи, що вона
# тут у тому самому каталозі:
$ rustc executable.rs --extern rary=library.rlib && ./executable
called rary's `public_function()`
called rary's `indirect_access()`, that
> called rary's `private_function()`

Cargo

cargo — офіційний інструмент керування пакетами Rust. Він має багато дійсно корисних можливостей для покращення якості коду та швидкості розробки! До них належать

  • Керування залежностями та інтеграція з crates.io (офіційним реєстром пакетів Rust)
  • Урахування модульних тестів
  • Урахування бенчмарків

У цій главі ми розглянемо деякі короткі основи, але ви можете знайти вичерпну документацію в The Cargo Book.

Залежності

Більшість програм мають залежності від деяких бібліотек. Якщо ви коли-небудь керували залежностями вручну, ви знаєте, яким це може бути клопотом. На щастя, екосистема Rust стандартно постачається з cargo! cargo може керувати залежностями для проєкту.

Щоб створити новий проєкт Rust,

# A binary
cargo new foo

# A library
cargo new --lib bar

Для решти цієї глави припустімо, що ми створюємо бінарний файл, а не бібліотеку, але всі концепції ті самі.

Після наведених вище команд ви маєте побачити ієрархію файлів на кшталт такої:

.
├── bar
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── foo
    ├── Cargo.toml
    └── src
        └── main.rs

main.rs — це кореневий вихідний файл для вашого нового проєкту foo – нічого нового тут. Cargo.toml — це файл конфігурації для cargo для цього проєкту. Якщо ви подивитеся всередину, ви маєте побачити щось подібне до цього:

[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]

[dependencies]

Поле name під [package] визначає назву проєкту. Це використовується crates.io, якщо ви опублікуєте крейт (далі буде більше). Це також назва вихідного бінарного файлу під час компіляції.

Поле version — це номер версії крейту, який використовує Semantic Versioning.

Поле authors — це список авторів, що використовується під час публікації крейту.

Розділ [dependencies] дає змогу додавати залежності для вашого проєкту.

Наприклад, припустімо, що ми хочемо, щоб наша програма мала чудовий CLI. Ви можете знайти багато чудових пакетів на crates.io (офіційному реєстрі пакетів Rust). Один популярний вибір — clap. На момент написання найновіша опублікована версія clap2.27.1. Щоб додати залежність до нашої програми, ми можемо просто додати таке до нашого Cargo.toml у розділі [dependencies]: clap = "2.27.1". І це все! Ви можете почати використовувати clap у своїй програмі.

cargo також підтримує інші типи залежностей. Ось лише невелика добірка:

[package]
name = "foo"
version = "0.1.0"
authors = ["mark"]

[dependencies]
clap = "2.27.1" # from crates.io
rand = { git = "https://github.com/rust-lang-nursery/rand" } # from online repo
bar = { path = "../bar" } # from a path in the local filesystem

cargo — це більше, ніж менеджер залежностей. Усі доступні параметри конфігурації перелічені в специфікації формату для Cargo.toml.

Щоб зібрати наш проєкт, ми можемо виконати cargo build будь-де в каталозі проєкту (включно з підкаталогами!). Ми також можемо виконати cargo run, щоб зібрати й запустити. Зверніть увагу, що ці команди розв’яжуть усі залежності, завантажать крейти, якщо потрібно, і зберуть усе, включно з вашим крейтом. (Зверніть увагу, що це лише перезбирає те, що ще не було зібрано, подібно до make).

Voilà! Це все!

Convention

У попередньому розділі ми побачили таку ієрархію каталогів:

foo
├── Cargo.toml
└── src
    └── main.rs

Припустімо, що ми, однак, хотіли мати два бінарники в одному проєкті. Що тоді?

Виявляється, cargo підтримує це. Назва бінарника за замовчуванням — main, як ми бачили раніше, але ви можете додати додаткові бінарники, розмістивши їх у каталозі bin/:

foo
├── Cargo.toml
└── src
    ├── main.rs
    └── bin
        └── my_other_bin.rs

Щоб сказати cargo компілювати або запускати лише цей бінарник, ми просто передаємо cargo прапорець --bin my_other_bin, де my_other_bin — це ім’я бінарника, з яким ми хочемо працювати.

Окрім додаткових бінарників, cargo підтримує більше можливостей таких як бенчмарки, тести та приклади.

У наступному розділі ми розглянемо тести ближче.

Тестування (Testing)

Як ми знаємо, тестування є невід’ємною частиною будь-якого програмного забезпечення! Rust має підтримку на рівні першого класу для модульного та інтеграційного тестування (див. цей розділ у TRPL).

Із розділів про тестування, наведених вище, ми бачимо, як писати модульні тести та інтеграційні тести. З організаційної точки зору, ми можемо розміщувати модульні тести в модулях, які вони тестують, а інтеграційні тести — у власному каталозі tests/:

foo
├── Cargo.toml
├── src
│   └── main.rs
│   └── lib.rs
└── tests
    ├── my_test.rs
    └── my_other_test.rs

Кожен файл у tests є окремим інтеграційним тестом, тобто тестом, який призначений для тестування вашої бібліотеки так, ніби її викликають із залежного крейту.

Розділ Тестування докладніше описує три різні стилі тестування: модульне тестування, документаційне тестування та інтеграційне тестування.

cargo природно надає простий спосіб запускати всі ваші тести!

$ cargo test

Ви повинні побачити такий вивід:

$ cargo test
   Compiling blah v0.1.0 (file:///nobackup/blah)
    Finished dev [unoptimized + debuginfo] target(s) in 0.89 secs
     Running target/debug/deps/blah-d3b32b97275ec472

running 4 tests
test test_bar ... ok
test test_baz ... ok
test test_foo_bar ... ok
test test_foo ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Ви також можете запускати тести, назва яких відповідає шаблону:

$ cargo test test_foo
$ cargo test test_foo
   Compiling blah v0.1.0 (file:///nobackup/blah)
    Finished dev [unoptimized + debuginfo] target(s) in 0.35 secs
     Running target/debug/deps/blah-d3b32b97275ec472

running 2 tests
test test_foo ... ok
test test_foo_bar ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out

Одне застереження: Cargo може запускати кілька тестів одночасно, тож переконайтеся, що вони не створюють стан гонки даних один з одним.

Один приклад того, як ця конкурентність може спричиняти проблеми, — якщо два тести виводять у файл, як показано нижче:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    // Import the necessary modules
    use std::fs::OpenOptions;
    use std::io::Write;

    // This test writes to a file
    #[test]
    fn test_file() {
        // Opens the file ferris.txt or creates one if it doesn't exist.
        let mut file = OpenOptions::new()
            .append(true)
            .create(true)
            .open("ferris.txt")
            .expect("Failed to open ferris.txt");

        // Print "Ferris" 5 times.
        for _ in 0..5 {
            file.write_all("Ferris\n".as_bytes())
                .expect("Could not write to ferris.txt");
        }
    }

    // This test tries to write to the same file
    #[test]
    fn test_file_also() {
        // Opens the file ferris.txt or creates one if it doesn't exist.
        let mut file = OpenOptions::new()
            .append(true)
            .create(true)
            .open("ferris.txt")
            .expect("Failed to open ferris.txt");

        // Print "Corro" 5 times.
        for _ in 0..5 {
            file.write_all("Corro\n".as_bytes())
                .expect("Could not write to ferris.txt");
        }
    }
}
}

Хоча задум полягає в тому, щоб отримати таке:

$ cat ferris.txt
Ferris
Ferris
Ferris
Ferris
Ferris
Corro
Corro
Corro
Corro
Corro

Насправді в ferris.txt потрапляє ось це:

$ cargo test test_file && cat ferris.txt
Corro
Ferris
Corro
Ferris
Corro
Ferris
Corro
Ferris
Corro
Ferris

Скрипти збірки

Іноді звичайної збірки за допомогою cargo недостатньо. Можливо, ваш крейт потребує деяких передумов перед тим, як cargo успішно скомпілює його, наприклад генерації коду або якогось нативного коду, який потрібно скомпілювати. Щоб розв’язати цю проблему, ми маємо скрипти збірки, які Cargo може запускати.

Щоб додати скрипт збірки до вашого пакета, його можна вказати в Cargo.toml таким чином:

[package]
...
build = "build.rs"

Інакше Cargo за замовчуванням шукатиме файл build.rs у каталозі проєкту.

Як використовувати скрипт збірки

Скрипт збірки — це просто ще один файл Rust, який буде скомпільовано та викликано перед компіляцією будь-чого іншого в пакеті. Отже, його можна використовувати для виконання передумов вашого крейта.

Cargo надає скрипту вхідні дані через змінні середовища specified here, які можна використовувати.

Скрипт надає вихідні дані через stdout. Усі надруковані рядки записуються до target/debug/build/<pkg>/output. Крім того, рядки з префіксом cargo: будуть інтерпретовані Cargo безпосередньо і, отже, можуть використовуватися для визначення параметрів компіляції пакета.

Для подальшої специфікації та прикладів прочитайте Cargo specification.

Атрибути

Атрибут — це метадані, застосовані до деякого модуля, крейту або елемента. Ці метадані можна використовувати для/для:

Атрибути мають вигляд #[outer_attribute] або #![inner_attribute], а різниця між ними полягає в тому, де вони застосовуються.

  • #[outer_attribute] застосовується до елемента, що безпосередньо йде після нього. Деякі приклади елементів: функція, оголошення модуля, константа, структура, перелік. Ось приклад, де атрибут #[derive(Debug)] застосовується до структури Rectangle:

    #![allow(unused)]
    fn main() {
    #[derive(Debug)]
    struct Rectangle {
        width: u32,
        height: u32,
    }
    }
  • #![inner_attribute] застосовується до зовнішнього елемента (зазвичай модуля або крейту). Іншими словами, цей атрибут інтерпретується як такий, що застосовується до всієї області видимості, у якій він розміщений. Ось приклад, де #![allow(unused_variables)] застосовується до всього крейту (якщо розміщено в main.rs):

    #![allow(unused_variables)]
    
    fn main() {
        let x = 3; // This would normally warn about an unused variable.
    }

Атрибути можуть приймати аргументи з різним синтаксисом:

  • #[attribute = "value"]
  • #[attribute(key = "value")]
  • #[attribute(value)]

Атрибути можуть мати кілька значень і також можуть бути розбиті на кілька рядків:

#[attribute(value, value2)]


#[attribute(value, value2, value3,
            value4, value5)]

dead_code

Компілятор надає dead_code lint, який попереджатиме про невикористовувані функції. Атрибут можна використати, щоб вимкнути lint.

fn used_function() {}

// `#[allow(dead_code)]` is an attribute that disables the `dead_code` lint
#[allow(dead_code)]
fn unused_function() {}

fn noisy_unused_function() {}
// FIXME ^ Add an attribute to suppress the warning

fn main() {
    used_function();
}

Зверніть увагу, що в реальних програмах ви маєте усунути dead code. У цих прикладах ми дозволятимемо dead code в деяких місцях через інтерактивну природу прикладів.

Крейт (Crates)

Атрибут crate_type можна використати, щоб сказати компілятору, чи крейт є бінарним або бібліотекою (і навіть який саме тип бібліотеки), а атрибут crate_name можна використати, щоб задати назву крейту.

Однак важливо зазначити, що і атрибут crate_type, і атрибут crate_name не мають жодного впливу під час використання Cargo, менеджера пакетів Rust. Оскільки Cargo використовується для більшості проєктів Rust, це означає, що реальні випадки використання crate_type і crate_name є відносно обмеженими.

// This crate is a library
#![crate_type = "lib"]
// The library is named "rary"
#![crate_name = "rary"]

pub fn public_function() {
    println!("called rary's `public_function()`");
}

fn private_function() {
    println!("called rary's `private_function()`");
}

pub fn indirect_access() {
    print!("called rary's `indirect_access()`, that\n> ");

    private_function();
}

Коли використовується атрибут crate_type, нам більше не потрібно передавати прапорець --crate-type до rustc.

$ rustc lib.rs
$ ls lib*
library.rlib

cfg

Умовні перевірки конфігурації можливі через два різні оператори:

  • атрибут cfg: #[cfg(...)] у позиції атрибута
  • макрос cfg!: cfg!(...) у булевих виразах

Хоча перший вмикає умовну компіляцію, другий умовно обчислюється до літералів true або false, що дає змогу виконувати перевірки під час виконання. Обидва використовують однаковий синтаксис аргументів.

cfg!, на відміну від #[cfg], не видаляє жодного коду і лише обчислюється як true або false. Наприклад, усі блоки в if/else виразі мають бути дійсними, коли cfg! використовується для умови, незалежно від того, що обчислює cfg!.

// Ця функція компілюється лише якщо цільова ОС — linux
#[cfg(target_os = "linux")]
fn are_you_on_linux() {
    println!("You are running linux!");
}

// І ця функція компілюється лише якщо цільова ОС — *не* linux
#[cfg(not(target_os = "linux"))]
fn are_you_on_linux() {
    println!("You are *not* running linux!");
}

fn main() {
    are_you_on_linux();

    println!("Are you sure?");
    if cfg!(target_os = "linux") {
        println!("Yes. It's definitely linux!");
    } else {
        println!("Yes. It's definitely *not* linux!");
    }
}

Дивіться також:

the reference, cfg!, and macros.

Custom

Деякі умовні параметри, як-от target_os, неявно надаються rustc, але користувацькі умовні параметри мають передаватися до rustc за допомогою прапорця --cfg.

#[cfg(some_condition)]
fn conditional_function() {
    println!("condition met!");
}

fn main() {
    conditional_function();
}

Спробуйте запустити це, щоб побачити, що станеться без користувацького прапорця cfg.

З користувацьким прапорцем cfg:

$ rustc --cfg some_condition custom.rs && ./custom
condition met!

Generics

Узагальнені типи — це тема узагальнення типів і функціональностей для ширших випадків. Це надзвичайно корисно для зменшення дублювання коду багатьма способами, але може вимагати досить складного синтаксису. А саме, бути узагальненим вимагає уважно вказувати, для яких типів узагальнений тип справді вважається дійсним. Найпростішим і найпоширенішим використанням узагальнених типів є параметри типу.

Параметр типу вказується як узагальнений за допомогою кутових дужок і верхнього camel case: <Aaa, Bbb, ...>. “Параметри узагальненого типу” зазвичай представлені як <T>. У Rust “generic” також описує будь-що, що приймає один або кілька параметрів узагальненого типу <T>. Будь-який тип, визначений як параметр узагальненого типу, є узагальненим, а все інше є конкретним (неузагальненим).

Наприклад, визначення узагальненої функції з назвою foo, яка приймає аргумент T будь-якого типу:

fn foo<T>(arg: T) { ... }

Оскільки T було вказано як параметр узагальненого типу за допомогою <T>, воно вважається узагальненим, коли використовується тут як (arg: T). Це так навіть якщо T раніше було визначено як struct.

Цей приклад показує деякий синтаксис у дії:

// A concrete type `A`.
struct A;

// In defining the type `Single`, the first use of `A` is not preceded by `<A>`.
// Therefore, `Single` is a concrete type, and `A` is defined as above.
struct Single(A);
//            ^ Here is `Single`s first use of the type `A`.

// Here, `<T>` precedes the first use of `T`, so `SingleGen` is a generic type.
// Because the type parameter `T` is generic, it could be anything, including
// the concrete type `A` defined at the top.
struct SingleGen<T>(T);

fn main() {
    // `Single` is concrete and explicitly takes `A`.
    let _s = Single(A);

    // Create a variable `_char` of type `SingleGen<char>`
    // and give it the value `SingleGen('a')`.
    // Here, `SingleGen` has a type parameter explicitly specified.
    let _char: SingleGen<char> = SingleGen('a');

    // `SingleGen` can also have a type parameter implicitly specified:
    let _t    = SingleGen(A); // Uses `A` defined at the top.
    let _i32  = SingleGen(6); // Uses `i32`.
    let _char = SingleGen('a'); // Uses `char`.
}

Див. також:

structs

Functions

Такий самий набір правил можна застосувати й до функцій: тип T стає узагальненим, коли перед ним стоїть <T>.

Використання узагальнених функцій інколи вимагає явного вказування типових параметрів. Це може бути випадком, якщо функцію викликають там, де тип повернення є узагальненим, або якщо компілятор не має достатньо інформації, щоб вивести необхідні типові параметри.

Виклик функції з явно вказаними типобічними параметрами виглядає так: fun::<A, B, ...>().

struct A;          // Concrete type `A`.
struct S(A);       // Concrete type `S`.
struct SGen<T>(T); // Generic type `SGen`.


// The following functions all take ownership of the variable passed into
// them and immediately go out of scope, freeing the variable.

// Define a function `reg_fn` that takes an argument `_s` of type `S`.
// This has no `<T>` so this is not a generic function.
fn reg_fn(_s: S) {}

// Define a function `gen_spec_t` that takes an argument `_s` of type `SGen<T>`.
// It has been explicitly given the type parameter `A`, but because `A` has not
// been specified as a generic type parameter for `gen_spec_t`, it is not generic.
fn gen_spec_t(_s: SGen<A>) {}

// Define a function `gen_spec_i32` that takes an argument `_s` of type `SGen<i32>`.
// It has been explicitly given the type parameter `i32`, which is a specific type.
// Because `i32` is not a generic type, this function is also not generic.
fn gen_spec_i32(_s: SGen<i32>) {}

// Define a function `generic` that takes an argument `_s` of type `SGen<T>`.
// Because `SGen<T>` is preceded by `<T>`, this function is generic over `T`.
fn generic<T>(_s: SGen<T>) {}

fn main() {
    // Using the non-generic functions
    reg_fn(S(A));          // Concrete type.
    gen_spec_t(SGen(A));   // Implicitly specified type parameter `A`.
    gen_spec_i32(SGen(6)); // Implicitly specified type parameter `i32`.

    // Explicitly specified type parameter `char` to `generic()`.
    generic::<char>(SGen('a'));

    // Implicitly specified type parameter `char` to `generic()`.
    generic(SGen('c'));
}

See also:

functions and structs

Реалізація

Подібно до функцій, реалізації потребують обережності, щоб залишатися узагальненими.

#![allow(unused)]
fn main() {
struct S; // Concrete type `S`
struct GenericVal<T>(T); // Generic type `GenericVal`

// impl of GenericVal where we explicitly specify type parameters:
impl GenericVal<f32> {} // Specify `f32`
impl GenericVal<S> {} // Specify `S` as defined above

// `<T>` Must precede the type to remain generic
impl<T> GenericVal<T> {}
}
struct Val {
    val: f64,
}

struct GenVal<T> {
    gen_val: T,
}

// impl of Val
impl Val {
    fn value(&self) -> &f64 {
        &self.val
    }
}

// impl of GenVal for a generic type `T`
impl<T> GenVal<T> {
    fn value(&self) -> &T {
        &self.gen_val
    }
}

fn main() {
    let x = Val { val: 3.0 };
    let y = GenVal { gen_val: 3i32 };

    println!("{}, {}", x.value(), y.value());
}

Дивіться також:

функції, що повертають посилання, impl і struct

Трейт (Traits)

Звісно, traits також можуть бути узагальненими. Тут ми визначаємо один, який перевизначає Drop trait як узагальнений метод, щоб drop саме його та вхідний аргумент.

// Non-copyable types.
struct Empty;
struct Null;

// A trait generic over `T`.
trait DoubleDrop<T> {
    // Define a method on the caller type which takes an
    // additional single parameter `T` and does nothing with it.
    fn double_drop(self, _: T);
}

// Implement `DoubleDrop<T>` for any generic parameter `T` and
// caller `U`.
impl<T, U> DoubleDrop<T> for U {
    // This method takes ownership of both passed arguments,
    // deallocating both.
    fn double_drop(self, _: T) {}
}

fn main() {
    let empty = Empty;
    let null  = Null;

    // Deallocate `empty` and `null`.
    empty.double_drop(null);

    //empty;
    //null;
    // ^ TODO: Try uncommenting these lines.
}

Див. також:

Drop, struct, and trait

Bounds

Коли ви працюєте з узагальненими типами, параметри типу часто мають використовувати трейт як обмеження, щоб задати, яку функціональність реалізує тип. Наприклад, у наведеному прикладі використовується трейт Display, щоб друкувати, і тому T потрібно обмежити Display; тобто T має реалізовувати Display.

// Define a function `printer` that takes a generic type `T` which
// must implement trait `Display`.
fn printer<T: Display>(t: T) {
    println!("{}", t);
}

Обмеження звужує узагальнений тип до типів, що відповідають обмеженням. Тобто:

struct S<T: Display>(T);

// Error! `Vec<T>` does not implement `Display`. This
// specialization will fail.
let s = S(vec![1]);

Ще одним наслідком обмеження є те, що екземпляри узагальнених типів можуть отримувати доступ до [методів] трейтів, указаних в обмеженнях. Наприклад:

// A trait which implements the print marker: `{:?}`.
use std::fmt::Debug;

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for Rectangle {
    fn area(&self) -> f64 { self.length * self.height }
}

#[derive(Debug)]
struct Rectangle { length: f64, height: f64 }
#[allow(dead_code)]
struct Triangle  { length: f64, height: f64 }

// The generic `T` must implement `Debug`. Regardless
// of the type, this will work properly.
fn print_debug<T: Debug>(t: &T) {
    println!("{:?}", t);
}

// `T` must implement `HasArea`. Any type which meets
// the bound can access `HasArea`'s function `area`.
fn area<T: HasArea>(t: &T) -> f64 { t.area() }

fn main() {
    let rectangle = Rectangle { length: 3.0, height: 4.0 };
    let _triangle = Triangle  { length: 3.0, height: 4.0 };

    print_debug(&rectangle);
    println!("Area: {}", area(&rectangle));

    //print_debug(&_triangle);
    //println!("Area: {}", area(&_triangle));
    // ^ TODO: Try uncommenting these.
    // | Error: Does not implement either `Debug` or `HasArea`.
}

Додатково, клаузи where також можна використовувати для застосування обмежень у деяких випадках, щоб зробити виразніше.

Дивіться також:

std::fmt, structs, and traits

Тестовий приклад: порожні обмеження

Наслідком того, як працюють обмеження, є те, що навіть якщо trait не містить жодної функціональності, ви все одно можете використовувати його як обмеження. Eq і Copy — приклади таких traits із бібліотеки std.

struct Cardinal;
struct BlueJay;
struct Turkey;

trait Red {}
trait Blue {}

impl Red for Cardinal {}
impl Blue for BlueJay {}

// These functions are only valid for types which implement these
// traits. The fact that the traits are empty is irrelevant.
fn red<T: Red>(_: &T)   -> &'static str { "red" }
fn blue<T: Blue>(_: &T) -> &'static str { "blue" }

fn main() {
    let cardinal = Cardinal;
    let blue_jay = BlueJay;
    let _turkey   = Turkey;

    // `red()` won't work on a blue jay nor vice versa
    // because of the bounds.
    println!("A cardinal is {}", red(&cardinal));
    println!("A blue jay is {}", blue(&blue_jay));
    //println!("A turkey is {}", red(&_turkey));
    // ^ TODO: Try uncommenting this line.
}

Дивіться також:

std::cmp::Eq, std::marker::Copy і traits

Multiple bounds

Кілька обмежень для одного типу можна застосувати за допомогою +. Як і зазвичай, різні типи розділяються ,.

use std::fmt::{Debug, Display};

fn compare_prints<T: Debug + Display>(t: &T) {
    println!("Debug: `{:?}`", t);
    println!("Display: `{}`", t);
}

fn compare_types<T: Debug, U: Debug>(t: &T, u: &U) {
    println!("t: `{:?}`", t);
    println!("u: `{:?}`", u);
}

fn main() {
    let string = "words";
    let array = [1, 2, 3];
    let vec = vec![1, 2, 3];

    compare_prints(&string);
    //compare_prints(&array);
    // TODO ^ Try uncommenting this.
    
    compare_types(&array, &vec);
}

Дивіться також:

std::fmt і traits

Де-clauses

Обмеження також може бути виражене за допомогою where-клауза безпосередньо перед відкривною {, а не при першому згадуванні типу. Крім того, where-клаузи можуть застосовувати обмеження до довільних типів, а не лише до параметрів типу.

Деякі випадки, у яких where-клауза корисна:

  • Коли окреме зазначення узагальнених типів і обмежень є зрозумілішим:
impl <A: TraitB + TraitC, D: TraitE + TraitF> MyTrait<A, D> for YourType {}

// Expressing bounds with a `where` clause
impl <A, D> MyTrait<A, D> for YourType where
    A: TraitB + TraitC,
    D: TraitE + TraitF {}
  • Коли використання where-клауза є більш виразним, ніж використання звичайного синтаксису. impl у цьому прикладі не можна прямо виразити без where-клауза:
use std::fmt::Debug;

trait PrintInOption {
    fn print_in_option(self);
}

// Because we would otherwise have to express this as `T: Debug` or
// use another method of indirect approach, this requires a `where` clause:
impl<T> PrintInOption for T where
    Option<T>: Debug {
    // We want `Option<T>: Debug` as our bound because that is what's
    // being printed. Doing otherwise would be using the wrong bound.
    fn print_in_option(self) {
        println!("{:?}", Some(self));
    }
}

fn main() {
    let vec = vec![1, 2, 3];

    vec.print_in_option();
}

Дивіться також:

RFC, struct, and trait

New Type Idiom

Ідіома newtype дає гарантії під час компіляції, що до програми подається правильний тип значення.

Наприклад, функції, яка вимірює відстань у милях, має бути передане значення типу Miles.

struct Miles(f64);

struct Kilometers(f64);

impl Miles {
    pub fn to_kilometers(&self) -> Kilometers {
        Kilometers(self.0 * 1.609344)
    }
}

impl Kilometers {
    pub fn to_miles(&self) -> Miles {
        Miles(self.0 / 1.609344)
    }
}

fn is_a_marathon(distance: &Miles) -> bool {
    distance.0 >= 26.2
}

fn main() {
    let distance = Miles(30.0);
    let distance_km = distance.to_kilometers();
    println!("Is a marathon? {}", is_a_marathon(&distance));
    println!("Is a marathon? {}", is_a_marathon(&distance_km.to_miles()));
    // println!("Is a marathon? {}", is_a_marathon(&distance_km));
}

Розкоментуйте останній оператор println!, щоб побачити, що поданий тип має бути Miles.

Щоб отримати значення newtype як базовий тип, ви можете використати кортежний синтаксис або синтаксис деструктуризації, ось так:

struct Miles(f64);

fn main() {
    let distance = Miles(42.0);
    let distance_as_primitive_1: f64 = distance.0; // Tuple
    let Miles(distance_as_primitive_2) = distance; // Destructuring
}

Дивіться також:

structs

Асоційовані елементи

“Associated Items” означає набір правил, що стосуються item-ів item різних типів. Це розширення для узагальнень trait, і воно дозволяє trait-ам внутрішньо визначати нові елементи.

Один із таких елементів називається асоційований тип (associated type), що забезпечує простіші шаблони використання, коли trait є узагальненим за своїм типом контейнера.

Дивіться також:

RFC

Проблема

trait, який є узагальнений за своїм типом контейнера, має вимоги до специфікації типів — користувачі trait мають вказати всі його узагальнені типи.

У прикладі нижче trait Contains дозволяє використовувати узагальнені типи A і B. Потім цей trait реалізується для типу Container, де для A і B вказується i32, щоб його можна було використати з fn difference().

Оскільки Contains є узагальнений, ми змушені явно вказати всі узагальнені типи для fn difference(). На практиці ми хочемо мати спосіб виразити, що A і B визначаються вхідним C. Як ви побачите в наступному розділі, асоційовані типи забезпечують саме таку можливість.

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains<A, B> {
    fn contains(&self, _: &A, _: &B) -> bool; // Explicitly requires `A` and `B`.
    fn first(&self) -> i32; // Doesn't explicitly require `A` or `B`.
    fn last(&self) -> i32;  // Doesn't explicitly require `A` or `B`.
}

impl Contains<i32, i32> for Container {
    // True if the numbers stored are equal.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }

    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    fn last(&self) -> i32 { self.1 }
}

// `C` contains `A` and `B`. In light of that, having to express `A` and
// `B` again is a nuisance.
fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container(number_1, number_2);

    println!("Does container contain {} and {}: {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(&container));
}

Дивіться також:

structs і traits

Асоційовані типи

Використання “Асоційовані типи” покращує загальну читабельність коду шляхом локального перенесення внутрішніх типів у трейт як типи виводу. Синтаксис для оголошення trait має такий вигляд:

#![allow(unused)]
fn main() {
// `A` and `B` are defined in the trait via the `type` keyword.
// (Note: `type` in this context is different from `type` when used for
// aliases).
trait Contains {
    type A;
    type B;

    // Updated syntax to refer to these new types generically.
    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
}
}

Зверніть увагу, що функції, які використовують трейт Contains, більше не зобов’язані взагалі вказувати A або B:

// Without using associated types
fn difference<A, B, C>(container: &C) -> i32 where
    C: Contains<A, B> { ... }

// Using associated types
fn difference<C: Contains>(container: &C) -> i32 { ... }

Перепишімо приклад із попереднього розділу, використовуючи асоційовані типи:

struct Container(i32, i32);

// A trait which checks if 2 items are stored inside of container.
// Also retrieves first or last value.
trait Contains {
    // Define generic types here which methods will be able to utilize.
    type A;
    type B;

    fn contains(&self, _: &Self::A, _: &Self::B) -> bool;
    fn first(&self) -> i32;
    fn last(&self) -> i32;
}

impl Contains for Container {
    // Specify what types `A` and `B` are. If the `input` type
    // is `Container(i32, i32)`, the `output` types are determined
    // as `i32` and `i32`.
    type A = i32;
    type B = i32;

    // `&Self::A` and `&Self::B` are also valid here.
    fn contains(&self, number_1: &i32, number_2: &i32) -> bool {
        (&self.0 == number_1) && (&self.1 == number_2)
    }
    // Grab the first number.
    fn first(&self) -> i32 { self.0 }

    // Grab the last number.
    fn last(&self) -> i32 { self.1 }
}

fn difference<C: Contains>(container: &C) -> i32 {
    container.last() - container.first()
}

fn main() {
    let number_1 = 3;
    let number_2 = 10;

    let container = Container(number_1, number_2);

    println!("Does container contain {} and {}: {}",
        &number_1, &number_2,
        container.contains(&number_1, &number_2));
    println!("First number: {}", container.first());
    println!("Last number: {}", container.last());

    println!("The difference is: {}", difference(&container));
}

Параметри фантомного типу

Параметр фантомного типу — це той, який не з’являється під час виконання, але перевіряється статично (і лише) під час компіляції.

Типи даних можуть використовувати додаткові узагальнені параметри типу, щоб виступати як маркери або виконувати перевірку типів під час компіляції. Ці додаткові параметри не містять значень у сховищі та не мають поведінки під час виконання.

У наведеному нижче прикладі ми поєднуємо std::marker::PhantomData з концепцією параметра фантомного типу, щоб створити кортежі, що містять різні типи даних.

use std::marker::PhantomData;

// A phantom tuple struct which is generic over `A` with hidden parameter `B`.
#[derive(PartialEq)] // Allow equality test for this type.
struct PhantomTuple<A, B>(A, PhantomData<B>);

// A phantom type struct which is generic over `A` with hidden parameter `B`.
#[derive(PartialEq)] // Allow equality test for this type.
struct PhantomStruct<A, B> { first: A, phantom: PhantomData<B> }

// Note: Storage is allocated for generic type `A`, but not for `B`.
//       Therefore, `B` cannot be used in computations.

fn main() {
    // Here, `f32` and `f64` are the hidden parameters.
    // PhantomTuple type specified as `<char, f32>`.
    let _tuple1: PhantomTuple<char, f32> = PhantomTuple('Q', PhantomData);
    // PhantomTuple type specified as `<char, f64>`.
    let _tuple2: PhantomTuple<char, f64> = PhantomTuple('Q', PhantomData);

    // Type specified as `<char, f32>`.
    let _struct1: PhantomStruct<char, f32> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };
    // Type specified as `<char, f64>`.
    let _struct2: PhantomStruct<char, f64> = PhantomStruct {
        first: 'Q',
        phantom: PhantomData,
    };

    // Compile-time Error! Type mismatch so these cannot be compared:
    // println!("_tuple1 == _tuple2 yields: {}",
    //           _tuple1 == _tuple2);

    // Compile-time Error! Type mismatch so these cannot be compared:
    // println!("_struct1 == _struct2 yields: {}",
    //           _struct1 == _struct2);
}

Див. також:

Derive, struct і tuple.

Testcase: уточнення одиниць

Корисний метод перетворення одиниць можна дослідити, реалізувавши Add за допомогою параметра типу-привида. trait Add розглядається нижче:

// This construction would impose: `Self + RHS = Output`
// where RHS defaults to Self if not specified in the implementation.
pub trait Add<RHS = Self> {
    type Output;

    fn add(self, rhs: RHS) -> Self::Output;
}

// `Output` must be `T<U>` so that `T<U> + T<U> = T<U>`.
impl<U> Add for T<U> {
    type Output = T<U>;
    ...
}

Уся реалізація:

use std::ops::Add;
use std::marker::PhantomData;

/// Створює порожні переліки, щоб визначити типи одиниць.
#[derive(Debug, Clone, Copy)]
enum Inch {}
#[derive(Debug, Clone, Copy)]
enum Mm {}

/// `Length` — це тип із параметром типу-привида `Unit`,
/// і він не є узагальненим за типом довжини (тобто `f64`).
///
/// `f64` уже реалізує трейти `Clone` і `Copy`.
#[derive(Debug, Clone, Copy)]
struct Length<Unit>(f64, PhantomData<Unit>);

/// Трейт `Add` визначає поведінку оператора `+`.
impl<Unit> Add for Length<Unit> {
    type Output = Length<Unit>;

    // add() повертає нову структуру `Length`, що містить суму.
    fn add(self, rhs: Length<Unit>) -> Length<Unit> {
        // `+` викликає реалізацію `Add` для `f64`.
        Length(self.0 + rhs.0, PhantomData)
    }
}

fn main() {
    // Вказує `one_foot` мати параметр типу-привида `Inch`.
    let one_foot:  Length<Inch> = Length(12.0, PhantomData);
    // `one_meter` має параметр типу-привида `Mm`.
    let one_meter: Length<Mm>   = Length(1000.0, PhantomData);

    // `+` викликає метод `add()`, який ми реалізували для `Length<Unit>`.
    //
    // Оскільки `Length` реалізує `Copy`, `add()` не споживає
    // `one_foot` і `one_meter`, а копіює їх у `self` і `rhs`.
    let two_feet = one_foot + one_foot;
    let two_meters = one_meter + one_meter;

    // Додавання працює.
    println!("one foot + one_foot = {:?} in", two_feet.0);
    println!("one meter + one_meter = {:?} mm", two_meters.0);

    // Безглузді операції зазнають невдачі, як і повинні:
    // Помилка часу компіляції: невідповідність типів.
    //let one_feter = one_foot + one_meter;
}

Дивіться також:

Запозичення (&), Обмеження (X: Y), enum, impl & self, Перевантаження, ref, Трейти (X for Y), and Кортежні структури.

Правила області видимості

Області видимості відіграють важливу роль у володінні, запозиченні та часах життя. Тобто вони вказують компілятору, коли запозичення є дійсними, коли ресурси можна звільнити, і коли змінні створюються або знищуються.

RAII

Змінні в Rust роблять більше, ніж просто зберігають дані в стеку: вони також володіють ресурсами, наприклад, Box<T> володіє пам’яттю в купі. Rust забезпечує RAII (Resource Acquisition Is Initialization), тож коли об’єкт виходить із області видимості, викликається його деструктор, і його власні ресурси звільняються.

Ця поведінка захищає від помилок витоку ресурсу — тому вам більше ніколи не доведеться вручну звільняти пам’ять або турбуватися про витоки пам’яті! Ось коротка демонстрація:

// raii.rs
fn create_box() {
    // Allocate an integer on the heap
    let _box1 = Box::new(3i32);

    // `_box1` is destroyed here, and memory gets freed
}

fn main() {
    // Allocate an integer on the heap
    let _box2 = Box::new(5i32);

    // A nested scope:
    {
        // Allocate an integer on the heap
        let _box3 = Box::new(4i32);

        // `_box3` is destroyed here, and memory gets freed
    }

    // Creating lots of boxes just for fun
    // There's no need to manually free memory!
    for _ in 0u32..1_000 {
        create_box();
    }

    // `_box2` is destroyed here, and memory gets freed
}

Звісно, ми можемо додатково перевірити наявність помилок пам’яті, використовуючи valgrind:

$ rustc raii.rs && valgrind ./raii
==26873== Memcheck, a memory error detector
==26873== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==26873== Using Valgrind-3.9.0 and LibVEX; rerun with -h for copyright info
==26873== Command: ./raii
==26873==
==26873==
==26873== HEAP SUMMARY:
==26873==     in use at exit: 0 bytes in 0 blocks
==26873==   total heap usage: 1,013 allocs, 1,013 frees, 8,696 bytes allocated
==26873==
==26873== All heap blocks were freed -- no leaks are possible
==26873==
==26873== For counts of detected and suppressed errors, rerun with: -v
==26873== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 2 from 2)

Жодних витоків тут немає!

Деструктор

Поняття деструктора в Rust надається через трейт Drop. Деструктор викликається, коли ресурс виходить із області видимості. Цей трейт не обов’язково реалізовувати для кожного типу; реалізуйте його для свого типу лише якщо вам потрібна власна логіка деструктора.

Запустіть приклад нижче, щоб побачити, як працює трейт Drop. Коли змінна в функції main виходить із області видимості, буде викликано користувацький деструктор.

struct ToDrop;

impl Drop for ToDrop {
    fn drop(&mut self) {
        println!("ToDrop is being dropped");
    }
}

fn main() {
    let x = ToDrop;
    println!("Made a ToDrop!");
}

Дивіться також:

Box

Володіння та переміщення

Оскільки змінні відповідають за звільнення власних ресурсів, ресурси можуть мати лише одного власника. Це запобігає звільненню ресурсів більше ніж один раз. Зверніть увагу, що не всі змінні володіють ресурсами (наприклад, [посиланнями]).

Під час присвоєнь (let x = y) або передавання аргументів функції за значенням (foo(x)), володіння ресурсами передається. У термінах Rust це відомо як переміщення.

Після переміщення ресурсів попереднього власника більше не можна використовувати. Це запобігає створенню висячих вказівників.

// This function takes ownership of the heap allocated memory
fn destroy_box(c: Box<i32>) {
    println!("Destroying a box that contains {}", c);

    // `c` is destroyed and the memory freed
}

fn main() {
    // _Stack_ allocated integer
    let x = 5u32;

    // *Copy* `x` into `y` - no resources are moved
    let y = x;

    // Both values can be independently used
    println!("x is {}, and y is {}", x, y);

    // `a` is a pointer to a _heap_ allocated integer
    let a = Box::new(5i32);

    println!("a contains: {}", a);

    // *Move* `a` into `b`
    let b = a;
    // The pointer address of `a` is copied (not the data) into `b`.
    // Both are now pointers to the same heap allocated data, but
    // `b` now owns it.

    // Error! `a` can no longer access the data, because it no longer owns the
    // heap memory
    //println!("a contains: {}", a);
    // TODO ^ Try uncommenting this line

    // This function takes ownership of the heap allocated memory from `b`
    destroy_box(b);

    // Since the heap memory has been freed at this point, this action would
    // result in dereferencing freed memory, but it's forbidden by the compiler
    // Error! Same reason as the previous Error
    //println!("b contains: {}", b);
    // TODO ^ Try uncommenting this line
}

Змінність (mutability)

Змінність даних може бути змінена, коли володіння передається.

fn main() {
    let immutable_box = Box::new(5u32);

    println!("immutable_box contains {}", immutable_box);

    // Mutability error
    //*immutable_box = 4;

    // *Move* the box, changing the ownership (and mutability)
    let mut mutable_box = immutable_box;

    println!("mutable_box contains {}", mutable_box);

    // Modify the contents of the box
    *mutable_box = 4;

    println!("mutable_box now contains {}", mutable_box);
}

Часткові переміщення

Під час [деструктуризації] однієї змінної обидва прив’язування зразка by-move і by-reference можуть використовуватися одночасно. Це призведе до часткового переміщення змінної, що означає , що частини змінної будуть переміщені, тоді як інші частини залишаться. У такому випадку батьківську змінну не можна буде використовувати після цього як ціле, однак частини, на які лише посилаються (і які не переміщено), ще можна використовувати. Зверніть увагу, що типи, які реалізують Drop trait, не можуть бути частково переміщені, тому що його метод drop після цього використав би його як ціле.

fn main() {
    #[derive(Debug)]
    struct Person {
        name: String,
        age: Box<u8>,
    }

    // Error! cannot move out of a type which implements the `Drop` trait
    //impl Drop for Person {
    //    fn drop(&mut self) {
    //        println!("Dropping the person struct {:?}", self)
    //    }
    //}
    // TODO ^ Try uncommenting these lines

    let person = Person {
        name: String::from("Alice"),
        age: Box::new(20),
    };

    // `name` is moved out of person, but `age` is referenced
    let Person { name, ref age } = person;

    println!("The person's age is {}", age);

    println!("The person's name is {}", name);

    // Error! borrow of partially moved value: `person` partial move occurs
    //println!("The person struct is {:?}", person);

    // `person` cannot be used but `person.age` can be used as it is not moved
    println!("The person's age from person struct is {}", person.age);
}

(У цьому прикладі ми зберігаємо змінну age у купі, щоб проілюструвати часткове переміщення: видалення ref у наведеному вище коді дало б помилку, оскільки володіння person.age було б переміщено до змінної age. Якби Person.age зберігалося в стеці, ref не було б потрібно, оскільки визначення age скопіювало б дані з person.age без переміщення їх.)

Див. також:

деструктуризації

Запозичення

Здебільшого ми хочемо отримувати доступ до даних, не беручи над ними володіння. Щоб досягти цього, Rust використовує механізм запозичення. Замість передавання об’єктів за значенням (T), об’єкти можна передавати за посиланням (&T).

Компілятор статично гарантує (за допомогою свого перевірника запозичень), що посилання завжди вказують на чинні об’єкти. Тобто, поки існують посилання на об’єкт, цей об’єкт не можна знищити.

// This function takes ownership of a box and destroys it
fn eat_box_i32(boxed_i32: Box<i32>) {
    println!("Destroying box that contains {}", boxed_i32);
}

// This function borrows an i32
fn borrow_i32(borrowed_i32: &i32) {
    println!("This int is: {}", borrowed_i32);
}

fn main() {
    // Create a boxed i32 in the heap, and an i32 on the stack
    // Remember: numbers can have arbitrary underscores added for readability
    // 5_i32 is the same as 5i32
    let boxed_i32 = Box::new(5_i32);
    let stacked_i32 = 6_i32;

    // Borrow the contents of the box. Ownership is not taken,
    // so the contents can be borrowed again.
    borrow_i32(&boxed_i32);
    borrow_i32(&stacked_i32);

    {
        // Take a reference to the data contained inside the box
        let _ref_to_i32: &i32 = &boxed_i32;

        // Error!
        // Can't destroy `boxed_i32` while the inner value is borrowed later in scope.
        eat_box_i32(boxed_i32);
        // FIXME ^ Comment out this line

        // Attempt to borrow `_ref_to_i32` after inner value is destroyed
        borrow_i32(_ref_to_i32);
        // `_ref_to_i32` goes out of scope and is no longer borrowed.
    }

    // `boxed_i32` can now give up ownership to `eat_box_i32` and be destroyed
    eat_box_i32(boxed_i32);
}

Змінність (mutability)

Змінні дані можна запозичувати як змінні за допомогою &mut T. Це називається змінним посиланням і надає доступ на читання/запис для того, хто запозичує. На відміну від цього, &T запозичує дані через незмінне посилання, і той, хто запозичує, може читати дані, але не може їх змінювати:

#[allow(dead_code)]
#[derive(Clone, Copy)]
struct Book {
    // `&'static str` is a reference to a string allocated in read only memory
    author: &'static str,
    title: &'static str,
    year: u32,
}

// This function takes a reference to a book
fn borrow_book(book: &Book) {
    println!("I immutably borrowed {} - {} edition", book.title, book.year);
}

// This function takes a reference to a mutable book and changes `year` to 2014
fn new_edition(book: &mut Book) {
    book.year = 2014;
    println!("I mutably borrowed {} - {} edition", book.title, book.year);
}

fn main() {
    // Create an immutable Book named `immutabook`
    let immutabook = Book {
        // string literals have type `&'static str`
        author: "Douglas Hofstadter",
        title: "Gödel, Escher, Bach",
        year: 1979,
    };

    // Create a mutable copy of `immutabook` and call it `mutabook`
    let mut mutabook = immutabook;

    // Immutably borrow an immutable object
    borrow_book(&immutabook);

    // Immutably borrow a mutable object
    borrow_book(&mutabook);

    // Borrow a mutable object as mutable
    new_edition(&mut mutabook);

    // Error! Cannot borrow an immutable object as mutable
    new_edition(&mut immutabook);
    // FIXME ^ Comment out this line
}

Дивіться також:

static

Aliasing

Дані можуть бути незмінно запозичені будь-яку кількість разів, але поки вони незмінно запозичені, оригінальні дані не можуть бути запозичені як змінні. З іншого боку, лише одне змінне запозичення дозволене одночасно. Оригінальні дані можуть бути запозичені знову лише після того, як на посилання змінних було використане востаннє.

struct Point { x: i32, y: i32, z: i32 }

fn main() {
    let mut point = Point { x: 0, y: 0, z: 0 };

    let borrowed_point = &point;
    let another_borrow = &point;

    // Data can be accessed via the references and the original owner
    println!("Point has coordinates: ({}, {}, {})",
                borrowed_point.x, another_borrow.y, point.z);

    // Error! Can't borrow `point` as mutable because it's currently
    // borrowed as immutable.
    // let mutable_borrow = &mut point;
    // TODO ^ Try uncommenting this line

    // The borrowed values are used again here
    println!("Point has coordinates: ({}, {}, {})",
                borrowed_point.x, another_borrow.y, point.z);

    // The immutable references are no longer used for the rest of the code so
    // it is possible to reborrow with a mutable reference.
    let mutable_borrow = &mut point;

    // Change data via mutable reference
    mutable_borrow.x = 5;
    mutable_borrow.y = 2;
    mutable_borrow.z = 1;

    // Error! Can't borrow `point` as immutable because it's currently
    // borrowed as mutable.
    // let y = &point.y;
    // TODO ^ Try uncommenting this line

    // Error! Can't print because `println!` takes an immutable reference.
    // println!("Point Z coordinate is {}", point.z);
    // TODO ^ Try uncommenting this line

    // Ok! Mutable references can be passed as immutable to `println!`
    println!("Point has coordinates: ({}, {}, {})",
                mutable_borrow.x, mutable_borrow.y, mutable_borrow.z);

    // The mutable reference is no longer used for the rest of the code so it
    // is possible to reborrow
    let new_borrowed_point = &point;
    println!("Point now has coordinates: ({}, {}, {})",
             new_borrowed_point.x, new_borrowed_point.y, new_borrowed_point.z);
}

Патерн ref

Під час виконання зіставлення зі зразком або деструктуризації через прив’язування let, ключове слово ref можна використовувати, щоб брати посилання на поля структури/кортежу. Наведений нижче приклад показує кілька випадків, коли це може бути корисно:

#[derive(Clone, Copy)]
struct Point { x: i32, y: i32 }

fn main() {
    let c = 'Q';

    // A `ref` borrow on the left side of an assignment is equivalent to
    // an `&` borrow on the right side.
    let ref ref_c1 = c;
    let ref_c2 = &c;

    println!("ref_c1 equals ref_c2: {}", *ref_c1 == *ref_c2);

    let point = Point { x: 0, y: 0 };

    // `ref` is also valid when destructuring a struct.
    let _copy_of_x = {
        // `ref_to_x` is a reference to the `x` field of `point`.
        let Point { x: ref ref_to_x, y: _ } = point;

        // Return a copy of the `x` field of `point`.
        *ref_to_x
    };

    // A mutable copy of `point`
    let mut mutable_point = point;

    {
        // `ref` can be paired with `mut` to take mutable references.
        let Point { x: _, y: ref mut mut_ref_to_y } = mutable_point;

        // Mutate the `y` field of `mutable_point` via a mutable reference.
        *mut_ref_to_y = 1;
    }

    println!("point is ({}, {})", point.x, point.y);
    println!("mutable_point is ({}, {})", mutable_point.x, mutable_point.y);

    // A mutable tuple that includes a pointer
    let mut mutable_tuple = (Box::new(5u32), 3u32);

    {
        // Destructure `mutable_tuple` to change the value of `last`.
        let (_, ref mut last) = mutable_tuple;
        *last = 2u32;
    }

    println!("tuple is {:?}", mutable_tuple);
}

Часи життя (lifetimes)

Час життя (lifetime) — це конструкція, яку компілятор (точніше, його перевірник запозичень (borrow checker)) використовує, щоб переконатися, що всі запозичення є дійсними. Зокрема, час життя змінної починається, коли її створено, і закінчується, коли її знищено. Хоча часи життя та області видимості часто згадують разом, це не одне й те саме.

Візьмемо, наприклад, випадок, коли ми запозичуємо змінну через &. Запозичення має час життя, який визначається тим, де його оголошено. У результаті запозичення є дійсним доти, доки воно завершується до знищення позичальника. Однак область видимості запозичення визначається тим, де використовується посилання.

У наступному прикладі та в решті цього розділу ми побачимо, як часи життя пов’язані з областями видимості, а також чим ці двоє відрізняються.

// Lifetimes are annotated below with lines denoting the creation
// and destruction of each variable.
// `i` has the longest lifetime because its scope entirely encloses
// both `borrow1` and `borrow2`. The duration of `borrow1` compared
// to `borrow2` is irrelevant since they are disjoint.
fn main() {
    let i = 3; // Lifetime for `i` starts. ────────────────┐
    //                                                     │
    { //                                                   │
        let borrow1 = &i; // `borrow1` lifetime starts. ──┐│
        //                                                ││
        println!("borrow1: {}", borrow1); //              ││
    } // `borrow1` ends. ─────────────────────────────────┘│
    //                                                     │
    //                                                     │
    { //                                                   │
        let borrow2 = &i; // `borrow2` lifetime starts. ──┐│
        //                                                ││
        println!("borrow2: {}", borrow2); //              ││
    } // `borrow2` ends. ─────────────────────────────────┘│
    //                                                     │
}   // Lifetime ends. ─────────────────────────────────────┘

Зверніть увагу, що для позначення часів життя не призначаються жодні імена або типи. Це обмежує те, як часи життя зможуть використовуватися, як ми побачимо.

Explicit annotation

Перевірник запозичень використовує явні анотації часу життя, щоб визначити, як довго посилання мають бути дійсними. У випадках, коли часи життя не скорочуються1, Rust вимагає явних анотацій, щоб визначити, яким має бути час життя посилання. Синтаксис для явного анотування часу життя використовує символ апострофа так:

foo<'a>
// `foo` has a lifetime parameter `'a`

Подібно до замикань, використання часів життя вимагає узагальнених типів. Крім того, цей синтаксис часу життя вказує, що час життя foo не може перевищувати час життя 'a. Явна анотація типу має форму &'a T, де 'a уже було введено.

У випадках із кількома часами життя синтаксис подібний:

foo<'a, 'b>
// `foo` has lifetime parameters `'a` and `'b`

У цьому випадку час життя foo не може перевищувати ні 'a, ні 'b.

Дивіться наведений нижче приклад використання явного анотування часу життя:

// `print_refs` takes two references to `i32` which have different
// lifetimes `'a` and `'b`. These two lifetimes must both be at
// least as long as the function `print_refs`.
fn print_refs<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("x is {} and y is {}", x, y);
}

// A function which takes no arguments, but has a lifetime parameter `'a`.
fn failed_borrow<'a>() {
    let _x = 12;

    // ERROR: `_x` does not live long enough
    let _y: &'a i32 = &_x;
    // Attempting to use the lifetime `'a` as an explicit type annotation
    // inside the function will fail because the lifetime of `&_x` is shorter
    // than that of `_y`. A short lifetime cannot be coerced into a longer one.
}

fn main() {
    // Create variables to be borrowed below.
    let (four, nine) = (4, 9);

    // Borrows (`&`) of both variables are passed into the function.
    print_refs(&four, &nine);
    // Any input which is borrowed must outlive the borrower.
    // In other words, the lifetime of `four` and `nine` must
    // be longer than that of `print_refs`.

    failed_borrow();
    // `failed_borrow` contains no references to force `'a` to be
    // longer than the lifetime of the function, but `'a` is longer.
    // Because the lifetime is never constrained, it defaults to `'static`.
}

Дивіться також:

узагальнені типи and замикання


  1. скорочення часу життя неявно анотує часи життя і тому відрізняється.

Functions

Ігноруючи скорочення часу життя, сигнатури функцій із часами життя мають кілька обмежень:

  • будь-яке посилання має мати анотований час життя.
  • будь-яке посилання, що повертається, має мати той самий час життя, що й вхідне, або бути static.

Крім того, зауважте, що повернення посилань без входу заборонене, якщо це призведе до повернення посилань на недійсні дані. Наведений нижче приклад показує кілька допустимих форм функцій із часами життя:

// One input reference with lifetime `'a` which must live
// at least as long as the function.
fn print_one<'a>(x: &'a i32) {
    println!("`print_one`: x is {}", x);
}

// Mutable references are possible with lifetimes as well.
fn add_one<'a>(x: &'a mut i32) {
    *x += 1;
}

// Multiple elements with different lifetimes. In this case, it
// would be fine for both to have the same lifetime `'a`, but
// in more complex cases, different lifetimes may be required.
fn print_multi<'a, 'b>(x: &'a i32, y: &'b i32) {
    println!("`print_multi`: x is {}, y is {}", x, y);
}

// Returning references that have been passed in is acceptable.
// However, the correct lifetime must be returned.
fn pass_x<'a, 'b>(x: &'a i32, _: &'b i32) -> &'a i32 { x }

//fn invalid_output<'a>() -> &'a String { &String::from("foo") }
// The above is invalid: `'a` must live longer than the function.
// Here, `&String::from("foo")` would create a `String`, followed by a
// reference. Then the data is dropped upon exiting the scope, leaving
// a reference to invalid data to be returned.

fn main() {
    let x = 7;
    let y = 9;

    print_one(&x);
    print_multi(&x, &y);

    let z = pass_x(&x, &y);
    print_one(z);

    let mut t = 3;
    add_one(&mut t);
    print_one(&t);
}

Дивіться також:

Функції

Methods

Methods are annotated similarly to functions:

struct Owner(i32);

impl Owner {
    // Annotate lifetimes as in a standalone function.
    fn add_one<'a>(&'a mut self) { self.0 += 1; }
    fn print<'a>(&'a self) {
        println!("`print`: {}", self.0);
    }
}

fn main() {
    let mut owner = Owner(18);

    owner.add_one();
    owner.print();
}

Дивіться також:

methods

Структури

Анотація часів життя у структурах також подібна до функцій:

// A type `Borrowed` which houses a reference to an
// `i32`. The reference to `i32` must outlive `Borrowed`.
#[derive(Debug)]
struct Borrowed<'a>(&'a i32);

// Similarly, both references here must outlive this structure.
#[derive(Debug)]
struct NamedBorrowed<'a> {
    x: &'a i32,
    y: &'a i32,
}

// An enum which is either an `i32` or a reference to one.
#[derive(Debug)]
enum Either<'a> {
    Num(i32),
    Ref(&'a i32),
}

fn main() {
    let x = 18;
    let y = 15;

    let single = Borrowed(&x);
    let double = NamedBorrowed { x: &x, y: &y };
    let reference = Either::Ref(&x);
    let number    = Either::Num(y);

    println!("x is borrowed in {:?}", single);
    println!("x and y are borrowed in {:?}", double);
    println!("x is borrowed in {:?}", reference);
    println!("y is *not* borrowed in {:?}", number);
}

Див. також:

structs

Traits

Анотація часів життя в методах трейтів загалом подібна до функцій. Зверніть увагу, що impl також може мати анотацію часів життя.

// A struct with annotation of lifetimes.
#[derive(Debug)]
struct Borrowed<'a> {
    x: &'a i32,
}

// Annotate lifetimes to impl.
impl<'a> Default for Borrowed<'a> {
    fn default() -> Self {
        Self {
            x: &10,
        }
    }
}

fn main() {
    let b: Borrowed = Default::default();
    println!("b is {:?}", b);
}

Дивіться також:

traits

Межі

Так само, як узагальнені типи можуть мати межі, часи життя (самі по собі узагальнені) також використовують межі. Символ : тут має дещо інше значення, але + — той самий. Зверніть увагу, як слід читати таке:

  1. T: 'a: Усі посилання в T мають переживати час життя 'a.
  2. T: Trait + 'a: Тип T має реалізовувати трейт Trait, і усі посилання в T мають переживати 'a.

Приклад нижче показує наведений вище синтаксис у дії, який використовується після ключового слова where:

use std::fmt::Debug; // Trait to bound with.

#[derive(Debug)]
struct Ref<'a, T: 'a>(&'a T);
// `Ref` contains a reference to a generic type `T` that has
// some lifetime `'a` unknown by `Ref`. `T` is bounded such that any
// *references* in `T` must outlive `'a`. Additionally, the lifetime
// of `Ref` may not exceed `'a`.

// A generic function which prints using the `Debug` trait.
fn print<T>(t: T) where
    T: Debug {
    println!("`print`: t is {:?}", t);
}

// Here a reference to `T` is taken where `T` implements
// `Debug` and all *references* in `T` outlive `'a`. In
// addition, `'a` must outlive the function.
fn print_ref<'a, T>(t: &'a T) where
    T: Debug + 'a {
    println!("`print_ref`: t is {:?}", t);
}

fn main() {
    let x = 7;
    let ref_x = Ref(&x);

    print_ref(&ref_x);
    print(ref_x);
}

Дивіться також:

generics, bounds in generics, and multiple bounds in generics

Коерціювання

Час життя більшої тривалості може бути приведений до меншого, щоб він працював у межах області видимості, де зазвичай не працював би. Це відбувається у формі виведеного коерціювання компілятором Rust, а також у формі оголошення різниці часу життя:

// Тут Rust виводить час життя, який є якомога коротшим.
// Потім два посилання приводяться до цього часу життя.
fn multiply<'a>(first: &'a i32, second: &'a i32) -> i32 {
    first * second
}

// `<'a: 'b, 'b>` читається як час життя `'a` є принаймні таким самим довгим, як `'b`.
// Тут ми отримуємо `&'a i32` і повертаємо `&'b i32` як результат коерціювання.
fn choose_first<'a: 'b, 'b>(first: &'a i32, _: &'b i32) -> &'b i32 {
    first
}

fn main() {
    let first = 2; // Час життя більшої тривалості

    {
        let second = 3; // Час життя меншої тривалості

        println!("The product is {}", multiply(&first, &second));
        println!("{} is the first", choose_first(&first, &second));
    };
}

Static

У Rust є кілька зарезервованих назв часів життя. Одна з них — 'static. Ви можете натрапити на неї у двох ситуаціях:

// A reference with 'static lifetime:
let s: &'static str = "hello world";

// 'static as part of a trait bound:
fn generic<T>(x: T) where T: 'static {}

Обидва випадки пов’язані, але дещо різняться, і це поширене джерело плутанини під час вивчення Rust. Ось кілька прикладів для кожної ситуації:

Reference lifetime

Як час життя посилання 'static означає, що дані, на які вказує посилання, живуть протягом решти часу життя виконуваної програми. Його все ще можна привести до коротшого часу життя.

Існує два поширені способи створити змінну з часом життя 'static, і обидва зберігаються в пам’яті лише для читання бінарного файлу:

  • Створити константу за допомогою оголошення static.
  • Створити рядковий літерал, який має тип: &'static str.

Дивіться наступний приклад, де показано кожен метод:

// Make a constant with `'static` lifetime.
static NUM: i32 = 18;

// Returns a reference to `NUM` where its `'static`
// lifetime is coerced to that of the input argument.
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}

fn main() {
    {
        // Make a `string` literal and print it:
        let static_string = "I'm in read-only memory";
        println!("static_string: {}", static_string);

        // When `static_string` goes out of scope, the reference
        // can no longer be used, but the data remains in the binary.
    }

    {
        // Make an integer to use for `coerce_static`:
        let lifetime_num = 9;

        // Coerce `NUM` to lifetime of `lifetime_num`:
        let coerced_static = coerce_static(&lifetime_num);

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

    println!("NUM: {} stays accessible!", NUM);
}

Оскільки посилання 'static мають бути дійсними лише протягом решти життя програми, їх можна створити під час виконання програми. Щоб це показати, приклад нижче використовує Box::leak для динамічного створення посилань 'static. У цьому випадку вони точно не живуть протягом усього часу, а лише від точки витоку далі.

extern crate rand;
use rand::Fill;

fn random_vec() -> &'static [u64; 100] {
    let mut rng = rand::rng();
    let mut boxed = Box::new([0; 100]);
    boxed.fill(&mut rng);
    Box::leak(boxed)
}

fn main() {
    let first: &'static [u64; 100] = random_vec();
    let second: &'static [u64; 100] = random_vec();
    assert_ne!(first, second)
}

Trait bound

Як обмеження трейтів, це означає, що тип не містить жодних не-static посилань. Наприклад, одержувач може тримати цей тип стільки, скільки захоче, і він ніколи не стане недійсним, доки його не буде звільнено.

Важливо розуміти, що це означає: будь-які власні дані завжди задовольняють обмеження часу життя 'static, але посилання на ці власні дані зазвичай — ні:

use std::fmt::Debug;

fn print_it(input: impl Debug + 'static) {
    println!("'static value passed in is: {:?}", input);
}

fn main() {
    // i is owned and contains no references, thus it's 'static:
    let i = 5;
    print_it(i);

    // oops, &i only has the lifetime defined by the scope of
    // main(), so it's not 'static:
    print_it(&i);
}

Компілятор повідомить вам:

error[E0597]: `i` does not live long enough
  --> src/lib.rs:15:15
   |
15 |     print_it(&i);
   |     ---------^^--
   |     |         |
   |     |         borrowed value does not live long enough
   |     argument requires that `i` is borrowed for `'static`
16 | }
   | - `i` dropped here while still borrowed

Дивіться також:

'static константи

Скорочення часу життя (lifetime elision)

Деякі шаблони часу життя є надзвичайно поширеними, тож перевірник запозичень дозволить вам опускати їх, щоб не витрачати час на введення та покращити читабельність. Це відомо як скорочення часу життя. Скорочення часу життя існує в Rust лише тому, що ці шаблони поширені.

Наведений нижче код показує кілька прикладів скорочення часу життя. Для більш вичерпного опису скорочення часу життя див. скорочення часу життя у книзі.

// `elided_input` and `annotated_input` essentially have identical signatures
// because the lifetime of `elided_input` is inferred by the compiler:
fn elided_input(x: &i32) {
    println!("`elided_input`: {}", x);
}

fn annotated_input<'a>(x: &'a i32) {
    println!("`annotated_input`: {}", x);
}

// Similarly, `elided_pass` and `annotated_pass` have identical signatures
// because the lifetime is added implicitly to `elided_pass`:
fn elided_pass(x: &i32) -> &i32 { x }

fn annotated_pass<'a>(x: &'a i32) -> &'a i32 { x }

fn main() {
    let x = 3;

    elided_input(&x);
    annotated_input(&x);

    println!("`elided_pass`: {}", elided_pass(&x));
    println!("`annotated_pass`: {}", annotated_pass(&x));
}

Див. також:

скорочення часу життя

Трейт

trait — це набір методів, визначених для невідомого типу: Self. Вони можуть отримувати доступ до інших методів, оголошених у тому самому трейт.

Трейти можна реалізувати для будь-якого типу даних. У наведеному нижче прикладі ми визначаємо Animal, групу методів. Потім trait Animal реалізується для типу даних Sheep, що дає змогу використовувати методи з Animal для Sheep.

struct Sheep { naked: bool, name: &'static str }

trait Animal {
    // Associated function signature; `Self` refers to the implementor type.
    fn new(name: &'static str) -> Self;

    // Method signatures; these will return a string.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;

    // Traits can provide default method definitions.
    fn talk(&self) {
        println!("{} says {}", self.name(), self.noise());
    }
}

impl Sheep {
    fn is_naked(&self) -> bool {
        self.naked
    }

    fn shear(&mut self) {
        if self.is_naked() {
            // Implementor methods can use the implementor's trait methods.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);

            self.naked = true;
        }
    }
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    // `Self` is the implementor type: `Sheep`.
    fn new(name: &'static str) -> Sheep {
        Sheep { name: name, naked: false }
    }

    fn name(&self) -> &'static str {
        self.name
    }

    fn noise(&self) -> &'static str {
        if self.is_naked() {
            "baaaaah?"
        } else {
            "baaaaah!"
        }
    }

    // Default trait methods can be overridden.
    fn talk(&self) {
        // For example, we can add some quiet contemplation.
        println!("{} pauses briefly... {}", self.name, self.noise());
    }
}

fn main() {
    // Type annotation is necessary in this case.
    let mut dolly: Sheep = Animal::new("Dolly");
    // TODO ^ Try removing the type annotations.

    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Виведення (Derive)

Компілятор здатний надавати базові реалізації для деяких трейтів за допомогою атрибута #[derive] атрибут. Ці трейт можуть і далі бути реалізовані вручну, якщо потрібна складніша поведінка.

Нижче наведено список трейтів, для яких можливе виведення:

  • Трейтів для порівняння: Eq, PartialEq, Ord, PartialOrd.
  • Clone, щоб створювати T з &T через копіювання.
  • Copy, щоб надати типу семантику “копіювання” замість семантики “переміщення”.
  • Hash, щоб обчислювати хеш з &T.
  • Default, щоб створювати порожній екземпляр типу даних.
  • Debug, щоб форматувати значення за допомогою форматера {:?}.
// `Centimeters`, a tuple struct that can be compared
#[derive(PartialEq, PartialOrd)]
struct Centimeters(f64);

// `Inches`, a tuple struct that can be printed
#[derive(Debug)]
struct Inches(i32);

impl Inches {
    fn to_centimeters(&self) -> Centimeters {
        let &Inches(inches) = self;

        Centimeters(inches as f64 * 2.54)
    }
}

// `Seconds`, a tuple struct with no additional attributes
struct Seconds(i32);

fn main() {
    let _one_second = Seconds(1);

    // Error: `Seconds` can't be printed; it doesn't implement the `Debug` trait
    //println!("One second looks like: {:?}", _one_second);
    // TODO ^ Try uncommenting this line

    // Error: `Seconds` can't be compared; it doesn't implement the `PartialEq` trait
    //let _this_is_true = (_one_second == _one_second);
    // TODO ^ Try uncommenting this line

    let foot = Inches(12);

    println!("One foot equals {:?}", foot);

    let meter = Centimeters(100.0);

    let cmp =
        if foot.to_centimeters() < meter {
            "smaller"
        } else {
            "bigger"
        };

    println!("One foot is {} than one meter.", cmp);
}

Дивіться також:

derive

Повернення трейтів із dyn

Компілятор Rust має знати, скільки місця потребує тип, що повертається, кожної функції. Це означає, що всі ваші функції мають повертати конкретний тип. На відміну від інших мов, якщо у вас є трейт, як-от Animal, ви не можете написати функцію, яка повертає Animal, тому що його різні реалізації потребуватимуть різної кількості пам’яті.

Однак є простий обхідний шлях. Замість того щоб повертати трейт-об’єкт безпосередньо, наші функції повертають Box, який містить деякий Animal. box — це просто посилання на певну пам’ять у купі. Оскільки посилання має розмір, відомий на етапі компіляції, і компілятор може гарантувати, що воно вказує на Animal, виділений у купі, ми можемо повертати трейт із нашої функції!

Rust намагається бути якомога явнішим щоразу, коли він виділяє пам’ять у купі. Тож якщо ваша функція повертає вказівник-на-трейт-у-купі таким способом, вам потрібно писати тип повернення з ключовим словом dyn, наприклад Box<dyn Animal>.

struct Sheep {}
struct Cow {}

trait Animal {
    // Instance method signature
    fn noise(&self) -> &'static str;
}

// Implement the `Animal` trait for `Sheep`.
impl Animal for Sheep {
    fn noise(&self) -> &'static str {
        "baaaaah!"
    }
}

// Implement the `Animal` trait for `Cow`.
impl Animal for Cow {
    fn noise(&self) -> &'static str {
        "moooooo!"
    }
}

// Returns some struct that implements Animal, but we don't know which one at compile time.
fn random_animal(random_number: f64) -> Box<dyn Animal> {
    if random_number < 0.5 {
        Box::new(Sheep {})
    } else {
        Box::new(Cow {})
    }
}

fn main() {
    let random_number = 0.234;
    let animal = random_animal(random_number);
    println!("You've randomly chosen an animal, and it says {}", animal.noise());
}

Перевантаження операторів

У Rust багато операторів можна перевантажити за допомогою trait-ів. Тобто деякі оператори можна використовувати для виконання різних завдань залежно від їхніх вхідних аргументів. Це можливо, тому що оператори є синтаксичним цукром для викликів методів. Наприклад, оператор + у a + b викликає метод add (як у a.add(b)). Цей метод add є частиною trait-у Add. Отже, оператор + може використовуватися будь-яким реалізатором trait-у Add.

Перелік trait-ів, таких як Add, що перевантажують оператори, можна знайти в core::ops.

use std::ops;

struct Foo;
struct Bar;

#[derive(Debug)]
struct FooBar;

#[derive(Debug)]
struct BarFoo;

// The `std::ops::Add` trait is used to specify the functionality of `+`.
// Here, we make `Add<Bar>` - the trait for addition with a RHS of type `Bar`.
// The following block implements the operation: Foo + Bar = FooBar
impl ops::Add<Bar> for Foo {
    type Output = FooBar;

    fn add(self, _rhs: Bar) -> FooBar {
        println!("> Foo.add(Bar) was called");

        FooBar
    }
}

// By reversing the types, we end up implementing non-commutative addition.
// Here, we make `Add<Foo>` - the trait for addition with a RHS of type `Foo`.
// This block implements the operation: Bar + Foo = BarFoo
impl ops::Add<Foo> for Bar {
    type Output = BarFoo;

    fn add(self, _rhs: Foo) -> BarFoo {
        println!("> Bar.add(Foo) was called");

        BarFoo
    }
}

fn main() {
    println!("Foo + Bar = {:?}", Foo + Bar);
    println!("Bar + Foo = {:?}", Bar + Foo);
}

Див. також

Add, Syntax Index

Drop

Трейт Drop має лише один метод: drop, який викликається автоматично, коли об’єкт виходить за межі області видимості. Основне використання трейта Drop — звільнити ресурси, якими володіє екземпляр, що реалізує його.

Box, Vec, String, File і Process — це кілька прикладів типів, які реалізують трейт Drop для звільнення ресурсів. Трейт Drop також може бути реалізований вручну для будь-якого власного типу даних.

У наступному прикладі до функції drop додається виведення в консоль, щоб повідомити коли її викликано.

struct Droppable {
    name: &'static str,
}

// This trivial implementation of `drop` adds a print to console.
impl Drop for Droppable {
    fn drop(&mut self) {
        println!("> Dropping {}", self.name);
    }
}

fn main() {
    let _a = Droppable { name: "a" };

    // block A
    {
        let _b = Droppable { name: "b" };

        // block B
        {
            let _c = Droppable { name: "c" };
            let _d = Droppable { name: "d" };

            println!("Exiting block B");
        }
        println!("Just exited block B");

        println!("Exiting block A");
    }
    println!("Just exited block A");

    // Variable can be manually dropped using the `drop` function
    drop(_a);
    // TODO ^ Try commenting this line

    println!("end of the main function");

    // `_a` *won't* be `drop`ed again here, because it already has been
    // (manually) `drop`ed
}

Для більш практичного прикладу ось як трейт Drop можна використати, щоб автоматично очищати тимчасові файли, коли вони більше не потрібні:

use std::fs::File;
use std::path::PathBuf;

struct TempFile {
    file: File,
    path: PathBuf,
}

impl TempFile {
    fn new(path: PathBuf) -> std::io::Result<Self> {
        // Note: File::create() will overwrite existing files
        let file = File::create(&path)?;

        Ok(Self { file, path })
    }
}

// When TempFile is dropped:
// 1. First, our custom drop implementation runs. The file is still open at this point,
//    but we can remove it from the filesystem by path.
// 2. Then, after our drop returns, Rust automatically drops each field,
//    so File's drop runs and closes the file handle.
impl Drop for TempFile {
    fn drop(&mut self) {
        // Note: the File is still open here — field destructors run after this method.
        if let Err(e) = std::fs::remove_file(&self.path) {
            eprintln!("Failed to remove temporary file: {}", e);
        }
        println!("> Dropped temporary file: {:?}", self.path);
        // After this method returns, Rust will drop each field (including `file`),
        // which closes the underlying file handle.
    }
}

fn main() -> std::io::Result<()> {
    // Create a new scope to demonstrate drop behavior
    {
        let temp = TempFile::new("test.txt".into())?;
        println!("Temporary file created");
        // File will be automatically cleaned up when temp goes out of scope
    }
    println!("End of scope - file should be cleaned up");

    // We can also manually drop if needed
    let temp2 = TempFile::new("another_test.txt".into())?;
    drop(temp2); // Explicitly drop the file
    println!("Manually dropped file");

    Ok(())
}

Iterators

Трейт Iterator використовується для реалізації ітераторів над колекціями, такими як масиви.

Трейт вимагає лише визначення методу для наступного елемента, який може бути визначений вручну в блоці impl або автоматично визначений (як у масивах і діапазонах).

Для зручності в поширених ситуаціях конструкція for перетворює деякі колекції на ітератори за допомогою методу .into_iter().

struct Fibonacci {
    curr: u32,
    next: u32,
}

// Implement `Iterator` for `Fibonacci`.
// The `Iterator` trait only requires a method to be defined for the `next` element,
// and an `associated type` to declare the return type of the iterator.
impl Iterator for Fibonacci {
    // We can refer to this type using Self::Item
    type Item = u32;

    // Here, we define the sequence using `.curr` and `.next`.
    // The return type is `Option<T>`:
    //     * When the `Iterator` is finished, `None` is returned.
    //     * Otherwise, the next value is wrapped in `Some` and returned.
    // We use Self::Item in the return type, so we can change
    // the type without having to update the function signatures.
    fn next(&mut self) -> Option<Self::Item> {
        let current = self.curr;

        self.curr = self.next;
        self.next = current + self.next;

        // Since there's no endpoint to a Fibonacci sequence, the `Iterator`
        // will never return `None`, and `Some` is always returned.
        Some(current)
    }
}

// Returns a Fibonacci sequence generator
fn fibonacci() -> Fibonacci {
    Fibonacci { curr: 0, next: 1 }
}

fn main() {
    // `0..3` is an `Iterator` that generates: 0, 1, and 2.
    let mut sequence = 0..3;

    println!("Four consecutive `next` calls on 0..3");
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());
    println!("> {:?}", sequence.next());

    // `for` works through an `Iterator` until it returns `None`.
    // Each `Some` value is unwrapped and bound to a variable (here, `i`).
    println!("Iterate through 0..3 using `for`");
    for i in 0..3 {
        println!("> {}", i);
    }

    // The `take(n)` method reduces an `Iterator` to its first `n` terms.
    println!("The first four terms of the Fibonacci sequence are: ");
    for i in fibonacci().take(4) {
        println!("> {}", i);
    }

    // The `skip(n)` method shortens an `Iterator` by dropping its first `n` terms.
    println!("The next four terms of the Fibonacci sequence are: ");
    for i in fibonacci().skip(4).take(4) {
        println!("> {}", i);
    }

    let array = [1u32, 3, 3, 7];

    // The `iter` method produces an `Iterator` over an array/slice.
    println!("Iterate the following array {:?}", &array);
    for i in array.iter() {
        println!("> {}", i);
    }
}

impl Trait

impl Trait можна використовувати в двох місцях:

  1. як тип аргументу
  2. як тип повернення

Як тип аргументу

Якщо ваша функція є узагальненою за трейтом, але вам не важливий конкретний тип, ви можете спростити оголошення функції, використовуючи impl Trait як тип аргументу.

Наприклад, розгляньте такий код:

fn parse_csv_document<R: std::io::BufRead>(src: R) -> std::io::Result<Vec<Vec<String>>> {
    src.lines()
        .map(|line| {
            // For each line in the source
            line.map(|line| {
                // If the line was read successfully, process it, if not, return the error
                line.split(',') // Split the line separated by commas
                    .map(|entry| String::from(entry.trim())) // Remove leading and trailing whitespace
                    .collect() // Collect all strings in a row into a Vec<String>
            })
        })
        .collect() // Collect all lines into a Vec<Vec<String>>
}

parse_csv_document є узагальненою, що дає змогу їй приймати будь-який тип, який реалізує BufRead, наприклад BufReader<File> або [u8], але неважливо, який саме тип R, і R використовується лише для оголошення типу src, тож функцію також можна записати так:

fn parse_csv_document(src: impl std::io::BufRead) -> std::io::Result<Vec<Vec<String>>> {
    src.lines()
        .map(|line| {
            // For each line in the source
            line.map(|line| {
                // If the line was read successfully, process it, if not, return the error
                line.split(',') // Split the line separated by commas
                    .map(|entry| String::from(entry.trim())) // Remove leading and trailing whitespace
                    .collect() // Collect all strings in a row into a Vec<String>
            })
        })
        .collect() // Collect all lines into a Vec<Vec<String>>
}

Зауважте, що використання impl Trait як типу аргументу означає, що ви не можете явно вказати, яку форму функції ви використовуєте, тобто parse_csv_document::<std::io::Empty>(std::io::empty()) не працюватиме з другим прикладом.

Як тип повернення

Якщо ваша функція повертає тип, який реалізує MyTrait, ви можете записати її тип повернення як -> impl MyTrait. Це може допомогти значно спростити ваші сигнатури типів!

use std::iter;
use std::vec::IntoIter;

// This function combines two `Vec<i32>` and returns an iterator over it.
// Look how complicated its return type is!
fn combine_vecs_explicit_return_type(
    v: Vec<i32>,
    u: Vec<i32>,
) -> iter::Cycle<iter::Chain<IntoIter<i32>, IntoIter<i32>>> {
    v.into_iter().chain(u.into_iter()).cycle()
}

// This is the exact same function, but its return type uses `impl Trait`.
// Look how much simpler it is!
fn combine_vecs(
    v: Vec<i32>,
    u: Vec<i32>,
) -> impl Iterator<Item=i32> {
    v.into_iter().chain(u.into_iter()).cycle()
}

fn main() {
    let v1 = vec![1, 2, 3];
    let v2 = vec![4, 5];
    let mut v3 = combine_vecs(v1, v2);
    assert_eq!(Some(1), v3.next());
    assert_eq!(Some(2), v3.next());
    assert_eq!(Some(3), v3.next());
    assert_eq!(Some(4), v3.next());
    assert_eq!(Some(5), v3.next());
    println!("all done");
}

Що важливіше, деякі типи Rust неможливо записати явно. Наприклад, кожне замикання має свій власний безіменний конкретний тип. До синтаксису impl Trait вам доводилося виділяти пам’ять у купі, щоб повернути замикання. Але тепер ви можете зробити все статично, ось так:

// Returns a function that adds `y` to its input
fn make_adder_function(y: i32) -> impl Fn(i32) -> i32 {
    let closure = move |x: i32| { x + y };
    closure
}

fn main() {
    let plus_one = make_adder_function(1);
    assert_eq!(plus_one(2), 3);
}

Ви також можете використовувати impl Trait, щоб повертати ітератор, який використовує замикання map або filter! Це робить використання map і filter простішим. Оскільки типи замикань не мають назв, ви не можете записати явний тип повернення, якщо ваша функція повертає ітератори із замиканнями. Але з impl Trait ви можете легко зробити це:

fn double_positives<'a>(numbers: &'a Vec<i32>) -> impl Iterator<Item = i32> + 'a {
    numbers
        .iter()
        .filter(|x| x > &&0)
        .map(|x| x * 2)
}

fn main() {
    let singles = vec![-3, -2, 2, 3];
    let doubles = double_positives(&singles);
    assert_eq!(doubles.collect::<Vec<i32>>(), vec![4, 6]);
}

Клонування та копіювання

Під час роботи з ресурсами поведінка за замовчуванням — передавати їх під час присвоювань або викликів функцій. Однак іноді нам також потрібно зробити копію ресурсу.

Трейт Clone допомагає нам зробити саме це. Найчастіше ми можемо використати метод .clone(), визначений трейтом Clone.

Copy: неявне клонування

Трейт Copy дозволяє типу бути продубльованим простим копіюванням бітів, без потреби в додатковій логіці. Коли тип реалізує Copy, присвоювання та виклики функцій неявно копіюватимуть значення замість його переміщення.

Важливо: Copy вимагає Clone — будь-який тип, що реалізує Copy, має також реалізовувати Clone. Це тому, що Copy визначено як підтрейт: trait Copy: Clone {}. Реалізація Clone для типів Copy просто копіює біти.

Не всі типи можуть реалізувати Copy. Тип може бути Copy лише якщо:

  • Усі його компоненти є Copy
  • Він не керує зовнішніми ресурсами (такими як пам’ять у купі, файлові дескриптори тощо)
// Unit-структура без ресурсів
// Примітка: Copy вимагає Clone, тож ми маємо вивести обидва
#[derive(Debug, Clone, Copy)]
struct Unit;

// Кортежна структура з ресурсами, яка реалізує трейт `Clone`
// Вона НЕ МОЖЕ бути Copy, тому що Box<T> не є Copy
#[derive(Clone, Debug)]
struct Pair(Box<i32>, Box<i32>);

fn main() {
    // Створити екземпляр `Unit`
    let unit = Unit;
    // Скопіювати `Unit` — це неявне копіювання, а не переміщення!
    // Оскільки Unit реалізує Copy, значення автоматично дублюється
    let copied_unit = unit;

    // Обидва `Unit` можна використовувати незалежно
    println!("original: {:?}", unit);
    println!("copy: {:?}", copied_unit);

    // Створити екземпляр `Pair`
    let pair = Pair(Box::new(1), Box::new(2));
    println!("original: {:?}", pair);

    // Перемістити `pair` у `moved_pair`, переміщує ресурси
    // Pair не реалізує Copy, тож це переміщення
    let moved_pair = pair;
    println!("moved: {:?}", moved_pair);

    // Помилка! `pair` втратила свої ресурси
    //println!("original: {:?}", pair);
    // TODO ^ Спробуйте розкоментувати цей рядок

    // Клонувати `moved_pair` у `cloned_pair` (ресурси включено)
    // На відміну від Copy, Clone є явним — ми маємо викликати .clone()
    let cloned_pair = moved_pair.clone();
    // Вивільнити переміщену початкову пару за допомогою std::mem::drop
    drop(moved_pair);

    // Помилка! `moved_pair` було вивільнено
    //println!("moved and dropped: {:?}", moved_pair);
    // TODO ^ Спробуйте розкоментувати цей рядок

    // Результат з .clone() все ще можна використовувати!
    println!("clone: {:?}", cloned_pair);
}

Супертрейтів

У Rust немає “успадкування”, але ви можете визначити трейт як надмножину іншого трейту. Наприклад:

trait Person {
    fn name(&self) -> String;
}

// Person is a supertrait of Student.
// Implementing Student requires you to also impl Person.
trait Student: Person {
    fn university(&self) -> String;
}

trait Programmer {
    fn fav_language(&self) -> String;
}

// CompSciStudent (computer science student) is a subtrait of both Programmer
// and Student. Implementing CompSciStudent requires you to impl both supertraits.
trait CompSciStudent: Programmer + Student {
    fn git_username(&self) -> String;
}

fn comp_sci_student_greeting(student: &dyn CompSciStudent) -> String {
    format!(
        "My name is {} and I attend {}. My favorite language is {}. My Git username is {}",
        student.name(),
        student.university(),
        student.fav_language(),
        student.git_username()
    )
}

struct CSStudent {
    name: String,
    university: String,
    fav_language: String,
    git_username: String
}

impl Programmer for CSStudent {
    fn fav_language(&self) -> String {
        self.fav_language.clone()
    }
}

impl Student for CSStudent {
    fn university(&self) -> String {
        self.university.clone()
    }
}

impl Person for CSStudent {
    fn name(&self) -> String {
        self.name.clone()
    }
}

impl CompSciStudent for CSStudent {
    fn git_username(&self) -> String {
        self.git_username.clone()
    }
}

fn main() {
    let student = CSStudent {
        name: String::from("Alice"),
        university: String::from("MIT"),
        fav_language: String::from("Rust"),
        git_username: String::from("alice_codes"),
    };

    let greeting = comp_sci_student_greeting(&student);
    println!("{}", greeting);
}

Дивіться також:

Розділ The Rust Programming Language про супертрейтів

Розрізнення перекривних трейтів

Тип може реалізовувати багато різних трейтів. Що, якщо два трейтів обидва вимагають однакову назву для функції? Наприклад, багато трейтів можуть мати метод із назвою get(). Вони навіть можуть мати різні типи повернення!

Добра новина: оскільки кожна реалізація трейта отримує власний блок impl, зрозуміло, який саме метод get якого трейта ви реалізовуєте.

А як бути, коли настає час викликати ці методи? Щоб розрізнити їх, нам потрібно використовувати Fully Qualified Syntax.

trait UsernameWidget {
    // Отримати вибране ім'я користувача з цього віджета
    fn get(&self) -> String;
}

trait AgeWidget {
    // Отримати вибраний вік з цього віджета
    fn get(&self) -> u8;
}

// Форма і з UsernameWidget, і з AgeWidget
struct Form {
    username: String,
    age: u8,
}

impl UsernameWidget for Form {
    fn get(&self) -> String {
        self.username.clone()
    }
}

impl AgeWidget for Form {
    fn get(&self) -> u8 {
        self.age
    }
}

fn main() {
    let form = Form {
        username: "rustacean".to_owned(),
        age: 28,
    };

    // Якщо ви розкоментуєте цей рядок, ви отримаєте помилку з повідомленням
    // "multiple `get` found". Бо, зрештою, існує кілька методів
    // з назвою `get`.
    // println!("{}", form.get());

    let username = <Form as UsernameWidget>::get(&form);
    assert_eq!("rustacean".to_owned(), username);
    let age = <Form as AgeWidget>::get(&form);
    assert_eq!(28, age);
}

Дивіться також:

Розділ The Rust Programming Language про Fully Qualified syntax

macro_rules!

Rust надає потужну систему макросів, яка дозволяє метапрограмування. Як ви бачили в попередніх розділах, макроси схожі на функції, окрім того, що їхня назва закінчується знаком оклику !, але замість створення виклику функції, макроси розгортаються у вихідний код, який компілюється разом з рештою програми. Однак, на відміну від макросів у C та інших мовах, макроси Rust розгортаються в абстрактні синтаксичні дерева, а не через попередню обробку рядків, тож ви не отримуєте неочікуваних помилок пріоритету.

Макроси створюються за допомогою макроса macro_rules!.

// This is a simple macro named `say_hello`.
macro_rules! say_hello {
    // `()` indicates that the macro takes no argument.
    () => {
        // The macro will expand into the contents of this block.
        println!("Hello!")
    };
}

fn main() {
    // This call will expand into `println!("Hello!")`
    say_hello!()
}

То чому макроси корисні?

  1. Не повторюйте себе. Є багато випадків, коли вам може знадобитися подібна функціональність у кількох місцях, але з різними типами. Часто написання макроса — це корисний спосіб уникнути повторення коду. (Докладніше про це далі)

  2. Предметно-орієнтовані мови. Макроси дозволяють вам визначати спеціальний синтаксис для конкретної мети. (Докладніше про це далі)

  3. Варіадичні інтерфейси. Іноді ви хочете визначити інтерфейс, який приймає змінну кількість аргументів. Прикладом є println!, який може приймати будь-яку кількість аргументів, залежно від рядка формату. (Докладніше про це далі)

Syntax

У наступних підрозділах ми покажемо, як визначати макроси в Rust. Є три основні ідеї:

Дизайнатори

Аргументи макроса передуються знаком долара $ і типізуються за допомогою дизайнатора:

macro_rules! create_function {
    // This macro takes an argument of designator `ident` and
    // creates a function named `$func_name`.
    // The `ident` designator is used for variable/function names.
    ($func_name:ident) => {
        fn $func_name() {
            // The `stringify!` macro converts an `ident` into a string.
            println!("You called {:?}()",
                     stringify!($func_name));
        }
    };
}

// Create functions named `foo` and `bar` with the above macro.
create_function!(foo);
create_function!(bar);

macro_rules! print_result {
    // This macro takes an expression of type `expr` and prints
    // it as a string along with its result.
    // The `expr` designator is used for expressions.
    ($expression:expr) => {
        // `stringify!` will convert the expression *as it is* into a string.
        println!("{:?} = {:?}",
                 stringify!($expression),
                 $expression);
    };
}

fn main() {
    foo();
    bar();

    print_result!(1u32 + 1);

    // Recall that blocks are expressions too!
    print_result!({
        let x = 1u32;

        x * x + 2 * x - 1
    });
}

Ось деякі з доступних дизайнаторів:

  • block
  • expr використовується для виразів
  • ident використовується для імен змінних/функцій
  • item
  • literal використовується для літеральних констант
  • pat (pattern)
  • path
  • stmt (statement)
  • tt (token tree)
  • ty (type)
  • vis (visibility qualifier)

Повний список дивіться в Rust Reference.

Перевантаження

Макроси можуть бути перевантажені, щоб приймати різні комбінації аргументів. У цьому сенсі macro_rules! може працювати подібно до блоку match:

// `test!` will compare `$left` and `$right`
// in different ways depending on how you invoke it:
macro_rules! test {
    // Arguments don't need to be separated by a comma.
    // Any template can be used!
    ($left:expr; and $right:expr) => {
        println!("{:?} and {:?} is {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left && $right)
    };
    // ^ each arm must end with a semicolon.
    ($left:expr; or $right:expr) => {
        println!("{:?} or {:?} is {:?}",
                 stringify!($left),
                 stringify!($right),
                 $left || $right)
    };
}

fn main() {
    test!(1i32 + 1 == 2i32; and 2i32 * 2 == 4i32);
    test!(true; or false);
}

Повторення

Макроси можуть використовувати + у списку аргументів, щоб вказати, що аргумент може повторюватися щонайменше один раз, або *, щоб вказати, що аргумент може повторюватися нуль або більше разів.

У наведеному нижче прикладі оточення зіставника $(...),+ буде зіставляти один або більше вираз, розділених комами. Також зауважте, що крапка з комою в останньому випадку є необов’язковою.

// `find_min!` will calculate the minimum of any number of arguments.
macro_rules! find_min {
    // Base case:
    ($x:expr) => ($x);
    // `$x` followed by at least one `$y,`
    ($x:expr, $($y:expr),+) => (
        // Call `find_min!` on the tail `$y`
        std::cmp::min($x, find_min!($($y),+))
    )
}

fn main() {
    println!("{}", find_min!(1));
    println!("{}", find_min!(1 + 2, 2));
    println!("{}", find_min!(5, 2 * 3, 4));
}

DRY (Don’t Repeat Yourself)

Макроси дозволяють писати DRY-код, виокремлюючи спільні частини функцій та/або наборів тестів. Ось приклад, що реалізує і тестує оператори +=, *= та -= на Vec<T>:

use std::ops::{Add, Mul, Sub};

macro_rules! assert_equal_len {
    // The `tt` (token tree) designator is used for
    // operators and tokens.
    ($a:expr, $b:expr, $func:ident, $op:tt) => {
        assert!($a.len() == $b.len(),
                "{:?}: dimension mismatch: {:?} {:?} {:?}",
                stringify!($func),
                ($a.len(),),
                stringify!($op),
                ($b.len(),));
    };
}

macro_rules! op {
    ($func:ident, $bound:ident, $op:tt, $method:ident) => {
        fn $func<T: $bound<T, Output=T> + Copy>(xs: &mut Vec<T>, ys: &Vec<T>) {
            assert_equal_len!(xs, ys, $func, $op);

            for (x, y) in xs.iter_mut().zip(ys.iter()) {
                *x = $bound::$method(*x, *y);
                // *x = x.$method(*y);
            }
        }
    };
}

// Implement `add_assign`, `mul_assign`, and `sub_assign` functions.
op!(add_assign, Add, +=, add);
op!(mul_assign, Mul, *=, mul);
op!(sub_assign, Sub, -=, sub);

mod test {
    use std::iter;
    macro_rules! test {
        ($func:ident, $x:expr, $y:expr, $z:expr) => {
            #[test]
            fn $func() {
                for size in 0usize..10 {
                    let mut x: Vec<_> = iter::repeat($x).take(size).collect();
                    let y: Vec<_> = iter::repeat($y).take(size).collect();
                    let z: Vec<_> = iter::repeat($z).take(size).collect();

                    super::$func(&mut x, &y);

                    assert_eq!(x, z);
                }
            }
        };
    }

    // Test `add_assign`, `mul_assign`, and `sub_assign`.
    test!(add_assign, 1u32, 2u32, 3u32);
    test!(mul_assign, 2u32, 3u32, 6u32);
    test!(sub_assign, 3u32, 2u32, 1u32);
}
$ rustc --test dry.rs && ./dry
running 3 tests
test test::mul_assign ... ok
test test::add_assign ... ok
test test::sub_assign ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured

Доменно-специфічні мови (DSLs)

DSL — це міні-“мова”, вбудована в макрос Rust. Це повністю коректний Rust, тому що система макросів розгортається у звичайні конструкції Rust, але це виглядає як маленька мова. Це дозволяє вам визначити лаконічний або інтуїтивний синтаксис для певної спеціальної функціональності (у межах можливого).

Припустімо, що я хочу визначити невеликий API калькулятора. Я хотів би передати вираз і мати виведений у консоль результат.

macro_rules! calculate {
    (eval $e:expr) => {
        {
            let val: usize = $e; // Force types to be unsigned integers
            println!("{} = {}", stringify!{$e}, val);
        }
    };
}

fn main() {
    calculate! {
        eval 1 + 2 // hehehe `eval` is _not_ a Rust keyword!
    }

    calculate! {
        eval (1 + 2) * (3 / 4)
    }
}

Вивід:

1 + 2 = 3
(1 + 2) * (3 / 4) = 0

Це був дуже простий приклад, але було розроблено набагато складніші інтерфейси, такі як lazy_static або clap.

Також зверніть увагу на дві пари фігурних дужок у макросі. Зовнішні з них є частиною синтаксису macro_rules!, на додаток до () або [].

Варіативні інтерфейси

Варіативний інтерфейс приймає довільну кількість аргументів. Наприклад, println! може приймати довільну кількість аргументів, як визначає рядок форматування.

Ми можемо розширити наш макрос calculate! із попереднього розділу, щоб він був варіативним:

macro_rules! calculate {
    // The pattern for a single `eval`
    (eval $e:expr) => {
        {
            let val: usize = $e; // Force types to be integers
            println!("{} = {}", stringify!{$e}, val);
        }
    };

    // Decompose multiple `eval`s recursively
    (eval $e:expr, $(eval $es:expr),+) => {{
        calculate! { eval $e }
        calculate! { $(eval $es),+ }
    }};
}

fn main() {
    calculate! { // Look ma! Variadic `calculate!`!
        eval 1 + 2,
        eval 3 + 4,
        eval (2 * 3) + 1
    }
}

Вивід:

1 + 2 = 3
3 + 4 = 7
(2 * 3) + 1 = 7

Обробка помилок

Обробка помилок — це процес опрацювання можливості збою. Наприклад, нездатність прочитати файл, а потім продовжувати використовувати ті погані вхідні дані, очевидно, була б проблематичною. Помічати та явно керувати тими помилками — це спосіб уберегти решту програми від різних пасток.

Існують різні способи працювати з помилками в Rust, які описані в наступних підрозділах. Усі вони мають більш-менш тонкі відмінності та різні випадки використання. Як правило:

Явний panic головним чином корисний для тестів і роботи з невідновними помилками. Для прототипування він може бути корисним, наприклад, під час роботи з функціями, які ще не були реалізовані, але в таких випадках більш описовий unimplemented кращий. У тестах panic — це прийнятний спосіб явно зазнати невдачі.

Тип Option використовується тоді, коли значення є необов’язковим або коли відсутність значення не є умовою помилки. Наприклад, у батьківського елемента каталогу — / і C: — його немає. Під час роботи з Options, unwrap підходить для прототипування та випадків, де абсолютно певно гарантовано наявність значення. Однак expect корисніший, оскільки він дає змогу вказати повідомлення про помилку на випадок, якщо щось усе ж піде не так.

Коли існує шанс, що щось піде не так, і викликач має розв’язувати цю проблему, використовуйте Result. Їх теж можна unwrap і expect (будь ласка, не робіть цього, якщо це не тест або швидкий прототип).

Для більш суворого обговорення обробки помилок зверніться до розділу про обробку помилок в офіційній книзі.

panic

Найпростіший механізм обробки помилок, який ми побачимо, — це panic. Він виводить повідомлення про помилку, починає розмотування стека і зазвичай завершує програму. Тут ми явно викликаємо panic на нашій умові помилки:

fn drink(beverage: &str) {
    // You shouldn't drink too many sugary beverages.
    if beverage == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("Some refreshing {} is all I need.", beverage);
}

fn main() {
    drink("water");
    drink("lemonade");
    drink("still water");
}

Перший виклик drink працює. Другий спричиняє паніку, і тому третій ніколи не викликається.

abort and unwind

Попередній розділ ілюструє механізм обробки помилок panic. Різні шляхи виконання можуть умовно компілюватися залежно від налаштування panic. Наразі доступні значення — unwind і abort.

Спираючись на попередній приклад із лимонадом, ми явно використовуємо стратегію panic, щоб задіяти різні рядки коду.

fn drink(beverage: &str) {
    // You shouldn't drink too much sugary beverages.
    if beverage == "lemonade" {
        if cfg!(panic = "abort") {
            println!("This is not your party. Run!!!!");
        } else {
            println!("Spit it out!!!!");
        }
    } else {
        println!("Some refreshing {} is all I need.", beverage);
    }
}

fn main() {
    drink("water");
    drink("lemonade");
}

Ось ще один приклад, зосереджений на переписуванні drink() і явному використанні ключового слова unwind.

#[cfg(panic = "unwind")]
fn ah() {
    println!("Spit it out!!!!");
}

#[cfg(not(panic = "unwind"))]
fn ah() {
    println!("This is not your party. Run!!!!");
}

fn drink(beverage: &str) {
    if beverage == "lemonade" {
        ah();
    } else {
        println!("Some refreshing {} is all I need.", beverage);
    }
}

fn main() {
    drink("water");
    drink("lemonade");
}

Стратегію panic можна встановити з командного рядка, використовуючи abort або unwind.

rustc  lemonade.rs -C panic=abort

Option & unwrap

У останньому прикладі ми показали, що можемо навмисно викликати збій програми. Ми сказали нашій програмі panic, якщо ми вип’ємо солодкий лимонад. Але що, якщо ми очікуємо якийсь напій, але не отримуємо жодного? Цей випадок був би так само поганим, тож його потрібно обробити!

Ми могли б перевіряти це на порожній рядок (""), як ми робимо з лимонадом. Оскільки ми використовуємо Rust, натомість нехай компілятор вказує на випадки, де немає напою.

Перелік Option<T> у бібліотеці std використовується, коли відсутність є можливою. Він проявляється як одна з двох “опцій”:

  • Some(T): Елемент типу T було знайдено
  • None: Жодного елемента не було знайдено

Ці випадки можна або явно обробляти через match, або неявно за допомогою unwrap. Неявна обробка або поверне внутрішній елемент, або викличе panic.

Зверніть увагу, що panic можна вручну налаштувати за допомогою expect, але unwrap інакше залишає нам менш змістовний вивід, ніж явна обробка. У наведеному нижче прикладі явна обробка дає більш контрольований результат, зберігаючи водночас можливість викликати panic, якщо це потрібно.

// Дорослий бачив усе і може добре впоратися з будь-яким напоєм.
// Усі напої обробляються явно за допомогою `match`.
fn give_adult(drink: Option<&str>) {
    // Визначте курс дій для кожного випадку.
    match drink {
        Some("lemonade") => println!("Yuck! Too sugary."),
        Some(inner)   => println!("{}? How nice.", inner),
        None          => println!("No drink? Oh well."),
    }
}

// Інші викличуть `panic` перед тим, як пити солодкі напої.
// Усі напої обробляються неявно за допомогою `unwrap`.
fn drink(drink: Option<&str>) {
    // `unwrap` повертає `panic`, коли отримує `None`.
    let inside = drink.unwrap();
    if inside == "lemonade" { panic!("AAAaaaaa!!!!"); }

    println!("I love {}s!!!!!", inside);
}

fn main() {
    let water  = Some("water");
    let lemonade = Some("lemonade");
    let void  = None;

    give_adult(water);
    give_adult(lemonade);
    give_adult(void);

    let coffee = Some("coffee");
    let nothing = None;

    drink(coffee);
    drink(nothing);
}

Розпакування опцій за допомогою ?

Ви можете розпаковувати Options за допомогою операторів match, але часто простіше використовувати оператор ?. Якщо x є Option, тоді обчислення x? поверне внутрішнє значення, якщо x є Some, інакше воно завершить виконання будь-якої функції, яка виконується, і поверне None.

fn next_birthday(current_age: Option<u8>) -> Option<String> {
    // If `current_age` is `None`, this returns `None`.
    // If `current_age` is `Some`, the inner `u8` value + 1
    // gets assigned to `next_age`
    let next_age: u8 = current_age? + 1;
    Some(format!("Next year I will be {}", next_age))
}

Ви можете ланцюжити багато ? разом, щоб зробити ваш код набагато читабельнішим.

struct Person {
    job: Option<Job>,
}

#[derive(Clone, Copy)]
struct Job {
    phone_number: Option<PhoneNumber>,
}

#[derive(Clone, Copy)]
#[allow(dead_code)]
struct PhoneNumber {
    area_code: Option<u8>,
    number: u32,
}

impl Person {

    // Отримує код міста номера телефону роботи людини, якщо він існує.
    fn work_phone_area_code(&self) -> Option<u8> {
        // Це потребувало б багатьох вкладених операторів `match` без оператора `?`.
        // Це зайняло б набагато більше коду - спробуйте написати це самі і подивіться, що
        // простіше.
        self.job?.phone_number?.area_code
    }
}

fn main() {
    let p = Person {
        job: Some(Job {
            phone_number: Some(PhoneNumber {
                area_code: Some(61),
                number: 439222222,
            }),
        }),
    };

    assert_eq!(p.work_phone_area_code(), Some(61));
}

Комбінатори: map

match є допустимим методом для обробки Options. Однак з часом ви можете вважати надмірне використання стомливим, особливо з операціями, які дійсні лише для вхідного значення. У таких випадках можна використовувати комбінатори, щоб керувати потоком керування модульним способом.

Option має вбудований метод під назвою map(), комбінатор для простого відображення Some -> Some і None -> None. Кілька викликів map() можна ланцюжком об’єднувати для ще більшої гнучкості.

У наведеному нижче прикладі process() замінює всі функції, що були до нього, зберігаючи компактність.

#![allow(dead_code)]

#[derive(Debug)] enum Food { Apple, Carrot, Potato }

#[derive(Debug)] struct Peeled(Food);
#[derive(Debug)] struct Chopped(Food);
#[derive(Debug)] struct Cooked(Food);

// Peeling food. If there isn't any, then return `None`.
// Otherwise, return the peeled food.
fn peel(food: Option<Food>) -> Option<Peeled> {
    match food {
        Some(food) => Some(Peeled(food)),
        None       => None,
    }
}

// Chopping food. If there isn't any, then return `None`.
// Otherwise, return the chopped food.
fn chop(peeled: Option<Peeled>) -> Option<Chopped> {
    match peeled {
        Some(Peeled(food)) => Some(Chopped(food)),
        None               => None,
    }
}

// Cooking food. Here, we showcase `map()` instead of `match` for case handling.
fn cook(chopped: Option<Chopped>) -> Option<Cooked> {
    chopped.map(|Chopped(food)| Cooked(food))
}

// A function to peel, chop, and cook food all in sequence.
// We chain multiple uses of `map()` to simplify the code.
fn process(food: Option<Food>) -> Option<Cooked> {
    food.map(|f| Peeled(f))
        .map(|Peeled(f)| Chopped(f))
        .map(|Chopped(f)| Cooked(f))
}

// Check whether there's food or not before trying to eat it!
fn eat(food: Option<Cooked>) {
    match food {
        Some(food) => println!("Mmm. I love {:?}", food),
        None       => println!("Oh no! It wasn't edible."),
    }
}

fn main() {
    let apple = Some(Food::Apple);
    let carrot = Some(Food::Carrot);
    let potato = None;

    let cooked_apple = cook(chop(peel(apple)));
    let cooked_carrot = cook(chop(peel(carrot)));
    // Let's try the simpler looking `process()` now.
    let cooked_potato = process(potato);

    eat(cooked_apple);
    eat(cooked_carrot);
    eat(cooked_potato);
}

Дивіться також:

замикання, Option, Option::map()

Комбінатори: and_then

map() було описано як спосіб, який можна ланцюжком використовувати, щоб спростити оператори match. Однак використання map() для функції, яка повертає Option<T>, призводить до вкладеного Option<Option<T>>. Ланцюжок кількох викликів разом може тоді ставати заплутаним. Саме тут і з’являється інший комбінатор під назвою and_then(), відомий у деяких мовах як flatmap.

and_then() викликає свою вхідну функцію з упакованим значенням і повертає результат. Якщо Option є None, тоді замість цього він повертає None.

У наведеному нижче прикладі cookable_v3() повертає Option<Food>. Використання map() замість and_then() дало б Option<Option<Food>>, що є недійсним типом для eat().

#![allow(dead_code)]

#[derive(Debug)] enum Food { CordonBleu, Steak, Sushi }
#[derive(Debug)] enum Day { Monday, Tuesday, Wednesday }

// We don't have the ingredients to make Sushi.
fn have_ingredients(food: Food) -> Option<Food> {
    match food {
        Food::Sushi => None,
        _           => Some(food),
    }
}

// We have the recipe for everything except Cordon Bleu.
fn have_recipe(food: Food) -> Option<Food> {
    match food {
        Food::CordonBleu => None,
        _                => Some(food),
    }
}

// To make a dish, we need both the recipe and the ingredients.
// We can represent the logic with a chain of `match`es:
fn cookable_v1(food: Food) -> Option<Food> {
    match have_recipe(food) {
        None       => None,
        Some(food) => have_ingredients(food),
    }
}

// This can conveniently be rewritten more compactly with `and_then()`:
fn cookable_v3(food: Food) -> Option<Food> {
    have_recipe(food).and_then(have_ingredients)
}

// Otherwise we'd need to `flatten()` an `Option<Option<Food>>`
// to get an `Option<Food>`:
fn cookable_v2(food: Food) -> Option<Food> {
    have_recipe(food).map(have_ingredients).flatten()
}

fn eat(food: Food, day: Day) {
    match cookable_v3(food) {
        Some(food) => println!("Yay! On {:?} we get to eat {:?}.", day, food),
        None       => println!("Oh no. We don't get to eat on {:?}?", day),
    }
}

fn main() {
    let (cordon_bleu, steak, sushi) = (Food::CordonBleu, Food::Steak, Food::Sushi);

    eat(cordon_bleu, Day::Monday);
    eat(steak, Day::Tuesday);
    eat(sushi, Day::Wednesday);
}

Дивіться також:

замикання, Option, Option::and_then(), and Option::flatten()

Розпакування варіантів і значень за замовчуванням

Існує більше ніж один спосіб розпакувати Option і перейти до значення за замовчуванням, якщо він None. Щоб вибрати той, що відповідає нашим потребам, нам потрібно врахувати таке:

  • чи потрібне нам жадібне або ліниве обчислення?
  • чи потрібно нам зберегти початкове порожнє значення без змін, або змінити його на місці?

or() є ланцюжковим, обчислюється жадібно, зберігає порожнє значення без змін

or() є ланцюжковим і жадібно обчислює свій аргумент, як показано в наведеному нижче прикладі. Зверніть увагу, що через жадібне обчислення аргументів or, змінна, передана до or, переміщується.

#[derive(Debug)]
enum Fruit { Apple, Orange, Banana, Kiwi, Lemon }

fn main() {
    let apple = Some(Fruit::Apple);
    let orange = Some(Fruit::Orange);
    let no_fruit: Option<Fruit> = None;

    let first_available_fruit = no_fruit.or(orange).or(apple);
    println!("first_available_fruit: {:?}", first_available_fruit);
    // first_available_fruit: Some(Orange)

    // `or` переміщує свій аргумент.
    // У прикладі вище `or(orange)` повернув `Some`, тому `or(apple)` не було викликано.
    // Але змінна з іменем `apple` усе одно була переміщена, і більше не може бути використана.
    // println!("Variable apple was moved, so this line won't compile: {:?}", apple);
    // TODO: розкоментуйте рядок вище, щоб побачити помилку компілятора
}

or_else() є ланцюжковим, обчислюється ліниво, зберігає порожнє значення без змін

Іншою альтернативою є використання or_else, який також є ланцюжковим і обчислюється ліниво, як показано в наведеному нижче прикладі:

#[derive(Debug)]
enum Fruit { Apple, Orange, Banana, Kiwi, Lemon }

fn main() {
    let no_fruit: Option<Fruit> = None;
    let get_kiwi_as_fallback = || {
        println!("Providing kiwi as fallback");
        Some(Fruit::Kiwi)
    };
    let get_lemon_as_fallback = || {
        println!("Providing lemon as fallback");
        Some(Fruit::Lemon)
    };

    let first_available_fruit = no_fruit
        .or_else(get_kiwi_as_fallback)
        .or_else(get_lemon_as_fallback);
    println!("first_available_fruit: {:?}", first_available_fruit);
    // Providing kiwi as fallback
    // first_available_fruit: Some(Kiwi)
}

get_or_insert() обчислюється жадібно, змінює порожнє значення на місці

Щоб переконатися, що Option містить значення, ми можемо використати get_or_insert, щоб змінити його на місці за допомогою значення за замовчуванням, як показано в наведеному нижче прикладі. Зверніть увагу, що get_or_insert жадібно обчислює свій параметр, тож змінна apple переміщується:

#[derive(Debug)]
enum Fruit { Apple, Orange, Banana, Kiwi, Lemon }

fn main() {
    let mut my_fruit: Option<Fruit> = None;
    let apple = Fruit::Apple;
    let first_available_fruit = my_fruit.get_or_insert(apple);
    println!("first_available_fruit is: {:?}", first_available_fruit);
    println!("my_fruit is: {:?}", my_fruit);
    // first_available_fruit is: Apple
    // my_fruit is: Some(Apple)
    //println!("Variable named `apple` is moved: {:?}", apple);
    // TODO: розкоментуйте рядок вище, щоб побачити помилку компілятора
}

get_or_insert_with() обчислюється ліниво, змінює порожнє значення на місці

Замість того щоб явно надавати значення за замовчуванням, ми можемо передати замикання до get_or_insert_with, ось так:

#[derive(Debug)]
enum Fruit { Apple, Orange, Banana, Kiwi, Lemon }

fn main() {
    let mut my_fruit: Option<Fruit> = None;
    let get_lemon_as_fallback = || {
        println!("Providing lemon as fallback");
        Fruit::Lemon
    };
    let first_available_fruit = my_fruit
        .get_or_insert_with(get_lemon_as_fallback);
    println!("first_available_fruit is: {:?}", first_available_fruit);
    println!("my_fruit is: {:?}", my_fruit);
    // Providing lemon as fallback
    // first_available_fruit is: Lemon
    // my_fruit is: Some(Lemon)

    // Якщо `Option` має значення, він залишається без змін, і замикання не викликається
    let mut my_apple = Some(Fruit::Apple);
    let should_be_apple = my_apple.get_or_insert_with(get_lemon_as_fallback);
    println!("should_be_apple is: {:?}", should_be_apple);
    println!("my_apple is unchanged: {:?}", my_apple);
    // Нижче наведено вивід. Зверніть увагу, що замикання `get_lemon_as_fallback` не викликається
    // should_be_apple is: Apple
    // my_apple is unchanged: Some(Apple)
}

Дивіться також:

closures, get_or_insert, get_or_insert_with, moved variables, or, or_else

Result

Result — це більш багата версія типу Option, яка описує можливу помилку замість можливої відсутності.

Тобто, Result<T, E> може мати один із двох результатів:

  • Ok(T): Елемент T було знайдено
  • Err(E): Було знайдено помилку з елементом E

За домовленістю, очікуваний результат — Ok, тоді як неочікуваний результат — Err.

Як і Option, Result має багато пов’язаних із ним методів. unwrap(), наприклад, або повертає елемент T, або викликає panic. Для обробки випадків існує багато комбінацій між Result і Option, які перекриваються.

Під час роботи з Rust ви, ймовірно, зіткнетеся з методами, що повертають тип Result, наприклад методом parse(). Не завжди може бути можливим розібрати рядок у інший тип, тому parse() повертає Result, що вказує на можливу невдачу.

Подивімося, що відбувається, коли ми успішно та неуспішно викликаємо parse() для рядка:

fn multiply(first_number_str: &str, second_number_str: &str) -> i32 {
    // Let's try using `unwrap()` to get the number out. Will it bite us?
    let first_number = first_number_str.parse::<i32>().unwrap();
    let second_number = second_number_str.parse::<i32>().unwrap();
    first_number * second_number
}

fn main() {
    let twenty = multiply("10", "2");
    println!("double is {}", twenty);

    let tt = multiply("t", "2");
    println!("double is {}", tt);
}

У неуспішному випадку parse() залишає нас із помилкою, на якій unwrap() може викликати panic. Крім того, panic завершує нашу програму і виводить неприємне повідомлення про помилку.

Щоб поліпшити якість нашого повідомлення про помилку, нам слід точніше вказати тип повернення і розглянути можливість явної обробки помилки.

Використання Result у main

Тип Result також може бути типом повернення функції main, якщо це явно вказано. Зазвичай функція main має такий вигляд:

fn main() {
    println!("Hello World!");
}

Однак main також може мати тип повернення Result. Якщо в межах функції main виникає помилка, вона поверне код помилки і виведе debug-представлення помилки (використовуючи трейт Debug). Наведений нижче приклад показує таку ситуацію і торкається аспектів, які розглядаються в [наступному розділі].

use std::num::ParseIntError;

fn main() -> Result<(), ParseIntError> {
    let number_str = "10";
    let number = match number_str.parse::<i32>() {
        Ok(number)  => number,
        Err(e) => return Err(e),
    };
    println!("{}", number);
    Ok(())
}

map для Result

Паніка в multiply з попереднього прикладу не робить код надійним. Загалом, ми хочемо повертати помилку викликачу, щоб він міг вирішити, який є правильний спосіб реагувати на помилки.

Спочатку нам потрібно знати, з яким типом помилки ми маємо справу. Щоб визначити тип Err, ми звертаємося до parse(), який реалізовано за допомогою трейтa FromStr для i32. У результаті тип Err вказується як ParseIntError.

У прикладі нижче прямолінійний оператор match призводить до коду, який загалом є більш громіздким.

use std::num::ParseIntError;

// With the return type rewritten, we use pattern matching without `unwrap()`.
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    match first_number_str.parse::<i32>() {
        Ok(first_number)  => {
            match second_number_str.parse::<i32>() {
                Ok(second_number)  => {
                    Ok(first_number * second_number)
                },
                Err(e) => Err(e),
            }
        },
        Err(e) => Err(e),
    }
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("10", "2");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);
}

На щастя, map, and_then та багато інших комбінацій Option також реалізовано для Result. Result містить повний перелік.

use std::num::ParseIntError;

// As with `Option`, we can use combinators such as `map()`.
// This function is otherwise identical to the one above and reads:
// Multiply if both values can be parsed from str, otherwise pass on the error.
fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    first_number_str.parse::<i32>().and_then(|first_number| {
        second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    })
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    // This still presents a reasonable answer.
    let twenty = multiply("10", "2");
    print(twenty);

    // The following now provides a much more helpful error message.
    let tt = multiply("t", "2");
    print(tt);
}

псевдоніми для Result

Як щодо випадку, коли ми хочемо багато разів повторно використовувати конкретний тип Result? Пам’ятайте, що Rust дозволяє нам створювати псевдоніми. Зручно, ми можемо визначити один для конкретного Result, про який ідеться.

На рівні модуля створення псевдонімів може бути особливо корисним. Помилки, знайдені в конкретному модулі, часто мають той самий тип Err, тож один псевдонім може стисло визначити всі пов’язані Results. Це настільки корисно, що бібліотека std навіть надає один: io::Result!

Ось короткий приклад, щоб показати синтаксис:

use std::num::ParseIntError;

// Define a generic alias for a `Result` with the error type `ParseIntError`.
type AliasedResult<T> = Result<T, ParseIntError>;

// Use the above alias to refer to our specific `Result` type.
fn multiply(first_number_str: &str, second_number_str: &str) -> AliasedResult<i32> {
    first_number_str.parse::<i32>().and_then(|first_number| {
        second_number_str.parse::<i32>().map(|second_number| first_number * second_number)
    })
}

// Here, the alias again allows us to save some space.
fn print(result: AliasedResult<i32>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

Дивіться також:

io::Result

Ранні повернення

У попередньому прикладі ми явно обробляли помилки за допомогою комбінацій. Інший спосіб впоратися з цим аналізом випадків — використовувати комбінацію операторів match і ранніх повернень.

Тобто ми можемо просто припинити виконання функції та повернути помилку, якщо вона виникає. Для деяких такий спосіб коду може бути простішим і для читання, і для написання. Розгляньте цю версію попереднього прикладу, переписану з використанням ранніх повернень:

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = match first_number_str.parse::<i32>() {
        Ok(first_number)  => first_number,
        Err(e) => return Err(e),
    };

    let second_number = match second_number_str.parse::<i32>() {
        Ok(second_number)  => second_number,
        Err(e) => return Err(e),
    };

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

На цьому етапі ми навчилися явно обробляти помилки за допомогою комбінацій і ранніх повернень. Хоча загалом ми хочемо уникати паніки, явна обробка всіх наших помилок є обтяжливою.

У наступному розділі ми представимо ? для випадків, коли нам просто потрібно unwrap, не спричиняючи можливу panic.

Представлення ?

Іноді ми просто хочемо простоти unwrap без можливості panic. Досі unwrap змушував нас вкладатися дедалі глибше, тоді як насправді ми хотіли просто отримати змінну назовні. Саме для цього і призначений ?.

Після виявлення Err є дві допустимі дії:

  1. panic!, чого ми вже вирішили, якщо можливо, уникати
  2. return, тому що Err означає, що його не можна обробити

? — це майже1 точно еквівалент unwrap, який виконує return замість panic при Err. Подивімося, як можна спростити попередній приклад, що використовував комбінатори:

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = first_number_str.parse::<i32>()?;
    let second_number = second_number_str.parse::<i32>()?;

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

Макрос try!

До появи ? та сама функціональність досягалася за допомогою макроса try!. Оператор ? тепер рекомендовано використовувати, але ви все ще можете зустріти try!, коли переглядаєте старіший код. Така сама функція multiply із попереднього прикладу виглядала б так із використанням try!:

// Щоб скомпілювати й запустити цей приклад без помилок, використовуючи Cargo, змініть значення
// поля `edition` у розділі `[package]` файлу `Cargo.toml` на "2015".

use std::num::ParseIntError;

fn multiply(first_number_str: &str, second_number_str: &str) -> Result<i32, ParseIntError> {
    let first_number = try!(first_number_str.parse::<i32>());
    let second_number = try!(second_number_str.parse::<i32>());

    Ok(first_number * second_number)
}

fn print(result: Result<i32, ParseIntError>) {
    match result {
        Ok(n)  => println!("n is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    print(multiply("10", "2"));
    print(multiply("t", "2"));
}

  1. Див. повернення до ? для докладнішої інформації.

Multiple error types

Попередні приклади завжди були дуже зручними; Results взаємодіють з іншими Results, а Options взаємодіють з іншими Options.

Іноді Option потрібно взаємодіяти з Result, або Result<T, Error1> потрібно взаємодіяти з Result<T, Error2>. У таких випадках ми хочемо керувати нашими різними типами помилок так, щоб вони були компонованими й легко взаємодіяли між собою.

У наведеному нижче коді два екземпляри unwrap генерують різні типи помилок. Vec::first повертає Option, тоді як parse::<i32> повертає Result<i32, ParseIntError>:

fn double_first(vec: Vec<&str>) -> i32 {
    let first = vec.first().unwrap(); // Generate error 1
    2 * first.parse::<i32>().unwrap() // Generate error 2
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("The first doubled is {}", double_first(numbers));

    println!("The first doubled is {}", double_first(empty));
    // Error 1: the input vector is empty

    println!("The first doubled is {}", double_first(strings));
    // Error 2: the element doesn't parse to a number
}

У наступних розділах ми побачимо кілька стратегій для обробки таких типів проблем.

Витягування Results з Options

Найпростіший спосіб обробки змішаних типів помилок — просто вкладати їх один в один.

use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Option<Result<i32, ParseIntError>> {
    vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    })
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("Першим подвоєним є {:?}", double_first(numbers));

    println!("Першим подвоєним є {:?}", double_first(empty));
    // Помилка 1: вхідний вектор порожній

    println!("Першим подвоєним є {:?}", double_first(strings));
    // Помилка 2: елемент не розбирається як число
}

Іноді нам потрібно буде зупиняти обробку на помилках (як із ?), але продовжувати, коли Option дорівнює None. Функція transpose стане в пригоді, щоб поміняти місцями Result і Option.

use std::num::ParseIntError;

fn double_first(vec: Vec<&str>) -> Result<Option<i32>, ParseIntError> {
    let opt = vec.first().map(|first| {
        first.parse::<i32>().map(|n| 2 * n)
    });

    opt.transpose()
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    println!("Першим подвоєним є {:?}", double_first(numbers));
    println!("Першим подвоєним є {:?}", double_first(empty));
    println!("Першим подвоєним є {:?}", double_first(strings));
}

Визначення типу помилки

Іноді це спрощує код — замаскувати всі різні помилки одним типом помилки. Ми покажемо це на прикладі власної помилки.

Rust дозволяє нам визначати власні типи помилок. Загалом, «добрий» тип помилки:

  • Представляє різні помилки одним і тим самим типом
  • Надає користувачеві гарні повідомлення про помилки
  • Легко порівнюється з іншими типами
    • Добре: Err(EmptyVec)
    • Погано: Err("Please use a vector with at least one element".to_owned())
  • Може містити інформацію про помилку
    • Добре: Err(BadChar(c, position))
    • Погано: Err("+ cannot be used here".to_owned())
  • Добре композиціонується з іншими помилками
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

// Define our error types. These may be customized for our error handling cases.
// Now we will be able to write our own errors, defer to an underlying error
// implementation, or do something in between.
#[derive(Debug, Clone)]
struct DoubleError;

// Generation of an error is completely separate from how it is displayed.
// There's no need to be concerned about cluttering complex logic with the display style.
//
// Note that we don't store any extra info about the errors. This means we can't state
// which string failed to parse without modifying our types to carry that information.
impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
        // Change the error to our new type.
        .ok_or(DoubleError)
        .and_then(|s| {
            s.parse::<i32>()
                // Update to the new error type here also.
                .map_err(|_| DoubleError)
                .map(|i| 2 * i)
        })
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("Подвоєне перше значення — {}", n),
        Err(e) => println!("Помилка: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Boxing errors

Спосіб писати простий код, зберігаючи початкові помилки, — це Box-увати їх. Недолік полягає в тому, що базовий тип помилки відомий лише під час виконання і не визначається статично.

stdlib допомагає нам Box-увати помилки, реалізуючи для Box перетворення з будь-якого типу, що реалізує трейт Error, у трейт-об’єкт Box<Error>, через From.

use std::error;
use std::fmt;

// Change the alias to use `Box<dyn error::Error>`.
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;

#[derive(Debug, Clone)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

impl error::Error for EmptyVec {}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    vec.first()
        .ok_or_else(|| EmptyVec.into()) // Converts to Box using Into trait.
        .and_then(|s| {
            s.parse::<i32>()
                .map_err(From::from) // Converts to Box using From::from fn pointer.
                .map(|i| 2 * i)
        })
}

fn print(result: Result<i32>) {
    match result {
        Ok(n) => println!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Дивіться також:

Динамічна диспетчеризація і Error trait

Інші використання ?

Зверніть увагу в попередньому прикладі, що наша негайна реакція на виклик parse — це map помилки з помилки бібліотеки в помилку в купі:

.and_then(|s| s.parse::<i32>())
    .map_err(|e| e.into())

Оскільки це проста й поширена операція, було б зручно, якби її можна було опустити. На жаль, оскільки and_then недостатньо гнучкий, це неможливо. Однак замість цього ми можемо використати ?.

? раніше пояснювався як або unwrap, або return Err(err). Це лише здебільшого правда. Насправді це означає unwrap або return Err(From::from(err)). Оскільки From::from — це утиліта перетворення між різними типами, це означає, що якщо ви застосуєте ? там, де помилка може бути перетворена на тип, що повертається, вона буде перетворена автоматично.

Тут ми переписуємо попередній приклад, використовуючи ?. У результаті, map_err зникне, коли From::from буде реалізовано для нашого типу помилки:

use std::error;
use std::fmt;

// Change the alias to use `Box<dyn error::Error>`.
type Result<T> = std::result::Result<T, Box<dyn error::Error>>;

#[derive(Debug)]
struct EmptyVec;

impl fmt::Display for EmptyVec {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "invalid first item to double")
    }
}

impl error::Error for EmptyVec {}

// The same structure as before but rather than chain all `Results`
// and `Options` along, we `?` to get the inner value out immediately.
fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(EmptyVec)?;
    let parsed = first.parse::<i32>()?;
    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => println!("Error: {}", e),
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Насправді це вже доволі охайно. Порівняно з оригінальним panic, це дуже схоже на заміну викликів unwrap на ?, за винятком того, що типи, що повертаються, — це Result. У результаті, їх потрібно розпаковувати на верхньому рівні.

Дивіться також:

From::from і ?

Обгортання помилок

Альтернативою пакуванню помилок у Box є обгорнути їх у власний тип помилки.

use std::error;
use std::error::Error;
use std::num::ParseIntError;
use std::fmt;

type Result<T> = std::result::Result<T, DoubleError>;

#[derive(Debug)]
enum DoubleError {
    EmptyVec,
    // Ми відкладемо реалізацію помилки розбору для їхньої помилки.
    // Надання додаткової інформації вимагає додавання більшої кількості даних до типу.
    Parse(ParseIntError),
}

impl fmt::Display for DoubleError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            DoubleError::EmptyVec =>
                write!(f, "please use a vector with at least one element"),
            // Обгорнена помилка містить додаткову інформацію і доступна
            // через метод source().
            DoubleError::Parse(..) =>
                write!(f, "the provided string could not be parsed as int"),
        }
    }
}

impl error::Error for DoubleError {
    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
        match *self {
            DoubleError::EmptyVec => None,
            // Причина — це базовий тип помилки реалізації. Він неявно
            // приводиться до об'єкта трейту `&error::Error`. Це працює, тому що
            // базовий тип уже реалізує трейт `Error`.
            DoubleError::Parse(ref e) => Some(e),
        }
    }
}

// Реалізуйте перетворення з `ParseIntError` у `DoubleError`.
// Це буде автоматично викликано `?`, якщо `ParseIntError`
// потрібно буде перетворити в `DoubleError`.
impl From<ParseIntError> for DoubleError {
    fn from(err: ParseIntError) -> DoubleError {
        DoubleError::Parse(err)
    }
}

fn double_first(vec: Vec<&str>) -> Result<i32> {
    let first = vec.first().ok_or(DoubleError::EmptyVec)?;
    // Тут ми неявно використовуємо реалізацію `From` для `ParseIntError` (яку
    // ми визначили вище), щоб створити `DoubleError`.
    let parsed = first.parse::<i32>()?;

    Ok(2 * parsed)
}

fn print(result: Result<i32>) {
    match result {
        Ok(n)  => println!("The first doubled is {}", n),
        Err(e) => {
            println!("Error: {}", e);
            if let Some(source) = e.source() {
                println!("  Caused by: {}", source);
            }
        },
    }
}

fn main() {
    let numbers = vec!["42", "93", "18"];
    let empty = vec![];
    let strings = vec!["tofu", "93", "18"];

    print(double_first(numbers));
    print(double_first(empty));
    print(double_first(strings));
}

Це додає трохи більше шаблонного коду для обробки помилок і може не бути потрібним у всіх застосунках. Є деякі бібліотеки, які можуть взяти на себе шаблонний код за вас.

Дивіться також:

From::from і Enums

Crates for handling errors

Ітерація over Results

Операція Iter::map може завершитися помилкою, наприклад:

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Results: {:?}", numbers);
}

Давайте розглянемо стратегії для обробки цього.

Ігнорування невдалих елементів за допомогою filter_map()

filter_map викликає функцію та відфільтровує результати, які є None.

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Vec<_> = strings
        .into_iter()
        .filter_map(|s| s.parse::<i32>().ok())
        .collect();
    println!("Results: {:?}", numbers);
}

Збирання невдалих елементів за допомогою map_err() і filter_map()

map_err викликає функцію з помилкою, тож, додавши це до попереднього рішення з filter_map, ми можемо відкласти їх убік під час ітерації.

fn main() {
    let strings = vec!["42", "tofu", "93", "999", "18"];
    let mut errors = vec![];
    let numbers: Vec<_> = strings
        .into_iter()
        .map(|s| s.parse::<u8>())
        .filter_map(|r| r.map_err(|e| errors.push(e)).ok())
        .collect();
    println!("Numbers: {:?}", numbers);
    println!("Errors: {:?}", errors);
}

Завершення всієї операції з помилкою за допомогою collect()

Result реалізує FromIterator, тож вектор результатів (Vec<Result<T, E>>) можна перетворити на результат з вектором (Result<Vec<T>, E>). Щойно знаходиться Result::Err, ітерація завершується.

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let numbers: Result<Vec<_>, _> = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .collect();
    println!("Results: {:?}", numbers);
}

Цю саму техніку можна використати з Option.

Збирання всіх валідних значень і помилок за допомогою partition()

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let (numbers, errors): (Vec<_>, Vec<_>) = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);
    println!("Numbers: {:?}", numbers);
    println!("Errors: {:?}", errors);
}

Коли ви дивитеся на результати, ви помітите, що все ще загорнуто в Result. Для цього потрібен трохи більший обсяг шаблонного коду.

fn main() {
    let strings = vec!["tofu", "93", "18"];
    let (numbers, errors): (Vec<_>, Vec<_>) = strings
        .into_iter()
        .map(|s| s.parse::<i32>())
        .partition(Result::is_ok);
    let numbers: Vec<_> = numbers.into_iter().map(Result::unwrap).collect();
    let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
    println!("Numbers: {:?}", numbers);
    println!("Errors: {:?}", errors);
}

Типи бібліотеки Std

Бібліотека std надає багато власних типів, які значно розширюють primitives. Деякі з них включають:

  • String, що зростає, як: "hello world"
  • вектори, що зростають: [1, 2, 3]
  • необов’язкові типи: Option<i32>
  • типи обробки помилок: Result<i32, i32>
  • вказівники, виділені в купі: Box<i32>

Дивіться також:

primitives і бібліотеку std

Box, стек та купа

Усі значення в Rust за замовчуванням виділяються у стеку. Значення можуть бути забоксовані (виділені в купі) шляхом створення Box<T>. Box — це розумний вказівник на значення типу T, виділене в купі. Коли box виходить з області видимості, викликається його деструктор, внутрішній об’єкт знищується, а пам’ять у купі звільняється.

До забоксованих значень можна отримати доступ через оператор *; це усуває один рівень непрямості.

use std::mem;

#[allow(dead_code)]
#[derive(Debug, Clone, Copy)]
struct Point {
    x: f64,
    y: f64,
}

// A Rectangle can be specified by where its top left and bottom right
// corners are in space
#[allow(dead_code)]
struct Rectangle {
    top_left: Point,
    bottom_right: Point,
}

fn origin() -> Point {
    Point { x: 0.0, y: 0.0 }
}

fn boxed_origin() -> Box<Point> {
    // Allocate this point on the heap, and return a pointer to it
    Box::new(Point { x: 0.0, y: 0.0 })
}

fn main() {
    // (all the type annotations are superfluous)
    // Stack allocated variables
    let point: Point = origin();
    let rectangle: Rectangle = Rectangle {
        top_left: origin(),
        bottom_right: Point { x: 3.0, y: -4.0 }
    };

    // Heap allocated rectangle
    let boxed_rectangle: Box<Rectangle> = Box::new(Rectangle {
        top_left: origin(),
        bottom_right: Point { x: 3.0, y: -4.0 },
    });

    // The output of functions can be boxed
    let boxed_point: Box<Point> = Box::new(origin());

    // Double indirection
    let box_in_a_box: Box<Box<Point>> = Box::new(boxed_origin());

    println!("Point occupies {} bytes on the stack",
             mem::size_of_val(&point));
    println!("Rectangle occupies {} bytes on the stack",
             mem::size_of_val(&rectangle));

    // box size == pointer size
    println!("Boxed point occupies {} bytes on the stack",
             mem::size_of_val(&boxed_point));
    println!("Boxed rectangle occupies {} bytes on the stack",
             mem::size_of_val(&boxed_rectangle));
    println!("Boxed box occupies {} bytes on the stack",
             mem::size_of_val(&box_in_a_box));

    // Copy the data contained in `boxed_point` into `unboxed_point`
    let unboxed_point: Point = *boxed_point;
    println!("Unboxed point occupies {} bytes on the stack",
             mem::size_of_val(&unboxed_point));
}

Вектори

Вектори — це масиви, що можуть змінювати розмір. Як і зрізи, їхній розмір невідомий під час компіляції, але вони можуть збільшуватися або зменшуватися будь-коли. Вектор представлено за допомогою 3 параметрів:

  • вказівник на дані
  • довжина
  • місткість

Місткість вказує, скільки пам’яті зарезервовано для вектора. Вектор може рости, доки довжина менша за місткість. Коли цю межу потрібно перевищити, вектор перевиділяється з більшою місткістю.

fn main() {
    // Iterators can be collected into vectors
    let collected_iterator: Vec<i32> = (0..10).collect();
    println!("Collected (0..10) into: {:?}", collected_iterator);

    // The `vec!` macro can be used to initialize a vector
    let mut xs = vec![1i32, 2, 3];
    println!("Initial vector: {:?}", xs);

    // Insert new element at the end of the vector
    println!("Push 4 into the vector");
    xs.push(4);
    println!("Vector: {:?}", xs);

    // Error! Immutable vectors can't grow
    collected_iterator.push(0);
    // FIXME ^ Comment out this line

    // The `len` method yields the number of elements currently stored in a vector
    println!("Vector length: {}", xs.len());

    // Indexing is done using the square brackets (indexing starts at 0)
    println!("Second element: {}", xs[1]);

    // `pop` removes the last element from the vector and returns it
    println!("Pop last element: {:?}", xs.pop());

    // Out of bounds indexing yields a panic
    println!("Fourth element: {}", xs[3]);
    // FIXME ^ Comment out this line

    // `Vector`s can be easily iterated over
    println!("Contents of xs:");
    for x in xs.iter() {
        println!("> {}", x);
    }

    // A `Vector` can also be iterated over while the iteration
    // count is enumerated in a separate variable (`i`)
    for (i, x) in xs.iter().enumerate() {
        println!("In position {} we have value {}", i, x);
    }

    // Thanks to `iter_mut`, mutable `Vector`s can also be iterated
    // over in a way that allows modifying each value
    for x in xs.iter_mut() {
        *x *= 3;
    }
    println!("Updated vector: {:?}", xs);
}

Більше методів Vec можна знайти в модулі std::vec

Рядки

Два найуживаніші типи рядків у Rust — це String і &str.

String зберігається як вектор байтів (Vec<u8>), але гарантовано завжди є дійсною послідовністю UTF-8. String розміщується в купі, може збільшуватися і не завершується нульовим байтом.

&str — це зріз (&[u8]), який завжди вказує на дійсну послідовність UTF-8, і може використовуватися для перегляду всередину String, так само як &[T] — це перегляд у Vec<T>.

fn main() {
    // (усі анотації типів є зайвими)
    // Посилання на рядок, розміщений у пам'яті лише для читання
    let pangram: &'static str = "the quick brown fox jumps over the lazy dog";
    println!("Pangram: {}", pangram);

    // Ітеруйте слова у зворотному порядку, новий рядок не розміщується
    println!("Words in reverse");
    for word in pangram.split_whitespace().rev() {
        println!("> {}", word);
    }

    // Скопіюйте символи у вектор, відсортуйте та видаліть дублікати
    let mut chars: Vec<char> = pangram.chars().collect();
    chars.sort();
    chars.dedup();

    // Створіть порожній і змінюваний `String`
    let mut string = String::new();
    for c in chars {
        // Вставте `char` у кінець рядка
        string.push(c);
        // Вставте рядок у кінець рядка
        string.push_str(", ");
    }

    // Обрізаний рядок є зрізом початкового рядка, отже жодного нового
    // виділення не виконується
    let chars_to_trim: &[char] = &[' ', ','];
    let trimmed_str: &str = string.trim_matches(chars_to_trim);
    println!("Used characters: {}", trimmed_str);

    // Розмістіть рядок у купі
    let alice = String::from("I like dogs");
    // Виділіть нову пам'ять і збережіть там змінений рядок
    let bob: String = alice.replace("dog", "cat");

    println!("Alice says: {}", alice);
    println!("Bob says: {}", bob);
}

Більше методів str/String можна знайти в std::str і std::string модулях

Літерали та екранування

Існує кілька способів записувати рядкові літерали зі спеціальними символами. Усі вони дають подібний &str, тож найкраще використовувати той формат, який найзручніше писати. Аналогічно існує кілька способів записувати байтові рядкові літерали, які всі дають &[u8; N].

Зазвичай спеціальні символи екрануються символом зворотної скісної риски: \. Так ви можете додати до свого рядка будь-який символ, навіть той, що не друкується, і той, який ви не знаєте, як ввести. Якщо вам потрібна буквальна зворотна скісна риска, екрануйте її ще однією: \\

Роздільники рядкового або символьного літерала, що зустрічаються всередині літерала, потрібно екранувати: "\"", '\''.

fn main() {
    // Ви можете використовувати екранування, щоб записувати байти за їхніми шістнадцятковими значеннями...
    let byte_escape = "I'm writing \x52\x75\x73\x74!";
    println!("Що ви робите\x3F (\\x3F означає ?) {}", byte_escape);

    // ...або кодові точки Unicode.
    let unicode_codepoint = "\u{211D}";
    let character_name = "\"DOUBLE-STRUCK CAPITAL R\"";

    println!("Символ Unicode {} (U+211D) називається {}",
                unicode_codepoint, character_name );


    let long_string = "String literals
                        can span multiple lines.
                        The linebreak and indentation here ->\
                        <- can be escaped too!";
    println!("{}", long_string);
}

Іноді є просто надто багато символів, які потрібно екранувати, або ж просто набагато зручніше записати рядок як є. Саме тут у гру вступають сирі рядкові літерали.

fn main() {
    let raw_str = r"Escapes don't work here: \x3F \u{211D}";
    println!("{}", raw_str);

    // Якщо вам потрібні лапки в сирому рядку, додайте пару #s
    let quotes = r#"And then I said: "There is no escape!""#;
    println!("{}", quotes);

    // Якщо вам потрібно "# у вашому рядку, просто використайте більше #s у роздільнику.
    // Ви можете використовувати до 255 #s.
    let longer_delimiter = r###"A string with "# in it. And even "##!"###;
    println!("{}", longer_delimiter);
}

Потрібен рядок, який не є UTF-8? (Пам’ятайте, str і String мають бути дійсним UTF-8). Або, можливо, ви хочете масив байтів, який здебільшого є текстом? На допомогу приходять байтові рядки!

use std::str;

fn main() {
    // Зверніть увагу, що це насправді не `&str`
    let bytestring: &[u8; 21] = b"this is a byte string";

    // Байтові масиви не мають трейту `Display`, тож друк їх трохи обмежений
    println!("A byte string: {:?}", bytestring);

    // Байтові рядки можуть мати екранування байтів...
    let escaped = b"\x52\x75\x73\x74 as bytes";
    // ...але не можуть мати екранування Unicode
    // let escaped = b"\u{211D} is not allowed";
    println!("Some escaped bytes: {:?}", escaped);


    // Сирі байтові рядки працюють так само, як і сирі рядки
    let raw_bytestring = br"\u{211D} is not escaped here";
    println!("{:?}", raw_bytestring);

    // Перетворення байтового масиву на `str` може завершитися невдачею
    if let Ok(my_str) = str::from_utf8(raw_bytestring) {
        println!("And the same as text: '{}'", my_str);
    }

    let _quotes = br#"You can also use "fancier" formatting, \
                    like with normal raw strings"#;

    // Байтові рядки не зобов'язані бути UTF-8
    let shift_jis = b"\x82\xe6\x82\xa8\x82\xb1\x82\xbb"; // "ようこそ" in SHIFT-JIS

    // Але тоді їх не завжди можна перетворити на `str`
    match str::from_utf8(shift_jis) {
        Ok(my_str) => println!("Conversion successful: '{}'", my_str),
        Err(e) => println!("Conversion failed: {:?}", e),
    };
}

Для перетворень між кодуваннями символів перегляньте крейт encoding.

Докладніший перелік способів запису рядкових літералів і символів екранування наведено в розділі ‘Tokens’ Rust Reference.

Option

Іноді бажано перехопити збій деяких частин програми замість виклику panic!; цього можна досягти за допомогою переліку Option.

Перелік Option<T> має два варіанти:

  • None, щоб вказати на збій або відсутність значення, і
  • Some(value), кортежна структура, яка обгортає value з типом T.
// An integer division that doesn't `panic!`
fn checked_division(dividend: i32, divisor: i32) -> Option<i32> {
    if divisor == 0 {
        // Failure is represented as the `None` variant
        None
    } else {
        // Result is wrapped in a `Some` variant
        Some(dividend / divisor)
    }
}

// This function handles a division that may not succeed
fn try_division(dividend: i32, divisor: i32) {
    // `Option` values can be pattern matched, just like other enums
    match checked_division(dividend, divisor) {
        None => println!("{} / {} failed!", dividend, divisor),
        Some(quotient) => {
            println!("{} / {} = {}", dividend, divisor, quotient)
        },
    }
}

fn main() {
    try_division(4, 2);
    try_division(1, 0);

    // Binding `None` to a variable needs to be type annotated
    let none: Option<i32> = None;
    let _equivalent_none = None::<i32>;

    let optional_float = Some(0f32);

    // Unwrapping a `Some` variant will extract the value wrapped.
    println!("{:?} unwraps to {:?}", optional_float, optional_float.unwrap());

    // Unwrapping a `None` variant will `panic!`
    println!("{:?} unwraps to {:?}", none, none.unwrap());
}

Result

Ми бачили, що enum Option можна використовувати як значення, що повертається з функцій які можуть зазнати невдачі, де None може бути повернено, щоб позначити невдачу. Однак, інколи важливо виразити чому операція зазнала невдачі. Для цього у нас є enum Result.

Enum Result<T, E> має два варіанти:

  • Ok(value), який вказує, що операція завершилася успішно, і загортає value, повернуте операцією. (value має тип T)
  • Err(why), який вказує, що операція зазнала невдачі, і загортає why, яке (сподіваємося) пояснює причину невдачі. (why має тип E)
mod checked {
    // Mathematical "errors" we want to catch
    #[derive(Debug)]
    pub enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    pub type MathResult = Result<f64, MathError>;

    pub fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            // This operation would `fail`, instead let's return the reason of
            // the failure wrapped in `Err`
            Err(MathError::DivisionByZero)
        } else {
            // This operation is valid, return the result wrapped in `Ok`
            Ok(x / y)
        }
    }

    pub fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    pub fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }
}

// `op(x, y)` === `sqrt(ln(x / y))`
fn op(x: f64, y: f64) -> f64 {
    // This is a three level match pyramid!
    match checked::div(x, y) {
        Err(why) => panic!("{:?}", why),
        Ok(ratio) => match checked::ln(ratio) {
            Err(why) => panic!("{:?}", why),
            Ok(ln) => match checked::sqrt(ln) {
                Err(why) => panic!("{:?}", why),
                Ok(sqrt) => sqrt,
            },
        },
    }
}

fn main() {
    // Will this fail?
    println!("{}", op(1.0, 10.0));
}

?

Ланцюжок результатів за допомогою match може стати досить неохайним; на щастя, оператор ? можна використати, щоб знову зробити все охайним. ? використовується в кінці виразу, що повертає Result, і є еквівалентом виразу match, де гілка Err(err) розгортається в раннє return Err(From::from(err)), а гілка Ok(ok) розгортається у вираз ok.

mod checked {
    #[derive(Debug)]
    enum MathError {
        DivisionByZero,
        NonPositiveLogarithm,
        NegativeSquareRoot,
    }

    type MathResult = Result<f64, MathError>;

    fn div(x: f64, y: f64) -> MathResult {
        if y == 0.0 {
            Err(MathError::DivisionByZero)
        } else {
            Ok(x / y)
        }
    }

    fn sqrt(x: f64) -> MathResult {
        if x < 0.0 {
            Err(MathError::NegativeSquareRoot)
        } else {
            Ok(x.sqrt())
        }
    }

    fn ln(x: f64) -> MathResult {
        if x <= 0.0 {
            Err(MathError::NonPositiveLogarithm)
        } else {
            Ok(x.ln())
        }
    }

    // Проміжна функція
    fn op_(x: f64, y: f64) -> MathResult {
        // якщо `div` "зазнає невдачі", тоді `DivisionByZero` буде `return`ed
        let ratio = div(x, y)?;

        // якщо `ln` "зазнає невдачі", тоді `NonPositiveLogarithm` буде `return`ed
        let ln = ln(ratio)?;

        sqrt(ln)
    }

    pub fn op(x: f64, y: f64) {
        match op_(x, y) {
            Err(why) => panic!("{}", match why {
                MathError::NonPositiveLogarithm
                    => "logarithm of non-positive number",
                MathError::DivisionByZero
                    => "division by zero",
                MathError::NegativeSquareRoot
                    => "square root of negative number",
            }),
            Ok(value) => println!("{}", value),
        }
    }
}

fn main() {
    checked::op(1.0, 10.0);
}

Обов’язково перегляньте документацію, оскільки існує багато методів для map/compose Result.

panic!

Макрос panic! можна використати, щоб згенерувати паніку й почати розгортання його стека. Під час розгортання runtime подбає про звільнення всіх ресурсів, якими володіє потік, викликаючи деструктор усіх його об’єктів.

Оскільки ми маємо справу з програмами лише з одним потоком, panic! спричинить те, що програма виведе повідомлення про паніку й завершиться.

// Re-implementation of integer division (/)
fn division(dividend: i32, divisor: i32) -> i32 {
    if divisor == 0 {
        // Division by zero triggers a panic
        panic!("division by zero");
    } else {
        dividend / divisor
    }
}

// The `main` task
fn main() {
    // Heap allocated integer
    let _x = Box::new(0i32);

    // This operation will trigger a task failure
    division(3, 0);

    println!("This point won't be reached!");

    // `_x` should get destroyed at this point
}

Давайте перевіримо, що panic! не витікає пам’ять.

$ rustc panic.rs && valgrind ./panic
==4401== Memcheck, a memory error detector
==4401== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==4401== Using Valgrind-3.10.0.SVN and LibVEX; rerun with -h for copyright info
==4401== Command: ./panic
==4401==
thread '<main>' panicked at 'division by zero', panic.rs:5
==4401==
==4401== HEAP SUMMARY:
==4401==     in use at exit: 0 bytes in 0 blocks
==4401==   total heap usage: 18 allocs, 18 frees, 1,648 bytes allocated
==4401==
==4401== All heap blocks were freed -- no leaks are possible
==4401==
==4401== For counts of detected and suppressed errors, rerun with -v
==4401== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0)

HashMap

На відміну від векторів, які зберігають значення за цілочисельним індексом, HashMaps зберігають значення за ключем. Ключами HashMap можуть бути булеві значення, цілі числа, рядки або будь-який інший тип, що реалізує трейти Eq і Hash. Докладніше про це — у наступному розділі.

Як і вектори, HashMaps можна збільшувати, але HashMap також можуть зменшуватися, коли мають надлишок вільного місця. Ви можете створити HashMap із певною початковою місткістю за допомогою HashMap::with_capacity(uint), або використати HashMap::new(), щоб отримати HashMap із початковою місткістю за замовчуванням (рекомендовано).

use std::collections::HashMap;

fn call(number: &str) -> &str {
    match number {
        "798-1364" => "We're sorry, the call cannot be completed as dialed.
            Please hang up and try again.",
        "645-7689" => "Hello, this is Mr. Awesome's Pizza. My name is Fred.
            What can I get for you today?",
        _ => "Hi! Who is this again?"
    }
}

fn main() {
    let mut contacts = HashMap::new();

    contacts.insert("Daniel", "798-1364");
    contacts.insert("Ashley", "645-7689");
    contacts.insert("Katie", "435-8291");
    contacts.insert("Robert", "956-1745");

    // Приймає посилання і повертає Option<&V>
    match contacts.get(&"Daniel") {
        Some(&number) => println!("Calling Daniel: {}", call(number)),
        _ => println!("Don't have Daniel's number."),
    }

    // `HashMap::insert()` повертає `None`
    // якщо вставлене значення нове, `Some(value)` в іншому разі
    contacts.insert("Daniel", "164-6743");

    match contacts.get(&"Ashley") {
        Some(&number) => println!("Calling Ashley: {}", call(number)),
        _ => println!("Don't have Ashley's number."),
    }

    contacts.remove(&"Ashley");

    // `HashMap::iter()` повертає ітератор, який видає
    // пари (&'a key, &'a value) у довільному порядку.
    for (contact, &number) in contacts.iter() {
        println!("Calling {}: {}", contact, call(number));
    }
}

Докладніше про те, як працюють хешування та хеш-мапи (інколи звані хеш-таблицями), дивіться Вікіпедія про хеш-таблиці

Альтернативні/користувацькі типи ключів

Будь-який тип, що реалізує трейти Eq і Hash, може бути ключем у HashMap. Це включає:

  • bool (хоча не дуже корисно, оскільки існує лише два можливі ключі)
  • int, uint, і всі їхні варіації
  • String і &str (protip: ви можете мати HashMap з ключем String і викликати .get() з &str)

Зверніть увагу, що f32 і f64 не реалізують Hash, ймовірно, тому що помилки точності чисел з плаваючою комою робили б використання їх як ключів hashmap жахливо схильним до помилок.

Усі класи колекцій реалізують Eq і Hash, якщо їхній тип, що міститься, також відповідно реалізує Eq і Hash. Наприклад, Vec<T> реалізує Hash, якщо T реалізує Hash.

Ви можете легко реалізувати Eq і Hash для користувацького типу лише одним рядком: #[derive(PartialEq, Eq, Hash)]

Компілятор зробить решту. Якщо ви хочете більше контролю над деталями, ви можете реалізувати Eq і/або Hash самостійно. Цей посібник не охоплюватиме специфіку реалізації Hash.

Щоб поекспериментувати з використанням struct у HashMap, спробуймо зробити дуже просту систему входу користувача:

use std::collections::HashMap;

// Eq requires that you derive PartialEq on the type.
#[derive(PartialEq, Eq, Hash)]
struct Account<'a>{
    username: &'a str,
    password: &'a str,
}

struct AccountInfo<'a>{
    name: &'a str,
    email: &'a str,
}

type Accounts<'a> = HashMap<Account<'a>, AccountInfo<'a>>;

fn try_logon<'a>(accounts: &Accounts<'a>,
        username: &'a str, password: &'a str){
    println!("Username: {}", username);
    println!("Password: {}", password);
    println!("Attempting logon...");

    let logon = Account {
        username,
        password,
    };

    match accounts.get(&logon) {
        Some(account_info) => {
            println!("Successful logon!");
            println!("Name: {}", account_info.name);
            println!("Email: {}", account_info.email);
        },
        _ => println!("Login failed!"),
    }
}

fn main(){
    let mut accounts: Accounts = HashMap::new();

    let account = Account {
        username: "j.everyman",
        password: "password123",
    };

    let account_info = AccountInfo {
        name: "John Everyman",
        email: "[email protected]",
    };

    accounts.insert(account, account_info);

    try_logon(&accounts, "j.everyman", "psasword123");

    try_logon(&accounts, "j.everyman", "password123");
}

HashSet

Розглядайте HashSet як HashMap, де ми просто дбаємо про ключі ( HashSet<T> фактично є лише обгорткою навколо HashMap<T, ()>).

“Який у цьому сенс?” — запитуєте ви. “Я міг би просто зберігати ключі у Vec.”

Унікальна властивість HashSet полягає в тому, що йому гарантовано не мати дубльованих елементів. Саме це й є контракт, який виконує будь-яка колекція множин. HashSet — лише одна з реалізацій. (див. також: BTreeSet)

Якщо ви вставляєте значення, яке вже присутнє в HashSet, (тобто нове значення дорівнює наявному і вони обидва мають однаковий хеш), тоді нове значення замінить старе.

Це чудово підходить для випадків, коли ви ніколи не хочете більше ніж одного чогось, або коли ви хочете знати, чи вже маєте щось.

Але множини можуть робити більше, ніж це.

Множини мають 4 основні операції (усі такі виклики повертають ітератор):

  • union: отримати всі унікальні елементи в обох множинах.

  • difference: отримати всі елементи, які є в першій множині, але не в другій.

  • intersection: отримати всі елементи, які є тільки в обох множинах.

  • symmetric_difference: отримати всі елементи, які є в одній множині або в іншій, але не в обох.

Спробуйте всі ці варіанти в наведеному нижче прикладі:

use std::collections::HashSet;

fn main() {
    let mut a: HashSet<i32> = vec![1i32, 2, 3].into_iter().collect();
    let mut b: HashSet<i32> = vec![2i32, 3, 4].into_iter().collect();

    assert!(a.insert(4));
    assert!(a.contains(&4));

    // `HashSet::insert()` повертає false, якщо
    // значення вже було присутнє.
    assert!(b.insert(4), "Value 4 is already in set B!");
    // FIXME ^ Comment out this line

    b.insert(5);

    // If a collection's element type implements `Debug`,
    // then the collection implements `Debug`.
    // It usually prints its elements in the format `[elem1, elem2, ...]`
    println!("A: {:?}", a);
    println!("B: {:?}", b);

    // Print [1, 2, 3, 4, 5] in arbitrary order
    println!("Union: {:?}", a.union(&b).collect::<Vec<&i32>>());

    // This should print [1]
    println!("Difference: {:?}", a.difference(&b).collect::<Vec<&i32>>());

    // Print [2, 3, 4] in arbitrary order.
    println!("Intersection: {:?}", a.intersection(&b).collect::<Vec<&i32>>());

    // Print [1, 5]
    println!("Symmetric Difference: {:?}",
             a.symmetric_difference(&b).collect::<Vec<&i32>>());
}

(Приклади адаптовано з документації.)

Rc

Коли потрібне множинне володіння, можна використовувати Rc(Reference Counting). Rc відстежує кількість посилань, що означає кількість власників значення, загорнутого всередину Rc.

Лічильник посилань Rc збільшується на 1 щоразу, коли Rc клонують, і зменшується на 1 щоразу, коли один клонований Rc виходить з області видимості. Коли лічильник посилань Rc стає нульовим (що означає, що не лишається жодного власника), і Rc, і значення всі разом вивільняються.

Клонування Rc ніколи не виконує глибоке копіювання. Клонування створює лише ще один вказівник на загорнуте значення і збільшує лічильник.

use std::rc::Rc;

fn main() {
    let rc_examples = "Rc examples".to_string();
    {
        println!("--- rc_a is created ---");

        let rc_a: Rc<String> = Rc::new(rc_examples);
        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));

        {
            println!("--- rc_a is cloned to rc_b ---");

            let rc_b: Rc<String> = Rc::clone(&rc_a);
            println!("Reference Count of rc_b: {}", Rc::strong_count(&rc_b));
            println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));

            // Two `Rc`s are equal if their inner values are equal
            println!("rc_a and rc_b are equal: {}", rc_a.eq(&rc_b));

            // We can use methods of a value directly
            println!("Length of the value inside rc_a: {}", rc_a.len());
            println!("Value of rc_b: {}", rc_b);

            println!("--- rc_b is dropped out of scope ---");
        }

        println!("Reference Count of rc_a: {}", Rc::strong_count(&rc_a));

        println!("--- rc_a is dropped out of scope ---");
    }

    // Error! `rc_examples` already moved into `rc_a`
    // And when `rc_a` is dropped, `rc_examples` is dropped together
    // println!("rc_examples: {}", rc_examples);
    // TODO ^ Try uncommenting this line
}

Дивіться також:

std::rc і std::sync::arc.

Arc

Коли потрібне спільне володіння між потоками, можна використати Arc(Atomically Reference Counted). Ця структура, через реалізацію Clone, може створити вказівник-посилання на розташування значення в купі пам’яті, одночасно збільшуючи лічильник посилань. Оскільки вона ділить володіння між потоками, коли останній вказівник-посилання на значення виходить з області видимості, змінну буде видалено.

use std::time::Duration;
use std::sync::Arc;
use std::thread;

fn main() {
    // This variable declaration is where its value is specified.
    let apple = Arc::new("the same apple");

    for _ in 0..10 {
        // Here there is no value specification as it is a pointer to a
        // reference in the memory heap.
        let apple = Arc::clone(&apple);

        thread::spawn(move || {
            // As Arc was used, threads can be spawned using the value allocated
            // in the Arc variable pointer's location.
            println!("{:?}", apple);
        });
    }

    // Make sure all Arc instances are printed from spawned threads.
    thread::sleep(Duration::from_secs(1));
}

Std misc

Багато інших типів надаються бібліотекою std для підтримки таких речей, як:

  • Потоки
  • Канали
  • File I/O

Вони розширюють те, що надають примітиви.

Див. також:

примітиви і бібліотека std

Threads

Rust надає механізм для створення власних потоків ОС через функцію spawn, аргументом цієї функції є замикання з переміщенням.

use std::thread;

const NTHREADS: u32 = 10;

// This is the `main` thread
fn main() {
    // Make a vector to hold the children which are spawned.
    let mut children = vec![];

    for i in 0..NTHREADS {
        // Spin up another thread
        children.push(thread::spawn(move || {
            println!("this is thread number {}", i);
        }));
    }

    for child in children {
        // Wait for the thread to finish. Returns a result.
        let _ = child.join();
    }
}

Ці потоки будуть заплановані ОС.

Testcase: map-reduce

Rust робить дуже легким паралелізацію обробки даних, без багатьох головних болів, традиційно пов’язаних із такою спробою.

Стандартна бібліотека надає чудові примітиви потоків з коробки. Ці примітиви, у поєднанні з концепцією Ownership у Rust і правилами аліасингу, автоматично запобігають станам гонки даних.

Правила аліасингу (одне змінне посилання XOR багато читаючих посилань) автоматично запобігають вам маніпулювати станом, який видимий для інших потоків. (Коли потрібна синхронізація, існують примітиви синхронізації такі як Mutexes або Channels.)

У цьому прикладі ми обчислимо суму всіх цифр у блоці чисел. Ми зробимо це, розподіливши шматки блоку між різними потоками. Кожен потік підсумує свій маленький блок цифр, а згодом ми підсумуємо проміжні суми, отримані кожним потоком.

Зверніть увагу, що хоча ми передаємо посилання через межі потоків, Rust розуміє, що ми передаємо лише посилання лише для читання, і тому не може виникнути ані небезпеки, ані станів гонки даних. Також через те, що посилання, які ми передаємо, мають часи життя 'static, Rust розуміє, що наші дані не будуть знищені, поки ці потоки все ще працюють. (Коли вам потрібно ділити дані з не-static часами життя між потоками, ви можете використати розумний вказівник, такий як Arc, щоб підтримувати дані живими й уникнути не-static часів життя.)

use std::thread;

// This is the `main` thread
fn main() {

    // This is our data to process.
    // We will calculate the sum of all digits via a threaded map-reduce algorithm.
    // Each whitespace separated chunk will be handled in a different thread.
    //
    // TODO: see what happens to the output if you insert spaces!
    let data = "86967897737416471853297327050364959
11861322575564723963297542624962850
70856234701860851907960690014725639
38397966707106094172783238747669219
52380795257888236525459303330302837
58495327135744041048897885734297812
69920216438980873548808413720956532
16278424637452589860345374828574668";

    // Make a vector to hold the child-threads which we will spawn.
    let mut children = vec![];

    /*************************************************************************
     * "Map" phase
     *
     * Divide our data into segments, and apply initial processing
     ************************************************************************/

    // split our data into segments for individual calculation
    // each chunk will be a reference (&str) into the actual data
    let chunked_data = data.split_whitespace();

    // Iterate over the data segments.
    // .enumerate() adds the current loop index to whatever is iterated
    // the resulting tuple "(index, element)" is then immediately
    // "destructured" into two variables, "i" and "data_segment" with a
    // "destructuring assignment"
    for (i, data_segment) in chunked_data.enumerate() {
        println!("data segment {} is \"{}\"", i, data_segment);

        // Process each data segment in a separate thread
        //
        // spawn() returns a handle to the new thread,
        // which we MUST keep to access the returned value
        //
        // 'move || -> u32' is syntax for a closure that:
        // * takes no arguments ('||')
        // * takes ownership of its captured variables ('move') and
        // * returns an unsigned 32-bit integer ('-> u32')
        //
        // Rust is smart enough to infer the '-> u32' from
        // the closure itself so we could have left that out.
        //
        // TODO: try removing the 'move' and see what happens
        children.push(thread::spawn(move || -> u32 {
            // Calculate the intermediate sum of this segment:
            let result = data_segment
                        // iterate over the characters of our segment..
                        .chars()
                        // .. convert text-characters to their number value..
                        .map(|c| c.to_digit(10).expect("should be a digit"))
                        // .. and sum the resulting iterator of numbers
                        .sum();

            // println! locks stdout, so no text-interleaving occurs
            println!("processed segment {}, result={}", i, result);

            // "return" not needed, because Rust is an "expression language", the
            // last evaluated expression in each block is automatically its value.
            result

        }));
    }


    /*************************************************************************
     * "Reduce" phase
     *
     * Collect our intermediate results, and combine them into a final result
     ************************************************************************/

    // combine each thread's intermediate results into a single final sum.
    //
    // we use the "turbofish" ::<> to provide sum() with a type hint.
    //
    // TODO: try without the turbofish, by instead explicitly
    // specifying the type of final_result
    let final_result = children.into_iter().map(|c| c.join().unwrap()).sum::<u32>();

    println!("Final sum result: {}", final_result);
}

Завдання

Не розумно дозволяти нашій кількості потоків залежати від введених користувачем даних. Що, якщо користувач вирішить вставити багато пробілів? Чи справді ми хочемо створити 2,000 потоків? Змініть програму так, щоб дані завжди розбивалися на обмежену кількість шматків, визначену статичною константою на початку програми.

Див. також:

Канали

Rust надає асинхронні channels для зв’язку між потоками. Канали дозволяють односпрямований потік інформації між двома кінцевими точками: Sender і Receiver.

use std::sync::mpsc::{Sender, Receiver};
use std::sync::mpsc;
use std::thread;

static NTHREADS: i32 = 3;

fn main() {
    // Channels have two endpoints: the `Sender<T>` and the `Receiver<T>`,
    // where `T` is the type of the message to be transferred
    // (type annotation is superfluous)
    let (tx, rx): (Sender<i32>, Receiver<i32>) = mpsc::channel();
    let mut children = Vec::new();

    for id in 0..NTHREADS {
        // The sender endpoint can be copied
        let thread_tx = tx.clone();

        // Each thread will send its id via the channel
        let child = thread::spawn(move || {
            // The thread takes ownership over `thread_tx`
            // Each thread queues a message in the channel
            thread_tx.send(id).unwrap();

            // Sending is a non-blocking operation, the thread will continue
            // immediately after sending its message
            println!("thread {} finished", id);
        });

        children.push(child);
    }

    // Here, all the messages are collected
    let mut ids = Vec::with_capacity(NTHREADS as usize);
    for _ in 0..NTHREADS {
        // The `recv` method picks a message from the channel
        // `recv` will block the current thread if there are no messages available
        ids.push(rx.recv());
    }

    // Wait for the threads to complete any remaining work
    for child in children {
        child.join().expect("oops! the child thread panicked");
    }

    // Show the order in which the messages were sent
    println!("{:?}", ids);
}

Path

Тип Path представляє шляхи до файлів у базовій файловій системі. На всіх платформах існує один std::path::Path, який абстрагує семантику шляхів і роздільники, специфічні для платформи. За потреби введіть його в область видимості за допомогою use std::path::Path;.

Path можна створити з OsStr, і він надає кілька методів для отримання інформації про файл/директорію, на яку вказує шлях.

Path є незмінним. Власною версією Path є PathBuf. Співвідношення між Path і PathBuf подібне до str і String: PathBuf можна змінювати на місці, і на нього можна розіменовуватися до Path.

Зверніть увагу, що Path не представлений внутрішньо як рядок UTF-8, а натомість зберігається як OsString. Тому перетворення Path на &str не є безкоштовним і може завершитися невдачею (повертається Option). Однак Path можна вільно перетворити на OsString або &OsStr за допомогою into_os_string і as_os_str відповідно.

use std::path::Path;

fn main() {
    // Create a `Path` from an `&'static str`
    let path = Path::new(".");

    // The `display` method returns a `Display`able structure
    let _display = path.display();

    // `join` merges a path with a byte container using the OS specific
    // separator, and returns a `PathBuf`
    let mut new_path = path.join("a").join("b");

    // `push` extends the `PathBuf` with a `&Path`
    new_path.push("c");
    new_path.push("myfile.tar.gz");

    // `set_file_name` updates the file name of the `PathBuf`
    new_path.set_file_name("package.tgz");

    // Convert the `PathBuf` into a string slice
    match new_path.to_str() {
        None => panic!("new path is not a valid UTF-8 sequence"),
        Some(s) => println!("new path is {}", s),
    }
}

Обов’язково перегляньте інші методи Path і структуру Metadata.

Дивіться також:

OsStr і Metadata.

File I/O

Структура File представляє файл, який було відкрито (вона обгортає дескриптор файлу), і надає доступ на читання та/або запис до базового файлу.

Оскільки під час виконання файлового I/O може піти не так багато чого, усі методи File повертають тип io::Result<T>, який є псевдонімом для Result<T, io::Error>.

Це робить збій усіх операцій I/O явним. Завдяки цьому програміст може бачити всі шляхи збою і заохочується обробляти їх проактивно.

open

Функцію open можна використовувати для відкриття файлу в режимі лише для читання.

File володіє ресурсом, дескриптором файлу, і дбає про закриття файлу, коли його drop-нуто.

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    // Create a path to the desired file
    let path = Path::new("hello.txt");
    let display = path.display();

    // Open the path in read-only mode, returns `io::Result<File>`
    let mut file = match File::open(&path) {
        Err(why) => panic!("couldn't open {}: {}", display, why),
        Ok(file) => file,
    };

    // Read the file contents into a string, returns `io::Result<usize>`
    let mut s = String::new();
    match file.read_to_string(&mut s) {
        Err(why) => panic!("couldn't read {}: {}", display, why),
        Ok(_) => print!("{} contains:\n{}", display, s),
    }

    // `file` goes out of scope, and the "hello.txt" file gets closed
}

Ось очікуваний успішний вивід:

$ echo "Hello World!" > hello.txt
$ rustc open.rs && ./open
hello.txt contains:
Hello World!

(Вас заохочують перевірити попередній приклад за різних умов помилки: hello.txt не існує, або hello.txt не можна прочитати, тощо.)

create

Функція create відкриває файл у режимі лише для запису. Якщо файл вже існував, старий вміст знищується. Інакше створюється новий файл.

static LOREM_IPSUM: &str =
    "Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
";

use std::fs::File;
use std::io::prelude::*;
use std::path::Path;

fn main() {
    let path = Path::new("lorem_ipsum.txt");
    let display = path.display();

    // Відкриває файл у режимі лише для запису, повертає `io::Result<File>`
    let mut file = match File::create(&path) {
        Err(why) => panic!("couldn't create {}: {}", display, why),
        Ok(file) => file,
    };

    // Записує рядок `LOREM_IPSUM` у `file`, повертає `io::Result<()>`
    match file.write_all(LOREM_IPSUM.as_bytes()) {
        Err(why) => panic!("couldn't write to {}: {}", display, why),
        Ok(_) => println!("successfully wrote to {}", display),
    }
}

Ось очікуваний успішний вивід:

$ rustc create.rs && ./create
successfully wrote to lorem_ipsum.txt

$ cat lorem_ipsum.txt
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

(Як і в попередньому прикладі, вам рекомендується протестувати цей приклад за умов відмови.)

Структуру OpenOptions можна використовувати, щоб налаштувати, як відкривається файл.

read_lines

Наївний підхід

Це може бути розумною першою спробою для першої реалізації початківця для читання рядків із файла.

#![allow(unused)]
fn main() {
use std::fs::read_to_string;

fn read_lines(filename: &str) -> Vec<String> {
    let mut result = Vec::new();

    for line in read_to_string(filename).unwrap().lines() {
        result.push(line.to_string())
    }

    result
}
}

Оскільки метод lines() повертає ітератор по рядках у файлі, ми також можемо виконати map inline і зібрати результати, отримуючи більш стислий і плавний вираз.

#![allow(unused)]
fn main() {
use std::fs::read_to_string;

fn read_lines(filename: &str) -> Vec<String> {
    read_to_string(filename)
        .unwrap()  // panic on possible file-reading errors
        .lines()  // split the string into an iterator of string slices
        .map(String::from)  // make each slice into a string
        .collect()  // gather them together into a vector
}
}

Зверніть увагу, що в обох прикладах вище ми маємо перетворити посилання &str, повернуте з lines(), на тип-власник String, використовуючи відповідно .to_string() і String::from.

Більш ефективний підхід

Тут ми передаємо володіння відкритим File структурам BufReader. BufReader використовує внутрішній буфер, щоб зменшити проміжні виділення.

Ми також оновлюємо read_lines, щоб повертати ітератор замість виділення нових об’єктів String у пам’яті для кожного рядка.

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

fn main() {
    // File hosts.txt must exist in the current path
    if let Ok(lines) = read_lines("./hosts.txt") {
        // Consumes the iterator, returns an (Optional) String
        for line in lines.map_while(Result::ok) {
            println!("{}", line);
        }
    }
}

// The output is wrapped in a Result to allow matching on errors.
// Returns an Iterator to the Reader of the lines of the file.
fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where P: AsRef<Path>, {
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}

Запуск цієї програми просто виводить рядки окремо.

$ echo -e "127.0.0.1\n192.168.0.1\n" > hosts.txt
$ rustc read_lines.rs && ./read_lines
127.0.0.1
192.168.0.1

(Зверніть увагу, що оскільки File::open очікує як аргумент узагальнений AsRef<Path>, ми визначаємо наш узагальнений метод read_lines() з таким самим узагальненим обмеженням, використовуючи ключове слово where.)

Цей процес є більш ефективним, ніж створення String у пам’яті з усім вмістом файла. Це особливо може спричиняти проблеми з продуктивністю під час роботи з більшими файлами.

Child processes

Структура process::Output представляє вихід завершеного дочірнього процесу, а структура process::Command — це конструктор процесу.

use std::process::Command;

fn main() {
    let output = Command::new("rustc")
        .arg("--version")
        .output().unwrap_or_else(|e| {
            panic!("failed to execute process: {}", e)
    });

    if output.status.success() {
        let s = String::from_utf8_lossy(&output.stdout);

        print!("rustc succeeded and stdout was:\n{}", s);
    } else {
        let s = String::from_utf8_lossy(&output.stderr);

        print!("rustc failed and stderr was:\n{}", s);
    }
}

(Ви заохочуєтеся спробувати попередній приклад з неправильним прапорцем, переданим до rustc)

Канали

Структура std::process::Child представляє дочірній процес і надає дескриптори stdin, stdout і stderr для взаємодії з базовим процесом через канали.

use std::io::prelude::*;
use std::process::{Command, Stdio};

static PANGRAM: &'static str =
"the quick brown fox jumps over the lazy dog\n";

fn main() {
    // Spawn the `wc` command
    let mut cmd = if cfg!(target_family = "windows") {
        let mut cmd = Command::new("powershell");
        cmd.arg("-Command").arg("$input | Measure-Object -Line -Word -Character");
        cmd
    } else {
        Command::new("wc")
    };
    let process = match cmd
                                .stdin(Stdio::piped())
                                .stdout(Stdio::piped())
                                .spawn() {
        Err(why) => panic!("couldn't spawn wc: {}", why),
        Ok(process) => process,
    };

    // Записуємо рядок до `stdin` для `wc`.
    //
    // `stdin` має тип `Option<ChildStdin>`, але оскільки ми знаємо, що цей
    // екземпляр обов'язково має один, ми можемо безпосередньо викликати `unwrap` для нього.
    match process.stdin.unwrap().write_all(PANGRAM.as_bytes()) {
        Err(why) => panic!("couldn't write to wc stdin: {}", why),
        Ok(_) => println!("sent pangram to wc"),
    }

    // Оскільки `stdin` не живе після наведених вище викликів, його `drop`-нуто,
    // і канал закрито.
    //
    // Це дуже важливо, інакше `wc` не почав би обробляти
    // введення, яке ми щойно надіслали.

    // Поле `stdout` також має тип `Option<ChildStdout>`, тож його треба розпакувати.
    let mut s = String::new();
    match process.stdout.unwrap().read_to_string(&mut s) {
        Err(why) => panic!("couldn't read wc stdout: {}", why),
        Ok(_) => print!("wc responded with:\n{}", s),
    }
}

Wait

Якщо ви хочете дочекатися завершення process::Child, ви повинні викликати Child::wait, який поверне process::ExitStatus.

use std::process::Command;

fn main() {
    let mut child = Command::new("sleep").arg("5").spawn().unwrap();
    let _result = child.wait().unwrap();

    println!("reached end of main");
}
$ rustc wait.rs && ./wait
# `wait` keeps running for 5 seconds until the `sleep 5` command finishes
reached end of main

Операції файлової системи

Модуль std::fs містить кілька функцій, що працюють із файловою системою.

use std::fs;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::prelude::*;
#[cfg(target_family = "unix")]
use std::os::unix;
#[cfg(target_family = "windows")]
use std::os::windows;
use std::path::Path;

// A simple implementation of `% cat path`
fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    match f.read_to_string(&mut s) {
        Ok(_) => Ok(s),
        Err(e) => Err(e),
    }
}

// A simple implementation of `% echo s > path`
fn echo(s: &str, path: &Path) -> io::Result<()> {
    let mut f = File::create(path)?;

    f.write_all(s.as_bytes())
}

// A simple implementation of `% touch path` (ignores existing files)
fn touch(path: &Path) -> io::Result<()> {
    match OpenOptions::new().create(true).write(true).open(path) {
        Ok(_) => Ok(()),
        Err(e) => Err(e),
    }
}

fn main() {
    println!("`mkdir a`");
    // Створити каталог, повертає `io::Result<()>`
    match fs::create_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(_) => {},
    }

    println!("`echo hello > a/b.txt`");
    // Попереднє зіставлення можна спростити, використавши метод `unwrap_or_else`
    echo("hello", &Path::new("a/b.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`mkdir -p a/c/d`");
    // Рекурсивно створити каталог, повертає `io::Result<()>`
    fs::create_dir_all("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`touch a/c/e.txt`");
    touch(&Path::new("a/c/e.txt")).unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`ln -s ../b.txt a/c/b.txt`");
    // Створити символічне посилання, повертає `io::Result<()>`
    #[cfg(target_family = "unix")] {
        unix::fs::symlink("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
            println!("! {:?}", why.kind());
        });
    }
    #[cfg(target_family = "windows")] {
        windows::fs::symlink_file("../b.txt", "a/c/b.txt").unwrap_or_else(|why| {
            println!("! {:?}", why.to_string());
        });
    }

    println!("`cat a/c/b.txt`");
    match cat(&Path::new("a/c/b.txt")) {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(s) => println!("> {}", s),
    }

    println!("`ls a`");
    // Прочитати вміст каталогу, повертає `io::Result<Vec<Path>>`
    match fs::read_dir("a") {
        Err(why) => println!("! {:?}", why.kind()),
        Ok(paths) => for path in paths {
            println!("> {:?}", path.unwrap().path());
        },
    }

    println!("`rm a/c/e.txt`");
    // Видалити файл, повертає `io::Result<()>`
    fs::remove_file("a/c/e.txt").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });

    println!("`rmdir a/c/d`");
    // Видалити порожній каталог, повертає `io::Result<()>`
    fs::remove_dir("a/c/d").unwrap_or_else(|why| {
        println!("! {:?}", why.kind());
    });
}

Ось очікуваний успішний вивід:

$ rustc fs.rs && ./fs
`mkdir a`
`echo hello > a/b.txt`
`mkdir -p a/c/d`
`touch a/c/e.txt`
`ln -s ../b.txt a/c/b.txt`
`cat a/c/b.txt`
> hello
`ls a`
> "a/b.txt"
> "a/c"
`rm a/c/e.txt`
`rmdir a/c/d`

А кінцевий стан каталогу a такий:

$ tree a
a
|-- b.txt
`-- c
    `-- b.txt -> ../b.txt

1 directory, 2 files

Альтернативний спосіб визначити функцію cat — за допомогою нотації ?:

fn cat(path: &Path) -> io::Result<String> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

Також дивіться:

cfg!

Аргументи програми

Стандартна бібліотека

До аргументів командного рядка можна отримати доступ за допомогою std::env::args, яка повертає ітератор, що видає String для кожного аргументу:

use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();

    // Перший аргумент — це шлях, який було використано для запуску програми.
    println!("My path is {}.", args[0]);

    // Решта аргументів — це передані параметри командного рядка.
    // Запустіть програму так:
    //   $ ./args arg1 arg2
    println!("I got {:?} arguments: {:?}.", args.len() - 1, &args[1..]);
}
$ ./args 1 2 3
My path is ./args.
I got 3 arguments: ["1", "2", "3"].

Крейт

Альтернативно, існує численна кількість крейтів, які можуть надати додаткову функціональність під час створення застосунків командного рядка. Один із найпопулярніших крейтів для аргументів командного рядка — clap.

Парсинг аргументів

Зіставлення можна використати для парсингу простих аргументів:

use std::env;

fn increase(number: i32) {
    println!("{}", number + 1);
}

fn decrease(number: i32) {
    println!("{}", number - 1);
}

fn help() {
    println!("usage:
match_args <string>
    Check whether given string is the answer.
match_args {{increase|decrease}} <integer>
    Increase or decrease given integer by one.");
}

fn main() {
    let args: Vec<String> = env::args().collect();

    match args.len() {
        // no arguments passed
        1 => {
            println!("My name is 'match_args'. Try passing some arguments!");
        },
        // one argument passed
        2 => {
            match args[1].parse() {
                Ok(42) => println!("This is the answer!"),
                _ => println!("This is not the answer."),
            }
        },
        // one command and one argument passed
        3 => {
            let cmd = &args[1];
            let num = &args[2];
            // parse the number
            let number: i32 = match num.parse() {
                Ok(n) => {
                    n
                },
                Err(_) => {
                    eprintln!("error: second argument not an integer");
                    help();
                    return;
                },
            };
            // parse the command
            match &cmd[..] {
                "increase" => increase(number),
                "decrease" => decrease(number),
                _ => {
                    eprintln!("error: invalid command");
                    help();
                },
            }
        },
        // all the other cases
        _ => {
            // show a help message
            help();
        }
    }
}

Якщо ви назвали вашу програму match_args.rs і скомпілювали її так rustc match_args.rs, ви можете виконати її так:

$ ./match_args Rust
Це не відповідь.
$ ./match_args 42
Це відповідь!
$ ./match_args do something
error: second argument not an integer
usage:
match_args <string>
    Check whether given string is the answer.
match_args {increase|decrease} <integer>
    Increase or decrease given integer by one.
$ ./match_args do 42
error: invalid command
usage:
match_args <string>
    Check whether given string is the answer.
match_args {increase|decrease} <integer>
    Increase or decrease given integer by one.
$ ./match_args increase 42
43

Інтерфейс взаємодії з зовнішніми функціями

Rust надає Foreign Function Interface (FFI) для бібліотек C. Зовнішні функції мають бути оголошені всередині блоку extern, позначеного атрибутом #[link] із назвою зовнішньої бібліотеки.

use std::fmt;

// this extern block links to the libm library
#[cfg(target_family = "windows")]
#[link(name = "msvcrt")]
extern {
    // this is a foreign function
    // that computes the square root of a single precision complex number
    fn csqrtf(z: Complex) -> Complex;

    fn ccosf(z: Complex) -> Complex;
}
#[cfg(target_family = "unix")]
#[link(name = "m")]
extern {
    // this is a foreign function
    // that computes the square root of a single precision complex number
    fn csqrtf(z: Complex) -> Complex;

    fn ccosf(z: Complex) -> Complex;
}

// Since calling foreign functions is considered unsafe,
// it's common to write safe wrappers around them.
fn cos(z: Complex) -> Complex {
    unsafe { ccosf(z) }
}

fn main() {
    // z = -1 + 0i
    let z = Complex { re: -1., im: 0. };

    // calling a foreign function is an unsafe operation
    let z_sqrt = unsafe { csqrtf(z) };

    println!("the square root of {:?} is {:?}", z, z_sqrt);

    // calling safe API wrapped around unsafe operation
    println!("cos({:?}) = {:?}", z, cos(z));
}

// Minimal implementation of single precision complex numbers
#[repr(C)]
#[derive(Clone, Copy)]
struct Complex {
    re: f32,
    im: f32,
}

impl fmt::Debug for Complex {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        if self.im < 0. {
            write!(f, "{}-{}i", self.re, -self.im)
        } else {
            write!(f, "{}+{}i", self.re, self.im)
        }
    }
}

Testing

Rust — це мова програмування, яка дуже дбає про коректність, і вона включає підтримку написання тестів програмного забезпечення всередині самої мови.

Тестування буває у трьох стилях:

Також Rust підтримує вказування додаткових залежностей для тестів:

See Also

Модульне тестування

Тести — це функції Rust, які перевіряють, що код, який не є тестовим, працює очікуваним чином. Тіла тестових функцій зазвичай виконують певне налаштування, запускають код, який ми хочемо протестувати, а потім стверджують, чи є результати такими, як ми очікуємо.

Більшість модульних тестів потрапляють у tests [модуль][mod] з #[cfg(test)] [атрибутом][attribute]. Тестові функції позначаються атрибутом #[test].

Тести зазнають невдачі, коли щось у тестовій функції [panic’иться][panic]. Є кілька допоміжних [макросів][macros]:

  • assert!(expression) - викликає panic, якщо expression обчислюється в false.
  • assert_eq!(left, right) і assert_ne!(left, right) - перевіряють лівий і правий вирази на рівність і нерівність відповідно.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

// This is a really bad adding function, its purpose is to fail in this
// example.
#[allow(dead_code)]
fn bad_add(a: i32, b: i32) -> i32 {
    a - b
}

#[cfg(test)]
mod tests {
    // Note this useful idiom: importing names from outer (for mod tests) scope.
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(1, 2), 3);
    }

    #[test]
    fn test_bad_add() {
        // This assert would fire and test will fail.
        // Please note, that private functions can be tested too!
        assert_eq!(bad_add(1, 2), 3);
    }
}

Тести можна запускати за допомогою cargo test.

$ cargo test

running 2 tests
test tests::test_bad_add ... FAILED
test tests::test_add ... ok

failures:

---- tests::test_bad_add stdout ----
        thread 'tests::test_bad_add' panicked at 'assertion failed: `(left == right)`
  left: `-1`,
 right: `3`', src/lib.rs:21:8
note: Run with `RUST_BACKTRACE=1` for a backtrace.


failures:
    tests::test_bad_add

test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

Тести та ?

Жоден із попередніх прикладів модульних тестів не мав типу, що повертається. Але в Rust 2018, ваші модульні тести можуть повертати Result<()>, що дає змогу використовувати в них ?! Це може зробити їх значно лаконічнішими.

fn sqrt(number: f64) -> Result<f64, String> {
    if number >= 0.0 {
        Ok(number.powf(0.5))
    } else {
        Err("negative floats don't have square roots".to_owned())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sqrt() -> Result<(), String> {
        let x = 4.0;
        assert_eq!(sqrt(x)?.powf(2.0), x);
        Ok(())
    }
}

Докладніше див. [“The Edition Guide”][editionguide].

Тестування panic

Щоб перевірити функції, які мають викликати panic за певних обставин, використовуйте атрибут #[should_panic]. Цей атрибут приймає необов’язковий параметр expected = з текстом повідомлення panic. Якщо ваша функція може викликати panic кількома способами, це допомагає переконатися, що ваш тест перевіряє правильний panic.

Примітка: Rust також дозволяє скорочену форму #[should_panic = "message"], яка працює точно так само, як #[should_panic(expected = "message")]. Обидві форми є чинними; остання є поширенішою і вважається більш явною.

pub fn divide_non_zero_result(a: u32, b: u32) -> u32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    } else if a < b {
        panic!("Divide result is zero");
    }
    a / b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_divide() {
        assert_eq!(divide_non_zero_result(10, 2), 5);
    }

    #[test]
    #[should_panic]
    fn test_any_panic() {
        divide_non_zero_result(1, 0);
    }

    #[test]
    #[should_panic(expected = "Divide result is zero")]
    fn test_specific_panic() {
        divide_non_zero_result(1, 10);
    }

    #[test]
    #[should_panic = "Divide result is zero"] // This also works
    fn test_specific_panic_shorthand() {
        divide_non_zero_result(1, 10);
    }
}

Запуск цих тестів дає нам:

$ cargo test

running 4 tests
test tests::test_any_panic ... ok
test tests::test_divide ... ok
test tests::test_specific_panic ... ok
test tests::test_specific_panic_shorthand ... ok

test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Запуск окремих тестів

Щоб запустити окремі тести, можна вказати ім’я тесту для команди cargo test.

$ cargo test test_any_panic
running 1 test
test tests::test_any_panic ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Щоб запустити кілька тестів, можна вказати частину імені тесту, яка збігається з усіма тестами, що мають бути запущені.

$ cargo test panic
running 3 tests
test tests::test_any_panic ... ok
test tests::test_specific_panic ... ok
test tests::test_specific_panic_shorthand ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out

   Doc-tests tmp-test-should-panic

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Ігнорування тестів

Тести можна позначити атрибутом #[ignore], щоб виключити деякі тести. Або щоб запустити їх за допомогою команди cargo test -- --ignored

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_add() {
        assert_eq!(add(2, 2), 4);
    }

    #[test]
    fn test_add_hundred() {
        assert_eq!(add(100, 2), 102);
        assert_eq!(add(2, 100), 102);
    }

    #[test]
    #[ignore]
    fn ignored_test() {
        assert_eq!(add(0, 0), 0);
    }
}
$ cargo test
running 3 tests
test tests::ignored_test ... ignored
test tests::test_add ... ok
test tests::test_add_hundred ... ok

test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-ignore

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

$ cargo test -- --ignored
running 1 test
test tests::ignored_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests tmp-ignore

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

[атрибутом][attribute]: ../attribute.md [panic]: ../std/panic.md [макросів][macros]: ../macros.md [модуль][mod]: ../mod.md [editionguide]: https://doc.rust-lang.org/edition-guide/rust-2018/error-handling-and-panics/question-mark-in-main-and-tests.html

Тестування документації

Основний спосіб документування проєкту Rust — це анотування вихідного коду. Коментарі документації пишуться за специфікацією CommonMark Markdown і підтримують у собі блоки коду. Rust дбає про коректність, тож ці блоки коду компілюються і використовуються як тести документації.

/// Перший рядок — це короткий підсумок, який описує функцію.
///
/// Наступні рядки містять докладну документацію. Блоки коду починаються з
/// потрійних зворотних лапок і мають неявний `fn main()` всередині
/// та `extern crate <cratename>`. Припустімо, що ми тестуємо крейт `playground`
/// або використовуємо дію Test у Playground:
///
/// ```
/// let result = playground::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

/// Зазвичай doc-коментарі можуть містити розділи "Examples", "Panics" і "Failures".
///
/// Наступна функція ділить два числа.
///
/// # Examples
///
/// ```
/// let result = playground::div(10, 2);
/// assert_eq!(result, 5);
/// ```
///
/// # Panics
///
/// Функція panic, якщо другий аргумент дорівнює нулю.
///
/// ```rust,should_panic
/// // panic при діленні на нуль
/// playground::div(10, 0);
/// ```
pub fn div(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Divide-by-zero error");
    }

    a / b
}

Блоки коду в документації автоматично тестуються під час запуску звичайної команди cargo test:

$ cargo test
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests playground

running 3 tests
test src/lib.rs - add (line 7) ... ok
test src/lib.rs - div (line 21) ... ok
test src/lib.rs - div (line 31) ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Мотивація, що стоїть за тестами документації

Основна мета тестів документації — слугувати прикладами, які перевіряють функціональність, що є одним із найважливіших настанов. Це дає змогу використовувати приклади з документації як повноцінні фрагменти коду. Але використання ? призводить до помилки компіляції, оскільки main повертає unit. Тут у пригоді стає можливість приховувати деякі рядки вихідного коду від документації: можна написати fn try_main() -> Result<(), ErrorType>, приховати його і unwrap-нути в прихованому main. Звучить складно? Ось приклад:

/// Використання прихованого `try_main` у doc-тестах.
///
/// ```
/// # // приховані рядки починаються зі символу `#`, але вони все одно компілюються!
/// # fn try_main() -> Result<(), String> { // рядок, що обгортає тіло, показане в doc
/// let res = playground::try_div(10, 2)?;
/// # Ok(()) // повернення з try_main
/// # }
/// # fn main() { // початок main, який буде робити unwrap()
/// #    try_main().unwrap(); // виклик try_main і розгортання
/// #                         // так, щоб тест panic у разі помилки
/// # }
/// ```
pub fn try_div(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("Divide-by-zero"))
    } else {
        Ok(a / b)
    }
}

Див. також

  • RFC505 щодо стилю документації
  • API Guidelines щодо настанов з документації

Інтеграційне тестування

Модульні тести тестують один модуль ізольовано за раз: вони невеликі і можуть тестувати приватний код. Інтеграційні тести є зовнішніми щодо вашого крейта і використовують лише його публічний інтерфейс так само, як це робив би будь-який інший код. Їхня мета — перевірити, що багато частин вашої бібліотеки правильно працюють разом.

Cargo шукає інтеграційні тести в каталозі tests поруч із src.

Файл src/lib.rs:

// Define this in a crate called `adder`.
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

Файл із тестом: tests/integration_test.rs:

#[test]
fn test_add() {
    assert_eq!(adder::add(3, 2), 5);
}

Запуск тестів за допомогою команди cargo test:

$ cargo test
running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

     Running target/debug/deps/integration_test-bcd60824f5fbfe19

running 1 test
test test_add ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

   Doc-tests adder

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Кожен файл вихідного коду Rust у каталозі tests компілюється як окремий крейт. Щоб поділитися деяким кодом між інтеграційними тестами, ми можемо створити модуль із публічними функціями, імпортуючи та використовуючи його всередині тестів.

Файл tests/common/mod.rs:

pub fn setup() {
    // some setup code, like creating required files/directories, starting
    // servers, etc.
}

Файл із тестом: tests/integration_test.rs

// importing common module.
mod common;

#[test]
fn test_add() {
    // using common code.
    common::setup();
    assert_eq!(adder::add(3, 2), 5);
}

Створення модуля як tests/common.rs також працює, але не рекомендується, оскільки запускальник тестів розглядатиме файл як крейт для тестів і спробує запустити тести всередині нього.

Залежності для розробки

Іноді виникає потреба мати залежності лише для тестів (або прикладів, або бенчмарків). Такі залежності додаються до Cargo.toml у розділі [dev-dependencies]. Ці залежності не поширюються на інші пакети, які залежать від цього пакета.

Одним із таких прикладів є pretty_assertions, який розширює стандартні макроси assert_eq! і assert_ne!, щоб надавати кольоровий diff. Файл Cargo.toml:

# standard crate data is left out
[dev-dependencies]
pretty_assertions = "1"

Файл src/lib.rs:

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq; // crate for test-only use. Cannot be used in non-test code.

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
}

Див. також

Cargo docs on specifying dependencies.

Небезпечні операції

Як вступ до цього розділу, запозичуючи з офіційної документації, “слід намагатися мінімізувати кількість unsafe-коду в кодовій базі.” З цим знанням, почнімо! Позначення unsafe у Rust використовуються для обходу захистів, які запроваджує компілятор; зокрема, є чотири основні речі, для яких використовується unsafe:

  • розіменування сирих вказівників
  • виклик функцій або методів, які є unsafe (зокрема, виклик функції через FFI, див. попередній розділ книги)
  • доступ до статичних змінних, що можуть змінюватися, або їх змінення
  • реалізація небезпечних трейтів

Сирі вказівники

Сирі вказівники * і посилання &T функціонують подібно, але посилання завжди безпечні, тому що гарантовано вказують на дійсні дані завдяки перевірнику запозичень. Розіменування сирого вказівника можна виконати лише через unsafe-блок.

fn main() {
    let raw_p: *const u32 = &10;

    unsafe {
        assert!(*raw_p == 10);
    }
}

Виклик unsafe-функцій

Деякі функції можуть бути оголошені як unsafe, що означає, що за коректність відповідає програміст, а не компілятор. Один із прикладів цього — std::slice::from_raw_parts, яка створює зріз на основі вказівника на перший елемент і довжини.

use std::slice;

fn main() {
    let some_vector = vec![1, 2, 3, 4];

    let pointer = some_vector.as_ptr();
    let length = some_vector.len();

    unsafe {
        let my_slice: &[u32] = slice::from_raw_parts(pointer, length);

        assert_eq!(some_vector.as_slice(), my_slice);
    }
}

Для slice::from_raw_parts одна з припущень, які мають бути дотримані, — це те, що переданий вказівник вказує на дійсну пам’ять і що пам’ять, на яку він вказує, має правильний тип. Якщо ці інваріанти не дотримані, то поведінка програми є невизначеною, і неможливо знати, що станеться.

Вбудована асемблерна мова

Rust надає підтримку вбудованої асемблерної мови через макрос asm!. Його можна використовувати, щоб вбудувати написану вручну асемблерну мову в асемблерний вивід, згенерований компілятором. Загалом це не повинно бути необхідним, але може знадобитися там, де потрібну продуктивність або точність у часі не можна досягти іншим способом. Доступ до низькорівневих апаратних примітивів, наприклад у коді ядра, також може вимагати цієї функціональності.

Примітка: приклади тут наведено на асемблері x86/x86-64, але також підтримуються й інші архітектури.

Наразі вбудована асемблерна мова підтримується на таких архітектурах:

  • x86 і x86-64
  • ARM
  • AArch64
  • RISC-V

Базове використання

Почнімо з найпростішого можливого прикладу:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

unsafe {
    asm!("nop");
}
}
}

Це вставить інструкцію NOP (no operation) в асемблерну мову, згенеровану компілятором. Зверніть увагу, що всі виклики asm! мають бути всередині блоку unsafe, оскільки вони можуть вставити довільні інструкції й порушити різні інваріанти. Інструкції, які треба вставити, перелічуються в першому аргументі макроса asm! як рядковий літерал.

Входи та виходи

Тепер вставити інструкцію, яка нічого не робить, досить нудно. Зробімо щось, що справді діє на дані:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let x: u64;
unsafe {
    asm!("mov {}, 5", out(reg) x);
}
assert_eq!(x, 5);
}
}

Це запише значення 5 у змінну u64 x. Ви можете бачити, що рядковий літерал, який ми використовуємо для вказування інструкцій, насправді є шаблонним рядком. На нього поширюються ті самі правила, що й на Rust рядки форматування. Аргументи, які вставляються в шаблон, однак, виглядають трохи інакше, ніж ви можете бути звиклими. Спершу нам потрібно вказати, чи є змінна входом або виходом вбудованої асемблерної мови. У цьому випадку це вихід. Ми оголосили це, написавши out. Нам також потрібно вказати, у який саме тип регістра асемблерна мова очікує змінну. У цьому випадку ми поміщаємо її в довільний регістр загального призначення, вказавши reg. Компілятор вибере відповідний регістр для вставлення в шаблон і прочитає змінну звідти після завершення виконання вбудованої асемблерної мови.

Розгляньмо ще один приклад, який також використовує вхід:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let i: u64 = 3;
let o: u64;
unsafe {
    asm!(
        "mov {0}, {1}",
        "add {0}, 5",
        out(reg) o,
        in(reg) i,
    );
}
assert_eq!(o, 8);
}
}

Це додасть 5 до входу в змінній i і запише результат у змінну o. Особливий спосіб, у який ця асемблерна мова це робить, полягає спершу в копіюванні значення з i до виходу, а потім у додаванні до нього 5.

Цей приклад показує кілька речей:

По-перше, ми можемо бачити, що asm! дозволяє кілька аргументів шаблонного рядка; кожен з них обробляється як окремий рядок асемблерного коду, ніби їх усі було об’єднано разом із новими рядками між ними. Це полегшує форматування асемблерного коду.

По-друге, ми можемо бачити, що входи оголошуються написанням in замість out.

По-третє, ми можемо бачити, що ми можемо вказати номер аргументу або ім’я, як у будь-якому рядку форматування. Для шаблонів вбудованої асемблерної мови це особливо корисно, оскільки аргументи часто використовуються більше одного разу. Для складнішої вбудованої асемблерної мови використання цього засобу загалом рекомендоване, оскільки це покращує читабельність і дозволяє переставляти інструкції без зміни порядку аргументів.

Ми можемо далі уточнити наведений вище приклад, щоб уникнути інструкції mov:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut x: u64 = 3;
unsafe {
    asm!("add {0}, 5", inout(reg) x);
}
assert_eq!(x, 8);
}
}

Ми можемо бачити, що inout використовується для вказування аргументу, який є і входом, і виходом. Це відрізняється від окремого вказування входу й виходу тим, що гарантується призначення обох до одного й того самого регістра.

Також можна вказати різні змінні для вхідної та вихідної частин операнда inout:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let x: u64 = 3;
let y: u64;
unsafe {
    asm!("add {0}, 5", inout(reg) x => y);
}
assert_eq!(y, 8);
}
}

Пізні вихідні операнди

Компілятор Rust є консервативним у виділенні операндів. Припускається, що out може бути записаний у будь-який момент, і тому не може спільно використовувати своє місце з будь-яким іншим аргументом. Однак для гарантування оптимальної продуктивності важливо використовувати якомога менше регістрів, щоб їх не доводилося зберігати й повторно завантажувати навколо блоку вбудованої асемблерної мови. Щоб досягти цього, Rust надає специфікатор lateout. Його можна використовувати для будь-якого виходу, який записується лише після того, як усі входи вже було спожито. Також існує варіант inlateout цього специфікатора.

Ось приклад, де inlateout не можна використовувати в режимі release або інших оптимізованих випадках:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
let c: u64 = 4;
unsafe {
    asm!(
        "add {0}, {1}",
        "add {0}, {2}",
        inout(reg) a,
        in(reg) b,
        in(reg) c,
    );
}
assert_eq!(a, 12);
}
}

У не оптимізованих випадках (наприклад, у режимі Debug), заміна inout(reg) a на inlateout(reg) a у наведеному вище прикладі може й надалі давати очікуваний результат. Однак у режимі release або інших оптимізованих випадках використання inlateout(reg) a натомість може призвести до кінцевого значення a = 16, через що перевірка зазнає невдачі.

Це тому, що в оптимізованих випадках компілятор вільний призначити один і той самий регістр для входів b і c, оскільки він знає, що вони мають однакове значення. Крім того, коли використовується inlateout, a і c можуть бути призначені до одного й того самого регістра, і в такому разі перша інструкція add перезапише початкове завантаження зі змінної c. Це на відміну від того, як використання inout(reg) a гарантує, що для a буде призначено окремий регістр.

Однак наступний приклад може використовувати inlateout, оскільки вихід змінюється лише після того, як усі вхідні регістри вже були прочитані:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
unsafe {
    asm!("add {0}, {1}", inlateout(reg) a, in(reg) b);
}
assert_eq!(a, 8);
}
}

Як ви можете бачити, цей фрагмент асемблерної мови все ще працюватиме правильно, якщо a і b буде призначено до одного й того самого регістра.

Явні регістрові операнди

Деякі інструкції вимагають, щоб операнди були в певному регістрі. Тому вбудована асемблерна мова Rust надає деякі більш специфічні специфікатори обмежень. Хоча reg загалом доступний на будь-якій архітектурі, явні регістри є дуже архітектурно специфічними. Наприклад, для x86 загальні регістри призначення eax, ebx, ecx, edx, ebp, esi і edi серед інших можна вказати за їхніми назвами.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let cmd = 0xd1;
unsafe {
    asm!("out 0x64, eax", in("eax") cmd);
}
}
}

У цьому прикладі ми викликаємо інструкцію out, щоб вивести вміст змінної cmd до порту 0x64. Оскільки інструкція out приймає лише eax (і його підрегістри) як операнд, нам довелося використати специфікатор обмеження eax.

Примітка: на відміну від інших типів операндів, явні регістрові операнди не можна використовувати в шаблонному рядку: ви не можете використовувати {} і маєте замість цього писати назву регістра безпосередньо. Також вони повинні з’являтися в кінці списку операндів після всіх інших типів операндів.

Розгляньте цей приклад, який використовує інструкцію x86 mul:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

fn mul(a: u64, b: u64) -> u128 {
    let lo: u64;
    let hi: u64;

    unsafe {
        asm!(
            // Інструкція x86 `mul` бере `rax` як неявний вхід і записує
            // 128-бітний результат множення до `rax:rdx`.
            "mul {}",
            in(reg) a,
            inlateout("rax") b => lo,
            lateout("rdx") hi
        );
    }

    ((hi as u128) << 64) + lo as u128
}
}
}

Це використовує інструкцію mul, щоб помножити два 64-бітні входи з 128-бітним результатом. Єдиним явним операндом є регістр, який ми заповнюємо зі змінної a. Другий операнд є неявним і має бути регістром rax, який ми заповнюємо зі змінної b. Нижні 64 біти результату зберігаються в rax, звідки ми заповнюємо змінну lo. Вищі 64 біти зберігаються в rdx, звідки ми заповнюємо змінну hi.

Зіпсовані регістри

У багатьох випадках вбудована асемблерна мова змінюватиме стан, який не потрібен як вихід. Зазвичай це відбувається або тому, що нам доводиться використовувати тимчасовий регістр в асемблерній мові, або тому, що інструкції змінюють стан, який нам не потрібно далі перевіряти. Цей стан загалом називають “зіпсованим”. Ми маємо повідомити про це компілятор, оскільки йому може знадобитися зберегти й відновити цей стан навколо блоку вбудованої асемблерної мови.

use std::arch::asm;

#[cfg(target_arch = "x86_64")]
fn main() {
    // три елементи по чотири байти кожен
    let mut name_buf = [0_u8; 12];
    // Рядок зберігається як ascii в ebx, edx, ecx у такому порядку
    // Оскільки ebx зарезервовано, asm має зберегти його значення.
    // Тому ми зберігаємо і відновлюємо його навколо основної asm.
    // 64-бітний режим на 64-бітних процесорах не дозволяє зберігати/відновлювати
    // 32-бітні регістри (такі як ebx), тому нам доводиться використовувати розширений регістр rbx.

    unsafe {
        asm!(
            "push rbx",
            "cpuid",
            "mov [rdi], ebx",
            "mov [rdi + 4], edx",
            "mov [rdi + 8], ecx",
            "pop rbx",
            // Ми використовуємо вказівник на масив для зберігання значень, щоб спростити
            // код Rust ціною кількох додаткових інструкцій asm
            // Однак це більш явно відображає, як працює asm, на відміну
            // від явних вихідних регістрів, таких як `out("ecx") val`
            // Сам *вказівник* є лише входом, хоча запис відбувається за ним
            in("rdi") name_buf.as_mut_ptr(),
            // вибрати cpuid 0, також позначити eax як зіпсований
            inout("eax") 0 => _,
            // cpuid також зіпсує ці регістри
            out("ecx") _,
            out("edx") _,
        );
    }

    let name = core::str::from_utf8(&name_buf).unwrap();
    println!("CPU Manufacturer ID: {}", name);
}

#[cfg(not(target_arch = "x86_64"))]
fn main() {}

У наведеному вище прикладі ми використовуємо інструкцію cpuid, щоб прочитати ідентифікатор виробника CPU. Ця інструкція записує в eax максимальний підтримуваний аргумент cpuid, а в ebx, edx і ecx — ідентифікатор виробника CPU як ASCII-байти в такому порядку.

Хоча eax ніколи не читається, нам усе одно потрібно повідомити компілятор, що регістр було змінено, щоб компілятор міг зберегти будь-які значення, які були в цих регістрах до asm. Це робиться шляхом оголошення його як виходу, але з _ замість імені змінної, що вказує на те, що вихідне значення має бути відкинуто.

Цей код також обходить обмеження, що ebx є зарезервованим регістром LLVM. Це означає, що LLVM припускає, що має повний контроль над регістром і його потрібно відновити до початкового стану перед виходом із блоку asm, тож його не можна використовувати як вхід або вихід за винятком випадку, якщо компілятор використовує його для задоволення загальної класової групи регістрів (наприклад, in(reg)). Це робить операнди reg небезпечними під час використання зарезервованих регістрів, оскільки ми можемо несвідомо пошкодити наш вхід або вихід, бо вони використовують той самий регістр.

Щоб обійти це, ми використовуємо rdi для зберігання вказівника на вихідний масив, зберігаємо ebx через push, читаємо з ebx усередині блоку asm у масив, а потім відновлюємо ebx до його початкового стану через pop. push і pop використовують повну 64-бітну версію регістра rbx, щоб гарантувати, що весь регістр буде збережено. На 32-бітних цільових платформах код натомість використовував би ebx у push/pop.

Це також можна використовувати із загальним класом регістрів, щоб отримати тимчасовий регістр для використання всередині коду asm:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

// Помножити x на 6 за допомогою зсувів і додавань
let mut x: u64 = 4;
unsafe {
    asm!(
        "mov {tmp}, {x}",
        "shl {tmp}, 1",
        "shl {x}, 2",
        "add {x}, {tmp}",
        x = inout(reg) x,
        tmp = out(reg) _,
    );
}
assert_eq!(x, 4 * 6);
}
}

Операнди символів і зіпсовані ABI

За замовчуванням asm! припускає, що будь-який регістр, не вказаний як вихід, збереже свій вміст завдяки коду асемблерної мови. Аргумент clobber_abi до asm! повідомляє компілятору автоматично вставити необхідні операнди зіпсування відповідно до заданого ABI виклику: будь-який регістр, який не повністю зберігається в цьому ABI, буде вважатися зіпсованим. Можна надати кілька аргументів clobber_abi, і всі зіпсування з усіх указаних ABI буде вставлено.

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

extern "C" fn foo(arg: i32) -> i32 {
    println!("arg = {}", arg);
    arg * 2
}

fn call_foo(arg: i32) -> i32 {
    unsafe {
        let result;
        asm!(
            "call {}",
            // Вказівник на функцію для виклику
            in(reg) foo,
            // 1-й аргумент у rdi
            in("rdi") arg,
            // Повернене значення в rax
            out("rax") result,
            // Позначити всі регістри, які не зберігаються ABI виклику "C",
            // як зіпсовані.
            clobber_abi("C"),
        );
        result
    }
}
}
}

Модифікатори шаблону регістра

У деяких випадках потрібен тонкий контроль над тим, як назва регістра форматуватиметься під час вставлення в шаблонний рядок. Це потрібно, коли асемблерна мова архітектури має кілька назв для того самого регістра, кожна з яких зазвичай є “вікном” у підмножину регістра (наприклад, нижні 32 біти 64-бітного регістра).

За замовчуванням компілятор завжди обиратиме назву, що відповідає повному розміру регістра (наприклад, rax на x86-64, eax на x86 тощо).

Це значення за замовчуванням можна перевизначити, використовуючи модифікатори в операндах шаблонного рядка, так само як ви б робили це з рядками форматування:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut x: u16 = 0xab;

unsafe {
    asm!("mov {0:h}, {0:l}", inout(reg_abcd) x);
}

assert_eq!(x, 0xabab);
}
}

У цьому прикладі ми використовуємо клас регістрів reg_abcd, щоб обмежити розподілювач регістрів чотирма класичними x86-регістрами (ax, bx, cx, dx), для перших двох байтів яких можна звертатися незалежно.

Припустімо, що розподілювач регістрів вирішив розмістити x у регістрі ax. Модифікатор h виведе назву регістра для старшого байта цього регістра, а модифікатор l — назву регістра для молодшого байта. Отже, код asm буде розгорнуто як mov ah, al, що копіює молодший байт значення в старший байт.

Якщо ви використовуєте менший тип даних (наприклад, u16) з операндом і забудете використати модифікатори шаблону, компілятор видасть попередження й запропонує правильний модифікатор для використання.

Операнди адреси пам’яті

Іноді інструкції асемблерної мови вимагають операндів, переданих через адреси пам’яті/розташування пам’яті. Вам потрібно вручну використовувати синтаксис адреси пам’яті, визначений цільовою архітектурою. Наприклад, на x86/x86_64, використовуючи інтелівський синтаксис асемблера, ви повинні загортати входи/виходи в [], щоб указати, що це операнди пам’яті:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

fn load_fpu_control_word(control: u16) {
    unsafe {
        asm!("fldcw [{}]", in(reg) &control, options(nostack));
    }
}
}
}

Мітки

Будь-яке повторне використання іменованої мітки, локальної чи іншої, може призвести до помилки асемблера або лінкера або може спричинити іншу дивну поведінку. Повторне використання іменованої мітки може статися різними способами, зокрема:

  • явно: використовуючи мітку більше одного разу в одному блоці asm! або кілька разів у різних блоках.
  • неявно через inline: компілятор може створити кілька копій блоку asm!, наприклад, коли функцію, яка його містить, вбудовано в кількох місцях.
  • неявно через LTO: LTO може спричинити, що код з інших крейтів буде розміщено в тій самій одиниці генерації коду, і таким чином може принести довільні мітки.

Як наслідок, у коді вбудованої асемблерної мови слід використовувати лише числові [локальні мітки] GNU assembler. Визначення символів у коді асемблерної мови може призвести до помилок асемблера та/або лінкера через дублікати визначень символів.

Більше того, на x86 під час використання стандартного інтелівського синтаксису, через [помилку LLVM], вам не слід використовувати мітки, що складаються виключно з цифр 0 і 1, наприклад 0, 11 або 101010, оскільки їх можуть інтерпретувати як двійкові значення. Використання options(att_syntax) усуне будь-яку неоднозначність, але це впливає на синтаксис усього блоку asm!. (Див. Параметри нижче для отримання додаткової інформації про options.)

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut a = 0;
unsafe {
    asm!(
        "mov {0}, 10",
        "2:",
        "sub {0}, 1",
        "cmp {0}, 3",
        "jle 2f",
        "jmp 2b",
        "2:",
        "add {0}, 2",
        out(reg) a
    );
}
assert_eq!(a, 5);
}
}

Це зменшить значення регістра {0} з 10 до 3, а потім додасть 2 і збереже його в a.

Цей приклад показує кілька речей:

  • По-перше, що одне й те саме число можна використовувати як мітку кілька разів в одному вбудованому блоці.
  • По-друге, що коли числову мітку використовують як посилання (наприклад, як операнд інструкції), до числової мітки слід додавати суфікси “b” (“backward”) або ”f” (“forward”). Тоді вона посилатиметься на найближчу мітку, визначену цим числом у цьому напрямку.

Параметри

За замовчуванням блок вбудованої асемблерної мови обробляється так само, як зовнішній виклик функції FFI з користувацькою угодою виклику: він може читати/записувати пам’ять, мати спостережувані побічні ефекти тощо. Однак у багатьох випадках бажано надати компілятору більше інформації про те, що саме робить код асемблерної мови, щоб він міг краще оптимізувати.

Розгляньмо наш попередній приклад інструкції add:

#![allow(unused)]
fn main() {
#[cfg(target_arch = "x86_64")] {
use std::arch::asm;

let mut a: u64 = 4;
let b: u64 = 4;
unsafe {
    asm!(
        "add {0}, {1}",
        inlateout(reg) a, in(reg) b,
        options(pure, nomem, nostack),
    );
}
assert_eq!(a, 8);
}
}

Параметри можна надати як необов’язковий фінальний аргумент макроса asm!. Тут ми вказали три параметри:

  • pure означає, що код asm не має спостережуваних побічних ефектів і що його вихід залежить лише від його входів. Це дозволяє оптимізатору компілятора викликати вбудований asm менше разів або навіть повністю його усунути.
  • nomem означає, що код asm не читає й не записує в пам’ять. За замовчуванням компілятор припускатиме, що вбудована асемблерна мова може читати або записувати будь-яку адресу пам’яті, до якої має доступ (наприклад, через вказівник, переданий як операнд, або глобальну змінну).
  • nostack означає, що код asm не поміщає жодних даних у стек. Це дозволяє компілятору використовувати оптимізації, такі як червона зона стека на x86-64, щоб уникнути коригувань вказівника стека.

Це дозволяє компілятору краще оптимізувати код, що використовує asm!, наприклад, усуваючи блоки asm!, виходи яких не потрібні.

Дивіться довідник для повного списку доступних параметрів і їхніх ефектів.

Compatibility

Мова Rust швидко розвивається, і через це можуть виникати певні проблеми сумісності, попри зусилля забезпечити сумісність із майбутніми версіями всюди, де це можливо.

Raw identifiers

Rust, як і багато мов програмування, має поняття “ключових слів”. Ці ідентифікатори означають щось для мови, і тому ви не можете використовувати їх у таких місцях, як імена змінних, імена функцій та інших місцях. Сирі ідентифікатори дають змогу використовувати ключові слова там, де їх зазвичай не дозволено. Це особливо корисно, коли Rust запроваджує нові ключові слова, а крейт , що використовує старіше видання Rust, має змінну або функцію з тією самою назвою , що й ключове слово, запроваджене в новішому виданні.

Наприклад, розгляньте крейт foo, скомпільований за виданням Rust 2015, який експортує функцію з назвою try. Це ключове слово зарезервоване для нової можливості у виданні 2018, тож без сирих ідентифікаторів ми б не мали способу назвати цю функцію.

extern crate foo;

fn main() {
    foo::try();
}

Ви отримаєте цю помилку:

error: expected identifier, found keyword `try`
 --> src/main.rs:4:4
  |
4 | foo::try();
  |      ^^^ expected identifier, found keyword

Ви можете записати це із сирим ідентифікатором:

extern crate foo;

fn main() {
    foo::r#try();
}

Meta

Деякі теми не зовсім пов’язані з тим, як працює ваша програма, але надають вам підтримку інструментів або інфраструктури, що просто робить усе кращим для всіх. Ці теми включають:

  • Документація: Генеруйте документацію крейту для користувачів за допомогою включеного rustdoc.
  • Пісочниця: Інтегруйте Rust Playground у вашу документацію.

Документація

Використовуйте cargo doc, щоб зібрати документацію в target/doc, cargo doc --open автоматично відкриє її у вашому веббраузері.

Використовуйте cargo test, щоб запустити всі тести (включно з documentation tests), і cargo test --doc, щоб запускати лише documentation tests.

Ці команди належним чином викличуть rustdocrustc) за потреби.

Doc comments

Doc comments дуже корисні для великих проєктів, які потребують документації. Під час запуску rustdoc саме ці коментарі компілюються в документацію. Вони позначаються за допомогою /// і підтримують Markdown.

#![crate_name = "doc"]

/// Людину тут представлено як
pub struct Person {
    /// Людина повинна мати ім'я, незалежно від того, як сильно Джульєт це може ненавидіти
    name: String,
}

impl Person {
    /// Створює людину з указаним ім'ям.
    ///
    /// # Приклади
    ///
    /// ```
    /// // Ви можете мати rust code між fences усередині коментарів
    /// // Якщо ви передасте --test до `rustdoc`, він навіть протестує це для вас!
    /// use doc::Person;
    /// let person = Person::new("name");
    /// ```
    pub fn new(name: &str) -> Person {
        Person {
            name: name.to_string(),
        }
    }

    /// Дарує дружнє привітання!
    ///
    /// Каже "Hello, [name](Person::name)" до `Person`, на якому його викликано.
    pub fn hello(&self) {
        println!("Hello, {}!", self.name);
    }
}

fn main() {
    let john = Person::new("John");

    john.hello();
}

Щоб запустити тести, спочатку зберіть код як бібліотеку, потім повідомте rustdoc, де знайти бібліотеку, щоб він міг зв’язати її з кожною doctest program:

$ rustc doc.rs --crate-type lib
$ rustdoc --test --extern doc="libdoc.rlib" doc.rs

Doc attributes

Нижче наведено кілька прикладів найпоширеніших #[doc] attributes, що використовуються з rustdoc.

inline

Використовується для вбудовування docs, замість посилання на окрему сторінку.

#[doc(inline)]
pub use bar::Bar;

/// docs для bar
pub mod bar {
    /// docs для Bar
    pub struct Bar;
}

no_inline

Використовується, щоб запобігти посиланню на окрему сторінку або будь-де.

// Приклад із libcore/prelude
#[doc(no_inline)]
pub use crate::mem::drop;

hidden

Використання цього вказує rustdoc не включати це в документацію:

// Приклад із бібліотеки futures-rs
#[doc(hidden)]
pub use self::async_await::*;

Для документації rustdoc широко використовується спільнотою. Саме його використовують, щоб згенерувати docs бібліотеки std: https://doc.rust-lang.org/std/.

Див. також:

Playground

Пісочниця Rust (Rust Playground) — це спосіб експериментувати з кодом Rust через вебінтерфейс.

Використання з mdbook

У mdbook ви можете зробити приклади коду придатними до запуску та редагування.

fn main() {
    println!("Hello World!");
}

Це дає змогу читачеві і запускати ваш зразок коду, і також змінювати та налаштовувати його. Ключ тут — додавання слова editable до блоку коду, розділеного комою.

```rust,editable
//...place your code here
```

Додатково, ви можете додати ignore, якщо хочете, щоб mdbook пропускав ваш код, коли він збирається та тестується.

```rust,editable,ignore
//...place your code here
```

Використання з документацією

Ви могли помітити в деяких з офіційної документації Rust кнопку з написом “Run”, яка відкриває зразок коду в новій вкладці в Rust Playground. Цю можливість увімкнено, якщо ви використовуєте атрибут #[doc], що називається html_playground_url.

#![doc(html_playground_url = "https://play.rust-lang.org/")]
//! ```
//! println!("Hello World");
//! ```

Див. також: