Hello world!


Представляю вашему вниманию первую часть большой шпаргалки по Rust.


Обратите внимание: шпаргалка рассчитана на людей, которые хорошо знают любой современный язык программирования, а не на тех, кто только начинает кодить ;)


Также настоятельно рекомендуется хотя бы по диагонали прочитать замечательный Учебник по Rust (на русском языке).


Содержание



Основы


Комментарии и документирование кода


Комментарии


// Строчные комментарии
/* Блочные комментарии */

Поддерживаются вложенные блочные комментарии.


Старайтесь избегать использования блочных комментариев.


Комментарии документации (doc comments)


Команда cargo doc генерирует документацию проекта с помощью rustdoc. Для генерации документации используются док-комментарии.


Обычно мы добавляем док-комментарии в библиотечные крейты (library crates). Внутри док-комментариев можно использовать Markdown.


/// Строчные комментарии; документируют следующий элемент
/** Блочные комментарии; документируют следующий элемент */

//! Строчные комментарии; документируют вложенный элемент
/*! Блочные комментарии; документируют вложенный элемент !*/

Пример:


/// Этот модуль содержит тесты; внешний комментарий
mod tests {
    // ...
}

mod tests {
    //! Этот модуль содержит тесты; внутренний комментарий
    // ...
}

Ключевое слово mod используется для модулей. Мы обсудим это позже.


Док-атрибуты (doc attributes)


Док-атрибуты являются альтернативой док-комментариям. Мы используем их для управления rustdoc. Подробнее о док-атрибутах можно почитать здесь.


В следующем примере каждый комментарий эквивалентен соответствующему док-атрибуту:


/// Внешний комментарий
#[doc = "Внешний комментарий"]

//! Внутренний комментарий
#![doc = "Внутренний комментарий"]

Атрибут — это общие метаданные в свободной форме, которые интерпретируются в соответствии с названием, соглашением, версиями языка и компилятора. Синтаксис:


  • внешний атрибут: #[attr]
  • внутренний атрибут: #![attr]

Перед тем, как двигаться дальше...


  • Используйте //! только для написания документации крейта. Для блоков mod используйте /// снаружи блоков. Взгляните на использование док-комментариев //! и /// в популярных крейтах на crates.io. Например, взгляните на serde/src/lib.rs и rand/src/lib.rs
  • выполните команду cargo new hello_lib --lib для создания образца крейта и замените код в файле src/lib.rs на следующий:

//! Простой крейт Hello World

/// Эта функция возвращает приветствие; Hello, world!
pub fn hello() -> String {
    ("Hello, world!").to_string()
}

#[cfg(test)]
mod tests {
    use super::hello;

    #[test]
    fn test_hello() {
        assert_eq!(hello(), "Hello, world!");
    }
}

Затем выполните cargo doc --open для генерации документации и ее открытия в вашем дефолтном браузере.


Переменные, константы и статики


  • В Rust переменные по умолчанию являются иммутабельными (неизменными/неизменяемыми) (immutable), поэтому они называются привязками переменных (variable bindings). Для объявления мутируемой (изменяемой) (mutable) переменной используется ключевое слово mut.
  • Rust — это статически типизированный язык: типы данных (data types) проверяются во время компиляции. Однако это не означает, что типы всех переменных должны указываться явно. Компилятор "смотрит" на использование переменной и устанавливает для нее лучший тип. Но для констант (constants) и статики (statics) типы должны указываться явно. Типы указываются после двоеточие (:).

В следующих примерах мы используем такие типы данных, как bool, i32, i64 и f64. Мы обсудим это позже.


Переменные


Для объявления переменной используется ключевое слово let. Название (имя) переменной может быть привязано к значению или функции. Также поскольку левая часть выражения привязки является "паттерном" (pattern) мы можем привязывать несколько названий к нескольким значениям или функциям.


// Иммутабельная переменная
let a; // Объявление (declaration); без типа данных
a = 5; // Присвоение/присваивание значения (assignment)

let b: i8; // Объявление; с типом данных
b = 5;

let t = true;        // Объявление + присвоение; без типа данных
let f: bool = false; // Объявление + присвоение; с типом данных

// Несколько переменных
let (x, y) = (1, 2); // x = 1 и y = 2

// Мутабельная переменная
let mut z = 5;
z = 6;

// Значением переменной становится результат выражения
let z = { x + y }; // z = 3
// Затенение/перезапись переменной (variable shadowing) (см. ниже)
let z = {
    let x = 1;
    let y = 2;

    x + y
}; // z = 3

В Rust точка с запятой (;) в конце строки кода является обязательной (в отличие, например, от JavaScript). Отсутствие ; обычно означает возврат результата выражения.


Константы


Для определения констант используется ключевое слово const. Константы являются иммутабельными. Они живут в течение всего жизненного цикла программы, но не имеют фиксированного адреса в памяти.


// Название в стиле SCREAMING_SNAKE_CASE
// Тип указывается явно
const CONST_VAR: i32 = 5;

Статики


Ключевое слово static используется для определения объекта типа "глобальная переменная". Для каждого значения существует только один экземпляр такого объекта. Значение находится в фиксированном месте в памяти.


static STATIC_VAR: i32 = 5;

Старайтесь всегда использовать const вместо static для определения констант. Привязка места в памяти к константе требуется очень редко. Использование const позволяет выполнять такие оптимизации, как распространение константы (constant propagation) не только в вашем крейте, но также в зависимых/подчиненных крейтах.


Затенение переменных (variable shadowing)


Иногда возникает необходимость преобразовать значение переменной из одних единиц в другие для дальнейшей обработки. Rust позволяет повторно объявлять переменные с другими типами данных и/или другими настройками мутабельности. Это называется затенением.


fn main() {
    let x: f64 = -20.48; // float - число с плавающей точкой
    // Явное приведение/преобразование типа
    let x: i64 = x.floor() as i64; // int - целое число
    println!("{}", x); // -21

    let s: &str = "hello"; // &str - строковый срез
    // Неявное преобразование типа
    let s: String = s.to_uppercase(); // String - строка
    println!("{}", s) // HELLO
}

Перед тем, как двигаться дальше...


  • Для названий переменных используется стиль snake_case, а для названий констант и статик — SCREAMING_SNAKE_CASE
  • обычно константы и статики определяются в начале файла снаружи функции (после импорта модулей/объявлений use)

const PI: f64 = 3.14159265359;

fn main() {
    println!("Значение π: {}", PI);
}

Функции


Именованные функции


  • объявляются с помощью ключевого слова fn
  • при использовании аргументов, необходимо определять их типы
  • по умолчанию функции возвращают пустой кортеж (tuple) (()). Возвращаемый тип определяется после ->

Hello world


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

Передача аргументов


fn print_sum(a: i8, b: i8) {
    println!("Cумма: {}", a + b);
}

Возврат значения


// Без ключевого слова `return`. Возвращается только последнее выражение
fn plus_one(a: i32) -> i32 {
    a + 1
    // В конце этой строки отсутствует `;`
    // Это выражение эквивалентно `return a + 1;`
}

// С ключевым словом `return`
fn plus_two(a: i32) -> i32 {
    return a + 2;
    // Ключевое слово `return` следует использовать только для условного/раннего возврата (early return).
    // Использование `return` в последнем выражении считается плохой практикой
}

Указатели на функцию (function pointers), использование в качестве типа данных


fn main() {
    // Без объявлений типов
    let p1 = plus_one;
    let x = p1(5); // 6

    // С объявлениями типов
    let p1: fn(i32) -> i32 = plus_one;
    let x = p1(5); // 6
}

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

Замыкания (closures)


  • Также известны как анонимные или лямбда-функции
  • типы аргументов и возвращаемого значения являются опциональными

Именованная функция без замыкания


fn main() {
  let x = 2;
  println!("{}", get_square_value(x));
}

fn get_square_value(i: i32) -> i32 {
    i * i
}

С опциональными объявлениями типов аргумента и возвращаемого значения


fn main() {
    let x = 2;
     // Аргументы передаются внутри | |, а тело выражения оборачивается в { }
    let square = |i: i32| -> i32 {
        i * i
    };
    println!("{}", square(x));
}

Без объявлений типов


fn main() {
    let x = 2;
    let square = |i| i * i; // { } являются опциональными для однострочных замыканий
    println!("{}", square(x));
}

С опциональными объявлениями типов; создание + вызов


fn main() {
    let x = 2;
    let x_square = |i: i32| -> i32 { i * i }(x); // { } являются обязательными при одновременном создании и вызове
    println!("{}", x_square);
}

Без объявлений типов; создание + вызов


fn main() {
    let x = 2;
    let x_square = |i| -> i32 { i * i }(x); // тип возвращаемого значения является обязательным
    println!("{}", x_square);
}

Примитивные типы данных


bool


true или false.


let x = true;
let y: bool = false;

char


Единичное скалярное значение Юникода.


// Кавычки должны быть одинарными
let x = 'x';
let y: char = '????';

Для поддержки Юникода char занимает не 1, а 4 байта (32 бита).


i8, i16, i32, i64, i128


8, 16, 32, 64 и 128-битные целые числа фиксированного размера со знаком (±).


Тип MIN MAX
i8 -128 127
i16 -32768 32767
i32 -2147483648 2147483647
i64 -9223372036854775808 9223372036854775807
i128 -170141183460469231731687303715884105728 170141183460469231731687303715884105727

Минимальное и максимальное значения рассчитываются по формуле: от -(2ⁿ⁻¹) до 2ⁿ⁻¹-1. Для получения минимального и максимального значения типа можно использовать методы min_value() и max_value(), соответственно, например, i8::min_value().


let x = 10; // дефолтным целочисленным типом в Rust является `i32`
let y: i8 = -128;

u8, u16, u32, u64, u128


8, 16, 32, 64 и 128-битные целые числа фиксированного размера без знака (0/+).


Тип MIN MAX
u8 0 255
u16 0 65535
u32 0 4294967295
u64 0 18446744073709551615
u128 0 340282366920938463463374607431768211455

Минимальное и максимальное значения рассчитываются по формуле: от 0 до 2ⁿ-1. Для получения минимального и максимального значения типа также можно использовать методы min_value() и max_value(), соответственно, например, u8::max_value().


isize, usize


Целочисленные типы со знаком и без знака размером с указатель (pointer sized). Реальный битовый размер зависит от архитектуры компьютера, для которого выполняется компиляция программы. По умолчанию размеры равны 32 битам на 32-битных платформах и 64 битам на 64-битных платформах.


f32, f64


Числа с плавающей запятой размером 32 и 64 бита (числа с десятичной точкой). Rust следует стандарту IEEE для двоичной арифметики с плавающей запятой. Тип f32 аналогичен типу float (одинарная точность) в других языках программирования, а тип f64 — типу double (двойная точность).


let x = 1.5; // дефолтным "плавающим" типом в Rust является `f64`
let y: f64 = 2.0;

Следует избегать использования f32, если только вам не нужно сильно сократить потребление памяти или если вы выполняете низкоуровневую оптимизацию, когда целевое оборудование не поддерживает двойную точность или когда одинарная точность на нем работает быстрее, чем двойная.


Массив (array)


Список элементов одинакового типа фиксированного размера.


let a = [1, 2, 3];
let a: [i32; 3] = [1, 2, 3]; // [тип; количество элементов]

let b: [i32; 0] = []; // пустой массив

let mut c: [i32; 3] = [1, 2, 3];
c[0] = 2;
c[1] = 4;
c[2] = 6;

println!("{:?}", c); // [2, 4, 6]
println!("{:#?}", c);
//  [
//      2,
//      4,
//      6,
//  ]

let d = [0; 5];   // [0, 0, 0, 0, 0]
let e = ["x"; 5]; // ["x", "x", "x", "x", "x"]

Массивы по умолчанию являются иммутабельными. Даже при использовании ключевого слова mut количество элементов массива не может быть изменено.


Векторы (vectors) являются динамическими/расширяемыми (growable) массивами. Они могут содержать элементы любого типа, но все элементы должны быть одинакового типа.


Кортеж (tuple)


Упорядоченный список элементов разных или одинакового типа фиксированного размера.


let a = (1, 1.5, true, 'a');
let a: (i32, f64, bool, char) = (1, 1.5, true, 'a');

let mut b = (1, 1.5);
b.0 = 2;
b.1 = 3.0;

println!("{:?}", b); // (2, 3.0)
println!("{:#?}", b);
// (
//   2,
//   3.0,
// )

let (c, d) = b; // c = 2, d = 3.0
let (e, _, _, f) = a; // e = 1, f = 'a'

let g = (0,); // одноэлементный кортеж
let h = (b, (2, 4), 5); // ((2, 3.0), (2, 4), 5)

Кортежи по умолчанию также являются иммутабельными. Даже при использовании ключевого слова mut количество элементов кортежа не может быть изменено. При изменении значения элемента кортежа, новое значение должно быть того же типа, что предыдущее.


Срез/фрагмент (slice)


Ссылка (reference) динамического размера на другую структуру данных.


Представьте, что хотите получить/передать часть массива или другой структуры данных. Вместо копирования этой части в другой массив, Rust позволяет создать представление (view) / ссылку для доступа только к части данных. Эта ссылка может быть мутабельной или иммутабельной.


let a: [i32; 4] = [1, 2, 3, 4]; // родительский массив

let b: &[i32] = &a; // срез всего массива
let c = &a[0..4]; // от первого до четвертого (не включая) элемента
let d = &a[..]; // срез всего массива

let e = &a[1..3]; // [2, 3]
let f = &a[1..]; // [2, 3, 4]
let g = &a[..3]; // [1, 2, 3]

str


Безразмерная (unsized) последовательность UTF-8 фрагментов строк Юникода.


let a = "Hello, world."; // a: &'static str
let b: &str = "こんにちは, 世界!";

Это иммутабельный/статически выделенный срез, содержащий последовательность кодовых точек UTF-8 неизвестного размера, хранящуюся где-то в памяти. &str используется для заимствования (borrow) и присвоения всего массива данной переменной.


Функция


p1 — это указатель на функцию plus_one():


fn main() {
    let p1: fn(i32) -> i32 = plus_one;
    let x = p1(5); // 6
}

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

Перед тем, как двигаться дальше...


  • В Rust дефолтным целочисленным типом является i32, а дефолтным плавающим типом — f64

let i = 10;   // let i: i32 = 10;
let f = 3.14; // let f: f64 = 3.14;

  • тип числа также может определяться в виде суффикса. Для улучшения читаемости длинные числа могут разделяться _

let a = 5i8; //let a: i8 = 5;

let b = 100_000_000; // let b = 100000000;
// `_` могут размещаться произвольно, например, 10000_0000 - тоже валидное число

let pi = 3.141_592_653_59f64; // let pi: f64 = 3.14159265359

const PI: f64 = 3.141_592_653_59; // тип констант и статик должен указываться после названия переменной

  • в Rust существует несколько строковых типов. Тип String — это строка, выделенная в куче (heap-allocated). Она является расширяемой с гарантированной кодировкой UTF-8. По общему правилу, когда нам необходимо владение, следует использовать String, когда нужно заимствовать строку, следует использовать &str
  • Тип String может быть сгенерирован из &str с помощью методов to_string() или String::from(). Метод as_str() позволяет конвертировать String в &str

let s: &str = "Hello"; // &str

let s = s.to_string(); // String
let s = String::from(s); // String

let s = s.as_str(); // &str

Операторы


Арифметические операторы


+ - * / %


let a = 5;
let b = a + 1; // 6
let c = a - 1; // 4
let d = a * 2; // 10
let e = a / 2; // 2, а не 2.5
let f = a % 2; // 1

let g = 5.0 / 2.0; // 2.5

Операторы сравнения


== != < > <= >=


let a = 1;
let b = 2;

let c = a == b; // false
let d = a != b; // true
let e = a < b; // true
let f = a > b; // false
let g = a <= a; // true
let h = a >= a; // true

let i = true > false; // true
let j = 'a' > 'A'; // true

Логические операторы


! && ||


let a = true;
let b = false;

let c = !a; // false
let d = a && b; // false
let e = a || b; // true

В целочисленных типах ! инвертирует отдельные биты в представление значения в виде дополнения до двух (two’s complement representation).


let a = !-2; // 1
let b = !-1; // 0
let c = !0; // -1
let d = !1; // -2

Побитовые операторы


& | ^ << >>


let a = 1;
let b = 2;

let c = a & b;  // 0  (01 && 10 -> 00)
let d = a | b;  // 3  (01 || 10 -> 11)
let e = a ^ b;  // 3  (01 != 10 -> 11)
let f = a << b; // 4  (добавляем b нулей в конец a -> '01'+'00' -> 100)
let g = a >> b; //0  (удаляем b битов с конца a -> '01'-'10' -> 0)

Операторы присваивания и составные (compound) операторы присваивания


let mut a = 2;

a += 5; // 2 + 5 = 7
a -= 2; // 7 - 2 = 5
a *= 5; // 5 * 5 = 25
a /= 2; // 25 / 2 = 12 not 12.5
a %= 5; // 12 % 5 = 2

a &= 2; // 10 && 10 -> 10 -> 2
a |= 5; // 010 || 101 -> 111 -> 7
a ^= 2; // 111 != 010 -> 101 -> 5
a <<= 1; // '101'+'0' -> 1010 -> 10
a >>= 2; // '1010'-'10' -> 10 -> 2

Оператор преобразования типа (type casting)


as


let a = 15;
let b = (a as f64) / 2.0; // 7.5

Операторы заимствования и разыменования


& &mut *


Операторы & и &mut используются для заимствования (borrow), а оператор * — для разыменования (dereference). Мы обсудим их позже.


Перед тем, как двигаться дальше...


  • Объединение/соединение (concatenation) строк:

let (s1, s2) = ("some", "thing"); // обе переменные имеют тип `&str`

// Типами остальных переменных является `String`
let s = String::from(s1) + s2; // String + &str

let mut s = String::from(s1); // String
s.push_str(s2); // + &str

let s = format!("{}{}", s1, s2); // &str/String + &str/String

let s = [s1, s2].concat(); // `&str` или массив `String`

Потоки управления


if else


  • if

let age = 13;

if age < 18 {
    println!("Hello, child!"); // код печатает это
}

  • if и else

let i = 7;

if i % 2 == 0 {
    println!("Четное");
} else {
    println!("Нечетное"); // код печатает это
}

  • let

let age: u8 = 13;
let is_below_eighteen = if age < 18 { true } else { false }; // true

  • еще примеры

// Простой пример
let team_size = 7;

if team_size < 5 {
    println!("Small");
} else if team_size < 10 {
    println!("Medium"); // код печатает это
} else {
    println!("Large");
}

// Отрефакторим предыдущий пример
let team_size = 7;
let team_size_in_text;

if team_size < 5 {
    team_size_in_text = "Small";
} else if team_size < 10 {
    team_size_in_text = "Medium";
} else {
    team_size_in_text = "Large";
}

println!("Current team size : {}", team_size_in_text); // Current team size : Medium

// Еще раз отрефакторим
let team_size = 7;
let team_size = if team_size < 5 {
    "Small" // ⭐️ no ;
} else if team_size < 10 {
    "Medium"
} else {
    "Large"
};

println!("Current team size : {}", team_size); // Current team size : Medium

В последнем примере тип возвращаемых значений должен быть одинаковым.


match


Ключевое слово match позволяет выполнять поиск совпадения (сопоставление с образцом) (pattern matching):


let tshirt_width = 20;
let tshirt_size = match tshirt_width {
    16 => "S", // проверяет 16
    17 | 18 => "M", // проверяет 17 и 18
    19 ..= 21 => "L", // проверяет от 19 до 21 (включительно)
    22 => "XL",
    _ => "Not Available",
};

println!("{}", tshirt_size); // L

let is_allowed = false;
let list_type = match is_allowed {
    true => "Full",
    false => "Restricted"
    // Дефолтное/_ условие может быть пропущено,
    // поскольку `is_allowed` имеет логическое значение и
    // все возможности исчерпаны
};

println!("{}", list_type); // Restricted

let marks_paper_a: u8 = 25;
let marks_paper_b: u8 = 30;

let output = match (marks_paper_a, marks_paper_b) {
    (50, 50) => "Full marks for both papers",
    (50, _) => "Full marks for paper A",
    (_, 50) => "Full marks for paper B",
    (x, y) if x > 25 && y > 25 => "Good",
    (_, _) => "Work hard"
};

println!("{}", output); // Work hard

loop


Ключевое слово loop позволяет объявлять бесконечные циклы (похоже на while true):


loop {
    println!("Loop forever!");
}

// Для управления циклом используются ключевые слова `break` и `continue`
let mut a = 0;

loop {
    if a == 0 {
        println!("Skip Value : {}", a);
        a += 1;
        // Пропускаем итерацию
        continue;
    } else if a == 2 {
        println!("Break At : {}", a);
        // Прерываем цикл
        break;
    }

    println!("Current Value : {}", a);
    a += 1;
}

// Для именования циклов используются метки (labels)
let mut b1 = 1;

'outer_loop: loop { // устанавливаем метку `outer_loop`
  let mut b2 = 1;

  'inner_loop: loop {
    println!("Current Value : [{}][{}]", b1, b2);

    if b1 == 2 && b2 == 2 {
        break 'outer_loop; // прерываем `outer_loop`
    } else if b2 == 5 {
        break;
    }

    b2 += 1;
  }

  b1 += 1;
}

while


let mut a = 1;

while a <= 10 {
    println!("Current value : {}", a);
    a += 1; // в Rust отсутствуют операторы `++` и `--`
}

// `break` и `continue`
let mut b = 0;

while b < 5 {
    if b == 0 {
        println!("Skip value : {}", b);
        b += 1;
        continue;
    } else if b == 2 {
        println!("Break At : {}", b);
        break;
    }

    println!("Current value : {}", b);
    b += 1;
}

// Метки
let mut c1 = 1;

'outer_while: while c1 < 6 { // устанавливаем метку `outer_while`
    let mut c2 = 1;

    'inner_while: while c2 < 6 {
        println!("Current Value : [{}][{}]", c1, c2);
        if c1 == 2 && c2 == 2 { break 'outer_while; } // прерываем `outer_while`
        c2 += 1;
    }

    c1 += 1;
}

for


// от 0 до 10 (не включая); В JavaScript это выглядит как `for (let i = 0; i < 10; i++)`
for i in 0..10 {
  println!("Current value : {}", i);
}

// от 1 до 10 (включительно); В JavaScript это выглядит как `for (let i = 1; i <= 10; i++)`
for i in 1..=10 {
  println!("Current value : {}", i);
}

// `break` и `continue`
for b in 0..6 {
  if b == 0 {
    println!("Skip Value : {}", b);
    continue;
  } else if b == 2 {
    println!("Break At : {}", b);
    break;
  }

  println!("Current value : {}", b);
}

// Метки
'outer_for: for c1 in 1..6 { // устанавливаем метку `outer_for`

  'inner_for: for c2 in 1..6 {
    println!("Current Value : [{}][{}]", c1, c2);
    if c1 == 2 && c2 == 2 { break 'outer_for; } // прерываем `outer_for`
  }

}

// Работа с массивами/векторами
let group : [&str; 4] = ["Mark", "Larry", "Bill", "Steve"];

for n in 0..group.len() { // group.len() = 4 -> 0..4, вообще проверять `group.len()` на каждой итерации - плохая практика
  println!("Current Person : {}", group[n]);
}

for person in group.iter() { // `group.iter()` превращает массив в простой итератор
  println!("Current Person : {}", person);
}

Больше, чем основы


Векторы (vectors)


Как вы помните, массив — это список элементов одинакового типа фиксированного размера. Даже при использовании ключевого слова mut, количество элементов массива не может быть изменено. Вектор — это своего рода динамический массив, но все его элементы также должны быть одинакового типа.


Вектор имеет общий (generic) тип Vec<T>. T — любой тип, например, типом вектора 32-битных целых чисел будет Vec<i32>. Данные вектора хранятся в динамически выделяемой куче.


Создание пустого вектора


let mut a = Vec::new(); // с помощью статического метода `new()`
let mut b = vec![]; // с помощью макроса `vec!`

Создание вектора с типом данных


let mut a2: Vec<i32> = Vec::new();
let mut b2: Vec<i32> = vec![];
let mut b3 = vec![1i32, 2, 3]; // с помощью суффикса первого элемента

let mut b4 = vec![1, 2, 3];
let mut b5: Vec<i32> = vec![1, 2, 3];
let mut b6  = vec![1i32, 2, 3];
let mut b7 = vec![0; 10]; // десять нулей

Доступ и изменение данных


// Доступ и изменение существующих данных
let mut c = vec![5, 4, 3, 2, 1];
c[0] = 1;
c[1] = 2;
//c[6] = 2; ошибка - индекс за пределами допустимого диапазона (0..6)
println!("{:?}", c); //[1, 2, 3, 2, 1]

// `push` и `pop`
let mut d: Vec<i32> = Vec::new();
d.push(1); // [1] : добавляем элемент в конец
d.push(2); // [1, 2]
d.pop(); // [1] : удаляем последний элемент

// Емкость (capacity) и повторное выделение памяти (reallocation)
let mut e: Vec<i32> = Vec::with_capacity(10);
println!("Length: {}, Capacity : {}", e.len(), e.capacity()); // Length: 0, Capacity : 10

// Для этого не требуется повторное выделение памяти
for i in 0..10 {
    e.push(i);
}
// А для этого требуется
e.push(11);

По сути, вектор представляет 3 вещи:


  • указатель на данные (pointer)
  • количество элементов (длина — length)
  • емкость (capacity) — пространство, доступное будущим элементам

Если длина начинает превышать емкость, емкость увеличивается. Но это приводит к повторному выделению памяти для элементов (что может быть медленным).


Тип данных String — это вектор байт в кодировке UTF-8 ([u8]). Однако символы строки не доступны по индексу из-за особенностей кодировки.


Векторы могут быть использованы совместно с итераторами 3 способами:


let mut v = vec![1, 2, 3, 4, 5];

for i in &v {
    println!("Ссылка на {}", i);
}

for i in &mut v {
    println!("Мутабельная ссылка на {}", i);
}

for i in v {
    println!("Забирает владение вектором и его элементом {}", i);
}

Структуры (structs)


Структуры используются для инкапсуляции связанных свойств в один унифицированный тип данных.


По соглашению структуры именуются в стиле PascalCase.


Существует 3 варианта структур:


  1. C-подобные (C-like) структуры:
    • разделенные запятыми пары ключ: значение
    • список в фигурных скобках
    • похожи на классы (без методов) в ООП-языках
    • поскольку поля имеют названия, они доступны через точечную нотацию
  2. Кортежные (tuple) структуры
    • разделенные запятыми значения
    • круглые скобки, как у кортежей
    • похожи на именованные кортежи
  3. Пустые (unit) структуры
    • не имеют членов
    • определяют новый тип, похожий на пустой кортеж (())
    • редко используются, полезны в дженериках (generics)

Говоря об ООП в Rust, следует отметить, что атрибуты и методы объектов размещаются отдельно в структурах и трейтах (traits). Структуры содержат только атрибуты, трейты — только методы. Они объединяются с помощью impl (реализаций). Мы обсудим это позже.


C-подобные структуры


// Объявление структуры
struct Color {
    red: u8,
    green: u8,
    blue: u8
}

fn main() {
  // Создание экземпляра
  let black = Color { red: 0, green: 0, blue: 0 };

  // Доступ к полям через точки
  println!("Black = rgb({}, {}, {})", black.red, black.green, black.blue); // Black = rgb(0, 0, 0)

  // Структуры являются иммутабельными по умолчанию,
  // ключевое слово `mut` позволяет сделать их мутабельными.
  // Это не относится к количеству или типу полей
  let mut link_color = Color { red: 0, green: 0, blue: 255 };
  link_color.blue = 238;
  println!("Link Color = rgb({}, {}, {})", link_color.red, link_color.green, link_color.blue); //Link Color = rgb(0, 0, 238)

  // Копируем элементы из другого экземпляра
  let blue = Color { blue: 255, ..link_color };
  println!("Blue = rgb({}, {}, {})", blue.red, blue.green, blue.blue); // Blue = rgb(0, 0, 255)

  // Деструктурируем экземпляр с помощью привязки `let`.
  // Это не уничтожает `blue`
  let Color { red: r, green: g, blue: b } = blue;
  println!("Blue = rgb({}, {}, {})", r, g, b); // Blue = rgb(0, 0, 255)

  // Создаем экземпляр с помощью функции
  let midnightblue = get_midnightblue_color();
  println!("Midnight Blue = rgb({}, {}, {})", midnightblue.red, midnightblue.green, midnightblue.blue); // Midnight Blue = rgb(25, 25, 112)

  // Деструктурируем экземпляр с помощью привязки `let`.
  let Color { red: r, green: g, blue: b } = get_midnightblue_color();
  println!("Midnight Blue = rgb({}, {}, {})", r, g, b); // Midnight Blue = rgb(25, 25, 112)
}

fn get_midnightblue_color() -> Color {
    Color {red: 25, green: 25, blue: 112}
}

Кортежные структуры


Когда кортежная структура содержит только один элемент, мы называем ее паттерном newtype (newtype pattern), потому что такая структура помогает создавать новые типы.


struct Color(u8, u8, u8);
struct Kilometers(i32);

fn main() {
  // Создаем новый экземпляр
  let black = Color(0, 0, 0);

  // Деструктурируем экземпляр с помощью привязки `let`.
  // Это не разрушает `black`
  let Color(r, g, b) = black;
  println!("Black = rgb({}, {}, {})", r, g, b); // Black = rgb(0, 0, 0);

  // Newtype pattern
  let distance = Kilometers(20);
  // Деструктурируем экземпляр с помощью привязки `let`.
  let Kilometers(distance_in_km) = distance;
  println!("The distance: {} km", distance_in_km); // The distance: 20 km
}

Пустые структуры


Сами по себе они используются редко, но могут быть полезными в сочетании с другими вещам, например, используемая в проекте библиотека может требовать создания структуры, реализующий определенный трейт для обработки событий. Если в структуре не нужно хранить данные, для этой цели вполне подойдет пустая структура:


struct Electron;

fn main() {
  let x = Electron;
}

Перечисления (enums)


Перечисление — это единичный тип. Оно содержит варианты (variants), т.е. возможные значения перечисления, например:


enum Day {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday
}

// `Day` - это перечисление
// Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday - варианты

Варианты доступны через нотацию ::, например, Day::Sunday.


Каждый вариант может:


  • быть пустым (не содержать данных)
  • содержать упорядоченные безымянные данные (кортежный вариант)
  • содержать именованные данные (структурный вариант)

enum FlashMessage {
  Success, // пустой (unit) вариант
  Warning{ category: i32, message: String }, // структурный вариант
  Error(String) // кортежный вариант
}

fn main() {
  let mut form_status = FlashMessage::Success;
  print_flash_message(form_status);

  form_status = FlashMessage::Warning { category: 2, message: String::from("Поле X является обязательным") };
  print_flash_message(form_status);

  form_status = FlashMessage::Error(String::from("Ошибка подключения"));
  print_flash_message(form_status);
}

fn print_flash_message(m : FlashMessage) {
  // Сопоставление с образцом (pattern matching)
  match m {
    FlashMessage::Success =>
      println!("Форма успешно отправлена"),
    FlashMessage::Warning { category, message } => // деструктуризация, названия полей должны совпадать
      println!("Предупреждение: {} - {}", category, message),
    FlashMessage::Error(msg) =>
      println!("Ошибка: {}", msg)
  }
}

Дженерики (generics)


Иногда при написании функции или типа данных, нам хочется, чтобы они работали для нескольких типов аргументов. В Rust это делается с помощью дженериков.


Суть заключается в том, что вместо объявления конкретного типа данных, мы используем прописную букву (идентификатор в стиле PascalCase), например, вместо x: u8 мы пишем x: T. Однако нам нужно сообщить компилятору о том, что T — это общий тип (может быть любым типом), поэтому мы добавляем <T> после названия функции, например.


Генерализация функций


fn takes_anything<T>(x: T) { // `x` имеет тип `T`, `T` - это общий тип
    // ...
}

fn takes_two_of_the_same_things<T>(x: T, y: T) { // `x` и `y` имеют одинаковый тип
    // ...
}

fn takes_two_things<T, U>(x: T, y: U) { // `x` и `y` имеют разные типы
    // ...
}

Генерализация структур


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

fn main() {
  let point_a = Point { x: 0, y: 0 }; // `T` становится `i32`
  let point_b = Point { x: 0.0, y: 0.0 }; // `T` становится `f64`
}

// При добавлении реализации для общей структуры параметр типа также должен быть добавлен после ключевого слова `impl`
//   impl<T> Point<T> {

Генерализация перечислений


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

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

Option и Result — это специальные общие типы, определенные в стандартной библиотеке Rust:


  • опциональное значение (Option) может быть некоторым значением (Some) или отсутствовать (None)
  • результат (Result) может быть успехом (Ok) или ошибкой (Err)

Примеры использования Option


// ---
fn get_id_by_username(username: &str) -> Option<usize> {
    // Если имя пользователя знакомо системе, возвращаем `userId`
        return Some(userId);
    // иначе
        None
}

// ---
struct Task {
    title: String,
    assignee: Option<Person>,
}

// Вместо `Person`, мы используем `Option<Person>`,
// поскольку задача может не быть никому назначена

// ---
// При использовании `Option` в качестве типа возвращаемого функцией значения,
// мы можем использовать `match` для перехвата соответствующего значения
fn main() {
    let username = "anonymous";
    match get_id_by_username(username) {
        None => println!("Пользователь не найден"),
        Some(i) => println!("Идентификатор пользователя: {}", i)
    }
}

Примеры использования Result


Тип Option — это способ, которым система типов Rust выражает возможность отсутствия значения. Тип Result — это способ, которым система типов Rust выражает возможность ошибки.


// ---
fn get_word_count_from_file(file_name: &str) -> Result<u32, &str> {
  // Если файл не найден в файловой системе, возвращаем ошибку
    return Err("Файл не найден")
  // иначе, считаем и возвращаем количество слов
    // let mut word_count: u32 = ...;
    Ok(word_count)
}

// ---
// Здесь мы также можем использовать `match`
fn main() {
    let mut file_name = "file_a";
    match get_word_count_from_file(file_name) {
        Ok(i) => println!("Количество слов: {}", i),
        Err(e) => println!("Ошибка: {}", e)
    }
}

Для Option и Result реализовано большое количество полезных методов (полистайте официальную документацию).


Реализации и трейты (impls & traits)


Реализации (impls) используются для определения методов структур и перечислений.


Трейты (traits) похожи на интерфейсы в ООП-языках. Они используются для определения функциональности, которую должен предоставлять тип. Для одного типа может быть реализовано несколько трейтов.


Трейты также могут содержать дефолтные реализации методов. Дефолтные реализации могут перезаписываться при реализации типов.


Реализации без трейтов


struct Player {
    first_name: String,
    last_name: String,
}

impl Player {
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

fn main() {
    let player_1 = Player {
        first_name: "Rafael".to_string(),
        last_name: "Nadal".to_string(),
    };

    println!("Player 01: {}", player_1.full_name());
}

// Тип и его реализация должны находиться в одном крейте

// Новые трейты могут реализовываться для существующих типов,
// даже для таких типов, как i8, f64 и др.
// Аналогичным образом существующие трейты могут реализовываться для новых типов.
// Но реализовывать существующие трейты для существующих типов нельзя.

Реализации с трейтами, но без дефолтных методов


struct Player {
    first_name: String,
    last_name: String,
}

trait FullName {
    fn full_name(&self) -> String;
}

impl FullName for Player {
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

fn main() {
    let player_2 = Player {
        first_name: "Roger".to_string(),
        last_name: "Federer".to_string(),
    };

    println!("Player 02: {}", player_2.full_name());
}

// В отличие от функций, трейты могут содержать константы и типы

Реализации с трейтами и дефолтными методами


trait Foo {
    fn bar(&self);
    fn baz(&self) { println!("We called baz."); }
}

Как видите, методы принимают первый специальный параметр — сам тип. Он может быть self (значение в стеке — владение), &self (ссылка на значение) или &mut self (мутабельная ссылка).


Реализации с ассоциированными функциями


Некоторые языки поддерживают статические методы. Такие методы вызываются на самом классе, а не на его экземпляре. В Rust такие методы называются ассоциированными функциями (associated functions). При их вызове на структуре используется :: вместо ., например, Person::new("Elon Musk Jr");:


struct Player {
    first_name: String,
    last_name: String,
}

impl Player {
    // Ассоциированная функция - метод структуры/статический метод
    fn new(first_name: String, last_name: String) -> Player {
        Player {
            first_name: first_name,
            last_name: last_name,
        }
    }

    // Метод экземпляра
    fn full_name(&self) -> String {
        format!("{} {}", self.first_name, self.last_name)
    }
}

fn main() {
    let player_name = Player::new("Serena".to_string(), "Williams".to_string()).full_name();
    println!("Player: {}", player_name);
}

// Мы используем нотацию `::` для `new()` и нотацию `.` для `full_name()`

// Вместо использования `new()` и `full_name()` по-отдельности,
// мы можем использовать цепочку методов, например, `player.add_points(2).get_point_count();`

Трейты с дженериками


trait From<T> {
    fn from(T) -> Self;
}
impl From<u8> for u16 {
    //...
}
impl From<u8> for u32{
    //...
}

Наследование трейтов


trait Person {
    fn full_name(&self) -> String;
}

trait Employee: Person { // `Employee` наследует от `Person`
    fn job_title(&self) -> String;
}

trait ExpatEmployee: Employee + Expat { // `ExpatEmployee` наследует от `Employee` и `Expat`
    fn additional_tax(&self) -> f64;
}

Трейт-объекты


Хотя Rust предпочитает статическую отправку (static dispatch), он также поддерживает динамическую отправку (dynamic dispatch) через механизм под названием "трейт-объекты" (trait objects).


Динамическая отправка — это процесс выбора реализации полиморфной операции (метода или функции) для вызова во время выполнения (runtime).


trait GetSound {
    fn get_sound(&self) -> String;
}

struct Cat {
    sound: String,
}
impl GetSound for Cat {
    fn get_sound(&self) -> String {
        self.sound.clone()
    }
}

struct Bell {
    sound: String,
}
impl GetSound for Bell {
    fn get_sound(&self) -> String {
        self.sound.clone()
    }
}

fn make_sound<T: GetSound>(t: &T) {
    println!("{}!", t.get_sound())
}

fn main() {
    let kitty = Cat { sound: "Meow".to_string() };
    let the_bell = Bell { sound: "Ding Dong".to_string() };

    make_sound(&kitty); // Meow!
    make_sound(&the_bell); // Ding Dong!
}

Сложная часть


Владение (ownership)


fn main() {
    let a = [1, 2, 3];
    let b = a;
    println!("{:?} {:?}", a, b); // [1, 2, 3] [1, 2, 3]
}

fn main() {
    let a = vec![1, 2, 3];
    let b = a;
    println!("{:?} {:?}", a, b); // ошибка; использование перемещенного (moved) значения: `a`
}

В этих примерах мы пытаемся присвоить b значение a. В обоих блоках код почти одинаковый, разница лишь в типах данных. Во втором случае возникает ошибка. Это связано с владением (ownership).


Что такое владение?


Привязки переменных владеют тем, к чему они привязаны. Данные могут одновременно иметь только одного владельца. Когда привязка выходит за пределы области видимости (scope), Rust освобождает связанные ресурсы. Так Rust обеспечивает безопасность работы с памятью.


Копируемые и перемещаемые типы


При присвоении переменной другой переменной или при передаче переменной функции (без ссылки), если тип данных является


  1. Копируемым (copy type)
    • данные копируются и присваиваются или передаются
    • состояние владения исходных данных устанавливается в состояние "скопировано"
    • характерно в основном для примитивных типов
  2. Перемещаемым (move type)
    • данные перемещаются в новую привязку и становятся недоступными через оригинальную привязку
    • состояние владения исходных данных устанавливается в состояние "перемещено"
    • характерно для непримитивных типов

Поведение типа определяется реализованными на нем трейтами. По умолчанию привязки переменных имеют "семантику перемещения" (move semantics). Однако если для типа реализован трейт core::marker::Copy, он имеет "семантику копирования".


Таким образом, во втором примере объект вектора перемещается в b, и a лишается владения для доступа к нему.


Заимствование (borrowing)


В реальных приложениях мы чаще всего передаем переменные функциям или присваиваем их другим переменным. В этих случаях мы ссылаемся (referencing) на оригинальные привязки, заимствуем (borrow) их данные.


Общее и мутабельное заимствование


Существует 2 типа заимствования:


  1. Общее/распределенное (shared) заимствование (&T)
    • данные могут быть заимствованы одним или несколькими пользователями, но не должны модифицироваться, т.е. доступны только для чтения
  2. Мутабельное заимствование (&mut T)
    • данные могут заимствоваться и модифицироваться одновременно только одним пользователем

Правила заимствования


  1. Данные могут быть заимствованы только как общее заимствование или как мутабельное заимствование, но не как то и другое одновременно.
  2. Заимствование применяется как к копируемым, так и к перемещаемым типам.
  3. Необходимо соблюдать правила времен жизни (lifetimes). Мы обсудим это позже.

fn main() {
  let mut a = vec![1, 2, 3];
  let b = &mut a;  //  &mut заимствование `a` начинается здесь
                   //  v
  // ...           //  v
  // ...           //  v
}                  //  &mut заимствование `a` заканчивается здесь

fn main() {
  let mut a = vec![1, 2, 3];
  let b = &mut a;  //  &mut заимствование `a` начинается здесь
  // ...

  println!("{:?}", a); // пытаемся получить доступ к `a` как к общему заимствованию, получаем ошибку
}                  //  &mut заимствование `a` заканчивается здесь

fn main() {
  let mut a = vec![1, 2, 3];
  {
    let b = &mut a;  //  &mut заимствование `a` начинается здесь
    // ...
  }                  //  &mut заимствование `a` заканчивается здесь

  println!("{:?}", a); // `a` доступна как общее заимствование
}

Примеры общего заимствования


fn main() {
    let a = [1, 2, 3];
    let b = &a;
    println!("{:?} {}", a, b[0]); // [1, 2, 3] 1
}

fn main() {
    let a = vec![1, 2, 3];
    let b = get_first_element(&a);

    println!("{:?} {}", a, b); // [1, 2, 3] 1
}

fn get_first_element(a: &Vec<i32>) -> i32 {
    a[0]
}

Примеры мутабельного заимствования


fn main() {
    let mut a = [1, 2, 3];
    let b = &mut a;
    b[0] = 4;
    println!("{:?}", b); // [4, 2, 3]
}

fn main() {
    let mut a = [1, 2, 3];
    {
        let b = &mut a;
        b[0] = 4;
    }

    println!("{:?}", a); // [4, 2, 3]
}

fn main() {
    let mut a = vec![1, 2, 3];
    let b = change_and_get_first_element(&mut a);

    println!("{:?} {}", a, b); // [4, 2, 3] 4
}

fn change_and_get_first_element(a: &mut Vec<i32>) -> i32 {
    a[0] = 4;
    a[0]
}

Времена жизни (lifetimes)


При работе с ссылками, мы должны убедиться, что ссылочные данные (данные, на которые указывают ссылки) остаются "живыми" до тех пор, пока мы используем ссылки.


Предположим, что у нас есть привязка переменной a. Мы ссылаемся на значение a из другой привязки переменной x. Мы должны обеспечить, чтобы a жила, пока мы не закончим использовать x.


Управление памятью — это управление ресурсами применительно к памяти компьютера. До середины 1990-х в большинстве языков программирования использовалось ручное управление памятью (manual memory management, MMM), которое требовало от программиста предоставления явных инструкций по определению и освобождению/удалению (deallocate) неиспользуемых объектов/мусора (garbage). В 1959 John McCarthy изобрел сборку мусора (garbage collection, GC) — разновидность автоматического управления памятью (automatic memory management, AMM). Сборщик мусора автоматически (без участия программиста) определяет неиспользуемую память и освобождает ее. Похожий функционал предоставляет автоматический подсчет ссылок (automatic reference counting, ARC), используемый в Objective-C и Swift.


Что такое время жизни?


В Rust


  • у ресурса может быть только один владелец в одно время. При выходе за пределы области видимости, Rust удаляет его
  • когда мы хотим повторно использовать ресурс, мы ссылаемся на него, т.е. заимствуем его содержимое
  • при работе с ссылками, мы должны указать аннотации времени жизни (lifetime annotations) для предоставления компилятору инструкций о том, как долго ссылочные ресурсы должны жить
  • поскольку аннотации времени жизни делают код более многословным, для того, чтобы сделать некоторые общие паттерны более эргономичными, Rust позволяет исключать (elided)/опускать эти аннотации в определениях fn. В этом случае компилятор присваивает времена жизни неявно

Времена жизни проверяются во время компиляции. Компилятор проверяет, когда данные используются в первый и последний раз. Согласно этому Rust управляет памятью во время выполнения. Это одна из основных причин медленной компиляции кода Rust.


  • В отличие от C и C++, Rust, как правило, не уничтожает (drop) значения явно
  • в отличие от GC, Rust не выполняет вызовы освобождения (deallocation calls), когда данные больше не используются
  • Rust выполняет вызовы освобождения, когда данные вот-вот выйдут за пределы области видимости, а затем обеспечивает отсутствие ссылок на этот ресурс

Использование


Времена жизни указываются с помощью апострофа ('). По соглашению для их именования используются строчные буквы. Обычно мы начинаем с 'a и идем по алфавиту, когда требуется несколько времен жизни.


При использовании ссылок:


1. В объявлениях функций


  • времена жизни ссылочных входных и выходных параметров указываются после &, например: (x: &'a mut str), -> &'a str
  • времена жизни указываются после названия функции как общие типы, например: fn foo<'a>(){}, fn bar<'a, 'b>(){}

// Без параметров, возвращается ссылка
fn function<'a>() -> &'a str {}

// Один параметр
fn function<'a>(x: &'a str) {}

// Один параметр и возвращаемое значение, оба имеют одинаковое время жизни.
// Результат должен жить, как минимум, также долго, как параметр
fn function<'a>(x: &'a str) -> &'a str {}

// Несколько параметров, только один параметр и результат имеют общее время жизни.
// Результат должен жить, как минимум, также долго, как параметр `y`
fn function<'a>(x: i32, y: &'a str) -> &'a str {}

// Несколько параметров и результат с общим временем жизни
// Результат должен жить, как минимум, также долго, как оба параметра
fn function<'a>(x: &'a str, y: &'a str) -> &'a str {}

// Несколько параметров с разными временами жизни
// Результат должен жить, как минимум, также долго, как параметр `x`
fn function<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {}

2. В объявлениях структур или перечислений


  • времена жизни ссылочных элементов указываются после &
  • времена жизни указываются как общие типы после названий структур/перечислений

// Один элемент
// Данные `x` должны жить, как минимум, также долго, как структура
struct Struct<'a> {
    x: &'a str
}

// Несколько элементов
// Данные `x` и `y` должны жить, как минимум, также долго, как структура
struct Struct<'a> {
    x: &'a str,
    y: &'a str
}

// Перечисление с одним элементом/вариантом
// Данные варианта должны жить, как минимум, также долго, как перечисление
enum Enum<'a> {
    Variant(&'a Type)
}

3. В реализациях и трейтах


struct Struct<'a> {
    x: &'a str
}
// Да, целых 3 общих типа
impl<'a> Struct<'a> {
    fn function<'a>(&self) -> &'a str {
        self.x
    }
}

struct Struct<'a> {
    x: &'a str,
    y: &'a str
}
impl<'a> Struct<'a> {
    fn new(x: &'a str, y: &'a str) -> Struct<'a> { // `<'a>` после `new` можно опустить, поскольку оно есть у `impl`
        Struct {
            x : x,
            y : y
        }
    }
}

// Одно и тоже
impl<'a> Trait<'a> for Type
impl<'a> Trait for Type<'a>

Неявное выведение времени жизни


Неявное выведение времени жизни (lifetime elision) — это процесс автоматического определения времен жизни общих паттернов компилятором.


В настоящее время неявное выведение времени жизни поддерживается только для fn. В будущем оно также будет поддерживаться для impl.


Времена жизни определений fn могут быть выведены неявно, если


  • только один параметр из списка передается по ссылке
  • параметром является &self или &mut self

fn triple(x: &u64) -> u64 { // только один параметр передается по ссылке
    x * 3
}

fn filter(x: u8, y: &str) -> &str { // только один параметр передается по ссылке
    if x > 5 { y } else { "invalid inputs" }
}

struct Player<'a> {
    id: u8,
    name: &'a str
}
    impl<'a> Player<'a> { // неявное выведение времени жизни для `impl` пока не поддерживается
        fn new(id: u8, name: &str) -> Player { // только один параметр передается по ссылке
            Player {
                id : id,
                name : name
            }
        }

        fn heading_text(&self) -> String { // параметром является `&self` (или `&mut self`)
            format!("{}: {}", self.id, self.name)
        }
    }

fn main() {
    let player1 = Player::new(1, "Serena Williams");
    let player1_heading_text = player1.heading_text()
    println!("{}", player1_heading_text);
}

В процессе неявного выведения времени жизни:


  • каждый аргумент, передаваемый по ссылке, получает отдельное время жизни: (x: &str, y: &str) -> <'a, 'b>(x: &'a str, y: &'b str)
  • если список параметров содержит только один параметр, передаваемый по ссылке, его время жизни присваивается возвращаемому функцией значению: (x: i32, y: &str) -> &str -> <'a>(x: i32, y: &'a str) -> &'a str
  • даже если по ссылке передается несколько аргументов, но одним из аргументов является &self или &mut self, время жизни этого аргумента становится временем жизни возвращаемого методом значения: impl Impl { fn function(&self, x: &str) -> &str {} } -> impl<'a> Impl<'a> { fn function(&'a self, x: &'b str) -> &'a str {} }
  • во всех остальных случаях время жизни должно указываться явно

Аннотации 'static


Аннотация времени жизни 'static является зарезервированной. Такие ссылки являются валидными на протяжении всей работы программы. Они сохраняются в сегменте данных исполняемого файла и имеют глобальную область видимости (не могут выйти за пределы области видимости).


 // Константа с временем жизни `'static`

static N: i32 = 5;

let a = "Hello, world."; // a: &'static str

fn index() -> &'static str { // не нужно указывать `<'static>` после названия функции
    "Hello, world!"
}

Еще несколько примеров


fn greeting<'a>() -> &'a str {
  "Hi!"
}

fn fullname<'a>(fname: &'a str, lname: &'a str) -> String {
  format!("{} {}", fname, lname)
}

struct Person<'a> {
    fname: &'a str,
    lname: &'a str
}
impl<'a> Person<'a> {
    fn new(fname: &'a str, lname: &'a str) -> Person<'a> { // `<'a>` после `new` можно опустить, поскольку оно есть у `impl`
        Person {
            fname : fname,
            lname : lname
        }
    }

    fn fullname(&self) -> String {
        format!("{} {}", self.fname , self.lname)
    }
}

fn main() {
    let player = Person::new("Serena", "Williams");
    let player_fullname = player.fullname();

    println!("Player: {}", player_fullname);
}

Это конец первой части шпаргалки.


Happy coding!




Комментарии (5)


  1. Lewigh
    16.01.2024 09:35
    +6

    После таких откровений от автора оригинала:

    Замыкания (closures)
    Также известны как анонимные или лямбда-функции

    Массивы по умолчанию являются иммутабельными. Даже при использовании ключевого слова mut количество элементов массива не может быть изменено.

    Кортежи по умолчанию также являются иммутабельными. Даже при использовании ключевого слова mut количество элементов кортежа не может быть изменено.

    Срез/фрагмент (slice)
    Ссылка (reference) динамического размера на другую структуру данных.

    str
    Безразмерная (unsized) последовательность UTF-8 фрагментов строк Юникода.

    Структуры используются для инкапсуляции связанных свойств в один унифицированный тип данных.

    забавно было читать. Оказывается анонимные функции и замыкания это одно и тоже а неизменяемость данные это невозможность изменить размер массива или кортежа.

    Куча ошибок и неточностей.

    Тратить время на прочтение не советую. Лучше уж официальную документацию.


    1. domix32
      16.01.2024 09:35
      +2

      Особенно учитывая размер статьи лучше сразу The Rust Book открывать, да. Там и про базовые вещи и про идиоматические паттерны. Уж лучшем б её локализовали для тех кто не spiki inglish


      1. ironcat
        16.01.2024 09:35
        +4

        Оно есть же по русски https://doc.rust-lang.ru/book/


      1. brsphhkg
        16.01.2024 09:35
        +1

        на странице с доступными переводами книги есть и русский
        https://doc.rust-lang.org/book/appendix-06-translation.html


  1. kekoz
    16.01.2024 09:35

    Если бы в те далёкие времена, когда я изучал C, существовало GNU, а изложение в книге K&R “C Programming Language” по образу этой статьи перемежалось бы выдержками из “GNU coding style”, то C я бы и до сих пор не знал. Ну, или изучал бы по другим учебникам.