Язык программирования Rust
Скачать 7.02 Mb.
|
Реализация поведения по умолчанию Иногда полезно иметь поведение по умолчанию для некоторых или всех методов в типаже вместо того, чтобы требовать реализации всех методов в каждом типе, реализующим данный типаж. Затем, когда мы реализуем типаж для определённого типа, можно сохранить или переопределить поведение каждого метода по умолчанию уже внутри типов. use aggregator::{Summary, Tweet}; fn main () { let tweet = Tweet { username: String ::from( "horse_ebooks" ), content: String ::from( "of course, as you probably already know, people" , ), reply: false , retweet: false , }; println! ( "1 new tweet: {}" , tweet.summarize()); } В примере 10-14 показано, как указать строку по умолчанию для метода summarize из типажа Summary вместо определения только сигнатуры метода, как мы сделали в примере 10-12. Файл: src/lib.rs Листинг 10-14: Определение типажа Summary с реализацией метода summarize по умолчанию Для использования реализации по умолчанию при создании сводки у экземпляров NewsArticle вместо определения пользовательской реализации, мы указываем пустой блок impl с impl Summary for NewsArticle {} Хотя мы больше не определяем метод summarize непосредственно в NewsArticle , мы предоставили реализацию по умолчанию и указали, что NewsArticle реализует типаж Summary . В результате мы всё ещё можем вызвать метод summarize у экземпляра NewsArticle , например так: Этот код печатает New article available! (Read more...) Создание реализации по умолчанию не требует от нас изменений чего-либо в реализации Summary для Tweet в листинге 10-13. Причина заключается в том, что синтаксис для переопределения реализации по умолчанию является таким же, как синтаксис для реализации метода типажа, который не имеет реализации по умолчанию. Реализации по умолчанию могут вызывать другие методы в том же типаже, даже если эти другие методы не имеют реализации по умолчанию. Таким образом, типаж может предоставить много полезной функциональности и только требует от разработчиков указывать небольшую его часть. Например, мы могли бы определить типаж Summary имеющий метод summarize_author , реализация которого требуется, а затем определить метод summarize который имеет реализацию по умолчанию, которая внутри вызывает метод summarize_author : pub trait Summary { fn summarize (& self ) -> String { String ::from( "(Read more...)" ) } } let article = NewsArticle { headline: String ::from( "Penguins win the Stanley Cup Championship!" ), location: String ::from( "Pittsburgh, PA, USA" ), author: String ::from( "Iceburgh" ), content: String ::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL." , ), }; println! ( "New article available! {}" , article.summarize()); Чтобы использовать такую версию типажа Summary , нужно только определить метод summarize_author , при реализации типажа для типа: После того, как мы определим summarize_author , можно вызвать summarize для экземпляров структуры Tweet и реализация по умолчанию метода summarize будет вызывать определение summarize_author которое мы уже предоставили. Так как мы реализовали метод summarize_author типажа Summary , то типаж даёт нам поведение метода summarize без необходимости писать код. Этот код печатает 1 new tweet: (Read more from @horse_ebooks...) Обратите внимание, что невозможно вызвать реализацию по умолчанию из переопределённой реализации того же метода. Типажи как параметры Теперь, когда вы знаете, как определять и реализовывать типажи, можно изучить, как использовать типажи, чтобы определить функции, которые принимают много различных типов. Мы будем использовать типаж Summary , реализованный для типов NewsArticle и Tweet в листинге 10-13, чтобы определить функцию notify , которая вызывает метод summarize для его параметра item , который имеет некоторый тип, реализующий типаж Summary . Для этого мы используем синтаксис impl Trait примерно так: pub trait Summary { fn summarize_author (& self ) -> String ; fn summarize (& self ) -> String { format! ( "(Read more from {}...)" , self .summarize_author()) } } impl Summary for Tweet { fn summarize_author (& self ) -> String { format! ( "@{}" , self .username) } } let tweet = Tweet { username: String ::from( "horse_ebooks" ), content: String ::from( "of course, as you probably already know, people" , ), reply: false , retweet: false , }; println! ( "1 new tweet: {}" , tweet.summarize()); Вместо конкретного типа у параметра item указывается ключевое слово impl и имя типажа. Этот параметр принимает любой тип, который реализует указанный типаж. В теле notify мы можем вызывать любые методы у экземпляра item , которые приходят с типажом Summary , такие как метод summarize . Мы можем вызвать notify и передать в него любой экземпляр NewsArticle или Tweet . Код, который вызывает данную функцию с любым другим типом, таким как String или i32 , не будет компилироваться, потому что эти типы не реализуют типаж Summary Синтаксис ограничения типажа Синтаксис impl Trait работает для простых случаев, но на самом деле является синтаксическим сахаром для более длинной формы, которая называется ограничением типажа (trait bound); это выглядит так: Эта более длинная форма эквивалентна примеру в предыдущем разделе, но она более многословна. Мы помещаем объявление параметра обобщённого типа с ограничением типажа после двоеточия внутри угловых скобок. Синтаксис impl Trait удобен и делает код более сжатым в простых случаях, в то время как более полный синтаксис с ограничением типажа в других случаях может выразить большую сложность. Например, у нас может быть два параметра, которые реализуют типаж Summary . Использование синтаксиса impl Trait выглядит так: Использовать impl Trait удобнее если мы хотим разрешить функции иметь разные типы для item1 и item2 (но оба типа должны реализовывать Summary ). Если же мы хотим заставить оба параметра иметь один и тот же тип, то мы должны использовать ограничение типажа так: Обобщённый тип T указан для типов параметров item1 и item2 и ограничивает функцию так, что конкретные значения типов переданные аргументами для item1 и item2 должны быть одинаковыми. Задание нескольких границ типажей с помощью синтаксиса + pub fn notify (item: & impl Summary) { println! ( "Breaking news! {}" , item.summarize()); } pub fn notify ( "Breaking news! {}" , item.summarize()); } pub fn notify (item1: & impl Summary, item2: & impl Summary) { pub fn notify Также можно указать более одного ограничения типажа. Допустим, мы хотели бы чтобы notify использовал как форматирование вывода так и summarize для параметра item : тогда мы указываем что в notify параметр item должен реализовывать оба типажа Display и Summary . Мы можем сделать это используя синтаксис + : Синтаксис + также допустим с ограничениями типажа для обобщённых типов: При наличии двух ограничений типажа, тело метода notify может вызывать summarize и использовать {} для форматирования item при его печати. Более ясные границы типажа с помощью where Использование слишком большого количества ограничений типажа имеет свои недостатки. Каждый обобщённый тип имеет свои границы типажа, поэтому функции с несколькими параметрами обобщённого типа могут содержать много информации об ограничениях между названием функции и списком её параметров затрудняющих чтение сигнатуры. По этой причине в Rust есть альтернативный синтаксис для определения ограничений типажа внутри предложения where после сигнатуры функции. Поэтому вместо того, чтобы писать так: можно использовать where таким образом: Сигнатура этой функции менее загромождена: название функции, список параметров, и возвращаемый тип находятся рядом, а сигнатура не содержит в себе множество ограничений типажа. Возврат значений типа реализующего определённый типаж Также можно использовать синтаксис impl Trait в возвращаемой позиции, чтобы вернуть значение некоторого типа реализующего типаж, как показано здесь: pub fn notify (item: &( impl Summary + Display)) { pub fn notify , U: Clone + Debug >(t: &T, u: &U) -> i32 { fn some_function where T: Display + Clone , U: Clone + Debug , { Используя impl Summary для возвращаемого типа, мы указываем, что функция returns_summarizable возвращает некоторый тип, который реализует типаж Summary без обозначения конкретного типа. В этом случае returns_summarizable возвращает Tweet , но код, вызывающий эту функцию, этого не знает. Возможность возвращать тип, который определяется только реализуемым им признаком, особенно полезна в контексте замыканий и итераторов, которые мы рассмотрим в Главе 13. Замыкания и итераторы создают типы, которые знает только компилятор или типы, которые очень долго указывать. Синтаксис impl Trait позволяет кратко указать, что функция возвращает некоторый тип, который реализует типаж Iterator без необходимости писать очень длинный тип. Однако, impl Trait возможно использовать, если возвращаете только один тип. Например, данный код, который возвращает значения или типа NewsArticle или типа Tweet , но в качестве возвращаемого типа объявляет impl Summary , не будет работать: fn returns_summarizable () -> impl Summary { Tweet { username: String ::from( "horse_ebooks" ), content: String ::from( "of course, as you probably already know, people" , ), reply: false , retweet: false , } } fn returns_summarizable (switch: bool ) -> impl Summary { if switch { NewsArticle { headline: String ::from( "Penguins win the Stanley Cup Championship!" , ), location: String ::from( "Pittsburgh, PA, USA" ), author: String ::from( "Iceburgh" ), content: String ::from( "The Pittsburgh Penguins once again are the best \ hockey team in the NHL." , ), } } else { Tweet { username: String ::from( "horse_ebooks" ), content: String ::from( "of course, as you probably already know, people" , ), reply: false , retweet: false , } } } Возврат либо NewsArticle либо Tweet не допускается из-за ограничений того, как реализован синтаксис impl Trait в компиляторе. Мы рассмотрим, как написать функцию с таким поведением в разделе "Использование объектов типажей, которые разрешены для значений или разных типов" Главы 17. Использование ограничений типажа для условной реализации методов Используя ограничение типажа с блоком impl , который использует параметры обобщённого типа, можно реализовать методы условно, для тех типов, которые реализуют указанный типаж. Например, тип Pair в листинге 10-16 всегда реализует функцию new для возврата нового экземпляра Pair (вспомните раздел “Определение методов” Главы 5 где Self является псевдонимом типа для типа блока impl , который в данном случае является Pair ). Но в следующем блоке impl тип Pair реализует метод cmp_display только если его внутренний тип T реализует типаж PartialOrd (позволяющий сравнивать) и типаж Display (позволяющий выводить на печать). Файл: src/lib.rs Листинг 10-15: Условная реализация методов у обобщённых типов в зависимости от ограничений типажа Мы также можем условно реализовать типаж для любого типа, который реализует другой типаж. Реализации типажа для любого типа, который удовлетворяет ограничениям типажа, называются общими реализациями и широко используются в стандартной библиотеке Rust. Например, стандартная библиотека реализует типаж ToString для use std::fmt::Display; struct Pair } impl (x: T, y: T) -> Self { Self { x, y } } } impl > Pair (& self ) { if self .x >= self .y { println! ( "The largest member is x = {}" , self .x); } else { println! ( "The largest member is y = {}" , self .y); } } } любого типа, который реализует типаж Display . Блок impl в стандартной библиотеке выглядит примерно так: Поскольку стандартная библиотека имеет эту общую реализацию, то можно вызвать метод to_string определённый типажом ToString для любого типа, который реализует типаж Display . Например, мы можем превратить целые числа в их соответствующие String значения, потому что целые числа реализуют типаж Display : Общие реализации приведены в документации к типажу в разделе "Implementors". Типажи и ограничения типажей позволяют писать код, который использует параметры обобщённого типа для уменьшения дублирования кода, а также указывая компилятору, что мы хотим обобщённый тип, чтобы иметь определённое поведение. Затем компилятор может использовать информацию про ограничения типажа, чтобы проверить, что все конкретные типы, используемые с нашим кодом, обеспечивают правильное поведение. В динамически типизированных языках мы получили бы ошибку во время выполнения, если бы вызвали метод для типа, который не реализует тип определяемый методом. Но Rust перемещает эти ошибки на время компиляции, поэтому мы вынуждены исправить проблемы, прежде чем наш код начнёт работать. Кроме того, мы не должны писать код, который проверяет своё поведение во время выполнения, потому что это уже проверено во время компиляции. Это повышает производительность без необходимости отказываться от гибкости обобщённых типов. impl ToString for T { // --snip-- } let s = 3 .to_string(); Валидация ссылок при помощи времён жизни Сроки (времена) жизни - ещё один вид обобщений, с которыми мы уже встречались. Если раньше мы использовали обобщения, чтобы убедиться, что тип обладает нужным нам поведением, теперь мы будем использовать сроки жизни для того, чтобы быть уверенными, что ссылки действительны как минимум столько времени в процессе исполнения программы, сколько нам требуется. В разделе "Ссылки и заимствование" главы 4, мы кое о чем умолчали: у каждой ссылки в Rust есть своё время жизни - область кода, на протяжении которого данная ссылка действительна (valid). В большинстве случаев сроки жизни выводятся неявно - так же, как у типов (нам требуется явно объявлять типы лишь в тех случаях, когда при автоматическом выведении типа возможны варианты). Точно так же мы должны явно объявлять сроки жизни тех ссылок, для которых времена жизни могут быть определены компилятором по-разному. Rust требует от нас объявлять взаимосвязи посредством обобщённых параметров сроков жизни - чтобы убедиться в том, что во время исполнения все действующие ссылки будут корректными. Аннотирование времени жизни — это концепция, отсутствующая в большинстве других языков программирования, так что она может показаться незнакомой. Хотя в этой главе мы не будем рассматривать времена жизни во всех деталях, тем не менее, мы обсудим основные ситуации, в которых вы можете столкнуться с синтаксисом времени жизни, что позволит вам получше ознакомиться с этой концепцией. Времена жизни предотвращают появление "повисших" ссылок Основное предназначение сроков жизни — предотвращать появление так называемых "повисших ссылок" (dangling references), из-за которых программа обращается не к тем данным, к которым она собиралась обратиться. Рассмотрим программу из листинга 10- 16, имеющую внешнюю и внутреннюю области видимости. Листинг 10-16: Попытка использования ссылки, значение которой вышло из области видимости Примечание: примеры в листингах 10-16, 10-17 и 10-23 объявляют переменные без указания их начального значения, поэтому имя переменной существует во fn main () { let r; { let x = 5 ; r = &x; } println! ( "r: {}" , r); } внешней области видимости. На первый взгляд может показаться, что это противоречит отсутствию в Rust нулевых (null) значений. Однако, если мы попытаемся использовать переменную, прежде чем присвоить ей значение, мы получим ошибку компиляции, которая показывает, что Rust действительно не разрешает нулевые (null) значения. Внешняя область видимости объявляет переменную с именем r без начального значения, а внутренняя область объявляет переменную с именем x с начальным значением 5 . Во внутренней области мы пытаемся установить значение r как ссылку на x . Затем внутренняя область видимости заканчивается и мы пытаемся напечатать значение из r . Этот код не будет скомпилирован, потому что значение на которое ссылается r исчезает из области видимости, прежде чем мы попробуем использовать его. Вот сообщение об ошибке: Переменная x «не живёт достаточно долго». Причина в том, что x выйдет из области видимости, когда эта внутренняя область закончится в строке 7. Но r все ещё является действительной во внешней области видимости; поскольку её охват больше, мы говорим, что она «живёт дольше». Если бы Rust позволил такому коду работать, то переменная r смогла бы ссылаться на память, которая уже была освобождена (в тот момент, когда x вышла из внутренней области видимости), и всё что мы попытались бы сделать с r работало бы неправильно. Как же Rust определяет, что этот код некорректен? Он использует для этого анализатор заимствований (borrow checker). |