Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 726
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
17 .4 . Определение собственных классов значений 379
ет. А зачем нужен тип без значений? Как говорилось в разделе 7.4,
Nothing используется, в частности, для того, чтобы сигнализировать об аварийном завершении операции.
Например, в объекте sys стандартной библиотеки Scala есть метод error
, имеющий такое определение:
def error(message: String): Nothing =
throw new RuntimeException(message)
Возвращаемым типом метода error является
Nothing
, что говорит пользо
вателю о ненормальном возвращении из метода (вместо этого метод сгене
рировал исключение). Поскольку
Nothing
— подтип любого другого типа, то методы, подобные error
, допускают весьма гибкое использование, например:
def divide(x: Int, y: Int): Int =
if y != 0 then x / y else sys.error("деление на ноль невозможно")
Ветка then данного условия, представленная выражением x
/
y
, имеет тип
Int
, а ветка else
, то есть вызов error
, имеет тип
Nothing
. Поскольку
Nothing
— подтип
Int
, то типом всего условного выражения, как и требова
лось, является
Int
17 .4 . Определение собственных классов значений
В разделе 17.1 говорилось, что в дополнение к встроенным классам значений можно определять собственные. Как и экземпляры встроенных, экземпляры ваших классов значений будут, как правило, компилироваться в байткод
Java, который не задействует классоболочку. В том контексте, где нужна оболочка, например, при использовании обобщенного кода, значения будут упаковываться и распаковываться автоматически
1
Классами значений можно сделать только вполне определенные классы.
Чтобы класс стал классом значений, он должен иметь только один параметр и не должен иметь внутри ничего, кроме def
определений. Более того, класс значений не может расширяться никакими другими классами и в нем не могут переопределяться методы equals или hashCode
1
Scala 3 также предлагает непрозрачные типы, что является некоторым ограниче
нием, но гарантирует, что значение никогда не будет упаковано.
380 Глава 17 • Иерархия Scala
Чтобы определить класс значений, его нужно сделать подклассом класса
AnyVal и поставить перед его единственным параметром префикс val
. При
мер класса значений выглядит так:
class Dollars(val amount: Int) extends AnyVal:
override def toString = "$" + amount
В соответствии с описанием, приведенным в разделе 10.6, префикс val по
зволяет иметь доступ к параметру amount как к полю. Например, следующий код создает экземпляр класса значений, а затем извлекает из него amount
:
val money = new Dollars(1_000_000)
money.amount // 1000000
В данном примере money ссылается на экземпляр класса значений. Эта пере
менная в исходном коде Scala имеет тип
Dollars
, но скомпилированный байткод Java будет напрямую использовать тип
Int
В этом примере определяется метод toString
, и компилятор понимает, ко
гда его использовать. Именно поэтому вывод значения money дает результат
$1000000
со знаком доллара, а вывод money.amount дает результат
1000000
Можно даже определить несколько типов значений, и все они будут опи
раться на одно и то же
Int
значение, например:
class SwissFrancs(val amount: Int) extends AnyVal:
override def toString = s"$amount CHF"
Несмотря на то что
Dollars и
SwissFrancs во время выполнения представ
лены в виде целых чисел, в процессе компиляции они становятся разными типами:
scala> val dollars: Dollars = new SwissFrancs(1000)
1 |val dollars: Dollars = new SwissFrancs(1000)
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: SwissFrancs
| Required: Dollars
Уход от монокультурности типов
Чтобы получить наибольшие преимущества от использования иерархии классов Scala, старайтесь для каждого понятия предметной области опре
делять новый класс, несмотря на то что будет возможность неоднократно применять его для различных целей. Даже если он относится к так называе
мому крошечному (tiny) типу, не имеющему методов или полей, определение дополнительного класса поможет компилятору принести вам больше пользы.
17 .4 . Определение собственных классов значений 381
Предположим, вы написали некий код для генерации HTML. В HTML название стиля представлено в виде строки. То же самое касается и иденти
фикаторов привязки. Сам код HTML также является строкой, поэтому при желании представить все здесь перечисленное можно с помощью определе
ния вспомогательного кода, используя строки наподобие этих:
def title(text: String, anchor: String, style: String): String =
s"
1 ... 35 36 37 38 39 40 41 42 ... 64
$text
"В данной сигнатуре четыре строки! Такой строчно-типизированный код с технической точки зрения является строго типизированным. Однако все, что находится здесь в поле зрения, относится к типу
String
, поэто
му компилятор не может помочь вам отличить один элемент структуры от другого. Например, не сможет уберечь вас от следующего искажения структуры:
scala> title("chap:vcls", "bold", "Value Classes")
val res17: String =
chap:vcls
Код HTML нарушен. Предполагаемый для вывода на экран текст
Value
Classes используется в качестве класса стиля, в то время как для отобра
жаемого текста chap.vcls предусматривалась роль гипертекстовой ссылки.
В довершение ко всему в качестве идентификатора такой ссылки выступила строка bold
, которая, в свою очередь, должна была выполнять роль класса стиля. Несмотря на всю череду ошибок, компилятор никак этому не вос
противился.
Если определить для каждого понятия предметной области крошечный тип, то компилятор сможет принести больше пользы. Например, можно опреде
лить собственный небольшой класс для стилей, идентификаторов гипертек
стовых ссылок, отображаемого текста и кода HTML. Поскольку эти классы имеют один параметр и не имеют элементов, то могут быть определены как классы значений:
class Anchor(val value: String) extends AnyVal class Style(val value: String) extends AnyVal class Text(val value: String) extends AnyVal class Html(val value: String) extends AnyVal
Наличие этих классов позволяет создать версию title
, обладающую менее тривиальной сигнатурой типов наподобие такой:
def title(text: Text, anchor: Anchor, style: Style): Html =
Html(
s"" +
382 Глава 17 • Иерархия Scala s"
" +
text.value +
"
")
Теперь при попытке воспользоваться этой версией с аргументами, указанны
ми в неверном порядке, компилятор сможет обнаружить ошибку, например:
scala> title(Anchor("chap:vcls"), Style("bold"),
Text("Value Classes"))
1 |title(new Anchor("chap:vcls"), new Style("bold"),
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Anchor
| Required: Text
1 |title(Anchor("chap:vcls"), Style("bold"),
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Style
| Required: Anchor
2 | Text("Value Classes"))
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: Text
| Required: Style
17 .5 . Типы пересечений
Вы можете объединить два и более типа с помощью амперсанда (&), чтобы сформировать тип пересечения, например
Incrementing
&
Filtering
(При
ращение и фильтрация). Вот пример использования классов и признаков, показанных в листингах 11.5, 11.6 и 11.9.
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...
Здесь q
инициализируется экземпляром анонимного класса, который расши
ряет
BasicIntQueue и смешивает
Incrementing
(Приращение) с последующей
Filtering
(Фильтрация). Его выводимый тип,
BasicIntQueue
&
Incrementing
&
Filtering
, представляет собой тип пересечения, который указывает, что объект, на который ссылается q
, является экземпляром всех трех упомянутых типов:
BasicIntQueue
,
Incrementing и
Filtering
Тип пересечения является подтипом всех комбинаций составляющих его типов. Например, тип
B
&
I
&
F
является подтипом типов
B
,
I
,
F
,
B
&
I
,
B
&
F
,
I
&
F
и самого себя. Более того, поскольку типы пересечения являются коммута
тивными, порядок появления типов в типе пересечения не имеет значения:
17 .6 . Типы объединения 383
например, тип
I
&
F
эквивалентен типу
F
&
I
. Следовательно,
B
&
I
&
F
также является подтипом
I
&
B
,
F
&
B
,
F
&
I
,
B
&
F
&
I
,
F
&
F
&
I
,
F
&
B
&
I
и т. д. Вот при
мер, иллюстрирующий эти взаимосвязи между типами пересечений:
// Компилируется, так как B & I & F <: I & F
val q2: Incrementing & Filtering = q
// Компилируется, так как I & F эквивалентно F & I
val q3: Filtering & Incrementing = q2 17 .6 . Типы объединения
Scala предлагает дубликат для типов пересечения, называемых типами объ-
единения, которые состоят из двух или более типов, соединенных вертикаль
ной чертой
(|)
, например
Plum
|
Apricot
. Тип объединения указывает, что объект является экземпляром по крайней мере одного из упомянутых типов.
Например, объект типа
Plum
|
Apricot является либо экземпляром
Plum
, либо экземпляром
Apricot
, либо и тем и другим
1
Как и типы пересечения, типы объединения являются коммутативными:
Plum
|
Apricot эквивалентно
Apricot
|
Plum
. В отличие от типов пересечения тип объединения является супертипом всех комбинаций входящих в него типов. Например,
Plum
|
Apricot является супертипом как
Plum
, так и
Apricot
Важно отметить, что
Plum
|
Apricot является не просто супертипом
Plum и
Apricot
, а их ближайшим общим супертипом, или наименьшей верхней
границей.
Добавление типов объединения и пересечения в Scala 3 гарантирует, что система типов Scala образует математическую решетку. Решетка — это частичный порядок, в котором любые два типа имеют как уникальную наи
меньшую верхнюю границу, или LUB, так и уникальную наибольшую нижнюю
границу. В Scala 3 наименьшей верхней границей любых двух типов является их объединение, а наибольшей нижней границей — их пересечение. Напри
мер, наименьшей верхней границей
Plum и
Apricot является
Plum
|
Apricot
Их наибольшая нижняя граница —
Plum
&
Apricot
Типы объединения имеют серьезные последствия для спецификации и реа
лизации вывода типов и проверки типов в Scala. В то время как в Scala 2 ал
горитм вывода типов должен был основываться на приближенном значении наименьшей верхней границы некоторых пар типов, фактическая наимень
1
Вы можете произносить Plum | Apricot как Plum или Apricot.
384 Глава 17 • Иерархия Scala шая верхняя граница которых была пределом бесконечной серии, то в Scala
3 можно просто сформировать объединение этих типов.
Чтобы представить себе это, рассмотрим следующую иерархию:
trait Fruit trait Plum extends Fruit trait Apricot extends Fruit trait Pluot extends Plum, Apricot
Эти четыре типа образуют иерархию, показанную на рис. 17.2.
Fruit является супертипом как для
Plum
, так и для
Apricot
, но он не является ближайшим общим супертипом. Скорее, тип объединения
Plum
|
Apricot является бли
жайшим общим супертипом, или наименьшей верхней границей, для
Plum и
Apricot
. Как показано на рис. 17.2, это означает, что тип объединения
Plum
|
Apricot является подтипом
Fruit
. И это действительно так, как показано на рисунке.
Рис. 17.2. Наименьшая верхняя и наибольшая нижняя границы val plumOrApricot: Plum | Apricot = new Plum {}
// Компилируется без проблем, так как Plum | Apricot <: Fruit val fruit: Fruit = plumOrApricot
// Нельзя использовать Fruit, так как нужен Plum | Apricot scala> val doesNotCompile: Plum | Apricot = fruit
1 |val doesNotCompile: Plum | Apricot = fruit
| ˆˆˆˆˆ
| Found: (fruit : Fruit)
| Required: Plum | Apricot
Двойной
Pluot является подтипом и для
Plum
, и для
Apricot
, но он не явля
ется ближайшим общим подтипом. Скорее, тип пересечения
Plum
&
Apricot
17 .6 . Типы объединения 385
является ближайшим общим подтипом, или наибольшей нижней границей, для
Plum и
Apricot
. Из представленной на рис. 17.2 схемы следует, что тип пересечения
Plum
&
Apricot является супертипом
Pluot
. И это действительно так:
val pluot: Pluot = new Pluot {}
// Компилируется без проблем, так как Pluot <: Plum & Apricot val plumAndApricot: Plum & Apricot = pluot
// Нельзя использовать Plum & Apricot, так как нужен Pluot scala> val doesNotCompile: Pluot = plumAndApricot
1 |val doesNotCompile: Pluot = plumAndApricot
| ˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
| Found: (plumAndApricot : Plum & Apricot)
| Required: Pluot
Вы можете вызвать любой метод или получить доступ к любому полю, опре
деленному в каждом из составляющих типов типа пересечения. Например, для экземпляра
Plum
&
Apricot вы можете вызывать любые методы, опреде
ленные в
Plum или
Apricot
. В отличие от этого в типе объединения вы можете получить доступ только к тем элементам супертипов, которые являются об-
щими для составляющих типов. Таким образом, в экземпляре
Plum
|
Apricot вы можете получить доступ к членам
Fruit
(включая элементы, которые он наследует от
AnyRef и
Any
), но вы не можете получить доступ к какимлибо элементам, добавленным в
Plum или
Apricot
. Чтобы получить к ним доступ, вы должны выполнить сопоставление с образцом, чтобы определить реаль
ный класс значения во время выполнения, например:
def errorMessage(msg: Int | String): String =
msg match case n: Int => s"Error number: ${n.abs}"
case s: String => s + "!"
Параметр msg метода errorMessage имеет тип
Int
|
String
. Поэтому вы можете напрямую вызывать в msg только методы, объявленные в
Any
, един
ственном общем супертипе
Int и
String
. Вы не можете напрямую вызывать никакие другие методы, определенные либо в
Int
, либо в
String
. Чтобы получить доступ, например, к методу abs в
Int или оператору конкатенации строк (
+
) в
String
, необходимо выполнить сопоставление с образцом в msg
, как показано в теле метода errorMessage
. Вот несколько примеров исполь
зования метода errorMessage
:
errorMessage("Oops") // "Oops!"
errorMessage(-42) // "Error number: 42"
386 Глава 17 • Иерархия Scala
17 .7 . Прозрачные трейты
У трейтов есть два основных применения: они позволяют определять клас
сы с помощью композиции примешивания и определяют типы. В основном трейт используется как примешивание, а не как тип. Например, трейты
Incrementing и
Filtering из раздела 11.3 полезны в качестве примешиваний, однако они также имеют ограниченную ценность в качестве типов. По умол
чанию можно выявить типы, определяемые этими трейтами. Например, компилятор Scala определит тип q
в следующей инструкции как тип пере
сечения, в котором упоминаются и
Incrementing
, и
Filtering
:
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...
Вы можете указать, что не хотите, чтобы имя трейта отображалось в выводи
мых типах, объявив его с помощью модификатора transparent
(прозрачный)
Например, объявив
Incrementing и
Filtering как прозрачные следующим образом:
transparent trait Incrementing extends IntQueue:
abstract override def put(x: Int) = super.put(x + 1)
transparent trait Filtering extends IntQueue:
abstract override def put(x: Int) =
if x >= 0 then super.put(x)
Теперь, когда трейты
Incrementing и
Filtering определены как прозрачные, их имена больше не будут отображаться в выводимых типах. Например, тип, выведенный из того же выражения создания экземпляра, показанного ранее, больше не будет упоминать
Incrementing или
Filtering
:
scala> val q = new BasicIntQueue with
Incrementing with Filtering val q: BasicIntQueue = anon$...
Модификатор transparent влияет только на вывод типов. Вы все еще можете использовать прозрачные трейты в качестве типов, если напишете их в явном виде. Вот пример, в котором прозрачные трейты
Incrementing и
Filtering отображаются в аннотации явного типа для переменной q
:
scala> val q: BasicIntQueue & Incrementing & Filtering =
new BasicIntQueue with Incrementing with Filtering val q: BasicIntQueue & Incrementing & Filtering = anon$...