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

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

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

Давайте изменим функцию area так, чтобы она имела экземпляр Rectangle в качестве входного параметра и сделаем её методом area, определённым для структуры Rectangle, как показано в листинге 5-13:

Файл: src/main.rs

#[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!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

Листинг 5-13: Определение метода area в структуре Rectangle

Чтобы определить функцию в контексте 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.

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

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

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

Файл: src/main.rs

#[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!("The rectangle has a nonzero width; it is {}", 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 есть функциональность называемая автоматическое обращение по ссылке и разыменование (automatic referencing and dereferencing). Вызов методов является одним из немногих мест в 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. То есть, как только мы определим метод can_hold, мы хотим иметь возможность написать программу, показанную в Листинге 5-14.

Файл: src/main.rs

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

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Листинг 5-14: Использование ещё не написанного метода can_hold

И ожидаемый результат будет выглядеть следующим образом, т.к. оба размера в экземпляре rect2 меньше, чем размеры в экземпляре rect1, а 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 соответственно. Давайте добавим новый метод can_hold в impl блок из листинга 5-13, как показано в листинге 5-15.

Файл: src/main.rs

#[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!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Листинг 5-15: Реализация метода can_hold для Rectangle, принимающего другой экземпляр Rectangle в качестве параметра

Когда мы запустим код с функцией 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!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Листинг 5-16: Переписанный Листинга 5-15 с использованием нескольких impl

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

Итоги

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

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