пятница, 31 января 2020 г.

Коллекции в Rust. Часть 3.

В этой статье мы рассмотрим использование замыканий с коллекциями и не только. Мы уже говорили про сами замыкания в статье Замыкания в Rust. Перед тем как читать дальше, лучше с ней ознакомиться. Но сначала коротко про итераторы (iterators). Итератор или курсор — это ссылка на следующий элемент коллекции. Есть функция (iterator generator), которая возвращает не мутирующий итератор, iter() и функция, которая возвращает мутирующий итератор, iter_mut(). Тут понятно, что с помощью не мутирующей ссылки мы не можем изменять значение элемента, а с помощью мутирующей, можем. Используя итератор удобно обходить коллекцию, и получить доступ к ее элементам, например в цикле for i in [0;3].iter_mut(){ *i +=1;} Для обхода символов в строках используется отдельный итератор chars(), который понимает utf-8.

Теперь мы можем выполнять различные операции над элементами коллекции, например, отфильтровать значения по определенному критерию, сосчитать, просуммировать и много чего еще. Эти операции настолько распространены и часто используются, что воплотились в специальные функции, в Rust они называются адаптерами для итераторов (iterator adapters), потому что могут соединяться в цепочки из нескольких адаптеров (как в электрике), передавая результат операции от одного к другому (с помощью итераторов), добавляя свои изменения к полученным на входе элементам коллекции. Некоторые из адаптеров принимают в качестве параметров замыкания, которые принято называть предикатами (predicates). Это очень удобно, давайте рассмотрим основные из них.
 
Адаптер фильтр (filter) делает выборку по условию заданному предикатом, например, .iter().filter(|x| **x < 0) отбирает только те элементы, которые меньше нуля, т.е. имеют отрицательное значение и возвращает итератор, который мы можем использовать в цикле. Каждый элемент коллекции присваивается локальной переменной x, которая сравнивается с нулем. Замыкание возвращает тру если элемент соответствует условию и адаптер-фильтр исключает элементы, которые ему не соответствуют. Мы используем два оператора разыменовывания (*) потому что “х” это ссылка(адаптер) на ссылку(итератор) на элемент в коллекции.
 
Адаптер преобразовать (map) применяет выражение, заданное предикатом к каждому элементу коллекции. Например, .iter().map(|x| *x * 2) умножает каждый элемент на 2, т.е. изменяет (удваивает) элемент в коллекции и возвращает итератор на новые значения. В данном случае у нас одно разыменование (*), т.к. ссылка(итератор) на элемент только одна.

Адаптер: перечислить (enumerate) создает кортеж, состоящий из счетчика (позиции) и ссылки на значение элемента в этой позиции. Адаптер получает и возвращает итератор, .iter().enumerate().
 
Потребитель итератора (iterator consumer) отличается от адаптера тем, что принимает, но не возвращает итератор. Их удобно использовать для различных проверок, например с помощью if-else. Обычно потребитель завершает цепочку из адаптеров.
 
Iterator generator -> Iterator Adapter -> … -> Iterator Adapter -> Iterator Consumer

Потребитель: любой (any) возвращает true (bool), если любой (какой-нибудь) из элементов коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().any(|e| *e == 0) проверяет элементы на равенство нулю. Полная версия выглядит так: .iter().any(|e: &i32| -> bool { *e == 0 }). К элементам применяется логическое ИЛИ.
 
Потребитель: все (all) возвращает true (bool), если все элементы коллекции, соответствует условию, заданному предикатом (замыканием). Например, .iter().all(|e| *e == 0) проверяет равны ли нулю все элементы коллекции. К элементам применяется логическое И.

Потребитель: подсчет (count) возвращает количество элементов в итераторе. Удобно использовать в связке с адаптером. Например, .iter().filter(|e| **e == 1).count(), возвращает количество элементов, которые равны нулю.

Потребитель: сумма (sum) возвращает общую сумму значений элементов в итераторе. Например, .iter().sum::<i32>(). Как мы видим, требуется указать тип возвращаемого значения, если Rust не может его вывести. Если итератор пустой, возвращается ноль. Элементы коллекции должны поддерживать операцию сложения (+).

Потребители: минимальное (min) и максимальное (max) возвращают минимальное и максимальное значение соответственно. Так как итератор может быть пустой, возвращается тип Option, с вариантами Some(n) и None. Элементы должны поддерживать операции сравнения (=, <) и могут быть строками.

Потребитель: собрать (collect) создает вектор из элементов, полученных от итератора. Например, .iter().collect::<Vec<&i32>>(), так как Rust может вывести тип, можно использовать “_” .iter().collect::<Vec<_>>() или .iter().collect(). Таким образом удобно сохранять результаты обработки коллекции. При обработке строк, от выбранного типа (String или Vec<char>), зависит в каком виде будут сохранены элементы вектора, как строки или символы. Смотрим пример.

Итераторы ленивы (lazy) они ничего не делают пока их не просит адаптер, потребитель или цикл, который выступает в роли потребителя. Если данные кем-то не запрошены, к ним нет доступа.

fn main() {
    //Iterator
    for element in vec![0; 3].iter_mut() {
        *element += 1;
        //println!("{:?}", element);
    }
    //Adapters
    //Filter
    for n in [-1, 3, 0].iter().filter(|x| **x < 0) {
        println!("{} ", n);
    }
    //Map
    for n in [2, 4, 6].iter().map(|x| *x * 2) {
        println!("{} ", n);
    }
    //Enumerate
    for (i, n) in [2, 4, 6].iter().enumerate() {
        println!("{} {}", i, n);
    }
    //Consumers
    //Any
    if [2, 0, 6].iter().any(|e| *e == 0) {
        println!("arr has 0");
    }
    //All
    if [1, 1, 1].iter().all(|e| *e == 1) {
        println!("all are 1");
    }
    //Count
    println!("count of 1: {}", [1, 0, 1].iter().filter(|e| **e == 1).count());
    //Sum
    println!("sum: {}", [2, 2].iter().sum::<i32>());
    let sum: i32 = [2, 2].iter().sum();
    println!("sum: {}", sum);
    //Min
    match [1, 2].iter().min() {
        Some(n) => println!("min: {}", n),
        None => println!("empty")
    }
    //Max
    match [0; 0].iter().max() {
        Some(n) => println!("max: {}", n),
        None => println!("empty")
    }
    //Collect
    let vec = [1, 2, 3].iter().collect::<Vec<&i32>>();
    println!("vec: {:?}", vec); //vec: [1, 2, 3]
    let vec1 = "hello world".to_string().chars().collect::<String>();
    println!("vec: {:?}", vec1); //vec: "hello world"
    let vec2 = "hello".to_string().chars().collect::<Vec<char>>();
    println!("vec: {:?}", vec2); //vec: ['h', 'e', 'l', 'l', 'o']
    //Chains
    let arr = [66, -8, 43, 19, 0, -31];
    let v: Vec<i32> = arr
        .iter()
        .filter(|x| **x > 0)
        .map(|x| *x * 2)
        .collect();
    print!("{:?}", v); //[132, 86, 38]
}
 
Перегуд В.

четверг, 30 января 2020 г.

Замыкания в Rust.

Мы уже говорили про замыкания (closures) в статье Обратные вызовы и лямбда выражения в си++. Сегодня мы рассмотрим, как Rust работает с замыканиями. Сначала поговорим про функции высшего порядка (highorder function), это функции, которые могут принимать в качестве своих параметров другие функции (низшего порядка). Это удобно использовать для обратных вызовов (callbacks), когда мы вызываем функцию, которая должна в свою очередь вызвать другую функцию, переданную ей в качестве аргумента.
 
Мы знаем, что параметры функции всегда имеют определение типа, чтобы указать тип - функция, мы используем обобщенный тип (generic), с требованием, что он должен имплементировать функциональную характеристику Fn, FnMut или FnOnce и указать сигнатуру функции например, Fn(i32)->i32, где в скобках указан тип параметра или параметров через запятую, а после -> указывается тип возвращаемого значения. На первый взгляд все немного сложно, это из-за правил владения ресурсами. Функциональные характеристики описывают правила доступа к окружению из замыканий. Для функций это не требуется, так как они не имеют доступа к окружению. Fn обозначает многократный не мутирующий доступ к значению, например по не мутирующей ссылке (&). FnOnce обозначает один мутирующий или один не мутирующий доступ к значению, например по мутирующей или не мутирующей ссылке (& или &mut). FnMut обозначает один мутирующий доступ к значению, например по мутирующей ссылке (&mut) и неявно включает FnOnce. Тема, конечно, не простая, но мы не будем углубляться и поговорим немного о другом.
 
Передача функции в качестве параметра (аргумента) другой функции имеет некоторые недостатки. Например, часто определенное действие требуется только один раз, но мы должны завернуть его в функцию, чья задача быть востребованной многократно, ситуация усугубляется при необходимости множества одноразовых действий. Проблема знакомая и нам на помощь приходят безымянные функции, которые мы можем создавать прямо в месте вызова. Из тела такой функции было бы очень удобно, получить доступ к переменным, находящимся в том же пространстве имен, т. е. к окружению (outside variables) функции. Про управление доступом к окружению мы говорили выше. Такие функции есть, они называются замыкания и имеют специальный синтаксис, который должен соответствовать функциональному типу (сигнатуре функции). Например, |x:i32|{x} соответствует сигнатуре (i32)->i32. Как мы видим Rust может вывести возвращаемый тип и его можно не указывать или указать явно |x:i32|->i32{x}. В примере показаны различные варианты использования.

//trait bounds for closures - access to outside variables
//1.Fn - multiple immutable access to value, can borrow as & (immutable reference)
fn higher_order_1(f: impl Fn(u32) -> u32) -> i32 {
    let x = f(5);
    x as i32
}

//2.FnMut - one mutable access to value, can borrow as &mut (mutable reference), implicit FnOnce
fn higher_order_2<F>(mut f: F) -> i32 where F: FnMut(u32) -> i32 {
    let x = f(5);
    x as i32 //
}

//3.FnOnce - one mut or immut access to value, can borrow as & (immut ref) or &mut (mut ref)
fn higher_order_3<F: FnOnce(u32) -> u32>(f: F) -> i32 {
    let x = f(5);
    x as i32
}

//simple function
fn foo_1(x: u32) -> u32 {
    x + x
}

//can't use outside
fn foo_2(x: u32) -> i32 {
    //inside string
    let mut str_x = "x".to_string();
    str_x.push('X');
    println!("in the closure, str_x is now {}", str_x);
    (x + x) as i32
} //inside string deleted

fn main() {
    //outside string
    let mut str_y = "y".to_string();

    println!("higher_order_1: {:?}\n", higher_order_1(foo_1)); //Fn
    println!("higher_order_2: {:?}\n", higher_order_2(foo_2)); //FnMut

    //Closure
    println!("higher_order_3: {:?}", higher_order_3( //FnOnce
                                                     |x: u32| {
                                                         str_y.push('Y'); //change outside string
                                                         println!("in the closure, str_y is now {}", str_y);
                                                         x
                                                     }
    )); //outside string is alive
    println!("after higher_order_3, str_y is {}", str_y);
}

В стандартной библиотеке Rust имеется множество функций, которые принимают в качестве своих аргументов замыкания, например sort_by(), которая принимает замыкание с сигнатурой (&T,  &T) -> Ordering. Перечисление Ordering состоит из вариантов Greater, Less и Equal. Эта функция работает с коллекциями, принимая на вход два соседних элемента по очереди, сравнивает их и сортирует. В замыкании мы можем сами определять способ сравнения, если это обычное больше-меньше, то можно использовать стандартную для этой цели функцию cmp(). Замечу, что если замыкание состоит из одного выражения, то его тело можно не заключать в блок (фигурные скобки). Смотрим пример.

use std::cmp::Ordering;

fn cmp1(a: &i32, b: &i32) -> Ordering {
    if a < b {
        Ordering::Greater
    } else if a > b {
        Ordering::Less
    } else {
        Ordering::Equal
    }
}

fn cmp2(a: &i32, b: &i32) -> Ordering {
    a.cmp(b)
}

fn main() {
    let mut arr = [4, 8, 1, 10, 0, 45, 12, 7];
    arr.sort_by(cmp1);
    println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
    arr.sort_by(cmp2);
    println!("{:?}", arr); //[0, 1, 4, 7, 8, 10, 12, 45]
    arr.sort_by(|a, b| b.cmp(a));
    println!("{:?}", arr); //[45, 12, 10, 8, 7, 4, 1, 0]
}

Перегуд В.

среда, 29 января 2020 г.

Коллекции в Rust. Часть 2.

Продолжаем про коллекции в Rust. Есть такое понятие, как приоритетная очередь (priority queue), ее можно зафейкать для вектора (Vector), если организовать сортировку элементов по какому-нибудь признаку, например, по большему значению элементов для простых типов или по какому-нибудь полю структуры, если это пользовательский тип. Как мы уже узнали, такая операция может обойтись нам очень дорого, так как потребуется перемещение элементов внутри массива, и бывает дешевле, если создать массив с нужной последовательностью заново. Часто нам нужно даже не иметь отсортированный вектор (вся последовательность элементов), а именно элемент удовлетворяющий нашему условию, в частности наибольший элемент и желательно, чтобы его можно было получить с наибольшей скоростью, т.е. организовать эффективный поиск (бинарный) – получить наибольший элемент, без полной сортировки, так мы сэкономим ресурсы. И в этом нам поможет коллекция из стандартной библиотеки BinaryHeap. Чтобы элементы такой коллекции могли быть отсортированы, они должны как минимум имплементировать характеристики PartialOrd и PartialEq. Базовые типы имплементируют их из коробки, для пользовательских нужно определить, что мы и сделали в статье Обобщенные типы в Rust.

Следующие коллекции, которые мы рассмотрим, это наборы данных (sets). В математике их называют множества, отличительная их особенность заключается в том, что они содержат данные без повторов, т.е. если мы добавляем элемент, который уже присутствует в коллекции, то в зависимости от наших потребностей, он либо перезаписывает имеющийся, либо не добавляется. Наборы данных бывают двух видов. Первый - несортированные (unordered sets) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashSet. Второй – отсортированные по значению (ordered sets), такой набор можно создать с помощью коллекции BTreeSet.

Последние коллекции, про которые мы поговорим, это словари (dictionaries). Словари представляют из себя наборы данных (sets), для которых, в качестве элементов используются пары ключ-значение (key-value). В результате чего в словаре не может быть больше одной пары с одинаковым ключом. И наоборот, допускается иметь несколько пар с одинаковым значением. При добавлении пары с существующим ключом, происходит перезапись старого значения. В словарях обращение к элементу происходит не по порядковому номеру всей пары (индексу), а только по ключу (key). Словари также бывают двух видов. Первый вид – несортированные (unordered dictionaries) элементы хранятся в случайном порядке, такой набор можно создать с помощью коллекции HashMap. Второй вид – отсортированные по ключу (ordered dictionaries), такой набор можно создать с помощью коллекции BTreeMap. Пары задаются с помощью кортежа (tuple), который в последствии можно деструктурировать в переменные key и value. Смотрим пример.

fn main() {
    const SIZE: usize = 3;
    //BinaryHeap init
    let mut priority_queue = std::collections::BinaryHeap::<i32>::new();
    //Add elements
    priority_queue.push(3);
    priority_queue.push(1);
    priority_queue.push(2);
    println!("{:?}", priority_queue); //[3, 1, 2]
    //Popping
    while !priority_queue.is_empty() {
        println!("{:?}", priority_queue.pop().unwrap()); //3 2 1
    }

    //Sets init
    let mut hash_set = std::collections::HashSet::<_>::new();
    let mut btree_set = std::collections::BTreeSet::<_>::new();
    //Add element
    for _ in 0..3 {
        hash_set.insert(2); //2,2,2
        hash_set.insert(1); //2,2,2,1,1,1
        btree_set.insert(2); //2,2,2
        btree_set.insert(1); //2,2,2,1,1,1
    }
    println!("{:?}", hash_set); //{2,1} random
    println!("{:?}", btree_set); //{1,2} ordered

    //Dictionaries init
    let mut hash_map = std::collections::HashMap::<_, _>::new();
    let mut btree_map = std::collections::BTreeMap::<_, _>::new();
    let tmp_arr = [(5, 'Z'), (1, 'X'), (2, 'V'), (3, 'Z'), (1, 'Y')];
    //Add element
    for &(key, value) in tmp_arr.iter() { //destructuring
        hash_map.insert(key, value);
        btree_map.insert(key, value);
    }
    println!("{:?}", hash_map); //{1: 'Y', 2: 'V', 5: 'Z', 3: 'Z'} random
    println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 5: 'Z'} ordered
    let to_find = [3, 5];
    for num in &to_find {
        match hash_map.get(num) {
            Some(character) => println!("{},{}", num, character), //3,Z 5,Z
            None => println!("key {} does not exist", num),
        }
    }
    if btree_map.contains_key(&5) {
        println!("key 5 exists")
    }
    // insert a key only if it doesn't already exist
    btree_map.entry(4).or_insert('X');
    println!("{:?}", btree_map); //{1: 'Y', 2: 'V', 3: 'Z', 4: 'X', 5: 'Z'}
    for val in btree_map.values() {
        println!("{}", val); //Y, V, Z, X, Z
    }
    for val in btree_map.keys() {
        println!("{}", val); //1, 2, 3, 4, 5
    }
    println!("{:?}", btree_map.get_key_value(&1).unwrap()); //(1,'Y')
}

Перегуд В.

Коллекции в Rust. Часть 1.

Сегодня рассмотрим коллекции, которые присутствуют в стандартной библиотеке Rust. Но сначала пару слов о внутреннем устройстве коллекций. Коллекция как группа элементов, которую можно хранить в переменной, должна иметь одну точку входа, через которую мы можем обращаться к ее элементам, например, по индексу. Если мы говорим про массив (Array), то это имя массива, которое представляет из себя указатель на начало массива (место в памяти), расположенного в стеке, все элементы которого располагаются друг за другом, чтобы позволить переходить от одного элемента к другому с шагом равным размеру типа элементов, индекс в данном случае это просто количество шагов от начала массива. Это самый быстрый тип коллекций, он располагается в стеке, что накладывает определенные ограничения, главное из них то, что массив не может менять свою длину.

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

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

Если нам нужно делать много вставок в середину, то мы можем использовать коллекцию под названием связанный список(LinkedList). Где элементы хранятся не друг за другом, а в разных местах кучи. Элементы помимо значений, хранят указатель на область памяти, в которой хранится следующий элемент, это для односвязного списка. Для двусвязного списка хранится также указатель на предыдущий элемент, что позволяет переходить по коллекции в обоих направления, вперед и назад, но такое прохождение очень неоптимальное по перфу, так как переходы происходят по указателям. Вставка и удаление единичных элементов в середину происходит быстро, путем добавления новых указателей в цепочку. Но если нужно вставлять или удалять группу элементов или проходить по длинному списку от начала или конца, то (почти всегда!) лучше использовать вектор или очередь. В стандартной библиотеке Rust есть методы, которые позволяют вставлять в начало и конец списка, но нет методов, которые позволяют вставлять в середину (они пока не стабилизированы). При необходимости их потребуется писать вручную, например используя итератор.

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

fn main() {
    const SIZE: usize = 3;
    //Vector init
    let vec0: Vec<i32> = vec![]; //empty
    let vec1 = vec![1, 2, 3]; //1,2,3
    let mut vec2 = vec![0; 3]; //0,0,0
    let vec3: Vec<i32> = Vec::new(); //empty
    let vec4 = Vec::<i32>::new(); //empty
    let mut vec5 = Vec::<i32>::with_capacity(SIZE); //empty
    println!("{:?}", vec5); //[]
    //Clearing
    vec2.clear();
    println!("{:?}", vec2); //[]
    //Check if it contains particular element
    if vec1.contains(&2){
        println!("{:?} contains 2", vec1); //[1, 2, 3] contains 2
    }
    //Add element
    for i in 0..SIZE {
        vec5.push(i as i32);
    }
    //Length
    println!("{:?}", vec5.len()); //3
    //Iterating immutable
    for element in vec5.iter() {
        print!("{:?} ", *element); //0 1 2
    }
    //Iterating mutable
    for element in vec5.iter_mut() {
        *element += 1;
    }
    println!("{:?}", vec5); //[1,2,3]
    //Popping from the end
    for _ in 0..3 {
        println!("{:?}", vec5.pop().unwrap()); //3 2 1
    }
    println!("{:?}", vec5.len()); //0
    //Inserting
    for i in 0..SIZE {
        vec5.insert(0, i as i32);
    }
    println!("{:?}", vec5); //[2,1,0]
    //Sorting
    vec5.sort();
    println!("{:?}", vec5); //[0,1,2]
    //Removing
    for _ in 0..SIZE {
        vec5.remove(0);
    }
    println!("{:?}", vec5); //[]
    //Check if it is empty
    if vec5.is_empty(){
        println!("{:?} is empty", vec5); //[] is empty
    }

    //VecDeque init
    let vec_deq = std::collections::VecDeque::from(vec5.clone());
    let mut vec_deq = std::collections::VecDeque::<i32>::new();
    //Add element to the end
    for i in 0..vec1.len() {
        vec_deq.push_back(i as i32);
    }
    println!("{:?}", vec_deq); //[0,1,2]
    //Popping from the begin
    while vec_deq.len() > 0 {
        vec_deq.pop_front();
    }
    println!("{:?}", vec_deq.len()); //0
    vec_deq.insert(0,5);
    println!("{:?}", vec_deq); //[5]
    vec_deq.remove(0);
    println!("{:?}", vec_deq); //[]

    //LinkedList init
    let mut list = std::collections::LinkedList::<i32>::new();
    list.push_front(0);
    list.push_front(1);
    println!("{:?}", list); //[1,0]
    //Iterate
    let mut iter = list.iter_mut();
    *iter.next().unwrap() = 2;
    println!("{:?}", list); //[2,0]
    println!("{:?}", list.len()); //2
    //References to the front and to the back
    println!("front: {:?}, back: {:?}", list.front().unwrap(), list.back().unwrap()); //front: 2, back: 0

}

Перегуд В.

среда, 22 января 2020 г.

Обобщенные типы в Rust.

Мы уже говорили про "генерики"(generic types), например, когда обсуждали Объекты в Rust. Сегодня мы поговорим о них более подробно.

Помните, я писал, что в Rust нет перегрузки (overload) функций, ну т. е. мы не можем создать несколько функций с одним и тем же именем, но с разными типами для параметров и возвращаемого значения, что могло быть удобно. Но это и не нужно, когда мы можем сделать тоже самое с помощью генерик функции, что делает возможным ее использование с любыми типами - именно то, что мы и хотели при перегрузке функции.

Объявить “общий” (generic) тип можно с помощью “<” и “>” , в которые мы заключаем любое имя для одного или нескольких (через запятую) общих типов, например <T> или <K, V>. После чего мы можем использовать эти имена внутри функции, в качестве заглушек, на месте которых может быть любой базовый или пользовательский тип.
 fn foo<T>(x: T) -> T { x }

Чтобы вызвать такую функцию, мы можем явно указать с помощью “::<i32>”, какой конкретный тип мы хотим использовать вместо общего типа. foo::<i32>(5); Или не указывать, и тогда Rust определит (inferr) его неявно, по типу значений для параметров и возвращаемого значения. foo(5); В данном случае значение “5” имеет тип “i32”.

Мы можем задать ограничение (trait bound) для общего типа, которое будет требовать, чтобы все конкретные типы имплементировали характеристику, т.е. гарантировали в своем составе определенные методы. Это делается с помощью “:Trait” или “:Trait1 + Trait2”, если характеристик несколько.  
 fn foo<T: Copy>(x: T) -> T { x }

Такое ограничение можно задать другим способом, с помощью “where T:Trait” или “where T: Trait1 + Trait2”. fn foo<T >(x: T) -> T where T: Copy + Clone { x } В данном случае конкретные типы должны поддерживать копирование.

Есть еще один способ задать ограничение, с помощью “impl Trait” вместо параметров и/или возвращаемого значения, когда нам не нужны имена-заглушки, но нужно гарантировать наличие характерных методов. Fn foo(x: impl Display)-> impl Display{ x } В данном случае тип будет выведен неявно на базе типа значения для параметра или возвращаемого значения с соблюдением условия, что этот тип имеет характеристику Display.

Дальше рассмотрим генерики в контексте пользовательских типов. Общий тип может быть использован для полей структуры и задается с помощью <T> после имени структуры. При создании такой структуры мы можем указать конкретный тип явно “::<i32>" или позволить Rustу вывести его.

Чтобы для такой структуры определить методы, которые могут использовать общий тип, нужно определить блок имплементации с помощью impl<T> , тем самым определяя имя заглушку для этого пространства имен (блока). Таким образом мы связываем заглушку структуры с заглушкой для методов.

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


point.rs
---------
use std::cmp::Ordering;

#[derive(Debug)]
pub struct Point<T> {
    pub x: T,
    pub y: T,
}

impl Point<i32>{
    //..
}

impl<T> Point<T> {
    pub fn from(_x: T, _y: T) -> Point<T> {
        Point {
            x: _x,
            y: _y,
        }
    }
    pub fn set(&mut self, _x: T, _y: T) {
        self.x = _x;
        self.y = _y;
    }
    pub fn bigger(p1: Point<T>, p2: Point<T>) -> Option<Point<T>> where T: PartialOrd + PartialEq {
        if p1 == p2 {
            None
        } else if p1 < p2 {
            Some(p2)
        } else {
            Some(p1)
        }
    }
}

impl<T> PartialEq for Point<T> where T: PartialEq {
    fn eq(&self, other: &Self) -> bool {
        if self.x == other.x && self.y == other.y {
            true
        } else {
            false
        }
    }
}

impl<T> PartialOrd for Point<T> where T: PartialOrd {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        self.x.partial_cmp(&other.x)
    }
}

main.rs
-----------
mod point;
use point::*;

fn main() {
    let p1 = Point { x: 5.0, y: 5.0 };
    //let p1 = Point::<f64> { x: 5.0, y: 5.0 };
    //let p1:Point<f64> = Point { x: 5.0, y: 5.0 };
    let mut p2 = Point::<f64>::from(0.1, 0.1);
    p2.set(10.1, 10.1);
    //println!("Bigger point is: {:?}", Point::<f64>::bigger(p1, p2).unwrap());
    println!("Bigger point is: {:?}", Point::bigger(p1, p2).unwrap());
}

Перегуд В.

вторник, 14 января 2020 г.

Объекты в Rust. Часть 2.

Сегодня мы поговорим про полиморфизм в Rust. Полиморфизм в других языках базируется на таких свойствах объектно-ориентированного программирования, как наследование, абстрактные классы и виртуальные методы. Стоп, Rust не является объектно-ориентированным языком, более того наследование в нем отсутствует. Мода на наследование давно прошла и считается что использовать композицию в пользовательских типах лучше. Перед тем как двигаться дальше будет полезно прочитать статьи про Полиморфизм и Абстрактные классы (интерфейсы) в си++. Но с полиморфизмом в Rust все в порядке, есть и статический полиморфизм, и динамический полиморфизм. Полиморфное поведение в Rust обеспечивается с помощью “характеристик” (trait), которые напоминают интерфейсы из Java.

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

Статический полиморфизм - полиморфизм времени компиляции базируется на основе работы генериков и ручного кастования объектов в нужный тип. И так как в Rust нет наследования, родительский тип и его привязка к наследникам задается с помощью глобального типа Any (похоже на суперкласс Object из Java) или характеристик, которые мы будем рассматривать далее. При этом создаются копии методов с нужным типом при компиляции и это не требует дополнительных действий при выполнении, что повышает скорость работы программы.

Динамический полиморфизм - полиморфизм времени выполнения базируется на основе динамического преобразования типов во время выполнения. Задается с помощью ключевого слова “dyn” в определении ссылки на Any или характеристику. Так как выбор типа и определение нужного метода происходит во время выполнения приложения, это замедляет его работу. Учитывая, что в работе с характеристиками требуется динамическое кастование, которое тоже нагружает систему, можно отдать предпочтение удобству.

Итак, что же такое характеристика (trait)? Образно говоря, это гарантия способности выполнять определенные действия. В нашем контексте это гарантия наличия у типа определенных методов, которые мы можем использовать. Rust позволяет обращаться к типам, которые реализуют (impl - for) характеристику через тип характеристики (&dyn), как обращение к наследникам через базовый тип в ООП. Более того, мы можем цеплять характеристику к чужим (базовым в том числе) типам или требовать (where) от чужих типов ее наличие. Например, чтобы определить оператор "плюс" (+) для пользовательского типа, мы должны гарантировать определение метода add() из характеристики Add, которая находится в стандартной библиотеке (std::ops). Нужно отметить один момент, мы можем потребовать определить для характеристики тип(ы), с помощью type. Смотрим пример.

shape.rs
-----------
use std::ops::Add;

pub trait Drawable {
    fn draw(&self);
}

pub fn generic_draw<T>(value: T) where T: Drawable {
    value.draw();
}

#[derive(Debug)]
pub struct Point {
    pub x: i32,
    pub y: i32,
}

impl Add for Point{
    type Output = Point;

    fn add(self, rhs: Self) -> Self::Output {
        Point{
            x:self.x + rhs.x,
            y:self.y + rhs.y,
        }
    }
}

circle.rs
------------
use super::shape::*;

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

impl Circle {
    pub fn new() -> Self {
        Circle {
            radius: 1,
            center: Point {
                x: 0,
                y: 0,
            },
        }
    }
}

impl Drawable for Circle {
    fn draw(&self) {
        println!("Draw circle: {:?}", self)
    }
}

triangle.rs
-------------
use super::shape::Drawable;

#[derive(Debug)]
pub struct Triangle {
    width: i32,
    heiht: i32,
}

impl Triangle {
    pub fn new() -> Self {
        Triangle {
            width: 5,
            heiht: 5,
        }
    }
}

impl Drawable for Triangle {
    fn draw(&self) {
        println!("Draw triangle: {:?}", self)
    }
}

main.rs
-----------
mod shape;
mod circle;
mod triangle;

use shape::*;
use circle::*;
use triangle::*;
use std::any::Any;

fn main() {
    let a = Point{x: 1, y: 1};
    let b = Point{x: 2, y: 2};
    let c = a + b;
    println!("Add operator: a + b = {:?}", c);

    let c1 = Circle::new();
    let t1 = Triangle::new();

    let objects: Vec<&dyn Any> = vec![&c1, &t1];
    for object in objects.iter() {
        if object.is::<Circle>() {
            println!("Circle:");
        } else if object.is::<Triangle>() {
            println!("Triangle:");
        }

        if let Some(circle) = object.downcast_ref::<Circle>() {
            circle.draw();
        } else if let Some(triangle) = object.downcast_ref::<Triangle>() {
            triangle.draw();
        }
    }

    //dynamic polymorphism
    let shapes: Vec<&dyn Drawable> = vec![&c1, &t1];
    for shape in shapes.iter() {
        shape.draw();
    }

    //static polymorphism
    generic_draw(c1);
    generic_draw(t1);
}

Перегуд В.

понедельник, 13 января 2020 г.

Управление памятью в Rust. Часть 2.

Сегодня мы более подробно остановимся на понятии “изменяемость” (mutability). Вспомним о том, что переменная или ссылка (&) в Rust может быть изменяемая (с помощью mut), или не изменяемая (по умолчанию). При работе с простыми типами, например i32, все просто. С составными типами все немного сложнее. Если у нас есть структура, включающая другие члены, то при создании изменяемого объекта этого типа (или имея изменяющую ссылку на него), мы можем изменять члены, и соответственно, если объект неизменяемый (или обычная ссылка на него), то и его члены не могут быть изменены. Это называется “внешняя изменяемость” (Exterior mutability).

Все неплохо пока нам не нужно изменить член, оставляя сам тип неизменяемым. Например, нам нужен тип (структура + методы), члены которого мы хотели бы оставить неизменяемыми при создании объекта и работе с ним, но один из методов этого типа, изменяет внутренние данные (поля), и вызов такого метода требует создания изменяемого объекта. И тут появляется понятие “внутренняя изменяемость” (Interior mutability), при котором мы имеем неизменяемую ссылку на объект, но можем изменять внутренние данные объекта (поля).

И как мы уже увидели, есть случаи, когда между внешней и внутренней изменяемостью возникает конфликт. Пример такого конфликта — это использование нескольких (неизменяющих) ссылок на один объект или умного указателя Rc. По правилам Rust, мы можем иметь несколько указателей общего владения объектом, которые не могут изменять объект, чтобы избежать гонки за данными. Соблюдение этого правила обеспечивает контролер владения на этапе компиляции. Но это не удобно и нам нужна возможность изменять внутренние данные с соблюдением условий, при которых мы гарантируем блокировку данных (lock), которые хотим изменить, чтобы исключить возможность одновременного доступа к данным из разных мест. Заблокировать данные и обеспечить внутреннюю изменяемость для неизменяемых объектов нам помогут обертки Cell и RefCell. Основное отличие между которыми в том, что RefCell делает проверку владения во время выполнения, а Cell нет. Образно мы помещаем данные в ячейку, которая блокирует доступ к ним из других мест (блоков кода). Эти обертки предназначены для работы в одном потоке.

Cell дает возможность прочитать данные с помощью get(), и изменить с помощью set(). Также есть другие полезные методы. Cell можно применять только к данным, которые имплементируют трейт (интерфейс) Copy. Так как проверки (во время компиляции и выполнения) не проводятся, нужно быть осторожным.

RefCell требует применять borrow() и borrow_mut() для получения нескольких простых или одной изменяющей ссылки на данные, для их чтения или изменения. Проверка времени жизни ссылок проводится во время выполнения. Данные будут заблокированы до того момента пока последняя ссылка не будет уничтожена, что можно принудительно сделать с помощью блока видимости {} или drop(). Обычно нам не нужно копировать обернутые данные и лучше использовать RefCell (перемещение по умолчанию).

Cell предоставляет значения, а RefCell предоставляет ссылки на данные. Выбирайте Cell, если оборачиваете данные, которые имплементируют Copy, т.е. простые типы (int, float). Выбирайте RefCell, если оборачиваете структуры, данные без Copy, или хотите динамически проверять владение.

use std::cell::{Cell, RefCell};
use std::rc::Rc;

#[derive(Debug, Clone, Copy)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 0, y: 0 };

    //Rc<RefCell<Point>>
    let rc = Rc::new(RefCell::new(p1));

    let rc1 = Rc::clone(&rc);
    let rc2 = Rc::clone(&rc);
    println!("RefCell: {:?}", rc2);

    //RefCell
    rc1.borrow_mut().x = 55;
    rc1.borrow_mut().y = 55;
    println!("RefCell: {:?}", rc2.borrow());

    //Rc<Cell<Point>>
    let rcc = Rc::new(Cell::new(p1));
    let rcc1 = Rc::clone(&rcc);
    let rcc2 = Rc::clone(&rcc);
    println!("Cell: {:?}", rcc2);

    //Cell
    //let x = rcc1.get();
    rcc1.set(Point { x: 33, y: 33 });
    println!("Cell: {:?}", rcc2.get());
}

Перегуд В.

четверг, 9 января 2020 г.

Управление памятью в Rust. Часть 1.

Основные принципы устройства памяти и работы с ней рассмотрены в статье Память в C++. Сегодня поговорим как работает с памятью Rust в контексте его парадигмы владения ресурсами.

Мы знаем, что память делится на статическую, стек и кучу, но до сих пор мы не говорили, где физически размещаются данные и как это происходит. Автоматические переменные и функции размещаются в стеке, это должно происходить максимально быстро и эффективно, поэтому организовано с помощью стека, отсюда и название. В стеке переменные и функции размещаются в своих областях по областям видимости и это значит, что изначально при переходе в другую область видимости, например при присвоении или передаче параметров в функцию и возврате из нее, всегда происходит копирование с дублированием памяти, из старой переменной (аргумент) в новую (параметр). Дальше мы можем либо не обращать внимания на старую переменную (как в с++) – копирование владения, либо в целях безопасности очистить (как в Rust) – передача владения. С точки зрения производительности очистка нагружает систему, поэтому для базовых типов ее специально не производят, и она происходит при уничтожении переменных (выход из области видимости). И тут важно понять, что и при копировании владения и при передаче, происходит копирование данных из одной части стека в другую, только либо без очистки, либо с очисткой. Даже при работе со ссылками происходит тоже самое с адресами памяти, которые являются значением наших переменных-ссылок, что конечно же эффективнее, чем передавать сами значения, на которые ссылаемся, и удобство в данном случае компенсирует затраты.

У размещения в стеке есть свои преимущества – скорость, удобство, но есть и свои недостатки, данные должны быть фиксированной длинны (требование для стека), и операции с тяжелыми данными (от 4 Kb) снижают эффективность, на которую мы надеялись. В этом случае нам поможет динамическое выделение памяти, когда мы сами управляем памятью вручную. И для этого служит область памяти под названием куча. В ней мы можем разместить наши тяжелые данные и менять их размер при необходимости, плюс мы можем получать к ним доступ в нужные нам моменты, без необходимости постоянно их куда-нибудь перекидывать. Главное требование – не создавать их больше чем это действительно необходимо и не забыть их очистить, чтобы не было утечек памяти и условий для возникновения ошибки при неправильном обращении к этим участкам памяти. В си++ для этого используются указатели, сырые с возможность вручную выделять (new) и освобождать (delete) память, и умные, которые сами следят за выделением и очисткой памяти (unique_ptr и shared_ptr). В Rust для этого служат умные указатели Box и Rc, которые отличаются тем, что с помощью Box можно менять данные, на которые он указывает, а с помощью Rc нет.

Первый из них Box, это аналог unique_ptr, который является единственным владельцем ресурса и следит за его очисткой. Для Box есть одна особенность, если он указывает на трейт или на Any, с помощью dyn (смотрите статью про Объекты в Rust), то кастование-вниз(downcast) этого указателя в тип имплементирующий трейт или Any, перемещает владение ресурсом.

Второй из них это Rc, аналог shared_ptr, который является совладельцем ресурса и тоже следит за очисткой ресурса, когда больше уже не остается совладельцев, с помощью подсчета ссылок. У Rc тоже есть особенность, наличие живых совладельцев ресурса, заставляет жить и сам ресурс, с другой стороны, при уничтожении всех совладельцев (Rc-указателей) очистится и ресурс, данные будут уничтожены и память очищена. Если мы хотим быть совладельцем ресурса, но в тоже время не влиять на его жизнеспособность, то можем ослабить (weak) указатель. Но тогда мы должны учитывать возможность, что ресурс может быть еще жив или уже мертв по причине смерти остальных сильных совладельцев. Ослабить указатель можно с помощью Rc::downgrade. Проверить жив ли еще ресурс можно с помощью upgrade(), которая возвращает Options<Some(Rc), None>.

Тут стоит обратить внимание на один момент, казалось бы, что такие структуры данных, как строки и векторы, которые могут содержать тяжелые данные, нужно отдельно размещать в куче, но этого специально делать не нужно, т.к. они и так реализуют механизм размещения своих элементов в куче, в отличие от массивов. А хранить в куче указатели на ресурсы в куче, это вообще не эффективно. Поэтому использование массивов для хранения указателей на ресурсы в куче лучше, т. к. не нужно производить лишнее разыменовывание элементов. И вообще, куча — это плохо, она медленная.

use std::rc::Rc;

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {

    //Box
    let mut v_box: Vec<Box<Point>> = Vec::new();

    v_box.push(Box::new(Point { x: 0, y: 0 }));
    v_box.push(Box::new(Point { x: 1, y: 1 }));
    *v_box[0] = Point { x: 99, y: 99 }; //Ok!

    println!("v_box.len:{} v_box:{:?}", v_box.len(), v_box);

    //Rc
    let rc = Rc::new(Point { x: 2, y: 2 });

    let rc1 = Rc::clone(&rc);
    let rc2 = Rc::clone(&rc);
    //*rc2 = Point { x: 3, y: 3 }; //Error!
    let rc3 = Rc::clone(&rc);
    //let rc4:Rc<Point> = rc.clone(); // weak: Some(Rc)

    //Weak
    let weak = Rc::downgrade(&rc);
    {
        let v_rc = vec![rc, rc1, rc2, rc3];
        println!("v_rc.len:{} v_rc:{:?}", v_rc.len(), v_rc);

    } //weak: None

    let wr = weak.upgrade();
    println!("weak: {:?}", wr);
}

Перегуд В.

вторник, 7 января 2020 г.

Объекты в Rust. Часть 1.

Начинаем очень интересную тему – объекты в Rust, и сегодня поговорим про основные понятия, вроде инкапсуляции, а также про создание простых объектов. Следующий раз рассмотрим наследование, композицию, полиморфизм и т.д.

Думаю, следует начать с инкапсуляции. В общих чертах это объединение данных и методов в пользовательские типы. Обычно принято делать данные закрытыми и получать к ним доступ через открытые функции, с помощью которых гарантируется правильное изменение этих данных. Например, в C++ пользовательскими типами являются классы, и для разделения на закрытые и открытые члены используются блоки private и public. Целью этого является не столько улучшить жизнь программисту, сколько закрыть библиотеки от пользователей. И тут у одних заголовочные файлы, у других все на наследуемых классах. И первые, и вторые пилят систему модулей, но у них еще долгий путь, так как стандартные библиотеки написаны без учета модулей, тоже самое можно сказать про старые фреймворки. Но лучше поздно чем никогда.

Наверно нам повезло, т.к. в Rust уже есть система модулей. Но система модулей — это не только пространство имен для определения доступа, и это не только единица компиляции исходного кода, но и единица для динамического анализатора кода, что в свою очередь позволяет более удобно писать код. Например, без необходимости объявлять функцию до ее использования. Это делать не обязательно, но это пример того, что код анализируется более глубоко, что и доказывает компилятор, давая различные подсказки и точно указывая на ошибки. Замечу, что в Rust нет перегрузки функций. Чтобы нам закрыть данные, нужно поместить наши пользовательские типы в блок модуля с помощью "mod", который станет новым пространством имен. При обращении к этим данным понадобится указывать имя модуля "::" или импортировать "use" его в текущий модуль. Текущим модулем является текущий файл с исходным кодом, и как вы поняли, все другие файлы с исходным кодом являются модулями с именами файлов. По умолчанию сам модуль и все данные и функции в модуле являются закрытыми, и чтобы их открыть нужно указать их как “pub”. Таким образом мы можем открыть модуль для доступа извне, открыть тип, его данные и методы по отдельности.

Теперь давайте рассмотрим, как создать пользовательский тип в Rust. Сначала нам нужен набор данных, который будет хранить состояние объекта нашего типа. Это может быть структура с полями, например, координаты точки х и у. Чтобы привязать к этим данным функции, которые станут методами нашего типа, нужно поместить их в блок “impl”. Методы можно вызывать из объекта (с доступом к состоянию объекта) или из типа. Чтобы вызвать из объекта, нужно передать объект “self” (себя) или ссылку на него “&self” (ссылка на себя) в качестве первого параметра метода. Если данные представлены базовыми типами, то произойдет копирование значений в метод, если пользовательскими, то перемещение. По ссылке владение не передается. Если мы хотим изменить значение полей, то ссылка должна быть модифицирующей “&mut self”. Вызов метода из типа делается с помощью ::. Если мы хотим вернуть из метода тип, методом которого являемся, то можно использовать не имя структуры, а “Self” (Себя). Доступ к состоянию объекта (полям) осуществляется с помощью оператора точка “self.”.

Чтобы создавать и уничтожать объекты, в других языках существуют специальные методы: конструкторы и деструкторы. В Rust их нет. Тут работают стандартные правила. При выходе из пространства видимости, объект уничтожается и память освобождается. Для создания объекта по определенным правилам можно использовать методы с именами по договоренности, например метод new(), выполняет функции конструктора по умолчанию, а метод с параметрами from(), выполняет функции пользовательского конструктора или конструкторов копирования-перемещения. Обратите внимание, что определение копирования-перемещения задается не с помощью параметров при определении метода, а с помощью аргументов, с использованием clone() или без, при вызове. Смотрим пример.

//module
mod point {
    //private data
    #[derive(Debug, Clone)]
    pub(crate) struct Point {
         x: i32,
         y: i32,
    }

    //public methods
    impl Point {
        pub fn new() -> Self {
            Point { x: 0, y: 0 }
        }
        pub fn from(p: Point) -> Self {
            Point { x: p.x, y: p.y }
        }
        pub fn get_xy(&self) -> String{
            let a = self.x.to_string();
            let b = self.y.to_string();
            format!("X:{} Y:{}", a, b)
        }
        pub fn set_xy(&mut self, x:i32, y:i32){
            self.x = x;
            self.y = y;
        }
    }
}

//namespace
use point::Point;

fn main() {
    //Error!
    //let p0 = point::Point { x: 1, y: 1 };
    //println!("{:?}", p0);

    //default constructor
    let p1 = point::Point::new();
    println!("{:?}", p1);

    //copy assign
    let p2 = p1.clone();
    println!("{:?}", p2);

    //move assign
    let p3 = p2;
    println!("{:?}", p3);

    //copy constructor
    let p4 = Point::from(p3.clone());
    println!("{:?}", p4);

    //move constructor
    let mut p5 = Point::from(p3);
    println!("{:?}", p5);
 
    //methods
    p5.set_xy(3,3);
    println!("P5<{}>", p5.get_xy());
}

Перегуд В.

среда, 1 января 2020 г.

Соответствие образцу в Rust.

Продолжая тему, начатую в статье про обработку исключений в Rust. Мы говорили про обработку Result, и использовали проверку на соответствие образцу (pattern matching), с помощью ключевого слова match. О том, что это такое мы поговорим сегодня более подробно.

Давайте рассмотрим выражение x = z;. Предположим у нас есть переменные “x” и “z” со значениями и затем мы “иксу присваиваем зет”. При этом все нужные условия соблюдены и все работает без ошибок. Что происходит в этот момент? Сначала происходит проверка на соответствие типу, потом мы присваиваем значение. Нужно сказать, что при x = 5; происходит тоже самое. Проверка на соответствие типу происходит всегда, чтобы не допустить такого: let x:i32 = 5.0; Мы рассмотрели простой тип, но тоже самое происходит и со сложными типами, и пользовательскими в том числе. Например, при присвоении структур, происходит проверка на соответствие типов, имеющихся в структуре переменных, которые в свою очередь тоже могут быть структурами. А значит нам нужно проверить довольно сложную иерархию, после чего присвоить нужные значения нужным переменным в этой иерархии. Происходит что-то подобное на работу с регулярными выражениями, когда набор типов представляет из себя образец данных. Имея такой инструмент, почему бы не использовать его для других целей, например, в составе оператора switch из C/C++?

Напоминаю, что оператор switch заменяет собой набор операторов if-else if-else и помогает выполнить нужное действие, которое соответствует условию равенства определенному значению. Подробно останавливаться не будем, скажу только, что в C++ условие равенства — это просто true-false. Мы можем проверять только простые переменные по отдельности, пусть они даже находятся в составе сложных типов. А вот было бы здорово проверить на входе сложный тип целиком, который в свою очередь может представлять иерархию типов, о чем мы говорили выше. Это и есть проверка данных по образцу в Rust. Замечу, что мы можем проверять на соответствие между собой образцы только одинакового типа, зато образцы могут содержать любые варианты в рамках специальных правил. Например, мы можем использовать конкретные значения, либо указать имя внутренней переменной, которая будет содержать значение из поданного на вход для проверки типа, либо символ “_”, который обозначает – любое значение. При совпадении имен внутренней переменной и поданной на вход, можно упростить запись, вместо “x: x” использовать просто “x”. Также некоторые образцы дублируют по смыслу друг друга. Например, “x: y” соответствует “x: _”, только в первом варианте происходит присвоение значения из “x” во внутреннюю “y”, а во втором нет, но по смыслу они одинаковы – любое значение. Выход из проверки происходит при первом совпадении, поэтому если в середине будет стоять образец с “_”, то последующие варианты могут быть никогда не достигнуты.

Еще одно отличное свойство match про которое нельзя не сказать, это полное покрытие вариантов, при котором мы обязаны перечислить все возможные варианты для проверки. Благодаря этому компилятор сообщит об ошибке, если мы случайно забудем проверить какой-то из вариантов, что значительно повысит надежность кода.

При проверке на соответствие образцу пользовательских типов, которые в свою очередь, могут содержать тяжелые данные, логично принимать такие данные во внутреннюю переменную по ссылке, иначе произойдет перемещение или копирование ресурса. Но тут есть один нюанс, мы не можем использовать “&”, при котором произойдет передача не данных, а самого указателя. Вместо этого есть специально выделенное слово “ref”. Смотрим пример.

P.S. Возвращаясь к нашему Result, который может содержать пользовательский тип в качестве правильного варианта, мы можем проверить на соответствие несколько правильных вариантов с разными образцами пользовательского типа.

#[derive(Debug)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let (x, y, z) = (1, 2, 3);
    println!("destructuring tuple (1,2,3) to x={}, y={}, z={}", x, y, z);

    let (_, sub_t @ (_, _)) = (1, (2, 3));
    println!("destructuring sub-tuple from (1,(1,2)) to sub_t={:?}", sub_t);

    let (v, ..) = (1, (2, 3));
    println!("destructuring first element from tuple (1,(1,2)) to v={:?}", v);

    let mut p1 = Point { x: 0, y: 5 };
    println!("\nmatch {:?} with:", p1);

    let X = 0;
    match p1 {
        Point { x: 5, y: 5 } => println!("pattern: 5, 5"),
        Point { x: z, y: 5 } if z != 0 => println!("pattern: z={}, 5", z),  //match guard
        Point { x, y: 5 } if x == X => println!("pattern: x={}, 5", x),  //match guard
        Point { x: _, y: 5 } => println!("pattern: _, 5"),
        _ => (),
    }

    let mut p2 = Point { x: 1, y: 10 };
    println!("\nmatch {:?} with:", p2);
    match p2 {
        Point { x: ref x, y: 10 } => println!("pattern: ref x={}, 10", x),
        Point { ref x, y: 10 } => println!("pattern: ref x={}, 10", x),
        _ => (),
    }
}

//Output:
>>destructuring tuple (1,2,3) to x=1, y=2, z=3
>>destructuring sub-tuple from (1,(1,2)) to sub_t=(2, 3)
>>destructuring one element from tuple (1,(1,2)) to v=1
>>
>>match Point { x: 0, y: 5 } with:
>>pattern: x=0, 5
>>
>>match Point { x: 1, y: 10 } with:
>>pattern: ref x=1, 10

Отдельный пример с перечислением (enumeration):

#[derive(Debug)]
pub enum Drive {
    Forward(u8),
    Turn { slight: bool, right: bool },
    Stop,
}

fn main() {
    let path = [
        Drive::Forward(3),
        Drive::Turn { slight: false, right: true },
        Drive::Forward(1),
        Drive::Stop,
    ];
    println!("path to home: {:?}", path);

    for step in path.iter() {
        match step {
            Drive::Forward(blocks) => {
                println!("Drive forward {} blocks", blocks);
            }
            Drive::Turn { slight, right } => {
                println!("Turn {}{}",
                         if *slight { "slightly" } else { "" },
                         if *right { "right" } else { "left" }
                );
            }
            Drive::Stop => {
                println!("You have reached your home!");
            }
        }
    }
}

//Output:
>>path to home: [Forward(3), Turn { slight: false, right: true }, Forward(1), Stop]
>>Drive forward 3 blocks
>>Turn right
>>Drive forward 1 blocks
>>You have reached your home!

Отдельный пример - матчим параметр (&self) метода damage():

#[derive(Debug)]
pub enum Status {
    Alive { health: u8 },  //u8 - 0..255
    Dead,
}

#[derive(Debug)]
pub struct Archer {
    name: String,
    status: Status,
}

impl Archer {
    pub fn new(_name: String, _health: u8) -> Self {
        Archer { name: _name, status: Status::Alive { health: _health } }
    }

    pub fn damage(&mut self, points: u8) {
        match self.status {
            Status::Alive { health: x } => self.status = Status::Alive { health: x - points },
            Status::Dead => {}
        }
    }
}

fn main() {
    let mut obj1 = Archer::new("Bob".to_string(), 100);
    obj1.damage(35);
    println!("{:?}", obj1.status)
}

//Output:
>>Alive { health: 65 }

Перегуд В.