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

Методы

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

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

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

Filename: 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()
    );
}
Listing 5-13: Определение метода area для структуры Rectangle

Чтобы определить функцию в контексте Rectangle, мы начинаем блок реализации impl для 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:

Filename: 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 есть возможность, которая называется автоматическое взятие ссылки и разыменование. Вызов методов – одно из немногих мест в 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.

Filename: 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));
}
Listing 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.

Filename: 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));
}
Listing 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));
}
Listing 5-16: Переписывание листинга 5-15 с использованием нескольких блоков impl

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

Итоги

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

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