Практика синтаксиса методов в Rust

RustRustBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Добро пожаловать в Method Syntax. Этот лаба является частью Rust Book. Вы можете практиковать свои навыки Rust в LabEx.

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/integer_types -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/boolean_type -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/function_syntax -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/expressions_statements -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/method_syntax -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} rust/traits -.-> lab-100397{{"Практика синтаксиса методов в Rust"}} end

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

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

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

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

Имя файла: src/main.rs

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

1 impl Rectangle {
  2 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.",
      3 rect1.area()
    );
}

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

Для определения функции в контексте Rectangle мы начинаем блок impl (реализации) для Rectangle [1]. Все, находящееся внутри этого блока impl, будет связано с типом Rectangle. Затем мы перемещаем функцию area внутри фигурных скобок impl [2] и меняем первый (и в этом случае единственный) параметр на self в сигнатуре и во всем теле. В main, где мы вызывали функцию area и передавали rect1 в качестве аргумента, мы можем вместо этого использовать синтаксис методов, чтобы вызвать метод area на нашем экземпляре Rectangle [3]. Синтаксис методов идет после экземпляра: мы добавляем точку, за которой следует имя метода, скобки и любые аргументы.

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

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

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

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

Имя файла: src/main.rs

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 соответствовал сигнатуре метода. Другими словами, следующие выражения эквивалентны:

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

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
    }
}

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

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

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

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

Ассоциированные функции, которые не являются методами, часто используются для конструкторов, которые возвращают новый экземпляр структуры. Они часто называются new, но new не является специальным именем и не встроено в язык. Например, мы могли бы выбрать предоставить ассоциированную функцию под названием square, которая будет иметь один параметр размерности и использовать его в качестве ширины и высоты, тем самым making it easier to create a square Rectangle rather than having to specify the same value twice:

Имя файла: src/main.rs

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

Ключевые слова Self в возвращаемом типе [1] и в теле функции [2] являются псевдонимами для типа, который появляется после ключевого слова impl, который в этом случае является Rectangle.

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

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

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

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
    }
}

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

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

Резюме

Поздравляем! Вы завершили лабораторию по синтаксису методов. Вы можете практиковаться в других лабораториях в LabEx, чтобы улучшить свои навыки.