ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1129
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Здесь мы определили, чтобы метод 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 есть функциональность называемая автоматическое обращение по ссылке и разыменование (automatic referencing and dereferencing). Вызов методов является одним из немногих мест в
Rust, в котором есть такое поведение.
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);
}
}
Вот как это работает: когда вы вызываете метод object.something()
, Rust автоматически добавляет
&
,
&mut или
*
, таким образом, чтобы object соответствовал сигнатуре метода. Другими словами, это то же самое:
Первый пример выглядит намного понятнее. Автоматический вывод ссылки работает потому, что методы имеют понятного получателя - тип self
. Учитывая получателя и имя метода, Rust может точно определить, что в данном случае делает код: читает ли метод (
&self
), делает ли изменение (
&mut self
) или поглощает
(
self
). Тот факт, что Rust делает заимствование неявным для принимающего метода, в значительной степени способствует тому, чтобы сделать владение эргономичным на практике.
Методы с несколькими параметрами
Давайте попрактикуемся в использовании методов, реализовав второй метод в структуре
Rectangle
. На этот раз мы хотим, чтобы экземпляр
Rectangle брал другой экземпляр
Rectangle и возвращал true
, если второй
Rectangle может полностью поместиться внутри self
(первый
Rectangle
); в противном случае он должен вернуть false
. То есть,
как только мы определим метод can_hold
, мы хотим иметь возможность написать программу, показанную в Листинге 5-14.
Файл: src/main.rs
Листинг 5-14: Использование ещё не написанного метода
can_hold p1.distance(&p2);
(&p1).distance(&p2); 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));
}
И ожидаемый результат будет выглядеть следующим образом, т.к. оба размера в экземпляре rect2
меньше, чем размеры в экземпляре rect1
, а rect3
шире, чем rect1
:
Мы знаем, что хотим определить метод, поэтому он будет находится в 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
Листинг 5-15: Реализация метода
can_hold
для
Rectangle
, принимающего другой экземпляр
Rectangle
в
качестве параметра
Когда мы запустим код с функцией main листинга 5-14, мы получим желаемый вывод.
Методы могут принимать несколько параметров, которые мы добавляем в сигнатуру после первого параметра self
, и эти параметры работают так же, как параметры в функциях.
Ассоциированные функции
Все функции, определённые в блоке impl
, называются ассоциированными функциями,
потому что они ассоциированы с типом, указанным после ключевого слова impl
. Мы можем определить ассоциированные функции, которые не имеют self в качестве первого параметра (и, следовательно, не являются методами), потому что им не нужен
Can rect1 hold rect2? true
Can rect1 hold rect3? false 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
}
}
экземпляр типа для работы. Мы уже использовали одну подобную функцию: функцию
String::from
, определённую для типа
String
Ассоциированные функции, не являющиеся методами, часто используются для конструкторов, возвращающих новый экземпляр структуры. Их часто называют new
, но new не является специальным именем и не встроена в язык. Например, мы можем предоставить ассоциированную функцию с именем square
, которая будет иметь один параметр размера и использовать его как ширину и высоту, что упростит создание квадратного
Rectangle
, вместо того, чтобы указывать одно и то же значение дважды:
Файл: src/main.rs
Ключевые слова
Self в возвращаемом типе и в теле функции являются псевдонимами для типа, указанного после ключевого слова impl
, которым в данном случае является
Rectangle
Чтобы вызвать эту ассоциированную функцию, мы используем синтаксис
::
с именем структуры; например, let sq = Rectangle::square(3);
. Эта функция входит в пространство имён структуры: синтаксис
::
используется как для ассоциированных функций, так и для пространств имён, созданных модулями. Мы обсудим модули в Главе
7.
String::from
, определённую для типа
String
Ассоциированные функции, не являющиеся методами, часто используются для конструкторов, возвращающих новый экземпляр структуры. Их часто называют new
, но new не является специальным именем и не встроена в язык. Например, мы можем предоставить ассоциированную функцию с именем square
, которая будет иметь один параметр размера и использовать его как ширину и высоту, что упростит создание квадратного
Rectangle
, вместо того, чтобы указывать одно и то же значение дважды:
Файл: src/main.rs
Ключевые слова
Self в возвращаемом типе и в теле функции являются псевдонимами для типа, указанного после ключевого слова impl
, которым в данном случае является
Rectangle
Чтобы вызвать эту ассоциированную функцию, мы используем синтаксис
::
с именем структуры; например, let sq = Rectangle::square(3);
. Эта функция входит в пространство имён структуры: синтаксис
::
используется как для ассоциированных функций, так и для пространств имён, созданных модулями. Мы обсудим модули в Главе
7.
1 ... 7 8 9 10 11 12 13 14 ... 62
Несколько блоков impl
Каждая структура может иметь несколько impl
. Например, Листинг 5-15 эквивалентен коду, показанному в листинге 5-16, в котором каждый метод находится в своём собственном блоке impl impl
Rectangle { fn square
(size: u32
) ->
Self
{
Self
{ width: size, height: size,
}
}
} 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,
рассматривающей обобщённые типы и свойства.
Итоги
Структуры позволяют создавать собственные типы, которые имеют смысл в вашей предметной области. Используя структуры, вы храните ассоциированные друг с другом фрагменты данных и даёте название частям данных, чтобы ваш код был более понятным. Методы позволяют определить поведение, которое имеют экземпляры ваших структур, а ассоциированные функции позволяют привязать функциональность к вашей структуре, не обращаясь к её экземпляру.
Но структуры — не единственный способ создавать собственные типы: давайте обратимся к перечислениям в Rust, чтобы добавить ещё один инструмент в свой арсенал.
Перечисления и сопоставление с
образцом
В этой главе мы рассмотрим перечисления (enumerations), также называемые enums.
Перечисления позволяют определять типы путём перечисления их возможных
вариантов. Во-первых, мы определим и используем перечисление, чтобы показать, как оно может объединить значения и данные. Далее мы рассмотрим особенно полезное перечисление
Option
, которое указывает, что значение может быть или чем-то, или ничем. Затем мы посмотрим, как сопоставление шаблонов в выражении match позволяет легко запускать разный код для разных значений перечислений. Наконец, мы узнаем, насколько конструкция if let удобна и лаконична для обработки перечислений в вашем коде.
Определение перечисления
Там, где структуры дают вам возможность группировать связанные поля и данные,
например
Rectangle с его width и height
, перечисления дают вам способ сказать, что значение является одним из возможных наборов значений. Например, мы можем захотеть сказать, что
Rectangle
— это одна из множества возможных фигур, в которую также входят
Circle и
Triangle
. Для этого Rust позволяет нам закодировать эти возможности в виде перечисления.
Давайте рассмотрим ситуацию, которую мы могли бы захотеть отразить в коде, и поймём, почему перечисления полезны и более уместны, чем структуры в этом случае.
Допустим, нам нужно работать с IP-адресами. В настоящее время для обозначения IP- адресов используются два основных стандарта: четвёртая и шестая версии. Поскольку это единственно возможные варианты IP-адресов, с которыми может столкнуться наша программа, мы можем перечислить все возможные варианты, откуда перечисление и получило своё название.
Любой IP-адрес может быть либо четвёртой, либо шестой версии, но не обеими одновременно. Эта особенность IP-адресов делает структуру данных enum подходящей,
поскольку значение enum может представлять собой только один из его возможных вариантов. Адреса как четвёртой, так и шестой версии по своей сути все равно являются
IP-адресами, поэтому их следует рассматривать как один и тот же тип, когда в коде обрабатываются задачи, относящиеся к любому типу IP-адресов.
Можно выразить эту концепцию в коде, определив перечисление
IpAddrKind и составив список возможных видов IP-адресов,
V4
и
V6
. Вот варианты перечислений:
IpAddrKind теперь является пользовательским типом данных, который мы можем использовать в другом месте нашего кода.
Значения перечислений
Экземпляры каждого варианта перечисления
IpAddrKind можно создать следующим образом:
Обратите внимание, что варианты перечисления находятся в пространстве имён вместе с его идентификатором, а для их обособления мы используем двойное двоеточие. Это enum
IpAddrKind
{
V4,
V6,
} let four = IpAddrKind::V4; let six = IpAddrKind::V6;
удобно тем, что теперь оба значения
IpAddrKind::V4
и
IpAddrKind::V6
относятся к одному типу:
IpAddrKind
. Затем мы можем, например, определить функцию, которая принимает любой из вариантов
IpAddrKind
:
Можно вызвать эту функцию с любым из вариантов:
Использование перечислений позволяет получить ещё больше преимуществ. Если подумать о нашем типе для IP-адреса, то выяснится, что на данный момент у нас нет возможности хранить собственно сам IP-адрес; мы будем знать только его тип. Учитывая,
что недавно в главе 5 вы узнали о структурах, у вас может возникнуть соблазн решить эту проблему с помощью структур, как показано в листинге 6-1.
Листинг 6-1. Сохранение данных и
IpAddrKind
IP-адреса с использованием
struct
Здесь мы определили структуру
IpAddr
, у которой есть два поля: kind типа
IpAddrKind
(перечисление, которое мы определили ранее) и address типа
String
. У нас есть два экземпляра этой структуры. Первый - home
, который является
IpAddrKind::V4
в качестве значения kind с соответствующим адресом
127.0.0.1
. Второй экземпляр - loopback
. Он в качестве значения kind имеет другой вариант
IpAddrKind
,
V6
, и с ним ассоциирован адрес
::1
. Мы использовали структуру для объединения значений kind и address вместе, таким образом тип формата адреса теперь ассоциирован со значением.
Однако представление этой же концепции с помощью перечисления более лаконично:
вместо того, чтобы помещать перечисление в структуру, мы можем поместить данные непосредственно в любой из вариантов перечисления. Это новое определение fn route
(ip_kind: IpAddrKind) {} route(IpAddrKind::V4); route(IpAddrKind::V6); enum
IpAddrKind
{
V4,
V6,
} struct
IpAddr
{ kind: IpAddrKind, address:
String
,
} let home = IpAddr { kind: IpAddrKind::V4, address:
String
::from(
"127.0.0.1"
),
}; let loopback = IpAddr { kind: IpAddrKind::V6, address:
String
::from(
"::1"
),
};
IpAddrKind::V4
и
IpAddrKind::V6
относятся к одному типу:
IpAddrKind
. Затем мы можем, например, определить функцию, которая принимает любой из вариантов
IpAddrKind
:
Можно вызвать эту функцию с любым из вариантов:
Использование перечислений позволяет получить ещё больше преимуществ. Если подумать о нашем типе для IP-адреса, то выяснится, что на данный момент у нас нет возможности хранить собственно сам IP-адрес; мы будем знать только его тип. Учитывая,
что недавно в главе 5 вы узнали о структурах, у вас может возникнуть соблазн решить эту проблему с помощью структур, как показано в листинге 6-1.
Листинг 6-1. Сохранение данных и
IpAddrKind
IP-адреса с использованием
struct
Здесь мы определили структуру
IpAddr
, у которой есть два поля: kind типа
IpAddrKind
(перечисление, которое мы определили ранее) и address типа
String
. У нас есть два экземпляра этой структуры. Первый - home
, который является
IpAddrKind::V4
в качестве значения kind с соответствующим адресом
127.0.0.1
. Второй экземпляр - loopback
. Он в качестве значения kind имеет другой вариант
IpAddrKind
,
V6
, и с ним ассоциирован адрес
::1
. Мы использовали структуру для объединения значений kind и address вместе, таким образом тип формата адреса теперь ассоциирован со значением.
Однако представление этой же концепции с помощью перечисления более лаконично:
вместо того, чтобы помещать перечисление в структуру, мы можем поместить данные непосредственно в любой из вариантов перечисления. Это новое определение fn route
(ip_kind: IpAddrKind) {} route(IpAddrKind::V4); route(IpAddrKind::V6); enum
IpAddrKind
{
V4,
V6,
} struct
IpAddr
{ kind: IpAddrKind, address:
String
,
} let home = IpAddr { kind: IpAddrKind::V4, address:
String
::from(
"127.0.0.1"
),
}; let loopback = IpAddr { kind: IpAddrKind::V6, address:
String
::from(
"::1"
),
};
перечисления
IpAddr гласит, что оба варианта
V4
и
V6
будут иметь соответствующие значения
String
:
Мы прикрепляем данные к каждому варианту перечисления напрямую, поэтому нет необходимости в дополнительной структуре. Здесь также легче увидеть ещё одну деталь того, как работают перечисления: имя каждого варианта перечисления, который мы определяем, также становится функцией, которая создаёт экземпляр перечисления. То есть
IpAddr::V4()
- это вызов функции, который принимает
String и возвращает экземпляр типа
IpAddr
. Мы автоматически получаем эту функцию-конструктор,
определяемую в результате определения перечисления.
Ещё одно преимущество использования перечисления вместо структуры заключается в том, что каждый вариант перечисления может иметь разное количество ассоциированных данных представленных в разных типах. Версия 4 для типа IP адресов всегда будет содержать четыре цифровых компонента, которые будут иметь значения между 0 и 255. При необходимости сохранить адреса типа
V4
как четыре значения типа u8
, а также описать адреса типа
V6
как единственное значение типа
String
, мы не смогли бы с помощью структуры. Перечисления решают эту задачу легко:
Мы показали несколько различных способов определения структур данных для хранения
IP-адресов четвёртой и шестой версий. Однако, как оказалось, желание хранить IP-адреса и указывать их тип настолько распространено, что в стандартной библиотеке есть определение, которое мы можем использовать! Давайте посмотрим, как стандартная библиотека определяет
IpAddr
: в ней есть точно такое же перечисление с вариантами,
которое мы определили и использовали, но она помещает данные об адресе внутрь этих вариантов в виде двух различных структур, которые имеют различные определения для каждого из вариантов:
enum
IpAddr
{
V4(
String
),
V6(
String
),
} let home = IpAddr::V4(
String
::from(
"127.0.0.1"
)); let loopback = IpAddr::V6(
String
::from(
"::1"
)); enum
IpAddr
{
V4(
u8
, u8
, u8
, u8
),
V6(
String
),
} let home = IpAddr::V4(
127
,
0
,
0
,
1
); let loopback = IpAddr::V6(
String
::from(
"::1"
));
IpAddr гласит, что оба варианта
V4
и
V6
будут иметь соответствующие значения
String
:
Мы прикрепляем данные к каждому варианту перечисления напрямую, поэтому нет необходимости в дополнительной структуре. Здесь также легче увидеть ещё одну деталь того, как работают перечисления: имя каждого варианта перечисления, который мы определяем, также становится функцией, которая создаёт экземпляр перечисления. То есть
IpAddr::V4()
- это вызов функции, который принимает
String и возвращает экземпляр типа
IpAddr
. Мы автоматически получаем эту функцию-конструктор,
определяемую в результате определения перечисления.
Ещё одно преимущество использования перечисления вместо структуры заключается в том, что каждый вариант перечисления может иметь разное количество ассоциированных данных представленных в разных типах. Версия 4 для типа IP адресов всегда будет содержать четыре цифровых компонента, которые будут иметь значения между 0 и 255. При необходимости сохранить адреса типа
V4
как четыре значения типа u8
, а также описать адреса типа
V6
как единственное значение типа
String
, мы не смогли бы с помощью структуры. Перечисления решают эту задачу легко:
Мы показали несколько различных способов определения структур данных для хранения
IP-адресов четвёртой и шестой версий. Однако, как оказалось, желание хранить IP-адреса и указывать их тип настолько распространено, что в стандартной библиотеке есть определение, которое мы можем использовать! Давайте посмотрим, как стандартная библиотека определяет
IpAddr
: в ней есть точно такое же перечисление с вариантами,
которое мы определили и использовали, но она помещает данные об адресе внутрь этих вариантов в виде двух различных структур, которые имеют различные определения для каждого из вариантов:
enum
IpAddr
{
V4(
String
),
V6(
String
),
} let home = IpAddr::V4(
String
::from(
"127.0.0.1"
)); let loopback = IpAddr::V6(
String
::from(
"::1"
)); enum
IpAddr
{
V4(
u8
, u8
, u8
, u8
),
V6(
String
),
} let home = IpAddr::V4(
127
,
0
,
0
,
1
); let loopback = IpAddr::V6(
String
::from(
"::1"
));
Этот код иллюстрирует что мы можем добавлять любой тип данных в значение перечисления: строку, число, структуру и пр. Вы даже можете включить в перечисление другие перечисления! Стандартные типы данных не очень сложны, хотя, потенциально,
могут быть очень сложными (вложенность данных может быть очень глубокой).
Обратите внимание, что хотя определение перечисления
IpAddr есть в стандартной библиотеке, мы смогли объявлять и использовать свою собственную реализацию с аналогичным названием без каких-либо конфликтов, потому что мы не добавили определение стандартной библиотеки в область видимости кода. Подробнее об этом поговорим в Главе 7.
Рассмотрим другой пример перечисления в листинге 6-2: в этом примере каждый элемент перечисления имеет свой особый тип данных внутри:
Листинг 6-2. Перечисление
Message
, в каждом из вариантов которого хранятся разные количества и типы
значений.
Это перечисление имеет 4 элемента:
Quit
- пустой элемент без ассоциированных данных,
Move имеет именованные поля, как и структура.
Write
- элемент с единственной строкой типа
String
,
ChangeColor
- кортеж из трёх значений типа i32
Определение перечисления с вариантами, такими как в листинге 6-2, похоже на определение значений различных типов внутри структур, за исключением того, что перечисление не использует ключевое слово struct и все варианты сгруппированы внутри типа
Message
. Следующие структуры могут содержать те же данные, что и предыдущие варианты перечислений:
struct
Ipv4Addr
{
// --snip--
} struct
Ipv6Addr
{
// --snip--
} enum
IpAddr
{
V4(Ipv4Addr),
V6(Ipv6Addr),
} enum
Message
{
Quit,
Move { x: i32
, y: i32
},
Write(
String
),
ChangeColor(
i32
, i32
, i32
),
}
Но когда мы использовали различные структуры, которые имеют свои собственные типы,
мы не могли легко определять функции, которые принимают любые типы сообщений,
как это можно сделать с помощью перечисления типа
Message
, объявленного в листинге
6-2, который является единым типом.
Есть ещё одно сходство между перечислениями и структурами: так же, как мы можем определять методы для структур с помощью impl блока, мы можем определять и методы для перечисления. Вот пример метода с именем call
, который мы могли бы определить в нашем перечислении
Message
:
В теле метода будет использоваться self для получения значение того объекта, у которого мы вызвали этот метод. В этом примере мы создали переменную m
,
содержащую значение
Message::Write(String::from("hello"))
, и именно это значение будет представлять self в теле метода call при выполнении m.call()
Теперь посмотрим на другое наиболее часто используемое перечисление из стандартной библиотеки, которое является очень распространённым и полезным:
Option
Перечисление Option и его преимущества перед Null-значениями
В этом разделе рассматривается пример использования
Option
, ещё одного перечисления, определённого в стандартной библиотеке. Тип
Option кодирует очень распространённый сценарий, в котором значение может быть чем-то, а может быть ничем.
Например, если вы запросите первое значение из списка, содержащего элементы, вы получите значение. Если вы запросите первое значение из пустого списка, вы ничего не получите. Выражение этой концепции в терминах системы типов означает, что компилятор может проверить, обработали ли вы все случаи, которые должны были struct
QuitMessage
;
// unit struct struct
MoveMessage
{ x: i32
, y: i32
,
} struct
WriteMessage
(
String
);
// tuple struct struct
ChangeColorMessage
(
i32
, i32
, i32
);
// tuple struct impl
Message { fn call
(&
self
) {
// method body would be defined here
}
} let m = Message::Write(
String
::from(
"hello"
)); m.call();