Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Тема
Описание
Доп.

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

1. Сумма (Sum Types) — выбор одного из вариантов.

enum Shape {
     Circle(f64),
     Rectangle(f64, f64),
}
Значение Shape — либо Circle, либо Rectangle, но не оба.
Если Circle может быть 10 значений, и Rectangle — 20, то сумма всех вариантов Shape может быть 10 + 20 = 30 возможных значений.

2. Произведение (Product Types) — комбинация нескольких значений. Это типы, которые содержат несколько значений одновременно. Примеры: tuple, struct — всё, где есть набор полей.

struct Point {
    x: i32,
    y: i32,
}
Каждый Point содержит и x, и y — это произведение значений. 
Если x может быть 10 значений, а y — 10 значений, то произведение всех вариантов Point может быть 10 × 10 = 100 разных значений.

3. Комбинирование Вы можете вложить сумму в произведение, и наоборот

struct Drawing {
    shape: Shape,
    color: String,
}  

Итого Drawing = (Circle(f64) + Rectangle(f64, f64)) × String

Алгебраические типы данных хорошо подходят для:

  • Pattern matching (сопоставление с образцом) — идеальный способ работы с sum types.
  • AST (синтаксические деревья),
  • моделей состояний (enum State { Init, Loading, Ready, Error(String) }),
  • парсинга и интерпретации,
  • сериализации/десериализации.

Типобезопасность (type safety) в Rust относится к строгому управлению типами данных, которое предотвращает ошибки, связанные с неправильным использованием типов данных. Это одна из ключевых особенностей языка, которая способствует надежности и безопасности кода.

Основные аспекты типобезопасности в Rust включают:

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

Сильная типизация: В Rust типы данных строго разделены, и не допускаются неявные преобразования между типами. Например, нельзя автоматически преобразовать i32 в u32 без явного указания разработчика.

Вывод типов: Хотя Rust строго типизирован, он также поддерживает вывод типов. Это значит, что компилятор может автоматически определить тип переменной на основе контекста, что упрощает написание кода.

Система заимствования и владения: Rust применяет концепцию владения и заимствования для управления памятью. Каждое значение имеет владельца, и система заимствования позволяет временно использовать значение без передачи прав владения. Это предотвращает ошибки, такие как использование неинициализированной памяти или двойное освобождение памяти.

Концепция Option и Result: Rust не имеет встроенных указателей на нулевые значения (null). Вместо этого используются типы Option и Result для обработки случаев, когда значение может отсутствовать или операция может завершиться с ошибкой. Это вынуждает разработчиков явно обрабатывать такие случаи, что способствует написанию более безопасного кода.

Занимаемая память на стеке байт

mem/fn.size_of

Размер типов sizedness-in-rust

measuring-type-sizes


fn main(){
 std::mem::size_of_val(&[1,2]) // возвращает размер переменной в байтах
 std::mem::size_of::()
 std::mem::align_of::<(f32, u8)>()
}

Значение любого типа, реализующего характеристику Sized, занимает постоянное число байтов в памяти и должно размещаться по адресу, кратному некоторой «границе выравнивания», которая определяется машинной архитектурой. Выравнивание любого типа является степенью двойки.

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


fn main(){
 std::mem::size_of::<(f32,u8)>() // == 8 bytes (Хотя f32 4 bytes + u8 1 byte = 5 bytes)

// Но выравнивание у этого типа  (f32,u8) занимает 4 bytes :
 assert_eq!(std::mem::align_of::<(f32, u8)>(), 4);.
// Т.е. адрес для этого типа в памяти не может быть +5, а как минимум +8
}

std::any::type_name_of_val(&T)

получить строковое имя типа, если у вас есть ссылка на значение


fn main(){
// std::any::type_name_of_val(&T)

 let my_int=6;
 println!("Тип my_int: {}", std::any::type_name_of_val(&my_int)); // Тип my_int: i32
}

Тип переменной

std::any::type_name()


fn print_type_of(_: &T) {
    println!("{}", std::any::type_name::())
}
fn main() {
    let s = "Hello";
    let i = 42;

    print_type_of(&s); // &str
    print_type_of(&i); // i32
    print_type_of(&main); // playground::main
    print_type_of(&print_type_of::); // playground::print_type_of
    print_type_of(&{ || "Hi!" }); // playground::main::{{closure}}
}

Способ создать переменные в одну строчку


fn main(){
    // Способ создать переменные в одну строчку
    let (a, b, c) = (1.2, 3.4, "Hello");
}

Аннотации:


fn main(){
    let a_float: f64 = 1.0;  // Обычная аннотация
    let an_integer   = 5i32; // Суффиксная аннотация

    let default_float   = 3.0; // `f64` присвоен тип по умолчанию.
}

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

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

  • шестнадцатеричного 0x
  • восьмеричного 0o
  • двоичного 0b

Для улучшения читаемости числовых литералов можно использовать подчёркивания:

  • 1_000 тоже самое, что и 1000
  • 0.000_001 тоже самое, что и 0.000001
Decimal    98_222
Hex           0xff
Octal         0o77
Binary       0b1111_0000
Byte (u8 only)        b'A'

fn main(){
 let hex_octal_bin = 0xffff_ffff + 0o777 + 0b1;
 let byte:u8 = b'a';
 assert_eq!(byte,65);
}

Литералы — целое число + суффикс:


fn main(){
    let y = 92_000_000i64;
    let hex_octal_bin = 0xffff_ffff + 0o777 + 0b1;// 16 + 8 +2 ричные системы счисления
    let byte: u8 = b'a'; // b' - ASCII кодировка. Код символа
    assert_eq!(byte, 65);

// Литералы с суффиксами. Их тип известен при инициализации.
    let x = 1u8;
    let y = 2u32;
    let z = 3f32;

// Литералы без суффиксов. Их тип будет зависеть от того, как их используют.
    let x = 1;
    let y = 1;

    let x2:i8 = x;
    let y2:i32 = y;
    assert_eq!(1, std::mem::size_of_val(&x)); 
    assert_eq!(4, std::mem::size_of_val(&y));
}

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

 &    println!("0011 И 0101 будет {:04b}", 0b0011u32 & 0b0101);
 |    println!("0011 ИЛИ 0101 будет {:04b}", 0b0011u32 | 0b0101);
 ^    println!("0011 исключающее ИЛИ 0101 будет {:04b}", 0b0011u32 ^ 0b0101);
 <<   println!("1 << 5 будет {}", 1u32 << 5);
 >>   println!("0x80 >> 2 будет 0x{:x}", 0x80u32 >> 2);

as - безопасный, transmute - не безопасный

Приведение типов

Нет неявного приведения типов, только явное через as и into

type-coercions


fn main(){
 let x: u16 = 1;
 let y: u32 = x; // error: mismatched types
 let y: u32 = x.into(); // Расширение без потери точности
 let z: u16 = y as u16; // Берём младшие биты
 let to_usize = 92u64 as usize;
 let from_usize = 92usize as u64;

// Приведение типов
// as — оператор явного приведения типов

 let y:u32 = 1u16.into(); // расширение без потери точности
 let z:u16 = y as u16; // берем младшие биты (явное приведение)

// Будьте осторожны с приведением больших типов к меньшим.
 assert_eq!(88_u8, 600_i32 as u8); // математически как 600 − 256 − 256 = 88
}

as - безопасный если не не допускать обрезания

Обрезание (truncating)


fn main(){
 let x: i32 = 5;
 let y = x as i64;
}

Проблемы переполнения типов при приведении.

Это можно представить как стакан, который может вместить только 100 мл воды.

Обрезание (truncating): если вы нальёте 120 мл, вода выльется, но система, не замечая этого, будет считать, что в стакане осталось 20 мл (120 - 100 = 20). Это может привести к непредсказуемым ошибкам, так как вы потеряли информацию.

Насыщение (saturating): если вы нальёте 120 мл, стакан просто заполнится до 100 мл, и система будет знать, что в нём 100 мл, даже если вы попытались налить больше. Если вы попытаетесь вылить больше, чем есть, количество воды будет ограничено нулём.

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


fn main(){
    let big_num: i32 = 1000;
    let small_num: i8 = big_num as i8;
    assert_eq!(small_num, -24);
    println!("Truncating: {} as i8 becomes {}", big_num, small_num);
    // 1000 в бинарном представлении i32 - это 00000000 00000000 00000011 11101000
    // i8 имеет только 8 бит может иметь значение от -128 до 127. Отбрасываем старшие биты
    // Остается 11101000, что в знаковом i8 является -24

    let a = 1024_f32 as u8; // a = 255 (максимум u8)  
    let b = 11.9_f32 as u8; // b = 11 (усечение)
}

Насыщение (saturating). Rust не выполняет насыщающее (saturating) преобразование типов автоматически с помощью as. В насыщающем преобразовании, если значение выходит за пределы целевого типа, оно не обрезается, а насыщается до минимального или максимального значения этого типа.

Для выполнения насыщающего преобразования необходимо использовать методы, такие как saturating_add(), saturating_sub(), saturating_mul(), но для самого преобразования типов напрямую такого метода нет.


fn main() {
    let a: u8 = 200;
    let b: u8 = 100;
    let c: u8 = a.saturating_add(b);

    println!("Результат насыщающего сложения 200 + 100: {}", c);
    assert_eq!(c, 255); // 200 + 100 = 300, но u8 максимум - 255, поэтому насыщается до 255.

    let d: i8 = -100;
    let e: i8 = -50;
    let f: i8 = d.saturating_add(e);

    println!("Результат насыщающего сложения -100 + (-50): {}", f);
    assert_eq!(f, -128); // -100 + (-50) = -150, но i8 минимум - -128, поэтому насыщается до -128.
}

Целочисленное переполнение

ch03-02-data-types

constant.PI/saturating

wrapping_*

wrapping_* методы выполняют арифметические операции, которые "оборачиваются" в случае переполнения. Это означает, что если результат превышает максимальное значение для типа, он начинается с минимального значения (и наоборот).


fn main() {
    let a: u8 = 250;
    let b: u8 = 10;
    // 250 + 10 = 260. 260 - 256 (максимум + 1) = 4
    let sum_wrapped = a.wrapping_add(b);
    println!("Результат wrapping_add: {}", sum_wrapped);
    assert_eq!(sum_wrapped, 4);

    let x: i8 = 120;
    let y: i8 = 10;
    // 120 + 10 = 130. 130 - 256 (переполнение) = -126
    let sum_wrapped_signed = x.wrapping_add(y);
    println!("Результат wrapping_add (знаковое): {}", sum_wrapped_signed);
    assert_eq!(sum_wrapped_signed, -126);
}

checked_*

checked_* методы возвращают Option<T>, где Some(T) содержит результат, если переполнения не было, и None, если оно произошло. Это позволяет вам явно обрабатывать ошибки переполнения.


fn main() {
    let a: u8 = 250;
    let b: u8 = 10;
    let sum_checked = a.checked_add(b);

    if let Some(sum) = sum_checked {
        println!("Сложение успешно, результат: {}", sum);
    } else {
        println!("Переполнение: 250 + 10 превышает u8");
    }
    assert_eq!(sum_checked, None);

    let x: u8 = 10;
    let y: u8 = 5;
    let sum_checked_ok = x.checked_add(y);
    assert_eq!(sum_checked_ok, Some(15));
}

overflowing_*

overflowing_* методы возвращают кортеж (T, bool). Первый элемент — это результат операции (который может быть переполнен), а второй — логическое значение, указывающее, произошло ли переполнение.


fn main() {
    let a: u8 = 250;
    let b: u8 = 10;
    let (sum_val, did_overflow) = a.overflowing_add(b);

    println!("Результат overflowing_add: {}", sum_val);
    println!("Было переполнение: {}", did_overflow);
    assert_eq!(sum_val, 4);
    assert_eq!(did_overflow, true);

    let x: u8 = 10;
    let y: u8 = 5;
    let (sum_val_ok, did_overflow_ok) = x.overflowing_add(y);
    assert_eq!(sum_val_ok, 15);
    assert_eq!(did_overflow_ok, false);
}

saturating_*

saturating_* методы выполняют арифметические операции с насыщением. Если результат превышает максимальное значение для типа, он "насыщается" до этого максимума. Если он становится меньше минимума, он насыщается до минимума.


fn main() {
    let a: u8 = 250;
    let b: u8 = 10;
    let sum_saturated = a.saturating_add(b);

    println!("Результат saturating_add: {}", sum_saturated);
    assert_eq!(sum_saturated, 255); // u8::MAX

    let x: i8 = -100;
    let y: i8 = -50;
    let sum_saturated_signed = x.saturating_add(y);
    
    println!("Результат saturating_add (знаковое): {}", sum_saturated_signed);
    assert_eq!(sum_saturated_signed, -128); // i8::MIN
}

transmute - не безопасный


use std::mem;
fn main(){
 unsafe {
    let a = [0u8, 0u8, 0u8, 0u8];
    let b = mem::transmute::<[u8; 4], u32>(a);
 }
}

добавление к типу turbofish ::<> позволяет задать подсказку типа

с/без turbofish:


fn main(){
 let v = "42".parse().unwrap(); // Компилятор сам решает, что `v: i32`
 let v = "42".parse::().unwrap(); // Явно указали тип `i32`
}

generic функция


fn max(a: T, b: T) -> T {
    if a > b { a } else { b }
}
fn main(){
 let n = max::(10, 20); // Явно указали тип `T = i32`
 // Без `::` компилятор смог бы догадаться, но если аргументы разные или контекст сложный — пригодится.
}

При работе с коллекциями


fn main(){
 let v = Vec::::new(); // Создаём пустой вектор i32
}

Пример с Option/Result


fn main(){
 let x = None; // ❌ Ошибка: компилятор не знает тип T
 let x = None::; // ✅ Теперь x: Option

 let x = Some(42); // Компилятор автоматически выведет Option
 let y = Some::(42); // ✅ Явно указали нужный тип u8

 let res = Ok::(10);       // ✅ через turbofish
 let err = Err::("Ошибка"); // ✅ Типы: Result
}

TryFrom / TryInto (безопасное преобразование)

Для случаев, когда возможна ошибка.

Если x вне диапазона u8, будет Err.


use std::convert::TryFrom;
fn main(){
   let x: i32 = 150;
   let y = u8::try_from(x); // Result
}

Преобразование from into и обратно

Если преобразовывать через from/into из типа большего диапазона в тип с меньшим диапазоном, то компилятор гарантирует, что преобразование будет работать без потери данных или переполнения.


fn main(){
    let x:u32 = u32::from(10u8); // безопасно
}

Если бы вы попытались сделать обратное преобразование (из u32 в u8), компилятор бы выдал ошибку, так как это не всегда безопасно, и потребовал бы явного приведения типа (as). Так как as может обрезать (truncating) биты и приводить к неожиданным результатам.


fn main(){
     // let x:u8 = u8::from(10u32); ❌ ERROR
     let x:u8 = 10u32 as u8; // ✅ OK
}


use std::convert::From;
#[derive(Debug)]
struct Number {
    value: i64
}
impl From for Number {
    fn from(item: i64) -> Self {
        Number { value: item }
    }
}
fn main() {
    let num = Number::from(30);
    //let num = Number{value:30};
    println!("My number is {:?}", num);

    let int = 5;
    let num: Number = int.into();
    println!("My number is {:?}", num);
}

Преобразование чисел


fn main(){
    let n:u16 = 255+255+255+255;// [3, 252] = 3*255 + 255-3 (u8 max 255)
    let s:[u8;2] = n.to_be_bytes();
    println!("\n{:?}",s); // [3, 252]
}

Строка → Числа


fn main(){
 let s = "42";
 let n = s.parse::().unwrap();
 assert_eq!(n, 42i32);
}

Преобразование чисел из Bytes

crate bytes


use bytes::{Bytes,BytesMut};
use std::convert::{From,TryFrom};
#[derive(Debug)]
struct Number(usize);
/* impl From for Number {
    fn from(item: Bytes) -> Self {
     let v =  item.into_iter().collect::>();
     Number(usize::from_be_bytes(v.try_into().unwrap()))
    }
} */
impl TryFrom for Number {
    type Error = String;
    fn try_from(item: Bytes) -> Result {
        let v = item.into_iter().collect::>();
        match v.try_into() {
            Ok(arr) =>{ Ok(Number(usize::from_be_bytes(arr))) },
            Err(e) => { Err(format!("{:?}",e)) }
        }
    }
}
fn main() {
    let n:u128 = 478;
    let arr:[u8; 16] = n.to_be_bytes();
    
    let n:usize = 478;
    //println!("size of `n` in bytes: {}", std::mem::size_of_val(&n));//8
    let arr:[u8; 8] = n.to_be_bytes();
    println!("{:?}",arr);// [0, 0, 0, 0, 0, 0, 1, 222]
    {
        let v:Vec = arr.to_vec();
        let b:Bytes = Bytes::from(v);
        println!("{:?}",b);// b"\0\0\0\0\0\0\x01\xde"
        
       //  Number::from(b);
       if let Ok(n) = Number::try_from(b){
           println!("n={:?}",n.0);
       }
    }
    {
        let mut b:BytesMut = BytesMut::with_capacity(64);
        b.extend_from_slice(&arr);
        let b_not_mut:Bytes = b.freeze();
        let v:Vec = arr.to_vec();
        let b:Bytes = v.into();
        println!("{:?}", b);// b"\0\0\0\0\0\0\x01\xde"
    }
    let a:Bytes = Bytes::from(&b"hello world"[..]);
    println!("{:?}",a);// b"hello world"
    
    let value:usize = usize::from_be_bytes([0, 0, 0, 0, 0, 0, 1, 222]);
    println!("{:?}",value);
    
    let b:[u8;8] = usize::to_be_bytes(n);
    println!("{:?}",b);
    
    /*  b.extend_from_slice(&arr);
    println!("{:?}", b);// b"\0\0\0\0\0\0\x01\xde"
    let mut b_not_mut = b.freeze();
    let arr:&[u8] = &*b_not_mut;
    
    unsafe{
        let s:String = String::from_utf8_unchecked(arr.to_vec());
        println!("{}", s);// муть :)))
    } */
}

as преобразования разных типов

type-conversions/Cast

1. Указатель -> Указатель (Pointer -> Pointer)


fn main(){
// Это преобразование меняет тип данных, на который указывает указатель, не изменяя его адреса. Это полезно, когда вы работаете с сырой памятью.
    let mut num = 10;
    let ptr_i32: *mut i32 = &mut num;
    // Безопасное преобразование *mut i32 в *mut u8
    let ptr_u8: *mut u8 = ptr_i32 as *mut u8;

    println!("Адрес num как i32: {:p}", ptr_i32);
    println!("Адрес num как u8:  {:p}", ptr_u8);
    // Адреса одинаковы, но теперь мы можем работать с памятью побайтно
}

2. Указатель -> Целое число (Pointer -> Integer)


fn main(){
// Это преобразование позволяет получить числовое представление адреса, на который указывает указатель. Тип usize гарантированно имеет достаточный размер, чтобы вместить любой указатель.
    let num = 100;
    let ptr: *const i32 = &num as *const i32;
    // Безопасное преобразование указателя в usize
    let address: usize = ptr as usize;

    println!("Адрес num в виде указателя: {:p}", ptr);
    println!("Адрес num в виде числа:    {}", address);
}

3. Перечисление без полей -> Целое число (enum w/o fields -> Integer)


// Это преобразование безопасно, так как каждому варианту перечисления без полей по умолчанию присваивается целочисленное значение, начиная с нуля.
#[derive(Debug)]
enum Status {
    Pending,
    Active,
    Completed,
}
fn main() {
    let status = Status::Completed;
    // Безопасное преобразование варианта Completed в его числовое представление
    let status_val = status as u8;

    println!("Значение `Status::Completed` как число: {}", status_val);
    assert_eq!(status_val, 2); // Pending=0, Active=1, Completed=2
}

4. bool -> Целое число (bool -> Integer)


fn main(){
// Преобразование логических значений в целые числа всегда безопасно, поскольку true всегда соответствует 1, а false — 0.
    let is_ok = true;
    let is_error = false;
    
    // Безопасное преобразование bool в i8
    let ok_val = is_ok as i8;
    let error_val = is_error as i8;

    println!("`true` как i8: {}", ok_val);
    println!("`false` как i8: {}", error_val);
    assert_eq!(ok_val, 1);
    assert_eq!(error_val, 0);
}

5. char -> Целое число (char -> Integer)


fn main(){
// В Rust char является 4-байтовым типом, представляющим символ Unicode (от U+0000 до U+D7FF и от U+E000 до U+10FFFF). Его можно безопасно преобразовать в целочисленный тип, который может вместить кодовую точку, например, u32.
    let ch = 'A';
    // Безопасное преобразование символа в его кодовую точку u32
    let char_code = ch as u32;

    println!("Символ '{}' как u32: {}", ch, char_code);
    assert_eq!(char_code, 65); // Кодовая точка ASCII для 'A'
}
Логические операции:
|| (дизъюнкция, логическое сложение) т.е. ИЛИ
&& (конъюнкция, логическое умножение) т.е. И
!  (операция отрицания) т.е. НЕ

let y: bool = false;


fn main(){
 let bool_val:bool = true && false || false;
 assert!(!bool_val);

 assert_eq!(true as i32, 1);
 assert_eq!(false as i32, 0);

 match bool_val {
    true => println!("keep praising!"),
    false => println!("you should praise!"),
 }
}
let to_be: bool = true;
let not_to_be = !to_be;
let the_question = to_be || not_to_be;

&& и || “ленивые”
нет неявного приведения к bool
let i = 1;
let b: bool = i == 0;
let i = b as i32;

число нельзя привести к Boolean

Boolean можно привести к числу


fn main(){
 let true_false = (true, false);
 println!("{} {}", true_false.0 as u8, true_false.1 as i32);
}

fn main(){
 let true_false: (i128, u16) = (true.into(), false.into());
 println!("{} {}", true_false.0, true_false.1);
}

Но число нельзя привести к Boolean

два метода, .then() и .then_some(), которые превращают bool в Option


fn main(){
 let (tru, fals) = (true.then(|| 8), false.then(|| 8));
 println!("{:?}, {:?}", tru, fals); // Some(8), None
}

fn main(){
    assert_eq!(true as i32, 1);
    assert_eq!(false as i32, 0);
}

Числовые типы

(i integer -1 - +1) знаковые  
(u 0-1) беззнаковые
(i32) фиксированного размера
(isize, usize) переменного размера
(f32, f64) числа с плавающей точкой 
(i32, u32) целые числа

Числовые литералы Пример
Десятичный        98_222
Шестнадцатеричный 0xff
Восьмеричный      0o77
Бинарный          0b1111_0000
Байтовый (только u8) b'A'

fn main(){
    let str_u8 = b"10000000";
    let a = 0b1000_0000;
    let b = a as i8;
    assert_eq!(-128,b);
}

Crate num, num-traits, num-derive.

crate num, num-traits, num-derive.

Не только для "числового" кода - полезные преобразования в целые числа и т. д.

Числа со знаком i8, i16, i32, i64, isize

Числа со знаком устроены так, что после самого большого положительного 127_i8 мы падаем в самое «дно» отрицательного -128_i8

Для i8 диапазон от -128 … +127

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

т.е. функция abs(x) должна возвращать абсолютное значение (модуль), то есть превращать минус в плюс.

abs(-128)$ ОШИБКА / ПАНИКА

Флаг в Cargo.toml: Ты можешь заставить Rust паниковать при переполнении даже в release. Это самый надежный способ:

[profile.release]
overflow-checks = true

Пример:


fn main() {
    let mut i:i8 = 127; 
    println!("{i} {:08b}", i);// 127 01111111
    println!("{:08b}+{:08b}={:08b}={}", 1,i, 1+i, 1+i);// 00000001+01111111=10000000=-128
    // в знаковых числах (дополнительном коде) самый старший бит имеет вес -128
    // и далее дойдем до -1 и когда итерируем еще на +1 получим 0, и мы снова в начале цыкла
    
    /*
    В компьютере нет «стенки» между положительными и отрицательными числами. 
    Все числа в 8-битной ячейке идут по кругу, как на циферблате часов.
    Дополнительный код — это просто соглашение, что старший бит 1 имеет «отрицательный вес» (-128 для 8 бит)
    */
    // В режиме DEBUG будет panic
    // В режиме RELEASE при i=255, i перескочит снова на i=0
    let mut i:i8 = 0; 
    for _ in 0..=256 {

        println!("{i} {:08b}", i);
        i+=1;
    }
    /*
    0 00000000
    1 00000001
    2 00000010
    ...
    125 01111101
    126 01111110
    127 01111111
    -128 10000000 !!!
    -127 10000001
    -126 10000010
    ...
    -2 11111110
    -1 11111111
    0 00000000 !!!
    */
}

Вариант вечного цикла, основан на переходе от 127 к -128 за счет подавления переполнения режимом release

fn main() {
    let mut i: i8 = 125; 
    while i <= 127 {
        println!("{i}");

        // Используем wrapping_add, чтобы принудительно пройти через переполнение типа от 127 -> -128 без предупреждений от компилятора
        // Это способ сказать компилятору и другим программистам: «Я специально хочу, чтобы это число зациклилось. Это не ошибка, это фича!»
        // i = i.wrapping_add(1);

        i +=1; // в режиме Debug будет panic, в режиме Relese пройдет через переполнение
        
        if i == 5 { break; } // Выходим, когда прошли круг и вернулись к 5
    }
}

Вещественные числа float

Представление значений с плавающей точкой в стандарте IEEE (процессор переключает представление автоматически в зависимости от значения)

  • Нормализованные, стандартный способ хранения чисел - Могут быть ошибки округления, чем больше само число, тем больше «шаг» между соседними представимыми числами.
  • Ненормализованные, когда экспонента состоит из одних нулей - Внезапное падение производительности (FPS в игре или скорость нейросети). Ненормализованные значения (очень близкие к нулю) могут сильно замедлять расчеты. Есть удобные библиотеки (crates), которые позволяют переключать режим процессора одной строчкой кода для всего потока. Это часто делают в аудио-плагинах или при обучении нейросетей. На многих процессорах операции с ненормализованными числами выполняются в 10-100 раз медленнее, потому что они обрабатываются микрокодом или вызывают прерывания.
  • Особые (Infinity и NaN) - Могут быть при ошибке логики, когда программа продолжает работать, выдавая NaN вместо данных. Infinity (Бесконечность): Возникает при делении на ноль или при переполнении. Например, у типов f32/f64 есть встроенные методы: x.is_nan() или x.is_infinite()

Например, числа начиная от 1.0 * 10^-42 уже ненормализованные

fn main(){
    // Пример на Rust
    let mut x: f32 = 1e-37; // Еще нормализованное
    for _ in 0..10 {
        x /= 10.0;
        println!("{}", x); // Скоро мы окажемся в зоне "тормозов"
    }
}

Пример, особые представления чисел (Infinity и NaN)

fn main(){
    let x = 0.0 / 0.0; // Это NaN
    let y = 1.0 / 0.0; // Это Infinity

    if x.is_nan() {
        println!("Это не число!");
    }

    if y.is_infinite() {
        println!("Мы улетели в бесконечность!");
    }

    if x.is_finite() {
        println!("Это нормальное конечное число");
    } else {
        println!("Это либо NaN, либо Infinity");
    }
}

В десятичной системе 0.1 — это короткая и «красивая» дробь. Но для компьютера, который мыслит двоичными разрядами, 0.1 — это такая же бесконечная дробь, как 1/3=0.333333333333... для нас.

В двоичной системе основание 2. Поэтому только дроби, где в знаменателе стоит степень двойки (1/2, 1/4, 1/8, 1/16...), будут конечными.

Человек глазами видит «0.1», но процессор видит «0.0999999998...». И когда ты сложишь это 10 раз, ты получишь не 1.0, а 0.999999998....

Современные инженеры не используют float для времени. Они делают так:

Хранят время в целых числах (тиках): long ticks = 3600000;


В Rust результат сравнения NaN == NaN всегда false. Из-за этого типы с плавающей точкой не реализуют трейт Eq (полное равенство), только PartialEq.

Ты не можешь просто так использовать f32 в качестве ключа в HashMap или сортировать их без специальных ухищрений.

📌 float из чего состоит

FloatConverter/IEEE754

Библиотека с работы числами

Float:

  • Книга Rust in Action — глава "Data in depth" (Глава 5)
  • Книга Programming Rust, 3rd Edition — глава "Fundamental Types"
  • Книга Компьютерные системы. Архитектура и программирование [2022] Брайант Р. Э., О'Халларон Д. Р. — глава 2.4.2. Представление значений с плавающей точкой в стандарте IEEE

Эта глава рисует низкоуровневую картину: как представляются данные, что происходит "под капотом", и как устроены числа с плавающей точкой на уровне битов.

Числа с плавающей запятой (f32, f64) в Rust – это реализация стандарта IEEE 754

  • f32 – 32-битное число с плавающей запятой (примерно 7 десятичных знаков точности).
  • f64 – 64-битное число с плавающей запятой (примерно 15–16 знаков точности).
// Например, f64 → 1 бит знак, 11 бит экспонента, 52 бита мантисса.

Для f32:

[ знак (1 бит) ][ экспонента ][ мантисса ]

[sign (1 bit)][exponent (8 bit)][fraction bits (23 bit)]


Основные особенности:

  • Нет бесконечной точности → некоторые числа невозможно представить точно (например, 0.1).

Есть спецзначения:

  • NaN (Not a Number) – результат некорректных операций (0.0 / 0.0)
  • inf и -inf – переполнение

Порядок операций имеет значение → (a + b) + c может дать другой результат, чем a + (b + c)

1. Потеря точности

fn main() {
    let x = 0.1_f64 + 0.2_f64;
    println!("{}", x); // ❌ 0.30000000000000004
// Потому что 0.1 и 0.2 не представимы точно в двоичной системе.
// В битах 0.1 выглядит как бесконечная дробь (как 1/3 в десятичной системе).
}

2. Сравнение через == опасно

fn main() {
    let a = 0.1_f64 + 0.2_f64;
    let b = 0.3_f64;
    println!("{}", a == b); // ❌ false
}
fn main(){
    let eps = 1e-10;
    if (a - b).abs() < eps {
        println!("почти равно"); // ✅
    }
}

3. Ассоциативность и накопление ошибки

fn main(){
    let x = 1e16_f64;
    println!("{}", x + 1.0 == x); // ❌ true (!)
    // Потому что 1.0 теряется в масштабе огромного числа.
}

4. NaN ловушки NaN никогда не равен ничему, даже самому себе:

fn main(){
    let x = f64::NAN;
    println!("{}", x == x); // ❌ false
    // Для проверки: x.is_nan()
    assert!(x.is_nan());
}

5. Переполнения и underflow

  • Слишком большое число → inf.
  • Слишком маленькое → 0.0
fn main(){
// Слишком большое число → inf
    let big = 1e308_f64; // очень большое число, близкое к пределу f64
    let bigger = big * 10.0; // ещё увеличиваем
    println!("big: {}", big);
    println!("bigger: {}", bigger); // ❌ выведет inf
// f64::MAX ≈ 1.7976931348623157e308. Умножив на 10, выходим за предел → +∞
// ------------------------------------------
// Слишком маленькое → 0.0
    let small = 1e-308_f64; // очень маленькое число
    let smaller = small / 1e10; // делим ещё на 10^10
    println!("small: {}", small);
    println!("smaller: {}", smaller); // ❌ выведет 0.0
// f64::MIN_POSITIVE ≈ 2.2250738585072014e-308. После деления получаем значение меньше этого → представляется как 0.0.
}

С плавающей точкой f32 (4 байта), f64 (8 байт)

fn main(){
    let d:f64 = 111.55555555555559; // двойная точность
    let f:f32 = 111.55556; // одинарная точность
    print!("float {}\n double {}\n",f,d);

// По умолчанию создается f64
    let d = 111.55;// default f64
    assert_eq!(8, std::mem::size_of_val(&d)); // 8 bytes
}

fn main(){
    let y = 0.0f32; // литерал f32
    let x = 0.0; // тип выводится, f64 по умолчанию
    // точка обязательна
    let z: f32 = 0; // error: expected f32, found integer variable
    let z: f32 = 0.0;
    let not_a_number: f32 = std::f32::NAN;
    let inf: f32 = std::f32::INFINITY;
    // есть куча методов
    8.5f32.ceil().sin().round().sqrt()
}

Чарльз Петцольд. КОД тайный язык информатики Фиксированная точка, плавающая точка

Например: число 16_777_216_f32

Число одинарной точности с плавающей точкой имеет точность до одной части из 224 (одной части из 16_777_216, примерно до шести частей из 100 миллионов). Что это значит на самом деле? Во-первых, если вы попытаетесь выразить значения 16_777_216 и 16_777_217 в виде чисел одинарной точности с плавающей точкой, они окажутся одинаковыми. Более того, любое число в промежутке между этими двумя значениями (например, 16_777_216,5) тоже будет совпадать с ними.

Это также первое целое число, которое не может быть точно представлено в формате f32 при попытке записать 16_777_217.0, так как для этого потребовался бы ненулевой бит в дробной части, а экспонента уже находится на границе, за которой следует шаг квантования больше 1.

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

fn main() {
    assert_eq!(16_777_216.123_f32, 16_777_216.321_f32);// ERROR
    // 2^24=16,777216×10⁶=16_777_216
    println!("{:.10}", 16_777_216.123_f32);
    println!("{:.10}", 16_777_216.321_f32);
    // сравнивать с допуском (epsilon):
    // assert!((16_777_216.321_f64 - 16_777_216.123_f64).abs() < 1e-6);// 1e-6=1 × 10⁻⁶=0,000001
    println!("{:e}", f32::MAX);
    
   
    println!("{:032b}", 16_777_216_f32.to_bits()); // Выведет: 01001011100000000000000000000000
    let n = format!("{:032b}", 16_777_216_f32.to_bits());
    assert_eq!(n, "01001011100000000000000000000000".to_string());
}

Экспонента в IEEE-754 вычисляется на основе нормализованной двоичной формы числа.

Представим наше число в двоичном виде.

fn main() {
    println!("{:032b}", 16_777_216_f32.to_bits());  
    let n = format!("{:032b}", 16_777_216_f32.to_bits());
    assert_eq!(n, "01001011100000000000000000000000".to_string());
}

где:

  • значение 01001011100000000000000000000000 это:
    • [sign] [exponent] [fraction bits]
    • [0] [10010111] [00000000000000000000000]
  • sign (1 бит) = 0
  • exponent (8 бит) = 10010111 = 151
    • битовое значение переводим в десятичное:
    • 12^7 + 02^6 + 02^5 + 12^4 + 02^3 + 12^2 + 12^1 + 12^0=
    • =1128 + 064 + 032 + 116 + 08 + 14 + 12 + 11 =
    • =128 + 16 + 4 + 2 + 1 = 151
  • fraction (mantissa) (23 бита) = 00000000000000000000000 = 0

Теперь применяем формулу:

(-1)^sign*(1 + fraction) * 2^(exponent - bias)=(-1)^0 * (1 + 0) * 2^(151 - 127)=
 
=1 * 1 * 2^24=16777216

Округление к ближайшему четному (Round to Even)

В стандарте IEEE 754 есть 4 основных режима, но по умолчанию к ближайшему четному

В IEEE 754: 0.5 округляется к ближайшему четному целому

  • 1.5 → округляется до 2
  • 2.5 → округляется до 2 (потому что 2 — четное)
  • 3.5 → округляется до 4

Как правильно сравнивать

fn main(){
    // В Rust
    let a: f32 = 0.1 + 0.2;
    let b: f32 = 0.3;

    if (a - b).abs() < f32::EPSILON {
        println!("Они считаются равными");
    }
}

Порядок вычислений в коде (например, в Rust или C) критически влияет на результат.

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

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

fn main(){
   float x = 100000000.0;
   float y = x + 1.0; // y по-прежнему будет равен 100000000.0! Единица «провалилась» в дыру
}

Вещественные числа в математики vs в комп'ютере

Суть «дыр» - это числа, которые математически существуют, но не могут быть представлены в формате float компьютера.

В компьютере под любое число float отведено ровно 32 бита. Это конечное количество комбинаций нулей и единиц. Это значит, что всего компьютер может запомнить ровно 2^{32} разных чисел. Ни одним больше. А чисел даже между 0.0 и 1.0 — бесконечное множество. Чтобы их «втиснуть» в 32 бита, компьютер делает их дискретными.

Приходится «прореживать» числа

Чтобы «втиснуть» бесконечность в конечное хранилище, компьютер:

  • выбирает лишь некоторые числа
  • остальные оказываются недоступны

Это и есть дыры — промежутки между соседними представимыми float-числами.

Важно: числа распределены не равномерно.

  • Ближе к нулю — числа плотные
  • Чем больше по модулю значение, тем больше расстояние между соседними float

То есть на больших числах мелкие изменения просто теряются.

0.0
│·······························│  ← субнормалы т.е. плотность высокая
│··············│                   ← [0.125, 0.25)
│········│                         ← [0.25, 0.5)
│····│                             ← [0.5, 1.0)
1.0

В итоге:

  • Ошибки сравнения (== почти всегда зло)
  • Накопление ошибки (каждая операция +/- округляется → ошибка накапливается)
  • Потеря значимости (catastrophic cancellation) если числа очень близки, результат может попасть в дыру и стать нулём.

Что такое usize

usize — это целочисленный тип, размер которого зависит от архитектуры, для которой компилируется ваша программа. Проще говоря, его размер равен размеру указателя (pointer) на этой архитектуре.

  • На 32-битных системах, где размер указателя составляет 4 байта, usize будет иметь размер 4 байта, как и u32.
  • На 64-битных системах, где размер указателя составляет 8 байтов, usize будет иметь размер 8 байтов, как и u64.

Почему это важно

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

Используйте usize всякий раз, когда вам нужно работать с:

  • Индексами в массивах, векторах или других коллекциях.
  • Размером объектов.
  • Адресами памяти.

crate bibicode

Конвертирование систем счисления


fn main(){
 // Convert 10 to hex
 let dec = bibicode::NumeralSystem::new("", vec!(vec!("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"))).unwrap();
 let hex = bibicode::NumeralSystem::new("0x", vec!(vec!("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"))).unwrap();
 let coder = bibicode::BibiCoder::new(dec, hex);
 let test = coder.swap("2000").unwrap();
 println!("{:?}",test);

// Convert 10 to 2
 let dec = bibicode::NumeralSystem::new("", vec!(vec!("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"))).unwrap();
 let bin = bibicode::NumeralSystem::new("", vec!(vec!("0", "1"))).unwrap();
 let coder = bibicode::BibiCoder::new(dec, bin);
 let test = coder.swap("123").unwrap();
 assert_eq!("1111011",&test);
// Convert 2 to 10
 let dec = bibicode::NumeralSystem::new("", vec!(vec!("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"))).unwrap();
 let bin = bibicode::NumeralSystem::new("", vec!(vec!("0", "1"))).unwrap();
 let coder = bibicode::BibiCoder::new(bin , dec);
 let test = coder.swap("1111011").unwrap();
 assert_eq!("123",&test);
}

преобразовать десятичное целое число в шестнадцатеричную строку


fn main(){
 let decimal_number = 19035; 
 let hex_string = format!("{:x}", decimal_number); 
 println!("Шестнадцатеричное представление {} равно 0x{}", decimal_number, hex_string); 
}

Перевод числа в двоичное представление

Временная сложность алгоритма - логарифмическая O(log N)

каждая итерация сокращает вдвое количество элементов/значение

Перевод числа в двоичное представление


fn algo_2(mut decimal:u8) -> Option{
    if decimal == 0 {return None;}
    let mut binary = String::from(""); 
    while decimal > 0 {
        binary = format!("{}{}",decimal%2,binary);
        decimal = decimal.div_floor(2);
    }
    Some(binary)
}
fn main(){
 println!("{:?}",algo_2(254)); // Some("11111110")
}

Конвертировать в HEX


fn main(){
// Из двоичной системы в десятичную и форматируем в HEX
 print!("{:#X},", u32::from_str_radix("101", 2).unwrap());// 0x5

// из десятичной тип i16 в HEX
 print!("{:#X},", i16::from_str_radix("-609", 10).unwrap());// 0xFD9F
}

Константы PI

consts

std::f32::consts::PI

cheats.rs/#basic-types

если вы работаете с числами, которые больше любого целого числа в std, взгляните на crate num_bigint

1 байт (byte) это 8 бит (bit) значений 0 или 1 т.е. 2 значения => любой 8-битный тип может вместить всего значений 2^8=255 , а любой 16 битный тип 2^16=35536

Signed typesMax ValueMin ValueSize
i8127-128(1 byte = 8 bit) последний бит исп. для знака
i1632_767-32_7682^16, 16 bit или 2 byte 2*8=16
i322_147_483_647-2_147_483_648это 32 bit, или 4 byte 4*8=32, 1 byte 2 цифры в 16-ричной
i649_223_372_036_854_775_807-9_223_372_036_854_775_8088 byte
i128170_141_183_460_469_231_731_687_303_715_884_105_727-170_141_183_460_469_231_731_687_303_715_884_105_72816 byte
isizeЗависит от платформы i16, i32, i64Зависит от платформы i16, i32, i64На 32-битной архитектуре 4 байта (32 бита). На 64-битной архитектуре: 8 байт (64 бита)

Unsigned TypesMax ValueMin ValueSize
u82550(1 byte = 8 bit) последний бит исп. для данных
u1665_53502^16, 16 bit или 2 byte 2*8=16
u324_294_967_2950это 32 bit, или 4 byte 4*8=32, 1 byte 2 цифры в 16-ричной
u6418_446_744_073_709_551_61508 byte
u128340_282_366_920_938_463_463_374_607_431_768_211_455016 byte
usizeЗависит от платформы u16, u32, u640На 32-битной архитектуре 4 байта (32 бита). На 64-битной архитектуре: 8 байт (64 бита)

fn main(){
// добавлением нулей слева для заполнения 8 позиций.
 println!("{:08b} {:08b}",1_u8,1_i8);// 00000001 00000001
 println!("{:016b} {:016b}",1_u16,1_i16);// 0000000000000001 0000000000000001
}


fn main(){
// Наибольшее значение, которое может быть представлено этим целым типом.
 assert_eq!( 2147483647, ::max_value());
// Проверка переполнения типа
 assert_eq!(5i32.overflowing_add(2), (7, false));
 assert_eq!(i32::MAX.overflowing_add(1), (i32::MIN, true)); да переполнение
 assert_eq!((i32::max_value() - 2).checked_add(3), None);
 assert_eq!(5i32.checked_rem(0), None);
 assert_eq!(i32::MIN.checked_rem(-1), None);
// Возведение в квадрат
 assert_eq!(2i32.pow(3), 8);
// Проверка на позитивное значение
 assert!(!(-10i32).is_positive());
// Первая цифра
 assert_eq!((-10i32).signum(), -1);
// Абсолютное значение
 assert_eq!((-10i32).abs(), 10);
// Явно при переполнение обновить счет на начало типа и прибавить значение переполнения
 assert_eq!(i32::max_value().wrapping_add(2), i32::min_value() + 1);
}

std::num::NonZeroI32 Целое число, которое не равно нулю

num


fn main(){
 let n:i32 = 0;
 assert_eq!(None, std::num::NonZeroI32::new(n));
}

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

type Name = String;

Используйте этот атрибут, чтобы не выводить предупреждение о именах не в стиле CamelCase

 #[allow(non_camel_case_types)]
 type u64_t = u64;

type alias vs use

Они внешне похожи, но type могут иметь дело только с параметрами типа.

use не могу этого сделать:

 pub type Strings = Vec<String>;
 pub type Map<I> where I: Iterator = HashMap<I::Item, String>;

Ключевое слово type объявить псевдоним типа


fn main(){
 type Name = String;

// Затем вы можете использовать этот псевдоним вместо реального типа:

 let x: Name = "Hello".to_string();
}

Псевдонимы типов с обобщённым кодом

В этом примере мы создаем свою версию типа Result, который всегда будет использовать перечисление ConcreteError в Result<T, E> вместо типа E

  use std::result;

  enum ConcreteError {
       Foo,
       Bar,
   }

  type Result<T> = result::Result<T, ConcreteError>;

Основной мотивирующий пример для пустого типа - недостижимость на уровне типа. Например, предположим, что API должен возвращать Result в целом, но конкретный случай на самом деле безошибочен. На самом деле можно сообщить об этом на уровне типа, вернув Result<T, Void>. Потребители API могут уверенно развернуть такой Результат, зная, что статически невозможно, чтобы это значение было равно Err, поскольку для этого потребуется предоставить значение типа Void

Rust также позволяет объявлять типы, которые даже не могут быть созданы.

Об этих типах можно говорить только на уровне типов, но никогда на уровне значений.

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

Пустые типы можно объявить, указав перечисление без вариантов


// Определение пустого типа
enum Void {} // No variants = EMPTY
 
// Тип, который мы хотим получить
struct Spam {
    value: String,
}

// Функция, которая "всегда успешна" и возвращает Result с Void в качестве ошибки
fn get_spam() -> Result {
    Ok(Spam { value: "Я спам!".to_string() })
}
 
// `Result == T` т.е. всегда Ok(T)

// Компилятор Rust знает, что у типа Void нет значений, поэтому ветка `Err(void)` в match никогда не будет достигнута. Из-за этого вы можете безопасно извлечь значение Ok(spam) без необходимости паниковать или обрабатывать ошибку. Это гарантирует, что `Result` всегда будет `Ok(T)`

fn extract(result: Result) -> Spam {
    match result {
        Ok(spam) => spam,
        Err(void) => match void {}, // Эта ветка никогда не будет достигнута
    }
}

fn main() {
    let spam_result = get_spam();
    let spam_value = extract(spam_result); 
    
    println!("Получено значение: {}", spam_value.value);
}

Кортеж - это коллекция значений разных типов фиксированного размера.

(i32,) - кортеж

(i32) - выражение в скобках

Литерал (Type,)

Здесь запятая после значения обязательна, чтобы отличить кортеж с одним элементом от простого выражения в скобках.


fn test() -> (String,){
    ("".to_string(),)
}

fn main(){
    let _r: (String,) = test();
}

Кортеж это значение в скобках (для одного элемента нужна еще запятая в конце иначе это будет простое выражение в скобках которое компилятор раскроет)


fn main() {
    let a = (8,);
    let b = (8);

    print_type_of(&a);// (i32,) => вот кортеж
    print_type_of(&b);// i32    => просто i32
}
fn print_type_of(_: &T) {
    println!("{}", std::any::type_name::())
}

Кортеж  (0,)

деконструкция


fn main(){
// это последовательность разных типов фиксированного размера
    let multi:(i32,&'static str,char) = (1,"hi",'s');
    print!("i32 = {} \n &str = {} \n char = {}\n",multi.0,multi.1,multi.2 );
    let (x , y, z) = multi; // деконструкция
    let (_ , _, z) = multi;// деконструкция с пропуском
   print!(" char = {}\n",z );
}


fn cortege(c:(i32,&str,char))->(i32,&str,char){
   // единичный тип одноэлементный кортеж
     let multi  = (0,);// убрать неоднозначность с (0) кортежем через запятую
    print!("единичный тип одноэлементный кортеж {}\n",multi.0);

    // доступ через индексы
    print!("i32 = {} \n &str = {} \n char = {}\n",c.0,c.1,c.2 );

    // доступ через деконструкцию
    let (x , y, z) = c;

    print!("i32 = {} \n &str = {} \n char = {}\n",x,y,z );

    (x , y, z)
}
fn main(){
    let multi:(i32,&'static str,char) = (1,"hi",'s');

    assert_eq!(multi, cortege(multi));
}

Кортеж деструкция

fn main(){
    let tuple = (1, "привет", 4.5, true);
    let (a, b, c, d) = tuple;

    let (x,y):(i32,&str) = (1,"hello");
    let x: (i32, f64, u8) = (500, 6.4, 1);

    let tuple = (1, "привет", 4.5, true);
    let (a, b, c, d) = tuple;
    println!("{:?}, {:?}, {:?}, {:?}", a, b, c, d);
}

// Кортеж reverse перевернутый
fn reverse(m:Matrix)->Matrix{
    let (a,b,c ,d) = (m.0,m.1,m.2,m.3);
    Matrix(d,c,b ,a)
}

пример

  • различных типов данных
  • доступ по индексу
  • длинные Кортежи не могут быть напечатаны

// Кортежи могут быть использованы как аргументы функции и как возвращаемые значения
fn reverse(pair: (i32, bool)) -> (bool, i32) {
    let (integer, boolean) = pair; // деструкция
    (boolean, integer)
}

#[derive(Debug)]
struct Matrix(f32, f32, f32, f32);

fn main() {
    // Кортеж с множеством различных типов данных
    let long_tuple = (1u8, 2u16, 3u32, 4u64,
                      -1i8, -2i16, -3i32, -4i64,
                      0.1f32, 0.2f64,
                      'a', true);

    // К значениям переменных внутри кортежа можно обратиться по индексу
     let pair: (f32, i32) = (0.0, 92);
     let (x, y) = pair;
     let x = pair.0;
     let y = pair.1;

    // Кортежи могут содержать в себе кортежи
    let tuple_of_tuples = ((1u8, 2u16, 2u32), (4u64, -1i8), -2i16);
}

Debug


fn main(){
// Кортежи можно напечатать
    println!("кортеж из кортежей: {:?}", tuple_of_tuples);
    
// Но длинные Кортежи не могут быть напечатаны
// let too_long_tuple = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13);
// println!("слишком длинный кортеж: {:?}", too_long_tuple);
}

Нет затрат на хранение лишних объектов, кортеж из одного элемента представлен в адресе как сам элемент


// main.rs
fn main() {
  let t = (92,);
  // достаем адрес в памяти
  println!("{:?}", &t as *const (i32,)); // 0x7ffc6b2f6aa4
  println!("{:?}", &t.0 as *const i32); // 0x7ffc6b2f6aa4
}

А например в python не так


# main.py  
t = (92,)
print(id(t), end="\")     # 12818144
print(id(t[0])) # 2879856

это тип данных, который представляет собой один из нескольких возможных вариантов


// Создадим `enum`, который классифицирует веб-событие. Обратите внимание как
// имена и тип вместе определяют вариант:
// `PageLoad != PageUnload` and `KeyPress(char) != Paste(String)`.
// Каждый из них уникален.
    enum WebEvent {
        // `enum` так же может быть `единичным`,
        PageLoad,
        PageUnload,
        // может быть как кортежная структура,
        KeyPress(char),
        Paste(String),
        // или как просто структура.
        Click { x: i64, y: i64 },
    }

//  Функция, которая принимает `WebEvent` в качестве аргумента
// и не возвращает ничего.
    fn inspect(event: WebEvent) {
        match event {
            WebEvent::PageLoad => println!("страница загружена"),
            WebEvent::PageUnload => println!("страница не загружена"),
            // Деструктурируем `c` из `enum`.
            WebEvent::KeyPress(c) => println!("нажата клавиша '{}'.", c),
            WebEvent::Paste(s) => println!("вставлено значение \"{}\".", s),
            // Деструктурируем `Click` в `x` и `y`.
            WebEvent::Click { x, y } => {
                println!("clicked at x={}, y={}.", x, y);
            },
        }
    }
fn main(){ 
    let pressed = WebEvent::KeyPress('x');
    // `to_owned()` создаёт копию `String` из среза строки.
    let pasted  = WebEvent::Paste("мой текст".to_owned());
    let click   = WebEvent::Click { x: 20, y: 80 };
    let load    = WebEvent::PageLoad;
    let unload  = WebEvent::PageUnload;

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

Strum - это набор макросов и трейтов для более простой работы с перечислениями и строками в Rust

actix_web::Either

Either: Строго два варианта (Left и Right). Его основное назначение - выбор между двумя возможными состояниями/типами.

Either можно рассматривать как частный случай enum с двумя несемантическими вариантами (Left и Right). Rust использует enum для построения таких важных типов, как Option и Result, которые покрывают большинство случаев использования Either, но с гораздо большей ясностью, безопасностью и гибкостью благодаря именованным вариантам и мощному сопоставлению с образцом.

enum Expr {
  Negation(Box<Expr>),
  BinOp { lhs: Box<Expr>, rhs: Box<Expr> },
  Unit,
}

варианты enum бывают такие же, как и структуры

квалификация обязательна Expr::BinOp { lhs, rhs}, но можно импортировать вариант: use Expr::BinOp

mem::size_of — размер самого большого варианта + дискриминант (жирное перечисление имеет такой же большой размер, как и его самый большой вариант) размер объекта не может быть бесконечным

варианты enum бывают такие же, как и у структуры


enum Animal{
 Fox{name:String},
 Elk{name:String,size:usize}
} 
 
impl Animal{
    fn say(&self){
        match self{
            Animal::Fox{name} =>{
                println!("{}",name);
            },
            Animal::Elk{name,..} =>{
                println!("{}",name);
            },
        }
        
    }
} 
fn main() {
  let fox = Animal::Fox{name:"agg".to_string()};
  fox.say();
}

Vec с разными типами


#[derive(Default)]
struct Point {  x: i32, y: i32,}
impl Point {
    fn inc(&mut self) { self.x += 1;self.y += 1; }
}
enum Stuff {
    Integer(i32),
    String(String),
    Point(Point),
}
fn map_stuff(mut stuff: Stuff) -> Stuff {
    match &mut stuff {
        Stuff::Integer(num) => *num += 1,
        Stuff::String(string) => *string += "!",
        Stuff::Point(point) => point.inc(),
    }
    stuff
}
fn main() {
    let mut vec = vec![
        Stuff::Integer(0),
        Stuff::String(String::from("a")),
        Stuff::Point(Point::default()),
    ];
    // vec = [0, "a", Point { x: 0, y: 0 }]
    vec = vec.into_iter().map(map_stuff).collect();
    // vec = [1, "a!", Point { x: 1, y: 1 }]
}

Деструктуризация с match

struct Point { x: f32, y: f32 }

enum Shape {
        Circle( Point, f32 ),
        Rectangle( Point, Point ),
        Triangle( Point, Point, Point )
}

fn area( sh: Shape ) -> f32 {
       match sh {
            Shape::Circle( _, radius ) => std::f32::consts::PI * radius * radius,
            Shape::Rectangle( Point { x, y }, Point { x: x2, y: y2 } ) => (x2 - x) * (y2 - y),
            Shape::Triangle( Point { x, y }, Point { x: x2, y: y2 }, Point { x: x3, y: y3 } ) =>
                0.5 * ((x - x3) * (y2 - y3) - (x2 - x3) * (y - y3))
       }
}  
fn main(){}

При необходимости можно игнорировать все поля, если в данной ветви сравнения записатьCircle(*)

Как поменять состояние enum без clone

Мутировать enum без клонирования используя std::mem::take Эквивалент mem::replace(name, String::new())


use std::mem;
enum MultiVariateEnum {
    A { name: String },
    B { name: String },
    C,
    D
}
fn swizzle(e: &mut MultiVariateEnum) {
    use MultiVariateEnum::*;
    *e = match e {
        // Правила владения не позволяют брать "name" по значению, но мы не можем 
        // берем значение из изменяемой ссылки, если мы не заменим его:
        A { name } => B { name: mem::take(name) },
        B { name } => A { name: mem::take(name) },
        C => D,
        D => C
    }
}
fn main() {
  let mut a = MultiVariateEnum::A{name:"agg".to_string()};
  swizzle(&mut a);
    if let MultiVariateEnum::B{name} = a {
        print!("{}",name);
    }
}

// enum с неопределённым перечислением (начинается с 0)
enum Number {
    Zero=3,
    One,
    Two,
}

// enum с определённым перечислением
enum Color {
    Red = 0xff0000,
    Green = 0x00ff00,
    Blue = 0x0000ff,
}

fn main() {
    // `enums` может быть преобразован в целочисленное значение.
    assert_eq!(3,Number::Zero as i32);
    assert_eq!(4,Number::One as i32);
   
    println!("красный цвет #{:06x}", Color::Red as i32);
    println!("голубой цвет #{:06x}", Color::Blue as i32);
}

Полезные enum'ы

// use std::cmp::Ordering;
enum Ordering {
   Less,
   Equal,
   Greater,
}
fn binary_search(xs: &[i32], x: i32) -> bool {
   if xs.is_empty() { return false; }
   let mid = xs.len() / 2;
   let subslice = match xs[mid].cmp(&x) {
           Ordering::Less => &xs[mid + 1..],
           Ordering::Equal => return true,
           Ordering::Greater => &xs[..mid],
   };
  binary_search(subslice, x)
}
fn main(){}

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

if let enum - проверка enum варианта

Полиморфизм


trait Say{
    type S:?Sized;
    fn say(&self)->&Self::S;
}
struct Cat(String);
struct Dog(i32);
impl Say for Cat{
   type S=str;
   fn say(&self)->&Self::S{&self.0} 
}
impl Say for Dog{
   type S=i32;
   fn say(&self)->&Self::S{&self.0} 
}
enum Animal{
    Cat(Cat),
    Dog(Dog)
}
impl Animal{
    fn say(&self){
        match self{
          Animal::Cat(cat) => print!("{}",cat.say()),
          Animal::Dog(dog) => print!("{}",dog.say())
        }
    }
}
fn main(){
    let cat = Animal::Cat(Cat(String::from("mya")));
    cat.say();
    let dog = Animal::Dog(Dog(123));
    dog.say();

     let dog = Animal::Dog(Dog(123));
     if let Animal::Dog(animal) = dog { print!("{:?}",animal.say());}
}

Преобразовать вектор строк в вектор enum варианта


enum Message{Write(String)}
fn main(){
    let v = vec!["Hello".to_string(),"World".to_string()];
    let res:Vec = v.into_iter().map(Message::Write).collect();
    
    for e in res{
        match e{
            Message::Write(s) => println!("{:?}",s)
        }
    }
}

Когда мы создаём экземпляр структуры в коде, программа выделяется память для всех полей структуры друг за другом (изменить #[repr(C)]).

Виды структур

// Классическая C-структура
struct Point { x: f64, y: f64 }

// tuple struct
struct Point(f64, f64);

// newtype (tuple) struct
struct Point1D(f64);

// unit struct
struct ThePoint; // ZST

полный синтаксис вызова метода (Ассоциированные функции)


struct A;

impl A{
    fn foo(&self, arg1:i32){
        print!("arg1");
    }
}

fn main() {
    let a = A;
    a.foo(1);        // сокращенный синтаксис вызова метода
    A::foo(&a,1); // полный синтаксис вызова метода
}

Zero Sized Types (ZST) - типы, которые не занимают места

zero-sized-types-zsts (ZST)

// Rust также позволяет указывать типы, которые не занимают места:

struct Nothing; // No fields = no size

// All fields have no size = no size
struct LotsOfNothing {
    foo: Nothing,
    qux: (),      // empty tuple has no size
    baz: [u8; 0], // empty array has no size
}
fn main(){}

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

Шаблон Newtype предотвращает недопустимое использование данных.

Зачем:

  • безопасность типа(защищенный вашими правилами, а не внутренним типом). Клиент не знает , как устроен или представлен ваш тип, что означает, что представление может измениться в будущем без нарушения клиентского кода
  • использование PhantomData для добавления времени жизни к необработанным указателям или для реализации Pattern Phantom type

Похожими свойствами обладает подход с impl Trait т.е. generic

phantom

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

Другое использование новых типов включает использование PhantomData для добавления времени жизни к необработанным указателям или для реализации шаблона Phantom type «фантомных типов».

use std::ops::Add;

struct Millimeters(f64);
struct Grams(f64);

impl Add<Millimeters> for Millimeters {
    type Output = Millimeters;

    fn add(self, other: Millimeters) -> Millimeters {
        Millimeters(self.0 + other.0)
    }
}
// Likewise: impl Add<Grams> for Grams {}
fn main(){}

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

nutype

derive_more

Обратной стороной использования шаблона newtype является необходимость написания большего количества шаблонного кода, поскольку вам придется самостоятельно обеспечивать реализацию общих свойств (например Clone, Copy, From/Into, AsRef​​/AsMut), поскольку без них тип не будет эргономичным в использовании.

Однако большинство из них могут быть получены автоматически с помощью std возможностей или сторонних крейтов (например, derive_more), поэтому стоимость в большинстве случаев приемлема. Более того, превосходный nutype ящик развивает эту идею еще дальше, стремясь обеспечить лучшую эргономику для модели newtype без ущерба для каких-либо гарантий, которые она дает.


extern crate derive_more;
use derive_more::{Add, Display, From, Into};

#[derive(PartialEq, From, Add)]
struct MyInt(i32);

#[derive(PartialEq, From, Into)]
struct Point2D {
    x: i32,
    y: i32,
}

#[derive(PartialEq, From, Add, Display)]
enum MyEnum {
    #[display(fmt = "int: {}", _0)]
    Int(i32),
    Uint(u32),
    #[display(fmt = "nothing")]
    Nothing,
}
fn main(){
    assert!(MyInt(11) == MyInt(5) + 6.into());
    assert!((5, 6) == Point2D { x: 5, y: 6 }.into());
    assert!(MyEnum::Int(15) == (MyEnum::Int(8) + 7.into()).unwrap());
    assert!(MyEnum::Int(15).to_string() == "int: 15");
    assert!(MyEnum::Uint(42).to_string() == "42");
    assert!(MyEnum::Nothing.to_string() == "nothing");
}

Декомпозиция (Деструкция) структуры

Декомпозиция структуры в аргументах функции

Декомпозиция структур с использованием match


#[derive(Debug)]
pub struct Id{
    identifier:i32,
    len:usize
}
impl Id {
    fn new() -> Id{
       Id{identifier:0,len:0}
    }
}
fn main() {
  let id = Id::new();
  let Id {identifier, ..} = id;
  println!("{} ",  identifier);
  
 let id @ Id {identifier, ..} = Id::new();
 println!("{} {}",id.identifier, identifier);
}


fn main() {
  struct Matrix(f32, f32, f32, f32);
  let matrix = Matrix(1.1, 1.2, 2.1, 2.2);
  let Matrix(item1, item2,item3,item4) = matrix;
}


struct S{
    x:u8,
    y:u8
}
fn main() {
    let obj:S = S{x:9u8,y:3u8};
    f(obj);
}
fn f( S { x,y }: S){
    println!("S=({},{})",x,y);
}


fn main(){
  struct Date { year: u16, month: u8, day: u8 }
  let deadline = Date { year: 2013, month: 6, day: 25 };
  match deadline {
    Date { year: y, month: 8, day: d } => { println!("{}",(d+2).to_str() + " августа " + y.to_str() + " - резервный срок сдачи проекта" ) },
    Date { year: y, month: m, day: d } => { println!("{}",d.to_str() + "." + m.to_str() + "." + y.to_str() + " - подписание акта сдачи/приёмки" ) }
  }
  let curryear: u16 = deadline.year;
  println( fmt!("В любом случае проект должен быть завершён в %d году", curryear as int ) );
}

Заполнение структурой ..

Синтаксис обновления (.. update syntax)


fn main(){
  let user1 = User {
        active: user1.active,
        username: user1.username,
        email: String::from("user1@example.com"),
        sign_in_count: user1.sign_in_count,
  }; 
 let user2 = User {
        email: String::from("user2@example.com"),
        ..user1
  };
}

Структура с приватным не используемым полем, что бы нельзя было создать объект этой структуры в обход метода new


mod private{
   pub struct PrivateStatic(());// self.0 private
   impl PrivateStatic {
       pub fn new()->Self{
           Self(())
       }
   }
} 
use private::PrivateStatic;

fn main() {
    let p = PrivateStatic::new();
}

pub (super) делает имя доступным в родительском модуле

mod a {
        pub (super) struct Foo;// даем pub для использования через super
        struct Foo_privat;
        mod b{
            use super::Foo;// ok
            use super::Foo_privat;// ok
        }
}

mod b {
     use super::a::Foo;// ok
     use super::a::Foo_privat;// Error: struct `Foo_privat` is private
}
fn main(){}

pub (crate) делает имя доступным во всем крейте

// pub( crate) делает имя доступным во всем крейте
mod a{
    pub(super) mod b{
        pub( crate ) struct Foo;
    }
}
mod b{
    use super::a::b::Foo;
}

pub(crate) use b::Foo; // Реэкспорт

 // Реэкспорт
 // Модификаторы доступа действуют на use.
 // Это можно использовать для реэкспорта
mod a{
    pub(crate) use b::Foo;
    mod b{
        pub(crate) struct Foo;;
    }
}

mod b{
    use super::a::Foo;
}
fn main(){}

Структура может быть

  • либо изменяемая вся
  • либо неизменяемая
  • либо изменяемая для отдельных полей с помощью Cell<T>

use std::cell::Cell;
// Структуры не могут иметь изменяемые поля только вся структура либо изменяема либо нет
// для реализации изменяемости на уровне полей поможет Cell

struct Point{
    x:i32,
    y:Cell
}
fn main(){
    let point:Point = Point{x:5,y:Cell::new(6)};
    point.y.set(7);

    let mut point_mut:Point_mut = Point_mut{x:5,y:6};
    point_mut.y = 7;
}

struct Point_mut{
    x:i32,
    y:i32
}

структуры с & объявляются с явным заданием времени жизни


struct Point {
    x: i32,
    y: i32,
}
struct PointRef<'a> {
    x: &'a mut i32,
    y: &'a mut i32,
}
fn main() {
    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;
    }
    assert_eq!(5, point.x);
    assert_eq!(6, point.y);
}

FromStr из строки в структуру


use std::str::FromStr;
use std::num::ParseIntError;
#[derive(Debug)]
struct Point {
    x: i32,
    y: i32
}

impl FromStr for Point {
    type Err = ParseIntError;

    fn from_str(s: &str) -> Result {
        let coords: Vec<&str> = s.trim_matches(|p| p == '(' || p == ')' )
            .split(",")
            .collect();

        let x_fromstr = coords[0].parse::()?;
        let y_fromstr = coords[1].parse::()?;

        Ok(Point { x: x_fromstr, y: y_fromstr })
    }
}
fn main(){
    // из строки в структуру
    let p = Point::from_str("(1,2)");
    assert_eq!(p.unwrap(), Point{ x: 1, y: 2} )
}

Абстрактный шаблон. Объяснить компилятору, что данный тип должен предоставлять определённую функциональность Типаж, содержащий лишь сигнатуру метода или дефолтную реализацию, а затем реализуем (impl) для нужной struct/enum

Все методы trait должны быть реализованы impl методами struct/enum и лишних не должно быть
Trait по умолчанию ограничен (trait bound) трейтом &Sized т.е. возможно не имеет размер

Виды трейтов

  • Trait as Intarface - простые типажи для предоставления контракта
  • Traite-object - обычные трейты с ограничениями для динамической диспетчиризации во время выполнения кода
  • Marker Traits - для ограничения (trait bound) трейтом trait Eq: PartialEq {}или Copy
  • Auto trait - unsafe auto trait Send {} или если есть From то Into автоматически реализуется, для всех Display автоматически ToString

Трейты могут быть:

  • с реализацией по умолчанию
  • с ограничением от супертрейта
  • иметь Associated Type (для возможности использования типа отличного от Self)
  • иметь связанные константы

extension traits - расширение существующих типов своими методами.

orphan rule - правила расширения

В Rust мы не можем напрямую менять чужие типы (из std или crate’ов), но можем добавить им новые методы через трейты, которые реализуешь локально.

Пример для ситуации: наш трейт + чужой тип (crates или из std)

Мы “добавим” метод к чужому типу (String), не изменяя его исходник.

// Расширяем стандартный тип String
trait StringUtils {
    fn is_palindrome(&self) -> bool;
}

// Реализация для String
impl StringUtils for String {
    fn is_palindrome(&self) -> bool {
        let s = self.chars().collect::<Vec<_>>();
        s == s.iter().rev().cloned().collect::<Vec<_>>()
    }
}

fn main() {
    let s = "racecar".to_string();
    println!("{}", s.is_palindrome()); // true
}

Или расширим Vec<i32>:

trait VecExt {
    fn sum_squares(&self) -> i32;
}

impl VecExt for Vec<i32> {
    fn sum_squares(&self) -> i32 {
        self.iter().map(|x| x * x).sum()
    }
}
 
fn main() {
    let buff:Vec<i32> = vec!(1,2,3);
    println!("{}", buff.sum_squares()); // 14
}

Пример для ситуации: чужой трейт fmt::Display + наш тип MyType:

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

struct MyType;

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

}

Когда это полезно:

  • Когда тебе нужно удобное API к стандартному типу (String, Vec, Path, Option, и т.д.).
  • Когда ты не можешь (или не хочешь) наследовать или оборачивать тип.
  • Когда это локальное расширение — не часть публичного контракта библиотеки.

Но в Rust действует orphan rule:

Мы можем реализовать трейты только если тип или трейт определён в нашем crate’е.

  • ✅ можно: наш трейт + чужой тип (crates или из std)
  • ✅ можно: чужой трейт + наш тип
  • ❌ нельзя: чужой трейт + чужой тип

асинхронные функции в трейтах

вы можете использовать async fn и -> impl Trait в сигнатурах методов трейта

async fn в трейтах: Позволяет объявлять метод трейта как async. Это означает, что он будет возвращать impl Future<Output = ...>, который можно будет await'ить.

-> impl Trait в трейтах (Return Position impl Trait - RPIT): Хотя эта возможность не является совершенно новой (RPIT в обычных функциях существовала раньше), ее использование становится особенно мощным в сочетании с async fn в трейтах.

async fn по своей сути возвращает impl Future, и эта синтаксическая конструкция позволяет вам явно указать, что метод возвращает тип, который реализует определенный трейт (например, Future), без необходимости указывать конкретный тип Future

use std::future::Future;

// Трейт с async fn и RPIT (impl Future)
trait DataFetcher {
    async fn fetch(&self, id: u32) -> String;
}

// Имплементация
struct MyFetcher;

impl DataFetcher for MyFetcher {
    async fn fetch(&self, id: u32) -> String {
        format!("fetched data for id = {}", id)
    }
}

// Использование
async fn use_fetcher(fetcher: &impl DataFetcher) {
    let result = fetcher.fetch(42).await;
    println!("Got: {}", result);
}

The orphan rule (Правило сироты)

Правило сироты применяется только к случаям, где и трейт, и тип принадлежат внешним крейтам тогда вы не можете их реализовывать

Это правило введено для предотвращения потенциальных конфликтов в реализации трейтов при использовании внешних библиотек.

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

Способ обойти правило сироты - создать NewType

Реализация свое трейта для внешнего типа разрешена.


trait SaysHello {
    fn hello(&self) {
        println!("Hello");
    }
}
 
impl SaysHello for i32 {}

fn main() {
    let i = 0_i32;
    i.hello();
} 

затирание типа


fn main(){
 trait Test{}
 struct TestImpl{}
 impl Test for TestImpl{}
 let test:Box<&dyn Test> = Box::new(&TestImpl{});
}

Traits


trait Animal {
    // Сигнатура статического метода, `Self` ссылается на реализующий тип.
    fn new(name: &'static str) -> Self;
    // Сигнатура метода экземпляра; они возвращают строки.
    fn name(&self) -> &'static str;
    fn noise(&self) -> &'static str;
    // Типаж может содержать определение метода по умолчанию
    fn talk(&self) { println!("{} says {}", self.name(), self.noise()); }
}
struct Sheep { naked: bool, name: &'static str }
impl Sheep {
    fn is_naked(&self) -> bool { self.naked}
    fn shear(&mut self) {
        if self.is_naked() {// Методы типа могут использовать методы типажа, реализованного для этого типа.
            println!("{} is already naked...", self.name());
        } else {
            println!("{} gets a haircut!", self.name);
            self.naked = true;
        }
    }
}
// Реализуем типаж `Animal` для `Sheep`.`Self` реализующий тип: `Sheep`.
impl Animal for Sheep {
    fn new(name: &'static str) -> Sheep { Sheep { name: name, naked: false }}
    fn name(&self) -> &'static str {self.name }
    fn noise(&self) -> &'static str { if self.is_naked() {  "baaaaah?"} else { "baaaaah!" } }
    // Методы по умолчанию могут быть переопределены.
    fn talk(&self) { println!("{} pauses briefly... {}", self.name, self.noise());}
}
fn main() {
    // Аннотация типа в данном случае необходима.
    let mut dolly: Sheep = Animal::new("Dolly");
    dolly.talk();
    dolly.shear();
    dolly.talk();
}

Наследование

trait Subtrait: Supertrait {}

что тоже самое

trait Subtrait where Self: Supertrait {}

trait A {
  fn a(&self);
}
trait B: A {
  fn b(&self);
}
impl B for Spam {
  fn b(&self) {}
}
impl A for Spam {
  fn a(&self) {
    self.b(); // вызываем метод B!
  }
}
fn main(){}

 trait Fn<Args>: FnMut<Args> ....
// тут Fn - это Subtrait, а FnMut - Supertrait

Запечатанные трейты защищают от последующих реализаций

как - приватный модуль содержит ограничивающий трейт который пользователь не сможет реализовать и следовательно ваш публичный трейт тоже

sealed-traits-protect-against-downstream-implementations-c-sealed

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

/// Эта черта запечатана и не может быть реализована для типов вне этого ящика.
pub trait TheTrait: private::Sealed {
    // Ноль или более методов, которые разрешено вызывать пользователю..
    fn ...();

    // Ноль или более частных методов, которые не могут вызывать пользователь.
    #[doc(hidden)]
    fn ...();
}

// Реализуйте для некоторых типов
impl TheTrait for usize {
    /* ... */
}

mod private {
    pub trait Sealed {}

    // Реализация для тех же типов, но не для других.
    impl Sealed for usize {}
}

Значение типа по умолчанию для обобщенных параметров типажа

default-generic-type-parameters-and-operator-overloading

trait Base<RHS=u32> {
    fn base(&self, rhs: RHS) -> String;
    fn default(&self,rhs:RHS)  where RHS:std::fmt::Debug{
        println!("Base default {:?}",rhs);
    }
    fn default_static(rhs:RHS) where RHS:std::fmt::Debug{
        println!("Base default {:?}",rhs);
    }
}
struct A{}
// Используем параметр общего тип по умолчанию т.е. rhs должен быть u32
impl Base for A{
    fn base(&self, rhs: u32) -> String{
        String::from("")
    }
}
// Переопределяем u32 на свой тип &'a str для типажа
impl<'a> Base<&'a str> for A{
    fn base(&self, rhs: &str) -> String{
        String::from("")
    }
    fn default_static(rhs:&str) {
        println!("Base for A rhs:&str = {:?}",rhs);
    }
}  
fn main(){}

Получается если тип реализован то он заменяет тип типажа по умолчанию даже если сам метод трейта не переопределен в реализации !


trait Base<RHS=u32> {
    fn base(&self, rhs: RHS) -> String;
    fn default(&self,rhs:RHS)  where RHS:std::fmt::Debug{
        println!("Base default {:?}",rhs);
    }
    fn default_static(rhs:RHS) where RHS:std::fmt::Debug{
        println!("Base default {:?}",rhs);
    }
}

struct A{

}
// Используем параметр общего тип по умолчанию т.е. rhs должен быть u32
impl Base for A{

    fn base(&self, rhs: u32) -> String{
        String::from("")
    }
}
// Переопределяем u32 на свой тип &'a str для типажа
impl<'a> Base<&'a str> for A{

    fn base(&self, rhs: &str) -> String{
        String::from("")
    }
    fn default_static(rhs:&str) {
        println!("Base for A rhs:&str = {:?}",rhs);
    }
}
// Переопределяем u32 на свой тип String для типажа
impl Base<String> for A{

    fn base(&self, rhs: String) -> String{
        String::from("")
    }
    fn default(&self,rhs:String) {
        println!("Base for A rhs:String = {:?}",rhs);
    }
}
// Переопределяем u32 на свой тип &'a [i32] для типажа
impl<'a> Base<&'a [i32]> for A{

    fn base(&self, rhs: &[i32]) -> String{
        String::from("")
    }
}
// Переопределяем u32 на свой тип i32 для типажа
impl<'a> Base<i32> for A{

    fn base(&self, rhs: i32) -> String{
        String::from("")
    }
    fn default(&self,rhs:i32) {
        println!("Base for A rhs:i32 = {:?}",rhs);
    }
    fn default_static(rhs:i32) {
        println!("Base for A rhs:i32 = {:?}",rhs);
    }
}
fn main(){
    let a:A = A{};
    a.base("str");
    a.base(String::from("String"));
    let v = vec![1,2,3];
    a.base(&v[..]);
    a.base(1_i32);

    <A as Base<u32>>::default_static(1_u32);// Base default 1 // т.е. отработает реализация Base по умолчанию

    <A as Base<i32>>::default_static(1_i32);// Base for A rhs:i32 = 1 //  т.е. отработает реализация A так как i32 реализованно для Base

    <A as Base<&str>>::default_static("str");// Base for A rhs:&str = "str" //  т.е. отработает реализация A так как &str реализованно для Base

    a.default(1_i32);// Base for A rhs:i32 = 1 //  т.е. отработает реализация A так как i32 реализованно для Base

    a.default(1_u32);// Base default 1 // т.е. отработает реализация Base по умолчанию потому что мы не реализовали для u32 вариант а u32 есть по умолчанию

    a.default("str");// Base default "str"// т.е. отработает реализация Base по умолчанию но с типом &str

    a.default(String::from("String"));// Base for A rhs:String = "String" // а у String реализации есть метод default ,поэтому отработает переопределенный метод String
    // Получается если тип реализован то он заменяет тип типажа по умолчанию даже если сам метод трейта не переопределен в реализации !
}

Для вызова метода трейт должен быть импортирован

При коллизии inherent метода и трейта, побеждает метод

При коллизии двух трейтов — ошибка компиляции

struct S { ... }
trait T {
  fn foo(&self);
}

impl T for S { ... }

fn bar(s: &S) {
    s.foo();
    S::foo(s)
    T::foo(s);
    <S as T>::foo(s);
}

const

Связанные константы (Ассоциированные константы)


trait Tagged {
    const TAG: &'static str;
}

struct Foo;
impl Tagged for Foo { const TAG: &'static str = "Foo"; }

struct Bar;
impl Tagged for Bar { const TAG: &'static str = "Bar"; }

fn by_tag(tag: &str) {
    match tag {
        Foo::TAG => println!("foo"),
        Bar::TAG => println!("bar"),
        _ => panic!("unknown tag: {:?}", tag)
    }
}
fn main(){
    by_tag("Foo");// foo
    print!("{}", Bar::TAG);
}


struct Foo([i32; N]);
impl Foo {
    const CONST: usize = N * 4;
    // .... fn
}
 
fn main() {
    let foo = Foo([1,2]);
    assert_eq!(8,Foo::<2>::CONST);
    print!("{:?}", Foo::<2>::CONST);
}

const

Связанные константы (Ассоциированные константы)

Можно переопределять const и инициализировать


trait HasNumbers {
    const SET_NUMBER: usize = 10;            
    const EXTRA_NUMBER: usize;               
}
 
struct NothingSpecial;
 
impl HasNumbers for NothingSpecial {
    const SET_NUMBER: usize = 20; 
    const EXTRA_NUMBER: usize = 10;
}
 
fn main() {
  print!("{} ", NothingSpecial::SET_NUMBER);// 20
  print!("{}", NothingSpecial::EXTRA_NUMBER);// 10
}

Какая-то муть с наследованием трейтов supertrait

how-do-i-disambiguate-associated-types

pub trait A {}
pub trait HasA {
    type A: A;
    fn gimme_a() -> <Self as HasA>::A;
}

pub trait RichA: A {}
pub trait RichHasA: HasA {
    type A: RichA;
    fn gimme_a() -> <Self as RichHasA>::A;
    // ... more things go here ...
}
fn main(){}

Ассоциированные типы

type F;

Реализация метода в трейте (аналог абстрактного класса) возвращаемый тип организован через type

use std::fmt::Debug;
// Реализация метода в трейте(аналог абстрактного класса)
// возвращаемый тип организован через type
trait T{
    type F;
    fn show(&self){ // метод по умолчанию, можно переопределить
      println!("show");
    }
    fn work(&self)->Self::F;
}

struct WeatherData;

impl T for WeatherData{
    type F=String;
    fn work(&self)->Self::F{
        "work".to_string()
    }
}
fn main() {
 let w:WeatherData = WeatherData;
 w.show(); // show
 println!("{}",w.work()); // work
}

fn domain_controller_new_order<'a,T>(uow: &'a mut T) -> ShortResult<()> 
    where T: UnitOfWork<'a, Context=Transaction<'a>>
{...}

trait Iterable {
    type Item;

    fn iter(&self) -> Iterator<Item = Self::Item>;
}
impl Iterable for Vec<i32> {
    type Item = i32;

    fn iter(&self) -> Iterator<Item = Self::Item> {
        self.iter()
    }
}

GAT (общие ассоциированные типы)

GATs-stabilization-push

// Они позволяют определять типы обобщенных типов, времени жизни или константы для связанных типов.

struct WindowsMut<'t, T> {
    slice: &'t mut [T],
    start: usize,
    window_size: usize,
}
impl<'t, T> LendingIterator for WindowsMut<'t, T> {
    type Item<'a> where Self: 'a = &'a mut [T]; // <----- GAT

    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
        let retval = self.slice[self.start..].get_mut(..self.window_size)?;
        self.start += 1;
        Some(retval)
    }
}

trait PointerFamily {
    type Pointer<T>: Deref<Target = T>; // <----- GAT

    fn new<T>(value: T) -> Self::Pointer<T>;
}

struct ArcFamily;
struct RcFamily;

impl PointerFamily for ArcFamily {
    type Pointer<T> = Arc<T>;
    ...
}
impl PointerFamily for RcFamily {
    type Pointer<T> = Rc<T>;
    ...
}

struct MyStruct<P: PointerFamily> {
    pointer: P::Pointer<String>,
}

Перегрузка по состоянию объекта

Выбор выполнение методов command зависит от переданного аргумента


#[derive(Debug)]
struct FoldUserState;// Типы команд
#[derive(Debug)]
struct CreateUser;
#[derive(Debug)]
enum TestEnum{
    One,Two
}
trait CommandStateT{}// Типаж команд
impl CommandStateT for FoldUserState{}
impl CommandStateT for CreateUser{}
impl CommandStateT for TestEnum{}
trait CommandGateway {
    type Result;
    fn command(&self, cmd: C) -> Self::Result;
}

struct Service;
// Перегрузка по состоянию объекта !!!!
// Реализация CommandGateway для разных типов CommandStateT
impl CommandGateway for Service {
    type Result = Option;
    fn command(&self, cmd:  FoldUserState) -> Self::Result {
         println!("FoldUserState={:?}",cmd);
         Some(1)
    }
}
impl CommandGateway for Service {
    type Result = Option;
    fn command(&self, cmd: CreateUser) -> Self::Result {
            println!("CreateUser={:?}",cmd);
            None
    }
}
impl CommandGateway for Service {
    type Result = Option;
    fn command(&self, cmd: TestEnum) -> Self::Result {
           println!("TestEnum={:?}",cmd);
           None
    }
}
fn main() {
   let service = Service{};
   service.command(CreateUser);// CreateUser=CreateUser
   service.command(FoldUserState);// FoldUserState=FoldUserState
   service.command(TestEnum::One);// TestEnum=One
   service.command(TestEnum::Two);// TestEnum=Two
}

Золотое правило типов с динамическим размером состоит в том, что мы всегда должны ставить значения типов динамического размера за какой-то указатель. Для использования признаков в качестве объектов признаков мы должны поместить их за указатель, например, &dyn Trait или Box<dyn Trait> (Rc<dyn Trait> тоже будет работать)

Первое слово в толстом указателе для типаж-объекта — это адрес значения, а второе — адрес виртуальной таблицы (для среза второе слово — просто размер)

Трейт-объект создается с помощью приведения coercion (или явного приведения ), когда значение, реализующее трейт, передается где-то там, где dyn Trait ожидается Трейт-объект стирает тип, реализующий тип после = &dyn Trait присваивания затирается и вернуть обратно его нельзя, известно только что он impl Trait

Не все трейты могут быть преобразованы в трейт-объекты: Трейт является объектно-безопасным, если она соответствует следующим требованиям:

  • трейта не требует Self: Sized
  • все методы трейта объектно-безопасны
  • метод требует Self: Sized или
  • метод использует только Self тип в позиции приемника

Трейт-объекты не имеют известного размера во время компиляции поэтому они !Sized

Трейт-объекты не имеют известного размера во время компиляции поэтому их можно использовать только за указателем &dyn Trait

// Следующие записи эквивалентны generic (это не трейт-обьекты):
fn use_generics<T: Trait>(x: T) {} // это синтаксис turbo-fish
fn use_impl(x: impl Trait) {}

// Это с трейт-обьектом
fn use_impl(x: &dyn Trait) {}

Trait Objects это толстый указатель

trait-objects

Trait Objects это ссылка на некоторый элемент, который реализует определенный признак. Он построен из простого указателя на элемент вместе с внутренним указателем на тип vtable, что дает размер 16 байт (на 64-битной платформе) Виртуальная таблица для реализации типа трейта содержит указатели функций для каждой реализации метода, что позволяет выполнять динамическую диспетчеризацию во время выполнения


trait Calculate {
    fn add(&self, l: u64, r: u64) -> u64;
    fn mul(&self, l: u64, r: u64) -> u64;
    fn check(&self, l: u64) -> u64;
}

struct Modulo(pub u64);

impl Calculate for Modulo {
    fn add(&self, l: u64, r: u64) -> u64 {
        (l + r) % self.0
    }
    fn mul(&self, l: u64, r: u64) -> u64 {
        (l * r) % self.0
    }
    fn check(&self, l: u64) -> u64 {
        l
    }
}
impl Drop for Modulo {
    fn drop(&mut self) {
        println!("Dropping Modulo with value: {}", self.0);
    }
}
fn main() {
    let mod3 = Modulo(3);
    let tobj: &dyn Calculate = &mod3;
    unsafe {
        // Преобразуем объект типа trait в сырые указатели на данные и vtable
        let (data_ptr, vtable_ptr): (*const Modulo, *const usize) = std::mem::transmute(tobj);
        println!("Data pointer: {:p}", data_ptr);
        println!("VTable pointer: {:p}", vtable_ptr);

        // Преобразуем vtable_ptr в указатель на массив указателей функций
        let vtable_ptr = vtable_ptr as *const *const ();

        // Структура vtable зависит от компилятора и платформы, но обычно она включает следующие элементы:
        // Указатель на type_id или type_info: Этот указатель используется для хранения информации о типе
        // Указатель на деструктор
        // Указатели на методы trait

        println!("VTable contents for &dyn Calculate:");
        for i in 0..20 {
            let func_ptr = *vtable_ptr.offset(i as isize);
            println!("vtable[{}]: {:p}", i, func_ptr);
        }

        // Получаем указатели на функции add и mul из vtable
        // Обычно у vtable сначала идет информация о типе, затем — указатель на деструктор, а потом — указатели на функции.
        // Порядок методов в vtable совпадает с порядком их объявления в trait. 
        // Получаем указатель на деструктор из vtable
        let drop_fn = *vtable_ptr.offset(0);// в нашем случае указатель на ф-цию Drop на 0 позиции
        let add_fn = *vtable_ptr.offset(3);// указатель на ф-цию add
        let mul_fn = *vtable_ptr.offset(4);// указатель на ф-цию mul
        let check_fn = *vtable_ptr.offset(5);// указатель на ф-цию check

        println!("Destructor pointer: {:p}", drop_fn);// 0x558577c77990
        println!("Add function pointer: {:p}", add_fn);// 0x558577c77d00  Разница 0x370 (880 в десятичной системе)
        println!("Mul function pointer: {:p}", mul_fn);// 0x558577c77d80 Разница 0x80 (128 в десятичной системе). выровнены с шагом 128 байт
        println!("Check function pointer: {:p}", check_fn);// 0x558577c77e00 Разница 0x80 (128 в десятичной системе). выровнены с шагом 128 байт

        // Преобразуем указатели в типы функций
        let add_fn: fn(&Modulo, u64, u64) -> u64 = std::mem::transmute(add_fn);
        let mul_fn: fn(&Modulo, u64, u64) -> u64 = std::mem::transmute(mul_fn);
        let check_fn: fn(&Modulo, u64) -> u64 = std::mem::transmute(check_fn);

        // Вызываем функции с использованием преобразованных указателей
        let add_result = add_fn(&*data_ptr, 2, 3);
        let mul_result = mul_fn(&*data_ptr, 2, 5);
        let check_result = check_fn(&*data_ptr, 11);

        println!("Result of add: {}", add_result);
        assert_eq!(add_result, 2);
        println!("Result of mul: {}", mul_result);
        assert_eq!(mul_result, 1);
        println!("Result of check: {}", check_result);
        assert_eq!(check_result, 11);

        let drop_fn: fn(*const Modulo) = std::mem::transmute(drop_fn); // Преобразуем указатель в тип функции деструктора
        drop_fn(data_ptr);// Вызов деструктора вручную
    }
}

impl dyn Trait

📌 ????

behavioral/state/state


fn check(test: impl Test){
    test.foo();
    //test.hola();// method not found in `impl Test`
}
trait Test{
    fn foo(&self);
}
impl dyn Test + '_{
    fn hola(&self){
        println!("hola");
    }
    fn play(self: Box<&Self>){
        println!("play");
    }
}

#[derive(Debug)]
struct TestImpl{}

impl Test for TestImpl{
    fn foo(&self){
        println!("TestImpl");
    }
}
fn main(){
    let test:&dyn Test = &TestImpl{};
    test.hola();
    test.foo();

    let test:Box<&dyn Test> = Box::new(&TestImpl{});
    test.play();
}

Проверка трейт-объекта

downcast-rust

Это быстрый и грязный трюк, который работает для этой демонстрации Во-первых, давайте проверим, что толстый указатель действительно широкий:


fn main(){
// Это даст нам 8, 16 и 16 соответственно на 64-битной машине. Так что указатели на трейт-объекты и срезы широкие.
    println!("{}", std::mem::size_of::<&String>());
    println!("{}", std::mem::size_of::<&[u8]>());
    println!("{}", std::mem::size_of::<&dyn X>());
}

Первое слово в толстом указателе для типаж-объекта — это адрес значения, а второе — адрес виртуальной таблицы (для среза второе слово — просто размер).

Виртуальная таблица имеет следующий вид (все поля имеют размер слова):

pointer to drop_in_place
size
align
pointer to fn 1
pointer to fn 2
…
pointer to fn N

trait Trait {
    fn do_something(&self);
    fn do_something_else(&self);
}
impl Trait for String {
    fn do_something(&self) { println!("a string: {}", self); }
    fn do_something_else(&self) { println!("a string: {}", self); }
}
impl Trait for u64 {
    fn do_something(&self) { println!("a u64: {}", self); }
    fn do_something_else(&self) { println!("a u64: {}", self); }
}
fn analyse_fatp(p: *const T, datasize: usize, vtsize: usize) {
    let addr = &p as *const *const T as *const usize;
    let second = (addr as usize + std::mem::size_of::()) as *const usize;
    let datap = unsafe { *addr } as *const usize;
    let vtp = unsafe { *second } as *const usize;
    let data = unsafe { std::slice::from_raw_parts(datap, datasize) };
    let vtable = unsafe { std::slice::from_raw_parts(vtp, vtsize) };
    let vtable = vtable
        .iter()
        .map(|val| format!("0x{:x}", val))
        .collect::>();

    println!("Addr of fat pointer (1st word): {:p}", addr);
    println!("Addr of fat pointer (2nd word): {:p}", second);
    println!("Addr of data:                   {:p}", datap);
    println!("Addr of vtable:                 {:p}", vtp);
    println!("Data:   {:?}", data);
    println!("VTable: {:?}", vtable);
}
fn main(){
    let obj: &dyn Trait = &String::from("hello");
    dbg!(String::do_something as *const ());
    dbg!(String::do_something_else as *const ());
    analyse_fatp(obj, std::mem::size_of::() / std::mem::size_of::(), 5);

    let obj: &dyn Trait = &12_u64;
    dbg!(u64::do_something as *const ());
    dbg!(u64::do_something_else as *const ());
    analyse_fatp(obj, std::mem::size_of::() / std::mem::size_of::(), 5);
}

Мы получаем:

[src/main.rs:38] String::do_something as *const () = 0x00005576c9fe48c0
[src/main.rs:39] String::do_something_else as *const () = 0x00005576c9fe4940
[src/main.rs:43] u64::do_something as *const () = 0x00005576c9fe49c0
[src/main.rs:44] u64::do_something_else as *const () = 0x00005576c9fe4a40

Addr of fat pointer (1st word): 0x7ffc1d178fd0
Addr of fat pointer (2nd word): 0x7ffc1d178fd8
Addr of data:                   0x7ffc1d179438
Addr of vtable:                 0x5576ca02a150
Data:   [93968711141840, 5, 5]
VTable: ["0x5576c9fdfa80", "0x18", "0x8", "0x5576c9fe48c0", "0x5576c9fe4940"]

Addr of fat pointer (1st word): 0x7ffc1d178fd0
Addr of fat pointer (2nd word): 0x7ffc1d178fd8
Addr of data:                   0x5576ca01a250
Addr of vtable:                 0x5576ca02a1f8
Data:   [12]
VTable: ["0x5576c9fdf9b0", "0x8", "0x8", "0x5576c9fe49c0", "0x5576c9fe4a40"]

Преобразование из базового трейта BalanceDao в дочерний BalanceReader

// При использовании трейт обьектов 
// Преобразование к родительскому трейту 

trait BalanceReader {  // ...}
trait BalanceWriter { // ...}

trait BalanceDao: BalanceReader + BalanceWriter + AsBalanceReader{ // ....}
fn foo(reader: &dyn BalanceReader){ // ...}
fn bar(dao: &dyn BalanceDao){
   // foo(dao);// cannot cast `dyn BalanceDao` to `dyn BalanceReader`, trait upcasting coercion is experimental
    foo(dao.as_reader());
}
// С помощью дополнительно трейта ограничителя сможем преобразовать из &dyn BalanceDao в &dyn BalanceReader
trait AsBalanceReader {
    fn as_reader(&self) -> &dyn BalanceReader;
}
impl<T:'static + BalanceReader> AsBalanceReader for T {
    fn as_reader(&self) -> &dyn BalanceReader {
        self
    }
}

Более общий вид

trait CastToSuper<Super; ?Sized> {
    fn as_super(&self) -> &Super;
    fn as_super_mut(&mut self) -> &mut Super;
    fn into_super_arc(self: Arc<Self>) -> Arc<Super>;
}

trait BalanceDao: BalanceReader + BalanceWriter + CastToSuper<dyn Balancereader> {
  // ...
}

impl<'a,T: 'a + BalanceReader> CastToSuper<dyn BalanceReader + 'a> for T {
    fn as_super(&self) -> &(dyn BalanceReader + 'a) {
        self
    }
    fn as_super_mut(&mut self) -> &mut (dyn BalanceReader + 'a) {
        self
    }
    fn into_super_arc(self: Arc<Self>) -> Arc<dyn BalanceReader + 'a> {
        self
    }
}

Необходимая безопасность для объектов-признаков

object-safety

error-index.html#E0038

Ограничения объекта признака

Типаж является объектно-безопасным, если все методы определённые в нем имеют следующие свойства:

  • Тип возвращаемого значения не является Self.
  • Нет обобщённых параметров типа.

Ключевым словом Self является псевдонимом типа мы реализующий черту или метод на. Объекты-черты должны быть объектно-безопасными, потому что после того, как вы использовали объект-черту, Rust больше не знает конкретный тип, реализующий эту черту. Если метод признака возвращает конкретный Self тип, но объект признака забывает точный тип, который Self есть, то метод не может использовать исходный конкретный тип. То же самое верно и для параметров универсального типа, которые заполняются параметрами конкретного типа при использовании признака: конкретные типы становятся частью типа, реализующего признак. Когда тип забывается из-за использования объекта-признака, невозможно узнать, какими типами следует заполнить параметры универсального типа.

Примером свойства, методы которого не являются объектно-безопасными,из него нельзя сделать trait object Т.е. в момент использования метода clone наш trait object перестанет быть абстрактным типом и станет конкретным типом который возвращали Self

pub trait Clone {
    fn clone(&self) -> Self;
}

Если нам нужен Trait для trait-object то: Trait не может требовать Self: Sized, мы не можем создать объект типа Box<Foo> или, &Foo

Обычно `Self: Sized` используется, чтобы указать, что признак не должен использоваться в качестве объекта признака.

Trait не должен ссылается в методах на Self тип в своих параметрах или возвращаемом типе

Trait должен содержать ссылку на self либо Self: Sized

  // Решение: добавляем `Sized`
   trait Foo {
      fn foo() -> u8 where Self: Sized;//  ✅ correct
      fn foo1(&self) -> u8; //  ✅ correct
      fn foo2() -> u8; // ❌ Error trait `Foo` не может быть превращен в объект
   }

Trait не может содержать связанных констант.

   trait  Foo {
     const  X : i32 ; ❌
   }
   // Решение: вспомогательный метод
    trait Foo {
       fn x(&self) -> i32; ✅ 
    }

Trait не должен имеет параметры универсального типа в методах.

Как упоминалось ранее, типажные объекты содержат указатели на таблицы методов.

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

Это нормально работает, но когда метод получает общие параметры, у нас могут возникнуть проблемы с бесконечностью вариантов создаваемых vtable

Это не происходит со статической диспетчеризацией мономорфных функций, а с trait object нужно создать vtable всех вариантов.

trait Trait {
    fn foo<T>(&self, on: T); ❌
    // more methods
}
impl ​Trait for String {
   ​fn foo<T>(&self, on: T) {
       ​// implementation 1
   ​}
}
impl ​Trait for u8 {
   ​fn foo<T>(&self, on: T) {
       ​// implementation 2
   ​}
}
// и еще 8 реализаций

fn call_foo(thing: Box<Trait>) {
    thing.foo(true); // this could be any one of the 8 types above
    thing.foo(1);
    thing.foo("hello");
}
fn main(){}

Нам не просто нужно создать таблицу всех реализаций всех методов Trait, нам нужно создать такую ​​таблицу для каждого типа, в который подается foo().

В данном случае это оказывается (10 реализаций типов Trait) * (подаваемые 3 типа foo()) = 30 реализаций!

Чтобы исправить это, предлагается использовать where Self: Sized границу, аналогичную исправлению для ошибки выше, если вы не собираетесь вызывать метод с параметрами типа:

trait  Trait {
   fn  foo<T>(&self, on: T) where Self: Sized;
   // more methods
}

Если это не вариант, подумайте о замене параметра типа другим типовым объектом (например, если T: OtherTrait, используйте on: Box<OtherTrait>).

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

Признак не может использоваться Self в качестве параметра типа в листинге супертрейтов.

   trait Super<A: ?Sized> {}
   trait ​Trait: Super<Self> { }
 // Решение реорганизовать код

динамический dyn возможность возвращать значения разных типов


trait Say{
    fn say(&self);
}
struct Dog;
struct Cat;
 
impl Say for Dog{
    fn say(&self){println!("Dog gav");}
} 
impl Say for Cat{
    fn say(&self){println!("Cat may");}
} 
 fn get()->Box {
      if true { Box::new(Cat) }else{ Box::new(Dog)}
}
fn main() {
 let cat:Cat = Cat;
 let cat:&dyn Say = &cat;

 let dog:Dog = Dog;
 let dog:&dyn Say = &dog;
   
 let animals:Vec<&dyn Say> = vec![cat,dog];  
 
 for animal in animals.iter(){
     animal.say();
 } 
}
                                                                                                                                                                                                                                                                                                             `dyn T + 'static`
// У `dyn T` типов тоже есть lifetime:

type Callback<'a> = Box<dyn Fn() + 'a>;

fn main() {
    let f: Callback<'static> = Box::new(|| println!("hello")); // может жить сколько угодно пока мы его не деаллокацируем

    let x = 92;
    let g: Callback<'_> =
    Box::new(|| println!("x = {}", x));// тут x захватывается по ссылке так как нет ключевого слова move значит x имеит ifetime соответствующий локальной переменной x
}

Box<dyn T> == Box<dyn T + 'static>
&'a dyn T == &'a (dyn T + 'a)
dyn T == dyn T +'static

Box<dyn ...


type Callback = Box u32>;

fn adder(x: u32) -> Callback {
  Box::new(move |y| x + y)
}

fn multiplier(x: u32) -> Callback {
  Box::new(move |y| x * y)
}

fn main(){
    let i = 4;
    let f = adder(i);
    let result = f(4);
    assert_eq!(8,result);
}

Ассоциированные (связанные) типы — это мощная часть системы типов в Rust.

Они связаны с идеей 'семейства типа', другими словами, группировки различных типов вместе.

Это полезно, когда нам нужно использовать какой-то тип, отличный от Self сигнатур функций, но все же хотелось бы, чтобы тип выбирался реализатором, а не был жестко закодирован в объявлении трейта.

Зачем нужны Ассоциированные типы

habr.com/ru/articles/441444

Зачем нужны Ассоциированные типы:

  • улучшает общую читаемость кода
  • переносит контроль над внутренним типов в реализацию (в отличии от generic, где контроль типа на стороне вызывающего код)

Пример:

Допустим, у нас есть коллекция, и мы хотим получить от неё итератор. Значения какого типа должен возвращать итератор? В точности того, который содержится в этой коллекции! Не вызывающая сторона должна решать, что вернёт итератор, а сам итератор лучше знает, что именно он умеет возвращать.

Вариант с generic.

тип T нужно снова и снова указывать в каждом месте, где упоминается итератор, а во-вторых, теперь стало возможно реализовать этот трейт несколько раз с разными типами.

Мы не можем просто взять и вызвать iter.next(), нужно обязательно дать компилятору знать, явно или неявно, какой тип будет возвращаться.

А всё потому, что мы смогли имплементировать трейт Iterator дважды с разным параметром для одного и того же MyIterator



trait Iterator {
    fn next(&mut self) -> Option;
}
struct MyIterator;
impl Iterator for MyIterator {
    fn next(&mut self) -> Option {Some(0)}
}
impl Iterator for MyIterator {
    fn next(&mut self) -> Option {Some(0)}
}
fn foo(container: &I) -> i32 where I: Iterator { 0} //❌  приходится прокидывать V тип и выразить возвращаемое значение i32 не получилось через Iterator
fn main() {
    let mut iter = MyIterator;
    let lolwhat: Option<_> = iter.next();// ❌ Error! Какую реализацию выбрать i32 или String (в случае с Associated types мы не можем создать несколько вариантов `impl Iterator...`)
    let lolwhat: Option = iter.next();//  контроль типа на стороне вызывающего код, что нам не желательно в данном случае
}

Вариант с Associated types.

Заметьте, что у итератора нет параметра типа, который позволил бы вызывающей стороне выбрать, что должен вернуть итератор.

Значения определяется самим итератором с помощью ассоциированного типа



trait Iterator {
    type Item;
    fn next(&mut self) -> Option;
}
struct MyIterator;
impl Iterator for MyIterator {
    type Item = i32;
    fn next(&mut self) -> Option { None }
}
fn foo(container: &C) -> ::Item { 0 } // ✅  коротко и возвращаемое значение выразить через Iterator
fn main() { 
    let mut iter = MyIterator;
    let lolwhat: Option<_> = iter.next(); //  ✅ 
}

1. улучшает общую читаемость кода

Неудобный синтаксис при передаче Graph в ф-цию как параметра

trait Graph<N, E> {
    fn has_edge(&self, &N, &N) -> bool;
    fn edges(&self, &N) -> Vec<E>;
}
fn distance<N, E, G: Graph<N, E>>(graph: &G, start: &N, end: &N) -> u32 {...}
fn main(){}

Решение

Чтобы сформировать какого-либо вида Graph, нужны соответствующие типы E и N, собранные вместе с помощью ассоциированных типов

trait Graph {
    type N;
    type E;

    fn has_edge(&self, &Self::N, &Self::N) -> bool;
    fn edges(&self, &Self::N) -> Vec<Self::E>;
}
fn distance<G: Graph>(graph: &G, start: &G::N, end: &G::N) -> u32 {...} // Больше нет необходимости иметь дело с типом E

// Реализация ассоциированных типов
struct Node;
struct Edge;
struct MyGraph;
impl Graph for MyGraph {
    type N = Node;
    type E = Edge;
    fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
        true
    }
    fn edges(&self, n: &Node) -> Vec<Edge> {
        Vec::new()
    }
}

// Типаж-объект с явным указанием ассоциативного типа
fn main(){
    let graph = MyGraph;
    let obj = Box::new(graph) as Box<Graph<N=Node,E=Edge>>
}

2. улучшает общую читаемость кода

Группировки различных типов вместе


trait Graph {
    fn has_edge(&self, &N, &N) -> bool;
    fn edges(&self, &N) -> Vec;
}

struct Node {
    position:T
}

//ребро
struct Edge< N>{
    start:N,
    end:N
}
 //узел и ребро
struct MyGraph{
    n:N,e:E
}
impl Graph for MyGraph {
    fn has_edge(&self, n1:&N, n2:&N) -> bool{
        true
    }

    fn edges(&self, n:&N) -> Vec{
        let mut vec = vec![];
        vec
    }
}
fn distance>(graph: &G, start: &N, end: &N) -> u32 {
    // print!("fn distance {:?} ",start.position);
    10
}
fn main(){
    let start:i32 = 1;
    let end:i32 = 2;
    let n_head = Node{position:start};
    let n_end = Node{position:end};
    let e = Edge{start:n_head,end:n_end};
    print!("fn distance {:?} ",e.start.position);

    // тип передается через структуру
    // тип E теперь значит Edge
    let n  = Node{position:start};
    let gr =  MyGraph{n:n ,e:e};

    let n_head2 = Node{position:start};
    let n_end2 = Node{position:end};

    let n = distance(&gr,&n_head2,&n_end2);
    print!("fn distance {:?} ",n);

 // вернуть должны Vec
    let mut v = gr.edges(&n_head2);

   if gr.has_edge(&n_head2,&n_end2){
       print!("yes");
   }
}

Чтобы сформировать какого-либо вида Graph, нужны соответствующие типы E и N, собранные вместе с помощью ассоциированных типов


trait Graph {
     type N;
     type E;

     fn has_edge(&self, &Self::N, &Self::N) -> bool;
     fn edges(&self, &Self::N) -> Vec;
}

 //Теперь наши клиенты могут абстрагироваться от определенного Graph
struct Node {
     position:i32
}

//ребро
struct Edge< N>{
    start:N,
    end:N
}
//узел и ребро
struct MyGraph{
    n:N,e:E
}

impl  Graph  for MyGraph  {
     type N = Node;
     type E = Edge;


     fn has_edge(&self, n1: &Node, n2: &Node) -> bool {
         true
     }

     fn edges(&self, n: &Node) -> Vec> {
         Vec::new()
     }
}
fn distance(graph: &G, start: &G::N, end: &G::N) -> u32 {
     print!("fn distance  ",);
     10
}
fn main(){
    let start:i32 = 1;
    let end:i32 = 2;
    let n_head = Node{position:start};
    let n_end = Node{position:end};
    let e = Edge{start:n_head,end:n_end};
    print!("fn distance {:?} ",e.start.position);

    // тип передается через структуру
    // тип E теперь значит Edge
    let n  = Node{position:start};
    let gr =  MyGraph{n:n ,e:e};

    let n_head2 = Node{position:start};
    let n_end2 = Node{position:end};

    let n = distance(&gr,&n_head2,&n_end2);
    print!("fn distance {:?} ",n);

    // вернуть должны Vec
    let mut v = gr.edges(&n_head2);

    if gr.has_edge(&n_head2,&n_end2){
        print!("yes");
    }
 }

Аннотация ассоциированных типов


trait Base{
    type N;
    type M:std::fmt::Debug;
    fn show(&self,n:Self::N)->Self::M;
}
struct User{
    name:String
}
impl Base for User{
    type N = u32;
    type M = String;
    fn show(&self,n:Self::N)->Self::M{
        n.to_string()
    }
}
fn test2> (val:&T,  n:u32) {
    println!("{:?}", val.show(n) );
}
fn test (val:&Base,  n:u32) {
    println!("{:?}", val.show(n) );
}
fn main() {
    let o:User = User{name:String::from("Egor")};
    println!("{}", o.show(8));
    test(&o,8_u32);
    let v:Vec>> = vec![Box::new(o)];
}

могут быть объявлены в любой области видимости, включая глобальную.

const: Неизменяемая переменная (в общем случае).

static: Возможно, изменяемая переменная с временем жизни 'static.

const - предназначен для значений, которые не изменяются и создаются во время компиляции.

static - похож на const, но имеет фиксированное расположение в памяти. Он может не быть создан во время компиляции.

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

Что означает использование значения static всегда будет обращаться к тем же данным.

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

Другое отличие между константами и статическими переменными состоит в том, что статические переменные могут быть изменчивыми.

Доступ к изменяемым статическим переменным и их изменение небезопасны.

Предпочтительней. Постоянные живут в течение всего времени работы программы. Вычисляются только во время компиляции. А именно, у них вообще нет определённого адреса в памяти !!!

тип постоянной надо указывать всегда.

const может быть:

  • использование констант из внешних крейтов
  • ссылка должна иметь время жизни 'static

не полагайтесь на адрес в памяти для const

Когда ты создаешь константу с временем жизни 'static, например с ключевым словом const, компилятор Rust гарантирует, что её значение будет одинаковым везде, где она используется. Но важный момент: компилятор может решить создать несколько копий этого значения в разных местах программы. Это значит, что если ты используешь константу в нескольких местах, компилятор может вставить её значение прямо в эти места (так называемое "встраивание" или "инлайн").

Теперь важная часть: если ты начинаешь использовать указатели или ссылки на эту константу и ожидаешь, что все они будут указывать на одну и ту же область памяти, это может быть не так. Компилятор может создать несколько копий одной и той же константы в разных участках памяти, и тогда твои указатели будут указывать на разные места, хотя значения будут одинаковыми.

Вот пример:


fn main(){
    const MY_CONST: i32 = 42;

    let x = &MY_CONST as *const i32;
    let y = &MY_CONST as *const i32;

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

На первый взгляд может показаться, что указатели x и y должны указывать на одно и то же место в памяти. Но из-за того, что компилятор может сделать несколько копий значения MY_CONST, x и y могут указывать на разные адреса в памяти, хотя значение в них будет одинаковым (оба будут равны 42).

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

можно выполнять сложные вычисления на этапе компиляции, а не в runetime

  • циклы (loop, while, for),
  • if/else, match,
  • аллокации в const-контексте (частично),
  • вызовы других const fn,
  • структуры с мутацией через &mut

Более интересный пример: создание массива в compile-time


const fn make_array(n: usize) -> [u32; 5] {
    let mut arr = [0; 5];
    let mut i = 0;
    while i < 5 {
        arr[i] = (i as u32) * (n as u32);
        i += 1;
    }
    arr
}

const RESULT: [u32; 5] = make_array(3);

fn main() {
    println!("{:?}", RESULT); // [0, 3, 6, 9, 12]
}

const-инициализация через match:



const fn classify(x: i32) -> &'static str {
    match x {
        n if n < 0 => "negative",
        0 => "zero",
        _ => "positive",
    }
}

const CATEGORY: &str = classify(-5);

fn main() {
    println!("Category: {}", CATEGORY); // "negative"
}

inline встраиваются


struct BitsNStrings<'a> {
    mybits: [u32; 2],
    mystring: &'a str,
}
const BIT1: u32 = 1 << 0;

fn main() {
  const BIT2: u32 = 1 << 1;

  const BITS: [u32; 2] = [BIT1, BIT2];
  const STRING: &'static str = "bitstring";

  const BITS_N_STRINGS: BitsNStrings<'static> = BitsNStrings {
      mybits: BITS,
      mystring: STRING,
  };
  print!("{}",BIT2);// встраиваются (inline) в каждое место, где есть их использование
}

в локальной области живут временно и const и static

fn main(){
   {
       const c:i32 =9;
       static s:i32 =8;
   }
   // cannot find value `c` in this scope
   //cannot find value `s` in this scope
   print!("{}{}",c,s);
}

Константа возвращается как 'static

но если есть реализация деструктора то будет ошибка

pub struct Wrapper(pub i32);

impl Drop for Wrapper {
    fn drop(&mut self) {}
}

const ANSWER: Wrapper = Wrapper(42);

pub fn the_answer() -> &'static Wrapper {
    // `Wrapper` has a destructor, so the promotion to the `'static`
    // lifetime for a reference to a constant does not apply.
    &ANSWER
}
fn main(){}

На константы можно брать ссылку


const X: i32 = 92;
const R: &'static i32 = &X;
fn foo(x: &i32) {
   println!("{}", x);
}
fn main() {
   foo(R);
}

В константах работает lifetime elision, результат — 'static

const WORDS: &[&str] = &[
   "hello",
   "world",
];

const fn функции

Применение const fn

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

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


const fn double(x: i32) -> i32 {
    x * 2
}

const TEN: i32 = double(5); // Это будет выполнено во время компиляции

fn main() {
    assert_eq!(10, TEN);
}

const в fn

generics

fn double<const N: i32>() {
    println!("doubled: {}", N * 2);
}

const SOME_CONST: i32 = 12;

fn example() {
    // Example usage of a const argument.
    double::<9>();
    double::<-123>();
    double::<{7 + 8}>();
    double::<SOME_CONST>();
    double::<{ SOME_CONST + 5 }>();
}
fn main(){}

Решение конфликта имен

type N = u32; // Псевдоним типа
struct Foo<const N: usize>;

// Следующее является ошибкой, так как `N` интерпретируется как псевдоним типа `N`
fn foo<const N: usize>() -> Foo<N> { todo!() } // ❌ ERROR

// Можно исправить, заключив его в фигурные скобки, чтобы он интерпретировался как константный параметр `N`:
fn bar<const N: usize>() -> Foo<{ N }> { todo!() } // ✅ ok
fn main(){}

Изменяемые данные, что-то вроде глобальной переменной, не встраиваются в место их использования.

Это значит, что каждое значение существует в единственном экземпляре, и у него есть определённый адрес

Более того, любой тип, хранимый в статической переменной, должен быть ограничен Sync и не может иметь реализации Drop

После выхода из области видимости недоступен но остается в памяти !

static [mut] IDENTIFIER: type = expr;
lazy_static! {static ref IDENTIFIER: type = expr; }

crate once_cell

для однократной инициализации данных

once_cell

lazy_static, once_cell для однократной инициализации данных

Ленивые инициализированные глобальные данные

use std::{sync::Mutex, collections::HashMap};
use once_cell::sync::OnceCell;
fn global_data() -> &'static Mutex<HashMap<i32, String>> {
    static INSTANCE: OnceCell<Mutex<HashMap<i32, String>>> = OnceCell::new();
    INSTANCE.get_or_init(|| {
        let mut m = HashMap::new();
        m.insert(13, "Spica".to_string());
        m.insert(74, "Hoyten".to_string());
        Mutex::new(m)
    })
}

использовать отдельную копию объекта для каждого потока

Macro std::thread_local - для статических переменных

ThreadLocal - для каждого объекта

macro.thread_local

thread_local

Макрос упаковывает любое количество статических объявлений и делает их локальными для потока.

use std::cell::RefCell;
thread_local! {
    pub static FOO: RefCell<u32> = RefCell::new(1);
    static BAR: RefCell<f32> = RefCell::new(1.0);
}

FOO.with(|foo| assert_eq!(*foo.borrow(), 1));
BAR.with(|bar| assert_eq!(*bar.borrow(), 1.0));
fn main(){}

Эта библиотека предоставляет ThreadLocal тип, который позволяет использовать отдельную копию объекта для каждого потока. Это позволяет использовать локальное хранилище потока для каждого объекта, в отличие от макроса стандартной библиотеки, thread_local! который допускает только статическое локальное хранилище потока.

use thread_local::ThreadLocal;
use std::sync::Arc;
use std::cell::Cell;
use std::thread;

let tls = Arc::new(ThreadLocal::new());

// Create a bunch of threads to do stuff
for _ in 0..5 {
    let tls2 = tls.clone();
    thread::spawn(move || {
        // Increment a counter to count some event...
        let cell = tls2.get_or(|| Cell::new(0));
        cell.set(cell.get() + 1);
    }).join().unwrap();
}
fn main(){
    // После завершения всех потоков соберите значения счётчиков и верните 
    // сумму всех локальных значений счётчиков потока.
    let tls = Arc::try_unwrap(tls).unwrap();
    let total = tls.into_iter().fold(0, |x, y| x + y.get());
    assert_eq!(total, 5);
}

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

Поэтому и чтение, и изменение статического изменяемого значения выполняют в небезопасном блоке unsafe


static  S1: i32 = 5;
  
static mut S2: i32 = 5;

fn test(){
    //print!("{}",M);//ошибка,  в этой области видимости ее нет
    unsafe {
        S2 += 1;// глобальная
        print!("{}",S2);
    }
}
fn main(){
    let  M: i32 = 5;
    test();
}

// При операциях со static mut , вызывающий код должен сам гарантировать, что не создаётся алиасинг
static mut COUNTER: usize = 0;
fn main() {
  unsafe{ COUNTER += 1;  }
  unsafe{ println!("{}",COUNTER); }
}

время жизни привязано к времени входного аргумента.


static NUM: i32 = 18;// `'static` lifetime.

//Возвращает ссылку на `NUM`, где ее` `static`
// время жизни привязано к времени входного аргумента.
fn coerce_static<'a>(_: &'a i32) -> &'a i32 {
    &NUM
}
fn main() {
    {
        let static_string = "I'm in read-only memory";
        println!("static_string: {}", static_string);
        // Когда `static_string` выходит за пределы области видимости, ссылка
          // больше нельзя использовать, но данные остаются в двоичном формате.
    }

    {
        //Создайте целое число, используемое для `coerce static`:
         let lifetime_num = 9;
        // Заклинание `NUMBER` на время жизни` lifetime_num`:
        let coerced_static = coerce_static(&lifetime_num);

        println!("coerced_static: {}", coerced_static);
    }
    println!("NUM: {} stays accessible!", NUM);
}

mut_static Безопасный статический мут

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

Тем не менее, введение взаимоисключающих блокировок вокруг данных позволяет безопасные для памяти изменяемые глобальные переменные. Это НЕ означает, что они логически безопасны!


#[macro_use]
extern crate lazy_static;
extern crate mut_static;
use mut_static::MutStatic;
pub struct MyStruct { value: usize }
impl MyStruct {
    pub fn new(v: usize) -> Self{ MyStruct { value: v } }
    pub fn getvalue(&self) -> usize { self.value }
    pub fn setvalue(&mut self, v: usize) { self.value = v }
}
lazy_static! {
    static ref MY_GLOBAL_STATE: MutStatic = MutStatic::new();
}
fn main() {
    // Here, I call .set on the MutStatic to put data inside it.
    // This can fail.
    MY_GLOBAL_STATE.set(MyStruct::new(0)).unwrap();
    {
        // Using the global state immutably is easy...
        println!("Before mut: {}", 
       MY_GLOBAL_STATE.read().unwrap().getvalue());
    }
    {
         // Using it mutably is too...
         let mut mut_handle = MY_GLOBAL_STATE.write().unwrap();
         mut_handle.setvalue(3);
         println!("Changed value to 3.");
    } 
    {
        // As long as there's a scope change we can get the 
        // immutable version again...
        println!("After mut: {}", 
                 MY_GLOBAL_STATE.read().unwrap().getvalue());
    }
    {
        // But beware! Anything can change global state!
        foo();
        println!("After foo: {}", 
                 MY_GLOBAL_STATE.read().unwrap().getvalue());
    }
}

// Note that foo takes no parameters
fn foo() {
    let val;
    {
        val = MY_GLOBAL_STATE.read().unwrap().getvalue();
    }
    {
        let mut mut_handle = 
            MY_GLOBAL_STATE.write().unwrap();
        mut_handle.setvalue(val + 1);
    }
}

Этот код выводит результат:

Before mut: 0
Changed value to 3.
After mut: 3
After foo: 4

Это не то, что должно произойти в Rust.

foo() не принимала изменчивую ссылку на что-либо, поэтому она не должна была ничего мутировать, и все же она сделала это. Это может привести к очень трудным для отладки логических ошибок.

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

extern crate lazy_static;

Используйте lazy_static для создания глобальных неизменяемых переменных, которые инициализируются во время выполнения.

lazy_static

tests/lazy_static

[dependencies]
lazy_static = "1.2.0"
#[macro_use]
extern crate lazy_static; 
 lazy_static! {
    static ref REGEX_EMAIL: Regex = Regex::new(".+@.+").unwrap();
}  //  компилируется один раз при запуске программы

fn is_email(email: &str) -> bool {
    REGEX_EMAIL.is_match(email)
}

#[macro_use]
extern crate lazy_static;
use std::sync::Mutex;

lazy_static! {
    static ref ARRAY: Mutex> = Mutex::new(vec![]);
}

fn do_a_call() {
    ARRAY.lock().unwrap().push(1);
}
fn main(){
    do_a_call();
    do_a_call();
    do_a_call();

    println!("called {}", ARRAY.lock().unwrap().len());
}

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

fn some_helper_function(text: &str) -> bool {
    lazy_static! {
        static ref RE: Regex = Regex::new("...").unwrap();
    }
    RE.is_match(text)
}
use std::collections::HashMap;
use std::sync::{Arc, RwLock};
use diesel::{Connection, RunQueryDsl, SqliteConnection};
lazy_static! {
    pub static ref USERS: Arc<RwLock<HashMap<String, User>>> = Arc::default();
}
pub struct User {
    pub username: String,
    pub color: String,
    pub password: String,
}
pub fn load(db: &str) -> Result<(), ()> {
    let db = SqliteConnection::establish(db).unwrap();
    let mut users = USERS.write().unwrap();
    ::db::schema::users::dsl::users
        .load::<::db::models::User>(&db)
        .map_err(|_| ())?
        .iter()
        .map(|user| User {
            username: user.username.clone(),
            color: user.color.clone(),
            password: user.password.clone(),
        })
        .for_each(|user| {
            let _ = users.insert(user.username.clone(), user);
        });
    Ok(())
}

Это включает в себя все, что требует распределения кучи, например векторов или хеш-карт, а также все, что требует вычисления неконстантных функций.


#[macro_use]
extern crate lazy_static;
use std::collections::HashMap;
lazy_static! {
    static ref HASHMAP: HashMap = {
        let mut m = HashMap::new();
        m.insert(0, "foo");
        m.insert(1, "bar");
        m.insert(2, "baz");
        m
    };
}
fn main(){
    // First access to `HASHMAP` initializes it
    println!("The entry for `0` is \"{}\".", HASHMAP.get(&0).unwrap());
    // Any further access to `HASHMAP` just returns the computed value
    println!("The entry for `1` is \"{}\".", HASHMAP.get(&1).unwrap());
}
lazy_static! {
    static ref S1: &'static str = "a";
    static ref S2: &'static str = "b";
}
lazy_static! {
    static ref S3: String = [*S1, *S2].join("");
}

Одним из основных вариантов использования является взаимодействие с C.

API C могут (и в зависимости от области, часто делают) представлять объединения, и поэтому это значительно упрощает написание API-оболочек для этих библиотек.


union MyUnion {
    f1: u32,
    f2: f32,
}
// Союзы вроде как перечисления, но они «без тегов». У перечислений есть «тег», который хранит, какой вариант является правильным во время выполнения;  союзы не имеют этого тега.

// Поскольку мы можем интерпретировать данные, хранящиеся в объединении, используя неправильный вариант, а Rust не может проверить это для нас, это означает, что чтение поля объединения небезопасно:
fn main(){
    let mut u = MyUnion { f1: 1 };
    u.f1 = 5;
    let value = unsafe { u.f1 };
}

// Сопоставление с шаблоном тоже работает:
fn f(u: MyUnion) {
    unsafe {
        match u {
            MyUnion { f1: 10 } => { println!("ten"); }
            MyUnion { f2 } => { println!("{}", f2); }
        }
    }
}