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

Структуры

Одним из представителей составных типов в Rust являются структуры.

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

Синтаксис объявления структуры следующий:

struct ИмяСтруктуры {
    поле_1: Тип1,
    поле_2: Тип2,
    ...
    поле_N: ТипN,
}

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

Поля структуры, как и переменные, должны именоваться в соответствии со змеиной нотацией. Имена самих структур должны следовать паскалевской нотации (Pascal case): имя начинается с заглавной буквы, и если в названии присутствует несколько слов, то каждое слово также начинается с заглавной буквы. Например: User, MainAddress, DatabaseConnection.

Пример структуры, которая хранит имя и фамилию человека:

#![allow(unused)]
fn main() {
struct Person {
    first_name: String,
    last_name: String,
}
}

Синтаксис создания экземпляра структуры:

let переменная = ИмяСтруктуры { поле_1: значение_1,  ..., поле_N: значение_N};

Например:

let person = Person {
    first_name: String::from("John"),
    last_name: String::from("Doe"),
};

Доступ к полю структуры осуществляется при помощи точки: структура.поле.

my_struct.field_1

Пример создания и использования структуры:

struct Person {
    first_name: String,
    last_name: String,
}

fn get_full_name(p: &Person) -> String {
    format!("{} {}", p.first_name, p.last_name)
}

fn main() {
    let p = Person {
        first_name: "John".to_string(),
        last_name: "Doe".to_string()
    };
    let full_name = get_full_name(&p);
    println!("{}", full_name); // "John Doe"
}

Если мы инициализируем поле структуры значением из переменной, чьё имя совпадает с именем этого поля, то вместо имя_поля: имя_переменной можно просто указать переменную.

#![allow(unused)]
fn main() {
let first_name = String::from("John");
let person = Person {
    first_name,
    last_name: String::from("Doe"),
};
}

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

fn main() {
    let mut p = Person {
        first_name: "John".to_string(),
        last_name: "Doe".to_string()
    };
    p.first_name = "Theodor".to_string();
}

Создание одного экземпляра из другого

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

let новый_объект = Структура {
    поле1: новое значение 1,
    поле2: новое значение 2,
    ..старый_объект
};

Пример:

struct Person {
    first_name: String,
    last_name: String,
}

fn main() {
    let p1 = Person {
        first_name: "John".to_string(),
        last_name: "Doe".to_string()
    };

    let p2 = Person { first_name: "Robert".to_string(), ..p1};

    println!("{} {}", p2.first_name, p2.last_name); // Robert Doe
}

Методы

В отличие от традиционных ООП языков, где методы объявляются в теле класса, в Rust методы структуры объявляются отдельно от неё.

Синтаксис:

impl ИмяСтруктуры {

  // метод, который НЕ меняет вызывающий объект
  fn метод_1(&self, аргумент_1: Тип1, …, аргумент_N: ТипN) -> ТипРезультата {
    ...
  }

  // метод, который меняет вызывающий объект
  fn метод_2(&mut self, аргумент_1: Тип1, …, аргумент_N: ТипN) -> ТипРезультата {
    ...
  }

  // "статический" метод, вызываемый на структуре, а не на объекте структуры
  fn метод_3(аргумент_1: Тип1, …, аргумент_N: ТипN) -> ТипРезультата {
    ...
  }
}

Аналогом ключевого слова this из C++ и Java является ключевое слово self, которое необходимо явно передавать в качестве первого аргумента метода.

  • Если метод не меняет состояние объекта, на котором он вызван, то self можно передавать по немутабельной ссылке — &self .
  • Если же методу необходимо изменять хотя бы одно из полей структуры, то self необходимо передавать по мутабельной ссылке — &mut self.
  • В редких случаях, когда метод должен забрать во владение объект, на котором он был вызван, self необходимо передавать по значению — self.

Пример:

struct Person {
  first_name: String,
  last_name: String,
  age: u32
}

impl Person {
  fn new(first: &str, last: &str) -> Person {
    Person {
      first_name: first.to_string(),
      last_name: last.to_string(),
      age: 0
    }
  }
	
  fn change_age(&mut self, new_age: u32) {
    self.age = new_age;
  }
	
  fn introduce(&self) -> String {
    format!("{} {} is {} years old", self.first_name, self.last_name, self.age)
  }
}

fn main() {
  let mut p = Person::new("John", "Doe");
  p.change_age(25);
  println!("{}", p.introduce());
}

Кортежные структуры (tuple structs)

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

Синтаксис:

struct Имя(Тип1, Тип2, …, ТипN);

Пример:

/// Представляет цвет, закодированный RGB каналами
struct RGB (u8, u8, u8);

impl RGB {
    /// Упаковывает все 3 канала в одно 4-байтное число
    fn as_u32(&self) -> u32 {
        ((self.0 as u32) << 16)
            + ((self.1 as u32) << 8)
            + (self.2 as u32)
    }
}

fn main() {
    let mut color: RGB = RGB(255, 0, 0);  // красный цвет
    println!("Red channel: {}", color.0); // Red канал: 255

    color.1 = 255; // Выставляет зелёный канал в 255

    // кортежную структуру можно разложить на составляющие
    // так же, как и обычный кортеж
    let RGB(r, g, b) = color;

    println!("R={r}, G={g}, B={b}"); // R=255, G=255, B=0

    println!("As number: {}", color.as_u32());
}

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

Структура-синглтон

В Rust имеется возможность создать структуру, в которой нет полей. Например:

#![allow(unused)]
fn main() {
struct Universe;
}

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

struct Universe;

impl Universe {
    fn includes(&self, p: &Planet) -> bool {
        true
    }
}

struct Planet {
    name: String
}

fn main() {
    let universe = Universe;

    let earth = Planet { name: "Earth".to_string() };
    println!("{}", universe.includes(&earth)); // true
}

Практическое применение таких структур станет понятнее после прочтения главы Трэйты.

Лайфтаймы у структур

В главе Лайфтаймы мы узнали, что такое лайфтаймы в целом и как задавать их для функций. Теперь давайте разберёмся с лайфтаймами для структур.

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

Рассмотрим пример: у нас будет строка String, которая хранит имя и фамилию, разделённые пробелом, и мы сделаем структуру с двумя полями:

  • ссылка на часть строки, где хранится имя
  • ссылка на часть строки, где хранится фамилия
#[derive(Debug)]
struct NameComponents<'a> {
    first_name: &'a str,
    last_name: &'a str,
}

fn main() {
    let full_name = "John Doe".to_string();

    let space_position = full_name.find(" ").unwrap();

    let components = NameComponents {
        first_name: &full_name[0..space_position],
        last_name: &full_name[space_position + 1 ..],
    };
    println!("{components:?}");
    // NameComponents { first_name: "John", last_name: "Doe" }
}

Благодаря лайфтайму компилятор сможет проконтролировать, что объект структуры NameComponents не “переживёт” строку, на которую ссылаются его поля-ссылки. Например, такой вариант не скомпилируется:

fn main() {
    let components;
    {
        let full_name = "John Doe".to_string();

        let space_position = full_name.find(" ").unwrap();

        // Error: `full_name` does not live long enough
        components = NameComponents {
            first_name: &full_name[0..space_position],
            last_name: &full_name[space_position + 1 ..],
        };
    }
    println!("{components:?}");
}

Лэйаут в памяти

Warning

Если вы плохо знакомы с архитектурой компьютера и плохо понимаете, как данные располагаются на стеке, то этот раздел, скорее всего, покажется непонятным. В таком случае не стоит заострять на нём внимание, так как эта информация не пригодится при разработке back-end приложений на Rust. Однако в будущем настоятельно рекомендуется разобраться с этой темой.

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

Note

Результаты нижеприведённых примеров получены на x86_64 процессоре и версии Rust 1.92

Рассмотрим пример: воспользуемся стандартной функцией size_of, которая возвращает размер типа в байтах.

struct MyStruct {
    a: i64,
    b: i32, 
}

fn main() {
    println!("Size = {}", std::mem::size_of::<MyStruct>()); // Size = 16
}

Как так получается, что структура, состоящая из полей, чей размер соответственно 8 и 4 байта, занимает 16 байт?

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

Note

Процессоры семейства x86_64 имеют регистры размером 8 байт. При операции чтения процессор может адресовать только блоки памяти, чей адрес кратен 8. Например, считать в регистр можно блоки ячеек по адресам 0-7, или 8-15, или 16-23 и т.д. Процессор не может запросить ячейки в диапазоне 4-11 или, тем более, 4-7.

Поэтому когда необходимо вычитать значение, чьё начало расположено по адресу, не кратному 8 (например, 4-11), то процессору приходится считывать куски из смежных блоков (т.е. 0-7 и 8-16), далее битовыми операциями “отрезать” от них нужную часть и “склеивать” воедино. Все эти операции занимают лишние машинные такты. Поэтому целесообразнее выровнять значения в памяти так, чтобы значения можно было считывать за одну операцию. Пусть платой за это и станет перерасход памяти. Получившиеся “пробелы” в памяти называют паддингами (padding).

Давайте посмотрим, по каким адресам располагаются поля a и b. Для этого мы получим указатель на каждое из полей, а потом воспользуемся методом addr(), который возвращает числовое значение адреса указателя.

struct MyStruct {
    a: i32,
    b: i64, 
}

fn main() {
    println!("Size = {}", std::mem::size_of::<MyStruct>()); // Size = 16

    let s = MyStruct { a: 1, b: 2 };
    println!("a: {}", ((&s.a) as *const i32).addr()); // a: 140731421349072
    println!("b: {}", ((&s.b) as *const i64).addr()); // b: 140731421349064
}

Оба адреса 140731421349072 и 140731421349064 кратны 8.

Note

Обычно для распечатки адреса используется форматирующая последовательность {:p} макроса println!, но мы просто хотели явно продемонстрировать, каким образом получается адрес переменной.

Теперь давайте посмотрим как располагаются в памяти массивы структур:

struct MyStruct {
    a: i32,
    b: i64, 
}

fn main() {
    let arr = [
        MyStruct { a: 1, b: 2 },
        MyStruct { a: 3, b: 4 },
        MyStruct { a: 5, b: 6 }
    ];
    println!("arr[0].a: {:p}", &arr[0].a); // arr[0].a: 0x7ffdc124d970
    println!("arr[0].b: {:p}", &arr[0].b); // arr[0].b: 0x7ffdc124d968
    println!("arr[1].a: {:p}", &arr[1].a); // arr[1].a: 0x7ffdc124d980
    println!("arr[1].b: {:p}", &arr[1].b); // arr[1].b: 0x7ffdc124d978
    println!("arr[2].a: {:p}", &arr[2].a); // arr[2].a: 0x7ffdc124d990
    println!("arr[2].b: {:p}", &arr[2].b); // arr[2].b: 0x7ffdc124d988
}

В памяти это выглядит так (здесь мы используем представление адресов в шестнадцатеричном формате):

Здесь хорошо видны как выравнивание, так и паддинги.

Также мы видим, что компилятор поменял местами поля a и b. Да, Rust не гарантирует, что поля структуры в памяти будут иметь такой же порядок, как и в коде объявления структуры.

Если необходимо, чтобы компилятор располагал поля в памяти в том же порядке, что и в объявлении структуры (обычно это нужно при вызове функций из библиотек, написанных на других языках), то структуру следует пометить аннотацией #[repr(C)].

#![allow(unused)]
fn main() {
#[repr(C)]
struct MyStruct {
    a: i32,
    b: i64, 
}
}

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