Синтаксис метода

Методы похожи на функции: мы объявляем их с помощью ключевого слова fn и имени; они могут иметь параметры и возвращаемое значение; они содержат код, запускающийся при вызове метода. В отличие от функций, методы определяются в контексте структуры (или перечисления или трейт-объекта, которые мы рассмотрим в Главе 6 и Главе 17 соответственно), а их первым параметром всегда является self, представляющий собой экземпляр структуры, на которой вызывается этот метод.

Определение методов

Давайте изменим функцию area, имеющую параметр типа &Rectangle, и сделаем из неё метод area, определённый для структуры Rectangle. Посмотрите на Листинг 5-13:

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "Площадь прямоугольника равна {} квадратных пикселей.",
        rect1.area()
    );
}

Чтобы определить функцию в контексте Rectangle, мы создаём блок impl (сокр. от implementation) для Rectangle. Всё, что находится в impl, будет связано с типом Rectangle. Затем мы перемещаем функцию area внутрь фигурных скобок impl и меняем первый (и, в данном случае, единственный) параметр на self — в сигнатуре и в теле. В main, где мы вызывали функцию area и передавали rect1 в качестве аргумента, мы теперь можем использовать синтаксис метода для вызова метода area нашего экземпляра Rectangle. Метод записывается после экземпляра: мы добавляем точку, за которой следует имя метода, а потом в круглых скобках перечисляем остальные аргументы, если таковые требуются.

В сигнатуре area мы используем &self вместо rectangle: &Rectangle. &self на самом деле является сокращением от self: &Self. Внутри блока impl тип Self является псевдонимом типа, для которого реализовывается блок impl. Методы обязаны иметь параметр с именем self типа Self, поэтому Rust позволяет использовать небольшое сокращение в виде единственного слова self на месте первого аргумента. Обратите внимание, что нам по-прежнему нужно использовать & перед сокращением self, чтобы указать на то, что этот метод заимствует экземпляр Self: точно так же, как мы делали это в rectangle: &Rectangle. Как и с любыми другими параметрами, методы могут как брать self во владение, так и заимствовать self: неизменяемо или (как мы поступили в данном случае) изменяемо.

Мы выбрали &self здесь по той же причине, по которой использовали &Rectangle в версии с функцией: мы не хотим брать структуру во владение, мы просто хотим прочитать данные в структуре, а не писать в неё. Если бы мы хотели изменить экземпляр, на котором мы вызывали метод, то мы бы использовали &mut self в качестве первого параметра. Методы, которые берут экземпляры во владение (используя просто self в качестве первого параметра), являются редкими; эта техника обычно используется, когда метод превращает self во что-либо другое и при этом вы хотите запретить вызывающей стороне использовать исходный экземпляр после превращения.

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

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

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!("Длина прямоугольника ненулевая и равна {}", rect1.width);
    }
}

Здесь мы определили, что метод width возвращает значение true, если значение в поле width экземпляра больше 0, и возвращает значение false, если значение равно 0: мы можем создавать методы с теми же именами, что и поля, и даже затем использовать любые поля в теле методов. В main, когда мы ставим после rect1.width круглые скобки, Rust понимает, что мы имеем в виду метод width. Если же мы не используем круглые скобки, Rust понимает, что мы имеем в виду поле width.

Часто, но не всегда, когда мы создаём методы с тем же именем, что и у поля, мы хотим, чтобы он только возвращал значение одноимённого поля и больше ничего не делал. Подобные методы называются геттерами, и Rust их не реализует автоматически для полей структур, как это делают некоторые другие языки. Геттеры полезны тем, что вы можете сделать поле приватным, а метод — публичным, и, таким образом, включить в API этого типа доступ к этому полю лишь для чтения. Мы обсудим, что такое публичность и приватность, и как обозначить поле или метод в качестве публичного или приватного, в Главе 7.

Есть ли тут оператор ->?

В C и C++ используются два разных оператора для вызова методов: . используется, если метод вызывается непосредственно у экземпляра структуры; -> используется, если метод вызывается на указателе на структуру, и потому которую необходимо разыменовать перед вызовом метода. Другими словами, если object — это указатель, то вызовы метода object->something() и (*object).something() являются, по сути, одним и тем же.

Rust не имеет эквивалента оператора ->. Вместо него в Rust есть механизм автоматического взятия ссылок и их разыменования. Вызов методов является одним из немногих мест в Rust, в котором есть такое поведение.

Вот как это работает: когда вы вызываете метод конструкцией object.something(), Rust автоматически добавляет &, &mut или *, таким образом, чтобы object соответствовал сигнатуре метода. Записи ниже равны друг другу:

#![allow(unused)]
fn main() {
#[derive(Debug,Copy,Clone)]
struct Point {
    x: f64,
    y: f64,
}

impl Point {
   fn distance(&self, other: &Point) -> f64 {
       let x_squared = f64::powi(other.x - self.x, 2);
       let y_squared = f64::powi(other.y - self.y, 2);

       f64::sqrt(x_squared + y_squared)
   }
}
let p1 = Point { x: 0.0, y: 0.0 };
let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);
}

Первый пример выглядит намного понятнее. Автоматическое взятие ссылки работает потому, что методу известно, на чём он вызывается — на self. Учитывая вызывающее значения и имя метода, Rust может точно определить, что в данном случае делает код: читает ли метод (&self), делает ли изменение (&mut self) или поглощает значение (self). Тот факт, что Rust может неявно заимствовать вызывающее значение, в значительной степени способствует тому, чтобы делать владение эргономичным и практичным.

Методы с несколькими параметрами

Давайте попрактикуемся в использовании методов, реализовав второй метод на структуре Rectangle. На этот раз мы хотим, чтобы экземпляр Rectangle брал другой экземпляр Rectangle и возвращал true, если второй Rectangle может полностью поместиться внутри self (то есть, первого Rectangle); в противном случае он должен вернуть false. С таким методом мы могли бы написать программу как в Листинге 5-14:

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
    println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}

Ожидаемый результат будет выглядеть так, как ниже: 1) оба измерения экземпляра rect2 меньше, чем измерения экземпляра rect1, и 2) rect3 шире, чем rect1.

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Мы знаем, что хотим определить именно метод, поэтому он будет находится в блоке impl Rectangle. Имя метода будет can_hold, и оно будет принимать в качестве параметра неизменяемое заимствование на другой Rectangle. Мы можем сказать, какой будет тип параметра, посмотрев на код вызова метода: rect1.can_hold(&rect2) передаёт в метод &rect2, что является неизменяемым заимствованием экземпляра rect2 типа Rectangle. В этом есть смысл, потому что нам нужно лишь читать rect2 (а не писать, что означало бы, что нужно изменяемое заимствование), и мы хотим, чтобы main сохранила владение экземпляром rect2, чтобы мы могли использовать его снова после вызова метода can_hold. Возвращаемое значение can_hold имеет логический тип, а реализация проверяет, являются ли ширина и высота self больше, чем, соответственно, ширина и высота другого Rectangle. В Листинге 5-15 приведено определение нового метода can_hold, добавленного в блок impl из Листинга 5-13.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
    println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}

Если мы запустим код с функцией main из Листинга 5-14, мы получим желаемый вывод. Методы могут принимать несколько параметров: мы добавляем их в сигнатуру после первого параметра self. Дополнительные параметры методов работают точно так же, как параметры функции.

Ассоциированные функции

Все функции, определяемые в блоке impl, называются ассоциированными функциями, потому что они ассоциированы с типом, указанным после ключевого слова impl. Мы можем определять и такие ассоциированные функции, которые не используют self в качестве первого параметра (и, следовательно, не являются методами), поскольку им не нужен экземпляр типа для работы. Мы уже использовали одну такую функцию: функцию String::from, определённую для типа String.

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

Файл: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn square(size: u32) -> Self {
        Self {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let sq = Rectangle::square(3);
}

Ключевые слова Self (в сигнатуре и в теле функции) являются псевдонимами для типа, указанного после ключевого слова impl, которым в данном случае является Rectangle.

Чтобы вызывать ассоциированные функции, пишется оператор :: и имя структуры после него; например, let sq = Rectangle::square(3);. Эта функция находится в пространстве имён структуры. Синтаксис :: используется как для ассоциированных функций, так и для пространств имён, образуемых модулями. Мы обсудим модули в Главе 7.

Несколько блоков impl

Любая структура может иметь несколько impl. Например, код Листинга 5-15 эквивалентен коду Листинга 5-16, определяющему каждый метод в отдельном блоке impl.

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
    println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}

В данном случае, нет причин разбивать методы на несколько блоков impl, однако это всё-таки реализуемо. Мы увидим случай, когда несколько impl могут оказаться полезными, в Главе 10, рассматривающей обобщённые типы и трейты.

Подведём итоги

Структуры позволяют вам создавать собственные типы, которые имеют смысл в вашей предметной области. Используя структуры, вы можете хранить вместе и именовать связанные друг с другом фрагменты данных, чтобы делать ваш код чище. В блоках impl вы можете определять 1) функции, ассоциированные с вашим типом, и 2) методы — своего рода ассоциированные функции, поведение которых зависит в том числе от экземпляров, на которых вы их вызываете.

Но структуры — не единственный способ создавать собственные типы. Давайте посмотрим на перечисления — ещё один инструмент в вашем арсенале.