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

Methods

Methods are similar to functions: We declare them with the fn keyword and a name, they can have parameters and a return value, and they contain some code that’s run when the method is called from somewhere else. Unlike functions, methods are defined within the context of a struct (or an enum or a trait object, which we cover in Chapter 6 and Chapter 18, respectively), and their first parameter is always self, which represents the instance of the struct the method is being called on.

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

Давайте изменим функцию 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!(
        "Площадь прямоугольника равна {} квадратных пикселей.",
        rect1.area()
    );
}
Listing 5-13: Defining an area method on the Rectangle struct

To define the function within the context of Rectangle, we start an impl (implementation) block for Rectangle. Everything within this impl block will be associated with the Rectangle type. Then, we move the area function within the impl curly brackets and change the first (and in this case, only) parameter to be self in the signature and everywhere within the body. In main, where we called the area function and passed rect1 as an argument, we can instead use method syntax to call the area method on our Rectangle instance. The method syntax goes after an instance: We add a dot followed by the method name, parentheses, and any arguments.

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

We chose &self here for the same reason we used &Rectangle in the function version: We don’t want to take ownership, and we just want to read the data in the struct, not write to it. If we wanted to change the instance that we’ve called the method on as part of what the method does, we’d use &mut self as the first parameter. Having a method that takes ownership of the instance by using just self as the first parameter is rare; this technique is usually used when the method transforms self into something else and you want to prevent the caller from using the original instance after the transformation.

Основная причина использования методов, а не функций (не считая возможности неявно передавать экземпляр в функцию), заключается в организации кода. Мы можем поместить всё, что мы можем сделать с экземпляром типа, в один 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!("Длина прямоугольника ненулевая и равна {}", rect1.width);
    }
}

Here, we’re choosing to make the width method return true if the value in the instance’s width field is greater than 0 and false if the value is 0: We can use a field within a method of the same name for any purpose. In main, when we follow rect1.width with parentheses, Rust knows we mean the method width. When we don’t use parentheses, Rust knows we mean the field width.

Often, but not always, when we give a method the same name as a field we want it to only return the value in the field and do nothing else. Methods like this are called getters, and Rust does not implement them automatically for struct fields as some other languages do. Getters are useful because you can make the field private but the method public and thus enable read-only access to that field as part of the type’s public API. We will discuss what public and private are and how to designate a field or method as public or private in Chapter 7.

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

In C and C++, two different operators are used for calling methods: You use . if you’re calling a method on the object directly and -> if you’re calling the method on a pointer to the object and need to dereference the pointer first. In other words, if object is a pointer, object->something() is similar to (*object).something().

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

Here’s how it works: When you call a method with object.something(), Rust automatically adds in &, &mut, or * so that object matches the signature of the method. In other words, the following are the same:

#![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:

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!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
    println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}
Listing 5-14: Using the as-yet-unwritten can_hold method

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

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

We know we want to define a method, so it will be within the impl Rectangle block. The method name will be can_hold, and it will take an immutable borrow of another Rectangle as a parameter. We can tell what the type of the parameter will be by looking at the code that calls the method: rect1.can_hold(&rect2) passes in &rect2, which is an immutable borrow to rect2, an instance of Rectangle. This makes sense because we only need to read rect2 (rather than write, which would mean we’d need a mutable borrow), and we want main to retain ownership of rect2 so that we can use it again after calling the can_hold method. The return value of can_hold will be a Boolean, and the implementation will check whether the width and height of self are greater than the width and height of the other Rectangle, respectively. Let’s add the new can_hold method to the impl block from Listing 5-13, shown in Listing 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!("Вместится ли rect2 в rect1? {}", rect1.can_hold(&rect2));
    println!("Вместится ли rect3 в rect1? {}", rect1.can_hold(&rect3));
}
Listing 5-15: Implementing the can_hold method on Rectangle that takes another Rectangle instance as a parameter

Если мы запустим код с функцией 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.

To call this associated function, we use the :: syntax with the struct name; let sq = Rectangle::square(3); is an example. This function is namespaced by the struct: The :: syntax is used for both associated functions and namespaces created by modules. We’ll discuss modules in Chapter 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));
}
Listing 5-16: Rewriting Listing 5-15 using multiple impl blocks

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

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

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

But structs aren’t the only way you can create custom types: Let’s turn to Rust’s enum feature to add another tool to your toolbox.