Основы Rust: структуры и трейты

Игорь Грегорченко

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

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

Структуры

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

На помощь приходят структуры Rust, которые содержат именованные поля:

// struct1.rs

struct Person {
    first_name: String,
    last_name: String
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Smith".to_string()
    };
    println!("person {} {}", p.first_name,p.last_name);
}

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

Инициализация этой структуры немного неуклюжа, поэтому мы хотим перенести создание Person в отдельную функцию. Эту функцию можно сделать ассоциированной функцией Person, поместив ее в блок impl:

// struct2.rs

struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

}

fn main() {
    let p = Person::new("John","Smith");
    println!("person {} {}", p.first_name,p.last_name);
}

Здесь нет ничего магического в имени new. Заметьте, что доступ к структуре осуществляется с использованием C++-подобной нотации с двойным двоеточием ::.

Вот метод Person, который принимает аргумент в виде ссылки self:

impl Person {
    ...

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

}
...
    println!("fullname {}", p.full_name());
// fullname John Smith

Self используется явно и передается как ссылка. Для простоты можно считать, что &self — это сокращение от self: &Person.

Ключевое слово Self относится к типу struct — можно мысленно заменить Person на Self:

fn copy(&self) -> Self {
        Self::new(&self.first_name,&self.last_name)
    }

Методы могут позволять изменять данные, используя изменяемый аргумент self:

fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

И данные перейдут в метод при использовании обычного аргумента self:

fn to_tuple(self) -> (String,String) {
       (self.first_name, self.last_name)
   }

Попробуйте сделать это с &self и получите ошибку — структуры не отпустят свои данные без боя!

Обратите внимание, что после вызова v.to_tuple() переместился и больше не доступен v. Очень важная мелочь, которую рекомендую неспешно обдумать.

Подведем промежуточные итоги:

  • без аргумента self: вы можете связывать функции со структурами, как новый конструктор
  • аргумент &self: можно использовать как значения структуры, но не изменять их
  • &mut аргумент self: может изменять значения
  • аргумент self: будет потреблять значение, которое будет перемещаться

Если вы попытаетесь сделать отладочный дамп Person, то получите информативную ошибку:

error[E0277]: the trait bound `Person: std::fmt::Debug` is not satisfied
  --> struct2.rs:23:21
   |
23 |     println!("{:?}", p);
   |                     ^ the trait `std::fmt::Debug` is not implemented for `Person`
   |
   = note: `Person` cannot be formatted using `:?`; if it is defined in your crate,
    add `#[derive(Debug)]` or manually implement it
   = note: required by `std::fmt::Debug::fmt`

Компилятор дает совет, поэтому мы ставим #[derive(Debug)] перед Person, и теперь есть разумный вывод:

Person { first_name: "John", last_name: "Smith" }

Директива заставляет компилятор генерировать реализацию Debug, что очень полезно. Хорошая практика — делать это для ваших структур, чтобы их можно было распечатать (или записать в виде строки с помощью format!). Кстати, делать это по умолчанию было бы очень некрасиво.

С учетом всего сказанного выше, вот окончательный вариант программы:

// struct4.rs
use std::fmt;

#[derive(Debug)]
struct Person {
    first_name: String,
    last_name: String
}

impl Person {

    fn new(first: &str, name: &str) -> Person {
        Person {
            first_name: first.to_string(),
            last_name: name.to_string()
        }
    }

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

    fn set_first_name(&mut self, name: &str) {
        self.first_name = name.to_string();
    }

    fn to_tuple(self) -> (String,String) {
        (self.first_name, self.last_name)
    }
}

fn main() {
    let mut p = Person::new("John","Smith");

    println!("{:?}", p);

    p.set_first_name("Jane");

    println!("{:?}", p);

    println!("{:?}", p.to_tuple());
    // p has now moved.

}
// Person { first_name: "John", last_name: "Smith" }
// Person { first_name: "Jane", last_name: "Smith" }
// ("Jane", "Smith")

Время жизни начинает кусаться

Обычно структуры содержат значения, но часто они также должны содержать ссылки. Скажем, мы хотим поместить в struct не строковое значение, а строковый фрагмент:

// life1.rs

#[derive(Debug)]
struct A {
    s: &str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}

И получаем такой вывод:

error[E0106]: missing lifetime specifier
 --> life1.rs:5:8
  |
5 |     s: &str
  |        ^ expected lifetime parameter

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

Строковые фрагменты заимствуют строковые литералы типа «hello» или значения String. Строковые литералы существуют в течение всего времени работы программы, которое называется статическим временем жизни.

Таким образом, мы сами гарантируем Rust, что строковый фрагмент всегда ссылается на такие статические строки:

// life2.rs

#[derive(Debug)]
struct A {
    s: &'static str
}

fn main() {
    let a = A { s: "hello dammit" };

    println!("{:?}", a);
}
// A { s: "hello dammit" }

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

Это также можно использовать для задания фрагмента строки, возвращаемого функцией:

fn how(i: u32) -> &'static str {
    match i {
    0 => "none",
    1 => "one",
    _ => "many"
    }
}

Это работает для особого случая статических строк, но это очень ограничивает.

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

// life3.rs

#[derive(Debug)]
struct A <'a> {
    s: &'a str
}

fn main() {
    let s = "I'm a little string".to_string();
    let a = A { s: &s };

    println!("{:?}", a);
}

После этого момента наша структура a и строка s связаны строгим контрактом: a заимствует у s и не может пережить его.

Имея такое определение структуры, мы хотим написать функцию, которая возвращает значение A:

fn makes_a() -> A {
    let string = "I'm a little string".to_string();
    A { s: &string }
}

Но A нужно время жизни — точнее, ожидаемый параметр времени жизни:

= help: this function's return type contains a borrowed value,
   but there is no value for it to be borrowed from
  = help: consider giving it a 'static lifetime

rustc дает нам прямой совет, поэтому мы следуем ему:

fn makes_a() -> A<'static> {
    let string = "I'm a little string".to_string();
    A { s: &string }
}

И теперь важная ошибка:

8 |      A { s: &string }
  |              ^^^^^^ does not live long enough
9 | }
  | - borrowed value only lives until here

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

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

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

Трейты

Обратите внимание, что в Rust не пишется struct class. Ключевое слово class в других языках настолько перегружено смыслом, что фактически блокирует нормальное понимание его предназначения.

Давайте сформулируем это следующим образом: структуры Rust не могут наследоваться от других структур; все они являются уникальными типами. Здесь нет никакой подтипизации — это просто «тупые» данные.

Как же установить отношения между типами? Вот тут-то и приходят на помощь трейты.

В rustc часто говорят о реализации признака X, поэтому пришло время поговорить о признаках как следует.

Вот небольшой пример определения трейта и его реализации для конкретного типа.

// trait1.rs

trait Show {
    fn show(&self) -> String;
}

impl Show for i32 {
    fn show(&self) -> String {
        format!("four-byte signed {}", self)
    }
}

impl Show for f64 {
    fn show(&self) -> String {
        format!("eight-byte float {}", self)
    }
}

fn main() {
    let answer = 42;
    let maybe_pi = 3.14;
    let s1 = answer.show();
    let s2 = maybe_pi.show();
    println!("show {}", s1);
    println!("show {}", s2);
}
// show four-byte signed 42
// show eight-byte float 3.14

Это довольно круто — мы добавили новый метод и в i32, и в f64!

Чтобы освоиться с Rust, необходимо изучить основные черты стандартной библиотеки.

Отладка очень распространена. Мы дали Person реализацию по умолчанию с удобным #[derive(Debug)], но, скажем, мы хотим, чтобы Person отображался как его полное имя:

use std::fmt;

impl fmt::Debug for Person {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.full_name())
    }
}
...
    println!("{:?}", p);
    // John Smith

write! — это очень полезный макрос: здесь f — это все, что реализует функцию Write. Это также работает с файлом или даже строкой.

Display управляет тем, как значения выводятся с помощью “{}“, и реализуется так же, как Debug. В качестве полезного побочного эффекта, ToString автоматически реализуется для всего, что реализует Display. Так что если мы реализуем Display для Person, то p.to_string() тоже будет работать.

Clone определяет метод clone, и может быть просто определен с помощью "#[derive(Clone)]", если все поля сами реализуют Clone.

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

Продолжение следует…

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

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

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023