1. Let
2. Функции
3. Типы данных
4. Операторы цикла: loop, while, for
5. Вектора (vec!)
6. Изменяемость (mut)
7. Методы (impl)
8. Строки
9. Дженерики
10. Типажи (traits)
11. Анонимные функции(closures)
12. UFCS
13. Const и static
Для инициализации переменных в расте используется команда let
let x = 5;
Можно проинициализировать сразу несколько переменных:
let (a,b,c) = (1,2,3);
При инициализации переменной ее тип можно указать явно:
let i: i32 = 5;
По умолчанию команда let создает константу, и следующий код работать не будет :
let x = 5;
x = 10;
Для этого нужно использовать модификатор mut
let mut x = 5;
x = 10;
Переменные в расте имеют область видимости:
let x:i32 = 17;
{
let y: i32 = 3;
println!("Значение x равно {} и значение y равно {}", x, y);
}
// Ошибка компиляции
println!("Значение x равно {} и значение y равно {}", x, y);
Функции в расте определяются с помощью ключевого слова fn
Функция в раст может вернуть одно значение - обратите внимание, что в данном случае точка с запятой не нужна
fn add_one(x: i32) -> i32 {
x + 1
}
Из функции значение можно вернуть досрочно с помощью ключевого слова return
fn add_one(x: i32) -> i32 {
return x;
x + 1
}
Если нам нужна функция, которая закончит выполнение всей программы
fn diverges() -> ! {
panic!("Эта функция не возвращает управление!");
}
Для получения подробной отладочной информации можно использовать переменную среды
RUST_BACKTRACE=1 cargo run
В расте можно создать указатель на функцию
fn plus_one(i: i32) -> i32 {
i + 1
}
let f = plus_one;
let six = f(5);
Раст включает в себя стандартные встроенные типы. Логический тип bool
let x = true;
let y: bool = false;
Тип char представляет одиночные юникодные символы размером в 4 байта
let x = 'x';
Rust имеет целый ряд числовых типов, разделённых на несколько категорий: знаковые и
беззнаковые, фиксированного и переменного размера, числа с плавающей точкой и целые
числа. Список числовых типов
i8
i16
i32
i64
u8
u16
u32
u64
isize
usize
f32
f64
Массив в расте - это последовательность элементов одного и того же типа, имеющая
фиксированный размер. Массивы неизменяемы по умолчанию.
let a = [1, 2, 3]; // a: [i32; 3]
let mut m = [1, 2, 3]; // m: [i32; 3]
Для инициализации всех элементов массива одним и тем же значением есть специальный
синтаксис. В следующем примере каждый элемент a будет инициализирован значением 0 :
let a = [0; 20];
Число элементов массива можно получить с помощью метода
a.len()
Можно получить элемент массива с помощью индекса, при этом индекс начинается с нуля
println!("Второе имя: {}", names[1]);
Срез в расте - это традиционная выборка, как и в других языках, использует квадратные скобки и ссылку на массив
let a = [0, 1, 2, 3, 4];
let complete = &a[..]; // Срез, содержащий все элементы массива `a`
let middle = &a[1..4]; // Срез a : только элементы 1, 2, и 3
Строковый тип в расте неограничен в размере.
Кортеж — это последовательность фиксированного размера
let x = (1, "привет");
Можно присваивать один кортеж другому, если они содержат значения одинаковых типов
let mut x = (1, 2);
let y = (2, 3);
x = y;
Доступ к полям кортежа можно получить с помощью индексации
let tuple = (1, 2, 3);
let x = tuple.0;
let y = tuple.1;
let z = tuple.2;
В Rust есть два вида комментариев: строчные комментарии и doc-комментарии
// строчные комментарии
/// doc-комментарии
Условный оператор if традиционен
let x = 5;
if x == 5 {
println!("x равняется пяти!");
} else if x == 6 {
println!("x это шесть!");
} else {
println!("x это ни пять, ни шесть :(");
}
В расте возможна и такая конструкция
let y = if x == 5 { 10 } else { 15 };
В расте есть три возможности для организации циклов
loop
while
for
Бесконечный цикл
loop {
println!("Зациклились!");
}
Цикл while
let mut x = 5;
let mut done = false;
while !done {
x += x - 3;
println!("{}", x);
if x % 5 == 0 {
done = true;
}
}
Цикл for
for (x = 0; x < 10; x++) {
printf("%d\n",x);
}
for x in 0..10 {
println!("{}", x);
}
Для инициализации цикла в диапазоне можно использовать функцию .enumerate()
for (i,j) in (5..10).enumerate() {
println!("i = {} и j = {}", i, j);
}
let lines = "привет\nмир\nhello\nworld".lines();
for (linenumber, line) in lines.enumerate() {
println!("{}: {}", linenumber, line);
}
В Rust для работы с циклами можно использовать стандартные break и continue.
Их можно использовать совместно с метками
'outer: for x in 0..10 {
'inner: for y in 0..10 {
if x % 2 == 0 {continue 'outer; } // продолжает цикл по x
if y % 2 == 0 {continue 'inner; } // продолжает цикл по y
println!("x: {}, y: {}", x, y);
}
}
Вектора
Вектор — это динамический массив, реализованный в
виде стандартного библиотечного типа Vec<T> (где <T> является обобщённым типом).
Вектора всегда размещают данные в куче. Вы можете создавать их с помощью макроса vec! :
let v = vec![1, 2, 3, 4, 5];
Обойти элементы вектора можно тремя способами
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);
}
У раста есть особенности, которые отличают его от других языков.
Рассмотрим следующий пример: создадим вектор, потом присвоим этот вектор другому вектору,
после чего попробуем вернуться к первому вектору
let v = vec![1, 2, 3];
let v2 = v;
println!("v[0] = {}", v[0]);
Мы получим ошибку - вектор был перемещен
error: use of moved value: `v`
println!("v[0] = {}", v[0]);
То же самое произойдет, когда мы создадим вектор, передадим его в качестве параметра в функцию,
а потом попытаемся обратиться к этому вектору
fn take(v: Vec) {
...
}
let v = vec![1, 2, 3];
take(v);
println!("v[0] ={}", v[0]);
Если еще раз посмотрим на пример
let v = vec![1, 2, 3];
let v2 = v;
Данные вектора находятся в куче, а указатель на вектор находится в стеке.
Второй указатель также находится в стеке. Проектировщики раста решили, что когда имеются два указателя,
указывающие на один и тот же обьект, это ни есть хорошо, и поэтому во второй строке происходит не только
создание второго указателя, но и одновременно уничтожение первого указателя.
Поэтому мы не можем использовать v.
Это т.н. стандартное умолчательное поведение раста при копировании обьектов.
Но в расте есть и другие механизмы копирования - как в любом нормальном языке.
Это механизм, использующий типаж
Copy
Такое копирование используется в типе данных i32, в который встроен Copy, в отличие от предыдущего примера
let v = 1;
let v2 = v;
println!("v = {}", v);
При передаче ссылок в функцию раст ведет себя стандарным образом. Ссылки по умолчанию - неизменяемые обьекты,
и ее нельзя будет изменить внутри этой функции.
Но ссылку можно сделать изменяемой с помощью модификатора mut. При этом действовать нужно аккуратно.
Область видимости любой ссылки должна находиться в пределах области видимости владельца.
Поясним на примере: следующий код не будет работать:
let mut x = 5;
let y = &mut x;
*y += 1;
println!("{}", x);
Этот код выдает нам такую ошибку:
error: cannot borrow `x` as immutable because it is also borrowed as mutable
println!("{}", x);
А следующий код будет работать:
let mut x = 5;
{
let y = &mut x;
*y += 1;
}
println!("{}", x);
Изменяемость
Как я уже говорил, возможность изменить какой-то обьект в расте работает иначе, чем в других языках.
Например, следующий обычный код в расте не работает:
let x = 5;
x = 6; // ошибка
Чтобы изменить переменную x, нужно добавить ключевое слово mut:
let mut x = 5;
x = 6; // работает
Т.е. фактически мы создаем копию обьекта x. Если же мы не хотим создавать копию обьекта, но хотим все же его изменить,
тогда нужно использовать изменяемую ссылку:
let mut x = 5;
let y = &mut x;
При этом y - неизменяемый обьект. Но и его можно сделать изменяемым:
let mut x = 5;
let mut y = &mut x;
Мы видим, что в расте изменение обьектов отличается от стандартного подхода и более запутанно.
Вообще, изменяемость — это свойство либо ссылки (&mut), либо имени ( let mut ). Это значит,
что, например, у вас не может быть структуры, часть полей которой изменяется, а другая
часть — нет. По умолчанию все поля структуры неизменяемы. И так писать нельзя:
struct Point{
x: i32,
mut y: 32,
}
Изменяемость структуры обьявляется на уровне создания обьектов структуры:
struct Point{
x: i32,
y: 32,
}
let mut a = Point(x:5, y:6);
a.x = 10;
В расте изменяемые поля структуры можно задавать с помощью &mut -
здесь 'a - время жизни:
struct Point{
x: i32,
y: 32,
}
struct PointRef<'a>{
x: &'a mut i32,
y: &'a mut i32,
}
let mut point = Point {x:0, y:0};
{
let r = PointRef(x: &mut point.x, y: &mut point.y);
*r.x = 5;
*r.y = 6;
}
В расте можно обьявить т.н. кортежную структуру - в ней поля не именуются:
struct Color(i32, i32, i32);
struct Point(i32, i32, i32);
let black = Color(0, 0, 0);
let origin = Point(0, 0, 0);
Полезной разновидностью кортежной структуры является структура с одни полем - она называется новым типом
struct Inches(i32);
let length = Inches(10);
let Inches(integer_length) = length;
println!("Длина в дюймах: {}", integer_length);
В Rust перечисление ( enum ) — это тип данных, который представляет собой один из
нескольких возможных вариантов. Каждый вариант в перечислении может быть также связан с
другими данными:
Простого if / else часто недостаточно, потому что нужно проверить больше, чем два
возможных варианта. Да и к тому же условия в else часто становятся очень сложными. Как
же решить эту проблему?
В Rust есть ключевое слово match , позволяющее заменить группы операторов
if / else чем-то более удобным
let x = 5;
match x {
1 => println!("один"),
2 => println!("два"),
3 => println!("три"),
4 => println!("четыре"),
5 => println!("пять"),
_ => println!("что-то ещё"),
}
Методы
В расте методы реализуются с помощью impl - в следующем примере метод area
struct Circle {
x: f64,
y: f64,
radius: f64,
}
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
}
fn main() {
let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
println!("{}", c.area());
}
Как и везде, в расте первым аргументом метода является &self.
Есть три варианта
self
&self
&mut self
В расте можно выполнить вызов цепочки вложенных методов типа
foo.bar().baz()
В предыдущем примере с Circle добавим в имплементацию еще один метод
impl Circle {
fn area(&self) -> f64 {
std::f64::consts::PI * (self.radius * self.radius)
}
fn grow(&self, increment: f64) -> Circle {
Circle { x: self.x, y: self.y, radius: self.radius + increment}
}
}
fn main() {
let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
println!("{}", c.area());
let d = c.grow(2.0).area();
println!("{}", d);
}
Если вы не хотите self, тогда можно применить статические методы.
В следующем примере со все тем же Circle мы используем стандартный вариант вызова статического метода
в виде Struct::method()
impl Circle {
fn new(x: f64, y: f64, radius: f64) -> Circle {
Circle {
x: x,
y: y,
radius: radius,
}
}
}
fn main() {
let c = Circle::new(0.0, 0.0, 2.0);
}
В расте нет перегрузки методов, именованных аргументов или переменного количества аргументов.
Строки
Строки — важное понятие для любого программиста. Система обработки строк в Rust
немного отличается от других языков, потому что это язык системного программирования.
Работать со структурами данных с переменным размером довольно сложно, и строки — как раз
такая структура данных. Кроме того, работа со строками в Rust также отличается и от
некоторых системных языков, таких как C.
string — это последовательность скалярных значений юникод,
закодированных в виде потока байт UTF-8. Все строки должны быть гарантированно
валидными UTF-8 последовательностями. Кроме того, строки не оканчиваются нулём и могут
содержать нулевые байты.
В Rust есть два основных типа строк: &str и String . &str —
это строковый срез. Строковые срезы имеют фиксированный размер и не могут быть
изменены. Они представляют собой ссылку на последовательность байт UTF-8:
let greeting = "Всем привет."; // greeting: &'static str
Строковые литералы могут состоять из нескольких строк. Такие литералы можно
записывать в двух разных формах. Первая будет включать в себя перевод на новую строку и
ведущие пробелы:
let s = "foo
bar";
Вторая форма, включающая в себя \ , вырезает пробелы и перевод на новую строку:
let s = "foo\
bar";
Тип String представляет собой строку, размещенную в
куче. Эта строка расширяема, и она также гарантированно является последовательностью UTF-8 .
String обычно создаётся путем преобразования из строкового среза с использованием
метода to_string .
let mut s = "Привет".to_string(); // mut s: String
println!("{}", s);
s.push_str(", мир.");
println!("{}", s);
String преобразуются в &str с помощью & :
fn takes_slice(slice: &str) {
println!("Получили: {}", slice);
}
fn main() {
let s = "Привет".to_string();
takes_slice(&s);
}
Строки не поддерживают индексацию
let s = "привет";
println!("Первая буква s — {}", s[0]); // ОШИБКА!!!
Можно получить срез строки с помощью синтаксиса
let dog = "hachiko";
let hachi = &dog[0..5];
Если у вас есть String , то вы можете присоединить к нему в конец &str :
let hello = "Hello ".to_string();
let world ="world!";
let hello_world = hello + world;
Но если у вас есть две String , то необходимо использовать & .
Это сделано потому, что &String может быть автоматически приведен к &str . Эта
возможность называется «Приведение при разыменовании»
let hello = "Hello ".to_string();
let world = "world!".to_string();
let hello_world = hello + &world;
Дженерики
Иногда, при написании функции или типа данных, мы можем захотеть, чтобы они
работали для нескольких типов аргументов. У Rust есть возможность, которая даёт
нам лучший способ реализовать это с помощью дженериков. Дженерики
еще называют «параметрическим полиморфизмом». Это
означает, что типы или функции имеют несколько форм (poly — кратно, morph — форма) по
данному параметру («параметрический»).
Стандартная библиотека Rust предоставляет несколько дженериков, в том числе Option:
enum Option<T> {
Some(T),
None,
}
Пример использования:
let x: Option<i32> = Some(5);
let y: Option<f64> = Some(5.0f64);
Дженерик позволяет использовать несколько параметров с помощью другого стандартного дженерика - Result<T, E> - :
enum Result {
Ok(T),
Err(E),
}
Вместо заглавных букв T, E можно использовать любые другие. Этот дженерик дает возможность возвращать кроме результата вычислений
еще и ошибку, если таковая имеется.
Дженерик-функции имеют аналогичный синтаксис:
fn takes_anything<T>(x: T) {
// do something with x
}
Несколько аргументов могут иметь один и тот же обобщённый тип:
Дженерик-функция может иметь несколько обобщенных типов:
fn takes_two_things<T, U>(x: T, y: U) {
// ...
}
Можно создать дженерик структуру
struct Point<T> {
x: T,
y: T,
}
let int_origin = Point { x: 0, y: 0 };
let float_origin = Point { x: 0.0, y: 0.0 };
Traits
Трэйты - их еще называют типажами - дают возможность придать типу дополнительную функциональность.
Трэйты похожи на методы, но отличается тем, что дают лишь определение для метода:
Трэйты могут быть использованы для ограничения обобщенных типов.
Реализация обычных методов для стандартных типов в расте считается плохой практикой программирования,
и вместо них рекомендуется использовать трэйты, хотя код при этом становится менее читабельным.
Трэйт может наследовать другой трэйт:
У раста есть встроенные стандартные трэйты, в частности - Drop. Он срабатывает тогда, когда программа выходит
из зоны видимости обьекта, которому принадлежит дроп. Этот трэйт может использоваться например для того,
чтобы освободить какие-то ресурсы:
struct HasDrop;
impl Drop for HasDrop {
fn drop(&mut self) {
println!("Dropping!");
}
}
fn main() {
let x = HasDrop;
// do stuff
} // здесь автоматически сработает дроп
Closures
Помимо именованных функций Rust предоставляет еще и анонимные функции .
Анонимные функции, которые имеют связанное окружение, называются 'замыкания', или closures. Они так
называются потому что они замыкают свое окружение. Например
let plus_one = |x: i32| x + 1;
assert_eq!(2, plus_one(1));
Мы создаем связывание, plus_one , и присваиваем ему анонимную функцию. Аргументы
замыкания располагаются между двумя вертикальными символами | , а телом замыкания является выражение,
в данном случае: x + 1 . Помните, что { } также является выражением, поэтому тело
замыкания может содержать много строк:
let plus_two = |x| {
let mut result: i32 = x;
result += 1;
result += 1;
result
};
assert_eq!(4, plus_two(2));
Обратите внимание, что есть несколько небольших различий между замыканиями и
обычными функциями, определенными с помощью fn . Первое отличие состоит в том, что для
замыкания мы не должны указывать ни типы аргументов, которые оно принимает, ни тип
возвращаемого им значения.
Второе отличие — синтаксис очень похож, но все же немного отличается:
fn plus_one_v1 (x: i32) -> i32 { x + 1 }
let plus_one_v2 = |x: i32| -> i32 { x + 1 };
let plus_one_v3 = |x: i32| x + 1 ;
UFCS
Когда раст имеет дело с перегрузкой функций, т.е. с функциями с одинаковыми именами,
нужно использовать специальное правило для вызова таких функций - оно называется -
Универсальный синтаксис вызова функций - universal function call syntax (ufcs).
Рассмотрим пример с двумя типажами:
trait Foo {
fn f(&self);
}
trait Bar {
fn f(&self);
}
struct Baz;
impl Foo for Baz {
fn f(&self) { println!("Baz’s impl of Foo"); }
}
impl Bar for Baz {
fn f(&self) { println!("Baz’s impl of Bar"); }
}
let b = Baz;
b.f();
Этот код вызовет ошибку.
Нам нужен способ указать, какой конкретно метод нужен, чтобы устранить
неоднозначность. Эта возможность называется ucfs, и выглядит это так:
Foo::f(&b);
Bar::f(&b);
Когда мы вызываем метод, используя синтаксис вызова метода, как например b.f() ,
Rust автоматически заимствует b , если f() принимает в качестве аргумента &self . В этом
же случае, Rust не будет использовать автоматическое заимствование, и поэтому мы должны
явно передать &b.
Сокращенная форма ufcs выглядит так
Trait::method(args);
Расширенная форма ufcs
<Type as Trait>::method(args);
Синтаксис <>:: является средством предоставления подсказки типа. Тип располагается
внутри <> . В этом случае типом является Type as Trait , указывающий, что мы хотим
здесь вызвать Trait версию метода. Часть as Trait является необязательной, если вызов
не является неоднозначным. То же самое что с угловыми скобками, отсюда и короткая форма.
Вот пример использования длинной формы записи с использованием угловых скобок, позволяющих вызывать трэйт(типаж):