Содержание


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

Comments

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

Одно из определений обобщённой функции (generic function) представляет её как функцию, которая использует параметрический полиморфизм (parametric polymorphism) или полиморфизм параметров. В соответствии с другим определением, это сущность (entity), сформированная только из методов, имеющих одно и то же имя. Таким образом, следует принять во внимание своеобразный дуализм обобщённой функции, заключающийся в том, что она является и собственно функцией (которая может быть вызвана с аргументами и применена к аргументам), и обычным объектом. Как бы то ни было, обобщённые функции применяются в практике программирования уже достаточно давно, поэтому вполне естественно, что их поддержка была включена в объектную систему языка Rust.

1. Обобщённые функции в Rust

Обобщённые функции отличаются от обычных тем, что в их сигнатуру включены параметры-типы (или параметризованные типы - parametrized types). Параметры-типы записываются сразу после имени функции в угловых скобках (по соглашению буквами верхнего регистра для удобства). Каждый параметр-тип должен бьть объявлен в явной форме, а если необходимо объявить несколько параметров-типов, то они разделяются запятыми. Примеры обобщённых функций показаны в листинге 1.

Листинг 1. Определения обобщённых функций с одним и двумя параметрами-типами
Определения обобщённых функций с одним и двумя параметрами-типами

fn iter<T>( seq: &[T], f: &fn(T) ) {
  for element in seq.iter() { f(element); }
}

fn map<T, U>( seq: &[T], f: &fn(vec: &T) -> U ) -> ~[U] {
  let mut accum = ~[];
  for element in seq.iter() { accum.push( f(element) ); }
  accum
}

Как в сигнатуре обобщённой функции, так и в её теле имя параметра-типа может быть использовано как имя типа.

При вызове обобщённой функции её тип определяется из контекста вызова. Например, при вызове обобщённой функции iter() из листинга 1 для вектора [1, 2] параметр-тип T определяется как int, при этом требуется, чтобы параметр-замыкание также имел тип fn(int).

Поскольку параметр-тип для обобщённой функции заранее неизвестен, набор операций, которые могут быть выполнены с этим типом, ограничен. Значения типа, задаваемого параметром, могут быть только перемещены, их нельзя скопировать:

fn id<T>( x: T ) -> T { x }

Следует отметить, что экземпляры обобщённых типов чаще всего передаются посредством указателя. Например, в том же листинге 1 параметру f() (функции обработки) передаётся указатель на значение типа T, а не само значение типа T. Это служит гарантией того, что данная функция будет работать с более широким диапазоном допустимых типов, поскольку копирование и передача по значению для некоторых типов данных связаны с чрезмерными накладными расходами или вовсе невозможны.

2. Обобщённые типы и структуры

Помимо функций обобщёнными в Rust могут быть комплексные типы данных, структуры и перечисления, как показано в листинге 2.

Листинг 2. Обобщённый составной тип данных, обобщённая структура и обобщённое перечисление
use std::hashmap::HashMap;
type Set<T> = HashMap<T, ()>;

struct Stack<T> {
  elements: ~[T]
}

enum Option<T> {
  Some(T),
  None
}

После объявлений, показанных в листинге 2, можно создавать корректные экземпляры типов Set<int> или Set<char>, Stack<int> или Stack<float>, Option<int> или Option<float> и т.д.

Самый последний тип в примере из листинга 2 - Option - достаточно часто встречается в rust-коде. Поскольку в Rust отстутствуют null-указатели (исключение составляют блоки unsafe-кода), необходим какой-то альтернативный способ написания функции, результат которой невозможно корректно определить по каждому потенциально возможному сочетанию аргументов некторых конкретных типов. Другими словами, для некоторых типов аргументов такой функции результат может не иметь никакого смысла. Вот для обработки таких случаев и пишутся функции, возвращающие Option<T> вместо T. Пример одной из подобных функций показан в листинге 3.

Листинг 3. Функция, обрабатывающая особые случаи передачи параметров, для которых результат не определён
fn radius( shape: Shape ) -> Option<float> {
  match shape {
    Circle( _, radius ) => Some(radius),
    Rectangle(*)        => None,
    Triangle(*)         => None
  }
}

3. Производительность обобщённых функций

Компилятор Rust компилирует обобщённые функции весьма эффективно, применяя к ним мономорфизацию (monomorphizing). Столь громко звучащий термин "мономорфизация" обозначает простую идею: генерация отдельной копии каждой обобщённой функции непосредственно в каждой точке её вызова. Таким образом, копия может быть адаптирована под конкретные типы аргументов, а следовательно, и оптимизирована для этих типов. В этом отношении обобщённые функции Rust сравнимы по характеристикам производительности шаблонов (templates) языка C++.

4. Обобщённые функции и трэйты

Как уже было сказано выше, в обобщённых функциях набор возможных операций над значениями передаваемых параметров-типов крайне ограничен из соображений безопасности. Впрочем эти ограничения можно несколько смягчить с помощью трэйтов, описанных в предыдущей статье. Кроме всех прочих достоинств трэйты в Rust являются самым мощным инструментом создания полиморфного кода. Разработчики, знакомые с языком Java, могут считать трэйты аналогами Java-интерфейсов. С этой точки зрения трэйты позволяют ограничить набор возможных параметров-типов; иногда это называют формой ограниченного полиморфизма (bounded polymorphism).

В качестве примера хорошо подходит операция копирования в Rust. Метод clone() определён не для всех типов языка. Одной из причин является возможность создавать деструкторы, определяемые пользователем: ведь копирование типа со специализированным деструктором может привести к тому, что этот деструктор будет вызван несколько раз. Таким образом, для типов со специализированными деструкторами операция копирования некорректна, если только разработчик не напишет явную реализацию Clone для этих типов.

Описанные выше ограничения усложняют обработку обобщённых функций. Если в обобщённую функцию передаётся параметр-тип T, то по правилам Rust значения этого типа копировать нельзя. Например, код, приведённый в листинге 2, компилироваться не будет.

Листинг 2. Демонстрация ограничений, налагаемых на операции с параметром-типом
fn head_trying<T>( vec: &[T] ) -> T {
  vec[0]   // Ошибка: попытка копирования значения, которое запрещено копировать
}

Из этой ситуации есть выход: можно сообщить компилятору о том, что функция head() предназначена только для копируемых типов, то есть, для тех типов, для которых существует реализация трэйта Clone. В этом случае необходимо явно создать копию значения с помощью метода clone(), и именно эта копия будет возвращена из данной функции, как показано в листинге 3.

Листинг 3. Пример обобщённой функции, копирующей значения только для копируемых типов
fn head<T: Clone>( vec: &[T] ) -> T {
  vec[0].clone()
}

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

4.1. Особые трэйты

Большинство трэйтов могут быть определены и реализованы в коде разработчика, но существуют три особых трэйта, которые автоматически наследуются и реализовываются компилятором для всех применимых типов. Эти трэйты не могут быть ни замещены, ни переписаны.

Send - типы, которые могут быть переданы (sendable types) между задачами. В Rust принято, что передаваться между задачами могут типы, которые не содержат управляемых блоков памяти (managed boxes) и указателей на них, управляемых замыканий (managed closures) и заимствованных указателей (borrowed pointers).

Freeze - постоянные (неизменяемые) типы. К ним относятся такие типы, которые не могут быть изменены как-либо в явной форме, а также не содержащие никаких объектов (подтипов), которые сами по себе являются изменяемыми, то есть, могут изменять объект данного типа неявно, скрыто. Примерами подобных "скрытых" изменяемых подтипов могут служить @mut и Cell в стандартной библиотеке.

'static - не заимствуемые типы (non-borrowed types). Такие типы не содержат каких бы то ни было данных, чьё время существования связано с конкретным фреймом стека. Вообще говоря, использование заимствованных указателей в таких типах запрещено, но есть одно исключение: они могут включать только заимствованные указатели с временем жизни 'static.

Кроме того, есть ещё один специальный трэйт - Drop, используемый для определения деструкторов. В нём определяется один метод с именем drop, автоматически вызываемый при уничтожении значения того типа, которое имеет реализацию данного трэйта, или при выходе значения этого типа из области видимости, или при обработке значения этого типа механизмом сборки мусора. В листинге 4 приведён пример определения метода drop() для конкретного типа данных.

Листинг 4. Определение метода-деструктора drop() для структуры
struct Counter {
  counter: uint,
  necessary: bool
}

impl Drop for Counter {
  fn drop( &mut self ) {
    self.necessary = false;
    println("счётчик не нужен");
  }
}

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

4.2. Связанные параметры-типы и статическое диспетчирование методов

Можно сказать, что трэйты предоставляют в распоряжение разработчика дополнительные языковые средства для определения предикатов, относящихся к параметрам-типам, или для определения абстрактных свойств, присущих параметрам-типам. Эти дополнительные языковые средства могут быть использованы для определения более конкретных связей для парметров-типов, таким образом, появляется возможность работать с обобщёнными типами, как показано в примере в листинге 5.

Листинг 5. Определение свойств параметра-типа
fn print_all<T: Printable>( printable_things: ~[T] ) {
  for thing in printable_things.iter() {
    thing.print();
  }
}

Объявление типа T, как соответствующего трэйту Printable, даёт возможность вызывать методы из этого трэйта для значений типа T внутри данной функции. Но это может привести и к ошибке времени компиляции при попытке передать в функцию print_all() массив, для типов элементов которого отсутствует реализация Printable.

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

Листинг 6. Определение нескольких связей для параметра-типа
fn print_all<T: Printable + Clone>( printable_things: ~[T] ) {
  let mut i = 0;
  while i < printable_things.len() {
    let copy_of_thing = printable_things[i].clone();
    copy_of_thing.print();
    i += 1;
  }
}

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

4.3. Объекты трэйтов и динамическое диспетчирование методов

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

Листинг 7. Обобщённая функция отображения геометрических фигур
trait Drawable { fn draw( &self ); }

fn draw_all<T: Drawable>( shapes: ~[T] ) {
  for shape in shapes.iter() { shape.draw(); }
}

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

Листинг 8. Альтернативное использование имени трэйта, как типа объекта
fn draw_all( shapes: &[@Drawable] ) {
  for shape in shapes.iter() { shape.draw(); }
}

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

Листинг 9. Использование альтернативного варианта функции draw_all()
impl Drawable for Circle { fn draw( &self ) { ... } }
impl Drawable for Rectangle { fn draw( &self ) { ... } }
impl Drawable for Triangle { fn draw( &self ) { ... } }
// могут существовать реализации для любых геометрических фигур
// ...
let c: @Circle = @new_circle();
let r: @Rectangle = @new_rectangle();
let t: @Triangle = @new_triangle();
draw_all( [c as @Drawable, r as @Drawable, t as @Drawable] );

Предполагается, что функции-конструкторы new_circle(), new_rectangle() и new_triangle() возвращают соответственно окружность, прямоугольник и треугольник с принятыми по умолчанию размерами.

Здесь следует отметить, что, подобно строкам и векторам, объекты также имеют динамически изменяемый размер, поэтому к ним можно обращаться только через один из типов указателей, причём применимы все три типа указателей. Приведение к трэйту (как типу) может быть выполнено только для совместимого указателя, так что нельзя, например, привести объект типа ~Circle к типу-трэйту @Drawable (см листинг 10).

Листинг 10. Примеры приведения совместимых указателей к типу-трэйту
// управляемый указатель на объект
let mng_circle: @Drawable = @new_circle() as @Drawable;
// собственный указатель на объект
let own_circle: ~Drawable = ~new_circle() as ~Drawable;
// заимствованный указатель на объект
let stck_circle: &Drawable = &new_circle() as &Drawable;

Вызовы методов с трэйтами как типами являются динамически диспетчируемыми (dynamically dispatched). Поскольку компилятор во время компиляции точно не знает, какая именно функция будет вызвана, он использует таблицу поиска (vtable или словарь) для выбора метода (функции) во время выполнения. Такое использование трэйтов можно сравнить с интерфейсами в языке Java.

Немаловажным является тот факт, что по умолчанию на содержимое трэйтов, относящихся к одному из классов хранения данных в памяти (показанных в листинге 10), накладываются следующие ограничения:

  • содержимое собственных трэйтов (~Trait) должно соответствовать ограничивающим условиям Send
  • содержимое управляемых трэйтов (@Trait) должно соответствовать ограничивающим условиям 'static
  • для содержимого заимствованных трэйтов (&Trait) нет каких-либо ограничений

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

~Trait:

Это означает, что содержимое собственного трэйта не соблюдает никакие ограничения. Другой вариант:

~Trait: Send+Freeze

В данном случае напротив, к ограничениям Send добавляются ограничения Freeze. Таким образом, на основании всего сказанного выше:

  • синтаксис ~Trait:Send равнозначен синтаксису ~Trait (внимание: не путать с синтаксисом ~Trait:)
  • синтаксис @Trait:'static равнозначен синтаксису @Trait
  • синтаксис &Trait: равнозначен синтаксису &Trait

Заключение

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


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


Похожие темы


Комментарии

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

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