Содержание


Rust - новый язык программирования: Часть 13. Методы и обобщённые функции

Comments

В предыдущих статьях цикла был сделан общий обзор языка программирования Rust, обсуждались основы синтаксиса: были описаны простейшие компоненты (переменные, простые типы данных, операторы и расширения), управляющие конструкции и составные структуры данных, функции и деструкторы. Особое внимание было уделено модели памяти в целом, концепции владения объектами, общим принципам управления памятью, использованию собственных и управляемых блоков общей памяти, а также заимствованных указателей. В отдельных статьях описывались средства ввода/вывода, а также векторы и строки и связанные с ними сущности - контейнеры и итераторы. Предыдущая статья была посвящена замыканиям и do-выражениям. В этой и следующей статьях рассматривается объектная система языка Rust, методы и обобщённые функции.

В первой статье данного цикла было отмечено, что Rust поддерживает объединение различных концепций и стилей программирования, в том числе и объектно-ориентированную парадигму. В Rust объектная система основана на трэйтах (traits). Трэйты определяют типы, а также соответствующие методы и реализации.

1. Трэйты

Итак, как уже было сказано выше, трэйт (trait) определяет некоторый тип и набор методов для этого типа. Также трэйт может содержать реализациии методов, принимаемые по умолчанию. Такие реализации пишутся с использованием специализированного типа self. Тип self может быть либо полностью неопределённым (неизвестным заранее, до этапа выполнения), либо ограниченным некоторым другим трэйтом. Пример трэйта приведён в листинге 1.

Листинг 1. Простой трэйт, определяющий тип Фигура (геометрическая)
trait Shape {
  fn draw( &self, Surface );
  fn bounding_box( &self ) -> BoundingBox;
}

В листинге 1 определён трэйт с двумя методами. Методы почти во всём похожи на обычные функции с единственным исключением: список аргументов метода должен начинаться с обязательного аргумента self, кроме тех случаев, когда определяются статические методы, которые будут рассмотрены более подробно ниже. Аргумент self можно считать аналогом this в языке C++ и некоторых других языках программирования.

Все объекты, для которых имеются реализации вышеописанного трэйта в соответствующей области видимости, могут вызывать свои методы с использованием dot-синтаксиса:

rectangle.bounding_box();

Таким образом, можно считать, что каждый трэйт фактически определяет новый тип, с тем же именем, что и данный трэйт. Значения (переменные) этого типа создаются посредством операции приведения указателя, соответствующего создаваемой переменной, к указателю на имя трэйта, используемого как тип, например:

let my_shape: @Shape = @my_circle as @Shape;

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

2. Методы и их реализация

Поскольку в предыдущем разделе было отмечено, что в трэйте содержатся объявления методов для соответствующего типа, но не обязательно содержатся реализации этих методов, то становится очевидным, что для каждого конкретного типа (и для создаваемого объекта этого типа) обязательно должны существовать реализации всех доступных методов.

Реализации начинаются с ключевого слова impl, как показано в листинге 2.

Листинг 2. Реализация трэйта, как типа
struct Point {
  x: float,
  y: float
}

struct Circle {
  radius: float,
  center: Point
}

impl Shape for Circle {
  fn draw( &self, s: Surface ) { do_draw_circle( s, *self ); }
  fn bounding_box( &self ) -> BoundingBox {
    let r = self.radius;
    BoundingBox { x: self.center.x - r, y: self.center.y - r,
                  width: 2.0 * r, height: 2.0 * r }
  }
}

Можно определить реализацию и без ссылки на трэйт, то есть, без указания имени трэйта и без ключевого слова for. Такие реализации могут определять методы для многих типов языка Rust, в том числе для структур и перечислений, как показано в листинге 3. Единственным ограничением является то, что реализация обязательно должна находиться в том же модуле, что и тип, обозначаемый self.

Листинг 3. Реализация метода для перечисления Shape (геометрическая фигура)
struct Point {
  x: float,
  y: float
}

enum Shape {
  Circle( Point, float ),
  Rectangle( Point, Point ),
  Triangle( Point, Point, Point )
}

impl Shape {
  fn draw( &self ) {
    match *self {
      Circle( p, f ) => draw_circle( p, f ),
      Rectangle( p1, p2 ) => draw_rectangle( p1, p2 ),
      Triangle( p1, p2, p3 ) => draw_triangle( p1, p2, p3 )
    }
  }
}

let shp = Circle( Point{ x: 1f, y: 2f }, 3f );
shp.draw();

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

3. Особый тип self

В примерах из предыдущих разделов активно использовался специализированный тип self, обозначающий тип объекта, который заранее, до фактического вызова метода как правило неизвестен. Вообще говоря, тип self имеет смысл только в методах, включённых в реализацию, то есть, в impl-элемент. В листинге 4 показан упрощённый пример использования специализированного типа self.

Листинг 4. Использование специализированного типа self в упрощённом примере
trait Printable {
  fn make_string( &self ) -> ~str;
}

impl Printable for ~str {
  fn make_string( &self ) -> ~str {
    (*self).clone()
  }
}

Сам тип self представляет тот тип, для которого определяется конкретная реализация метода, или указатель на данный тип. Таким образом, возможны следующие варианты записи аргумента: self, &self, @self, ~self. Вызывающая сторона должна в обязательном порядке обеспечить наличие совместимого типа указателя при вызове метода, как показано в листинге 5.

Листинг 5. Использование совместимых указателей при вызове методов
impl Shape {
  fn draw_borrowed( &self ) { ... }
  fn draw_managed( @self ) { ... }
  fn draw_owned( ~self ) { ... }
  fn draw_value( self ) { ... }
}

let shp = Circle( Point{ x: 1f, y: 2f }, 3f );

(@shp).draw_managed();
(~shp).draw_owned();
(&shp).draw_borrowed();
shp.draw_value();

4. Статические методы реализации

В любом трэйте могут быть определены статические (static) методы. Статические методы отличаются от обычных методов отсутствием self в списке аргументов. Это означает, что они вызываются не как методы объекта object.func(), а как независимые функции func(x). С помощью статических методов чаще всего определяются методы-конструкторы (это рекомендуемая и наиболее предпочтительная практика создания конструкторов, см. листинг 6).

Листинг 6. Определение конструктора объекта, как статического метода
impl Circle {
  fn area( &self ) -> float { ... }   // это обычный метод
  fn new( area: float ) -> Circle { ... }  // этот статический метод-конструктор
}

Чтобы вызвать такой статический метод по имени, необходимо квалифицировать его с помощью имени соответствующего трэйта, то есть объекта. Таким образом, в данном случае имя трэйта выступает в роли имени модуля; например, как в листинге 7.

Листинг 7. Вызов статического метода с префиксом - именем трэйта (объекта)
use std::float::consts::pi;
struct Circle {
  radius: float,
  center: Point
}

impl Circle {
  fn area( &self ) -> float { pi * self.radius * self.radius }
  fn new( area: float ) -> Circle { Circle { radius: (area / pi).sqrt() } }
}

let cir = Circle::new( 42.5 );

В листинге 8 демонстрируется пример определения статического метода для элементарного числового типа int.

Листинг 8. Определение статического метода для преобразования int в другой тип
trait Num {
  fn from_int( n: int ) -> Self;
}
impl Num for float {
  fn from_int( n: int ) -> float { n as float }
}
let x: float = Num::from_int( 42 );

Таким образом нетрудно написать реализации для преобразования целочисленного значения в любой другой тип.

5. Наследование трэйтов

Трэйты, а стало быть и соответствующие им типы могут быть определены с помощью наследования (inheritance) от других трэйтов. Например, следующим образом:

trait Shape { fn area() -> float; }
trait Circle: Shape { fn radius() -> float; }

Здесь синтаксическая конструкция Circle: Shape означает, что типы, которые будут реализовывать методы Circle, непременно должны содержать и реализацию Shape. Если необходимо указать несколько супертрэйтов (трэйтов, от которых производится наследование), то он разделяются пробелами, например:

trait Circle: Shape Eq { fn radius() -> float; }

В реализации Circle для некоторого конкретного типа T методы этого типа могут обращаться к методам Shape, так как подсистема контроля типов считает, что тип с реализацией Circle также содержит и реализацию Shape.

В функциях с параметрами-типами методы супертрэйта могут вызываться значениями (переменными), привязанными к типу трэйта-наследника. Пример такого вызова показан в листинге 9.

Листинг 9. Вызов метода супертрэйта из функции с параметром-типом наследника
fn radius_times_area<T:Circle>( c: T ) -> float {
  // здесь 'c' имеет тип и Circle, и Shape
  c.radius() * c.area()
}

Подобным же образом методы супертрэйта могут быть вызваны самим объектом, имеющим тип трэйта-наследника:

let mycircle: Circle = @mycircle as @Circle;
let approx_pi = mycircle.area() / (mycircle.radius() * mycircle.radius());

6. Реализация с параметрами-типами

Реализация любого трэйта также может иметь параметры-типы, по которым различаются реализации для конкретных типов данных. Параметры-типы реализации записываются после ключевого слова impl, как показано в листинге 10.

Листинг 10. Реализация с параметрами-типами
impl<T> Seq<T> for ~[T] {
  ...
}
impl Seq<bool> for u32 {
  // реализация для данного типа интерпретирует целое число, как последовательность битов
}

Использование в последних двух разделах параметров-типов напоминает о том, что имеется ещё одна заслуживающая подробного описания тема - обобщённые функции (generics), которые будут рассматриваться в следующей статье.

Заключение

В данной статье была описана объектная система языка Rust, основанная на трэйтах, как сущностях, представляющих собой новые типы данных. Рассматривались обычные и статические методы трэйтов и их реализация. Было отмечено, что реализации с параметрами-типами позволяют обеспечить универсализацию или обобщение работы с типами. В следующей статье будут рассматриваться обобщённые функции.


Ресурсы для скачивания


Похожие темы


Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux, Open source
ArticleID=952070
ArticleTitle=Rust - новый язык программирования: Часть 13. Методы и обобщённые функции
publish-date=11072013