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

Функции — это хорошо, но если вы хотите вызвать несколько связных функций для каких-либо данных, то это может быть неудобно. Рассмотрим этот код:

baz(bar(foo)));

Читать данную строку кода следует слева направо, поэтому мы наблюдаем такой порядок: «baz bar foo». Но он противоположен порядку, в котором функции будут вызываться: «foo bar baz». Было бы классно записать вызовы в том порядке, в котором они происходят, не так ли?

foo.bar().baz();

К счастью, как вы уже наверно догадались, это возможно! Rust предоставляет возможность использовать такой синтаксис вызова метода с помощью ключевого слова impl.

Вызов методов

Вот как это работает:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area());
}

Этот код напечатает 12.566371.

Мы создали структуру, которая представляет собой круг. Затем мы написали блок impl и определили метод area внутри него.

Методы принимают специальный первый параметр, &self. Есть три возможных варианта: self, &self и &mut self. Вы можете думать об этом специальном параметре как о x в x.foo(). Три варианта соответствуют трем возможным видам элемента x: self — если это просто значение в стеке, &self — если это ссылка и &mut self — если это изменяемая ссылка. Мы передаем параметр &self в метод area, поэтому мы можем использовать его так же, как и любой другой параметр. Так как мы знаем, что это Circle, мы можем получить доступ к полю radius так же, как если бы это была любая другая структура.

По умолчанию следует использовать &self, также как следует предпочитать заимствование владению, а неизменные ссылки изменяемым. Вот пример, включающий все три варианта:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn reference(&self) {
       println!("принимаем self по ссылке!");
    }

    fn mutable_reference(&mut self) {
       println!("принимаем self по изменяемой ссылке!");
    }

    fn takes_ownership(self) {
       println!("принимаем владение self!");
    }
}

Цепочка вызовов методов

Итак, теперь мы знаем, как вызвать метод, например foo.bar(). Но что насчет нашего первоначального примера, foo.bar().baz()? Это называется «цепочка вызовов», и мы можем сделать это, вернув self.

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }

    fn grow(&self, increment: f64) -> Circle {
        Circle { x: self.x, y: self.y, radius: self.radius + increment }
    }
}

fn main() {
    let c = Circle { x: 0.0, y: 0.0, radius: 2.0 };
    println!("{}", c.area());

    let d = c.grow(2.0).area();
    println!("{}", d);
}

Проверьте тип возвращаемого значения:

# struct Circle;
# impl Circle {
fn grow(&self) -> Circle {
# Circle } }

Мы просто указываем, что возвращается Circle. С помощью этого метода мы можем создать новый круг, площадь которого будет в 100 раз больше, чем у старого.

Статические методы

Вы также можете определить методы, которые не принимают параметр self. Вот шаблон программирования, который очень распространен в коде на Rust:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn new(x: f64, y: f64, radius: f64) -> Circle {
        Circle {
            x: x,
            y: y,
            radius: radius,
        }
    }
}

fn main() {
    let c = Circle::new(0.0, 0.0, 2.0);
}

Этот статический метод, который создает новый Circle. Обратите внимание, что статические методы вызываются с помощью синтаксиса: Struct::method(), а не ref.method().

Шаблон «строитель» (Builder Pattern)

Давайте предположим, что нам нужно, чтобы наши пользователи могли создавать круги и чтобы у них была возможность задавать только те свойства, которые им нужны. В противном случае, атрибуты x и y будут 0.0, а radius будет 1.0. Rust не поддерживает перегрузку методов, именованные аргументы или переменное количество аргументов. Вместо этого мы используем шаблон «строитель». Он выглядит следующим образом:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct CircleBuilder {
    x: f64,
    y: f64,
    radius: f64,
}

impl CircleBuilder {
    fn new() -> CircleBuilder {
        CircleBuilder { x: 0.0, y: 0.0, radius: 0.0, }
    }

    fn x(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.x = coordinate;
        self
    }

    fn y(&mut self, coordinate: f64) -> &mut CircleBuilder {
        self.y = coordinate;
        self
    }

    fn radius(&mut self, radius: f64) -> &mut CircleBuilder {
        self.radius = radius;
        self
    }

    fn finalize(&self) -> Circle {
        Circle { x: self.x, y: self.y, radius: self.radius }
    }
}

fn main() {
    let c = CircleBuilder::new()
                .x(1.0)
                .y(2.0)
                .radius(2.0)
                .finalize();

    println!("площадь: {}", c.area());
    println!("x: {}", c.x);
    println!("y: {}", c.y);
}

Всё, что мы сделали здесь — это создали ещё одну структуру, CircleBuilder. В ней мы определили методы строителя. Также мы определили метод area() в Circle. Мы также сделали еще один метод в CircleBuilder: finalize(). Этот метод создаёт наш окончательный Circle из строителя. Таким образом, мы можем использовать методы CircleBuilder чтобы уточнить создание Circle.