Основы
Комментарии и документирование кода
Комментарии
// Строчные комментарии
/* Блочные комментарии */
Поддерживаются вложенные блочные комментарии.
Старайтесь избегать использования блочных комментариев.
Комментарии документации (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