Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 789
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
465
конкретным, чем другие. Это полный аналог перегрузки методов. Если вы попытаетесь вызвать foo(null)
, а в вашем коде есть две разные пере
груженные версии foo
, которые принимают null
, компилятор откажется делать выбор и выведет сообщение о том, что вызов метода имеет неодно
значную цель.
Однако если один из гивенов безусловно является более конкретным, чем другие, компилятор выберет именно его. Идея в следующем: если есть при
чина полагать, что программист в любом случае предпочтет один из гивенов, его не нужно заставлять записывать это явно. В конце концов, перегрузка методов имеет такое же послабление. Возвращаясь к предыдущему примеру, если один из доступных методов foo принимает
String
, остальные —
Any
, компилятор выберет версию со
String
. Она явно более конкретная.
Строго говоря, один гивен конкретнее другого, если выполняется одно из следующих условий:
z z
тип первого является подтипом второго;
z z
внешний класс первого наследует внешний класс второго.
Если у вас есть два подходящих гивена, один из которых явно должен быть выбран в первую очередь, вы можете разместить другой экземпляр в трейте
LowPriority
, а свой первостепенный выбор — в подклассе или подобъекте этого трейта. Компилятор выберет первый экземпляр, если тот подходит, даже если второй был бы неоднозначным вариантом. Если гивен с более низким приоритетом подходит, а с более высоким — нет, компилятор сделает выбор в пользу первого.
21 .8 . Отладка гивенов
Гивены являются мощным инструментом Scala, с которым тем не менее может быть сложно управиться. Этот раздел содержит несколько советов об отладке гивенов.
Иногда не совсем понятно, почему компилятор не нашел гивен, который, казалось бы, должен был быть доступен. В таких случаях бывает полезно отредактировать код так, чтобы гивен передавался явно, с помощью using
Если сообщение об ошибке остается и после этого, причина, по которой компилятор не сумел применить ваш гивен, становится очевидной. Если же явная подстановка гивена позволяет избавиться от ошибки, вы можете быть уверены в том, что проблема была в одном из других правил (например, в правиле видимости).
466 Глава 21 • Гивены
При отладке программы иногда бывает полезно видеть, какие гивены под
ставляет компилятор. Для этого компилятору можно передать параметр
-Xprint:typer
. В этом случае вы узнаете, как выглядит ваш код после того, как средство проверки типов добавило все гивены. Пример этого показан в листингах 21.8 и 21.9. Если взглянуть на последнюю инструкцию каждого из этих листингов, можно увидеть, что компилятор подставил второй список параметров, enjoy("reader")
, который был опущен в листинге 21.8:
Mocha.enjoy("reader")(Mocha.pref)
Если вы чувствуете жажду приключений, попробуйте выполнить команду scala
-Xprint:typer
, чтобы получить интерактивную оболочку с выводом исходного кода после типизации, предназначенного для внутреннего ис
пользования компилятором. Но будьте готовы к тому, что интересующие вас участки будут спрятаны в груде шаблонного кода
1
Листинг 21.8. Пример кода, в котором используется контекстный параметр object Mocha:
class PreferredDrink(val preference: String)
given pref: PreferredDrink = new PreferredDrink("mocha")
def enjoy(name: String)(using drink: PreferredDrink): Unit =
print(s"Welcome, $name")
print(". Enjoy a ")
print(drink.preference)
println("!")
def callEnjoy: Unit = enjoy("reader")
Листинг 21.9. Пример кода, полученного после проверки типов и подстановки гивенов
$ scalac -Xprint:typer Mocha.scala package {
final lazy module val Mocha: Mocha$ = new Mocha$()
def callEnjoy: Unit = Mocha.enjoy("reader")(Mocha.pref)
final module class Mocha$() extends Object() {
this: Mocha.type =>
// ...
final lazy given val pref: Mocha.PreferredDrink =
new Mocha.PreferredDrink("mocha")
def enjoy(name: String)(using drink:
1
У таких IDE, как IntelliJ и Metals, есть параметры для вывода подставленных ги
венов.
Резюме 467
Mocha.PreferredDrink): Unit = {
print(
_root_.scala.StringContext.apply(["Welcome,
","" : String]:String*).s([name : Any]:Any*)
)
print(". Enjoy a ")
print(drink.preference)
println("!")
}
def callEnjoy: Unit = Mocha.enjoy("reader")(Mocha.pref)
}
}
Резюме
Контекстные параметры могут сделать сигнатуры ваших функций более лег
кими для восприятия: вместо того чтобы пытаться разобраться в шаблонных аргументах, читатель может сосредоточиться на том, для чего в действи
тельности предназначена ваша функция, делегируя предоставление этих шаблонных аргументов ее контексту. Поиск этих контекстных параметров тоже происходит во время компиляции, что гарантирует доступность их значений на этапе выполнения.
В этой главе был описан механизм гивенов, способный неявно передавать аргументы функциям. Это уменьшает количество шаблонного кода и при этом позволяет функциям потреблять все данные, с которыми они работа
ют, в виде аргументов. Как вы сами видели, поиск контекстных параметров основан на типе параметров функции: если для неявной передачи доступно значение подходящего типа, компилятор передаст его функции. Вы также видели несколько примеров использования этого механизма для реализации специального полиморфизма, как в случае с классом типов
Ordering
. В сле
дующей главе вы узнаете, как классы типов можно использовать в сочетании с методами расширения, а в главе 23 будет представлено несколько примеров использования заданных экземпляров для классов типов.
22
Методы расширения
Если вы пишете функцию, которая в основном работает с конкретным клас
сом объектов, у вас может возникнуть желание определить ее в качестве члена этого класса. В таком объектноориентированном языке, как Scala, программистам, которые будут вызывать эту функцию, данный подход мо
жет показаться наиболее естественным. Тем не менее в некоторых случаях класс нельзя изменять. Бывают также ситуации, когда функционал должен принадлежать givenэкземпляру класса типа (type class), определенному для класса. В связи с этим в Scala предусмотрен механизм, создающий видимость того, что функция определена как метод класса, хотя в реальности она ему не принадлежит.
В Scala 3 на смену подходу с неявными классами пришел новый механизм,
методы расширения. В этой главе вы узнаете, как создавать собственные и использовать чужие методы расширения.
22 .1 . Основы
Представьте, что вам нужно проверять строки на равенство, применяя два особых правила к пробельным символам. Вопервых, пробельные символы, находящиеся в начале или конце строки, нужно игнорировать. Вовторых, участки пробельных символов внутри строк должны совпадать по размеру и позиции, но могут отличаться по содержанию. Для этого вы могли бы об
резать пробельные символы в начале и в конце обеих строк, заменить любые внутренние последовательности пробельных символов одним пробелом и затем проверить получившиеся строки на равенство. Вот функция, которая выполняет это преобразование:
22 .1 . Основы 469
def singleSpace(s: String): String =
s.trim.split("\\s+").mkString(" ")
Функция singleSpace принимает строку и делает ее подходящей для сравне
ния с использованием
==
. Сначала она удаляет пробельные символы на обоих концах строки с помощью trim
. Затем она вызывает split
, чтобы разделить обрезанную строку по участкам последовательных пробельных символов.
В результате получается массив. И в завершение она использует mkString для объединения непробельных строк в массиве, разделяя каждую из них единственным символом пробела. Вот несколько примеров:
singleSpace("A Tale\tof Two Cities")
// "A Tale of Two Cities"
singleSpace(" It was the\t\tbest\nof times. ")
// "It was the best of times."
singleSpace можно использовать для проверки строк на равенство, игнори
руя различия в пробельных символах:
val s = "One Fish, Two\tFish "
val t = " One Fish, Two Fish"
singleSpace(s) == singleSpace(t) // true
Этот подход вполне логичен. singleSpace можно поместить в подходящий объектодиночку и перейти к следующей задаче. Но, если взглянуть на это с человеческой точки зрения, вам может показаться, что ваши пользователи предпочли бы вызывать этот метод напрямую из
String
, как показано ниже:
s.singleSpace == t.singleSpace // к сожалению, это выражение ложное
Данный синтаксис придал бы ощущение объектной ориентированности при работе с этой функцией. Но, поскольку класс
String входит в стандартную библиотеку, этот синтаксис было бы проще всего реализовать путем опреде
ления singleSpace в качестве метода расширения
1
. Это продемонстрировано в листинге 22.1.
Листинг 22.1. Метод расширения для String extension (s: String)
def singleSpace: String =
s.trim.split("\\s+").mkString(" ")
1
К числу других, более сложных вариантов относится добавление singleSpace в
String с использованием Java Community Process
SM
или Scala Improvement
Process.
470 Глава 22 • Методы расширения
Ключевое слово extension позволяет создать иллюзию того, что вы доба
вили в класс функциючлен, не меняя при этом сам класс. В скобках после extension указывается одна переменная того типа, к которому вы хотите добавить метод. Объект, на который ссылается эта переменная, называется
получателем метода расширения. В данном случае
(s:
String)
означает, что вы хотите добавить метод в
String
. Вслед за этой вводной частью идет самый обычный метод, единственная особенность которого в том, что в его теле используется получатель, s
Процесс использования метода расширения называется применением.
Например, здесь singleSpace применяется дважды для сравнения двух строк:
s.singleSpace == t.singleSpace // Возвращает true!
Определение метода расширения чемто напоминает определение аноним
ного класса, который принимает объектполучатель в качестве параметра своего конструктора и тем самым делает этот объект доступным для своих методов. Однако это впечатление обманчивое. Определение метода рас
ширения заменяется методом, который принимает получатель напрямую в качестве параметра. Например, компилятор перепишет определение ме
тода расширения из листинга 22.1, чтобы привести его к виду, показанному в листинге 22.2.
Листинг 22.2. Метод расширения после переписывания компилятором
// С внутренним обозначением расширения def singleSpace(s: String): String =
s.trim.split("\\s+").mkString(" ")
Единственной особенностью переписанного метода является то, что ком
пилятор присваивает ему внутреннее обозначение, сигнализирующее о том, что это метод расширения. Чтобы сделать этот метод доступным, проще всего разместить имя его переписанной версии в лексической области видимости. Вот пример в REPL:
scala> extension (s: String)
def singleSpace: String =
s.trim.split("\\s+").mkString(" ")
def singleSpace(s: String): String
Поскольку в этом сеансе REPL singleSpace находится в лексической области видимости и обозначен как метод расширения, его можно применить:
scala> s.singleSpace == t.singleSpace val res0: Boolean = true
22 .2 . Обобщенные расширения 471
Ввиду того что Scala заменяет методы расширения, при их применении не происходит ненужных преобразований. В Scala 2, где используется подход с неявными классами, это было не всегда так. Таким образом методы расши
рения предоставляют «синтаксический сахар без отрицательных эффектов».
Метод расширения, вызываемый из получателя, как в случае с s.singleSpace
, всегда имеет ту же производительность, что и передача получателя соответ
ствующему нерасширяющему методу — например, singleSpace(s)
22 .2 . Обобщенные расширения
Вы можете определять методы расширения, которые являются обобщенными.
В качестве примера возьмем метод head из класса
List
, который возвращает первый элемент списка, но генерирует исключение, если список пустой
1
:
List(1, 2, 3).head // 1
List.empty.head // генерирует NoSuchElementException
Если вы не уверены в том, что имеющийся у вас список чтото содержит, можете использовать вместо этого метод headOption
, который возвращает первый элемент, завернутый в
Some
. Если же список пустой, headOption воз
вращает
None
:
List(1, 2, 3).headOption // Some(1)
List.empty.headOption // None
List также предлагает метод tail
, который возвращает все, кроме первого элемента. Как и head
, он генерирует исключение, если список пустой:
List(1, 2, 3).tail // List(2, 3)
List.empty.tail // генерирует NoSuchElementException
Однако класс
List не предлагает безопасную альтернативу для получения оставшейся части списка, завернутой в
Option
. Если вам нужен такой метод, можете реализовать его в виде обобщенного расширения. Для этого нужно ука
зать один или несколько параметров типа после ключевого слова extension
, но перед скобками с получателем внутри. Пример показан в листинге 22.3.
Листинг 22.3. Обобщенный метод расширения extension [T](xs: List[T])
def tailOption: Option[List[T]] =
if xs.nonEmpty then Some(xs.tail) else None
1
Метод head из
List описан в разделе 14.4.
472 Глава 22 • Методы расширения
Метод расширения tailOption является обобщенным только для одного типа,
T
. Вот несколько примеров использования tailOption
, в которых из
T
создается экземпляр
Int и
String
:
List(1, 2, 3).tailOption // Some(List(2, 3))
List.empty[Int].tailOption // None
List("A", "B", "C").tailOption // Some(List(B, C))
List.empty[String].tailOption // None
Обычно имеет смысл разрешить автоматическое определение такого пара
метра типа, как это было сделано в предыдущих примерах, но вы можете указать его явно. Для этого метод необходимо вызвать напрямую, то есть не как метод расширения:
tailOption[Int](List(1, 2, 3)) // Some(List(2, 3))
22 .3 . Групповые расширения
Когда несколько методов нужно добавить в один и тот же тип, их можно определить вместе с помощью группового расширения. Например, поскольку многие операции с
Int могут вызывать переполнение буфера, вам, возможно, захочется определить несколько методов расширения для
Int
, способных распознать переполнение.
Чтобы представить значение
Int в дополнительном коде, нужно инвертиро
вать все его биты и прибавить единицу. Это представление позволяет реали
зовать вычитание в виде операции в дополнительном коде, вслед за которой идет сложение. Оно также имеет всего одно нулевое значение вместо двух: положительного и отрицательного
1
. К тому же, учитывая отсутствие отри
цательного нуля, у нас остается один разряд для дополнительного значения.
Это значение размещается в самом конце отрицательных целых чисел. Вот по
чему наименьшее отрицательное значение
Int
, взятое по модулю, на единицу меньше наибольшего положительного целого числа, которое можно выразить:
Int.MaxValue // 2147483647
Int.MinValue // -2147483648
Именно изза этой асимметричности между максимальным и минимальным значениями некоторые методы
Int могут переполняться. Например, метод
1
В обратном коде целые нулевые значения могут быть как положительными, так и отрицательными, по аналогии с форматом чисел с плавающей запятой IEEE 754, который используется в
Float и
Double
22 .3 . Групповые расширения 473
abs из
Int вычисляет абсолютное значение целого числа. В
Int абсолютное значение минимума составляет 2 147 483 648, однако это число невоз
можно выразить с помощью этого типа. Максимальное значение
Int равно
2 147 483 647, что на единицу меньше, поэтому вызов abs для
Int.MinValue приводит к переполнению и вы получаете исходное значение
MinValue
:
Int.MinValue.abs // -2147483648 (переполнение)
Если вам нужен метод, который возвращает абсолютное значение
Int
, но при этом распознает переполнение, вы можете определить метод расширения, как показано далее:
extension (n: Int)
def absOption: Option[Int] =
if n != Int.MinValue then Some(n.abs) else None
Для значения
Int.MinValue
, изза которого переполняется abs
, absOption возвращает
None
. Для остальных значений absOption возвращает результат работы abs
, завернутый в
Some
. Вот несколько примеров использования absOption
:
42.absOption // Some(42)
-42.absOption // Some(42)
Int.MaxValue.absOption // Some(2147483647)
Int.MinValue.absOption // None
Еще одной операцией, способной вызвать переполнение для минимального значения
Int
, является изменение знака числа на отрицательный. Операция unary_- с
Int.MinValue возвращает все то же
MinValue
1
:
-Int.MinValue // -2147483648 (переполнение)
Если вам нужен безопасный вариант unary_-
, вы можете определить его вместе с absOption в групповом расширении, как показано в листинге 22.4.
Листинг 22.4. Групповое расширение extension (n: Int)
def absOption: Option[Int] =
if n != Int.MinValue then Some(n.abs) else None def negateOption: Option[Int] =
if n != Int.MinValue then Some(-n) else None
1
Как было описано в разделе 5.4, компилятор Scala подставляет вместо
-Int.MinVa- lue метод unary_- для
Int.MinValue
— то есть
Int.MinValue.unary_-
474 Глава 22 • Методы расширения
Это расширение добавляет в
Int сразу два метода: absOption и negateOption
Вот несколько примеров использования последнего.
-42.negateOption // Some(42)
42.negateOption // Some(42)
Int.MaxValue.negateOption // Some(2147483647)
Int.MinValue.negateOption // None
Методы, определенные вместе в групповом расширении, называются ме-
тодами того же уровня. Из одного метода в групповом расширении можно вызывать другие, как если бы они были членами одного класса. Например, как показано в листинге 22.5, если вы решите добавить в
Int еще один метод расширения, isMinValue
, у вас будет возможность вызывать его напрямую из двух других методов, absOption и negateOption
конкретным, чем другие. Это полный аналог перегрузки методов. Если вы попытаетесь вызвать foo(null)
, а в вашем коде есть две разные пере
груженные версии foo
, которые принимают null
, компилятор откажется делать выбор и выведет сообщение о том, что вызов метода имеет неодно
значную цель.
Однако если один из гивенов безусловно является более конкретным, чем другие, компилятор выберет именно его. Идея в следующем: если есть при
чина полагать, что программист в любом случае предпочтет один из гивенов, его не нужно заставлять записывать это явно. В конце концов, перегрузка методов имеет такое же послабление. Возвращаясь к предыдущему примеру, если один из доступных методов foo принимает
String
, остальные —
Any
, компилятор выберет версию со
String
. Она явно более конкретная.
Строго говоря, один гивен конкретнее другого, если выполняется одно из следующих условий:
z z
тип первого является подтипом второго;
z z
внешний класс первого наследует внешний класс второго.
Если у вас есть два подходящих гивена, один из которых явно должен быть выбран в первую очередь, вы можете разместить другой экземпляр в трейте
LowPriority
, а свой первостепенный выбор — в подклассе или подобъекте этого трейта. Компилятор выберет первый экземпляр, если тот подходит, даже если второй был бы неоднозначным вариантом. Если гивен с более низким приоритетом подходит, а с более высоким — нет, компилятор сделает выбор в пользу первого.
21 .8 . Отладка гивенов
Гивены являются мощным инструментом Scala, с которым тем не менее может быть сложно управиться. Этот раздел содержит несколько советов об отладке гивенов.
Иногда не совсем понятно, почему компилятор не нашел гивен, который, казалось бы, должен был быть доступен. В таких случаях бывает полезно отредактировать код так, чтобы гивен передавался явно, с помощью using
Если сообщение об ошибке остается и после этого, причина, по которой компилятор не сумел применить ваш гивен, становится очевидной. Если же явная подстановка гивена позволяет избавиться от ошибки, вы можете быть уверены в том, что проблема была в одном из других правил (например, в правиле видимости).
466 Глава 21 • Гивены
При отладке программы иногда бывает полезно видеть, какие гивены под
ставляет компилятор. Для этого компилятору можно передать параметр
-Xprint:typer
. В этом случае вы узнаете, как выглядит ваш код после того, как средство проверки типов добавило все гивены. Пример этого показан в листингах 21.8 и 21.9. Если взглянуть на последнюю инструкцию каждого из этих листингов, можно увидеть, что компилятор подставил второй список параметров, enjoy("reader")
, который был опущен в листинге 21.8:
Mocha.enjoy("reader")(Mocha.pref)
Если вы чувствуете жажду приключений, попробуйте выполнить команду scala
-Xprint:typer
, чтобы получить интерактивную оболочку с выводом исходного кода после типизации, предназначенного для внутреннего ис
пользования компилятором. Но будьте готовы к тому, что интересующие вас участки будут спрятаны в груде шаблонного кода
1
Листинг 21.8. Пример кода, в котором используется контекстный параметр object Mocha:
class PreferredDrink(val preference: String)
given pref: PreferredDrink = new PreferredDrink("mocha")
def enjoy(name: String)(using drink: PreferredDrink): Unit =
print(s"Welcome, $name")
print(". Enjoy a ")
print(drink.preference)
println("!")
def callEnjoy: Unit = enjoy("reader")
Листинг 21.9. Пример кода, полученного после проверки типов и подстановки гивенов
$ scalac -Xprint:typer Mocha.scala package
final lazy module val Mocha: Mocha$ = new Mocha$()
def callEnjoy: Unit = Mocha.enjoy("reader")(Mocha.pref)
final module class Mocha$() extends Object() {
this: Mocha.type =>
// ...
final lazy given val pref: Mocha.PreferredDrink =
new Mocha.PreferredDrink("mocha")
def enjoy(name: String)(using drink:
1
У таких IDE, как IntelliJ и Metals, есть параметры для вывода подставленных ги
венов.
Резюме 467
Mocha.PreferredDrink): Unit = {
print(
_root_.scala.StringContext.apply(["Welcome,
","" : String]:String*).s([name : Any]:Any*)
)
print(". Enjoy a ")
print(drink.preference)
println("!")
}
def callEnjoy: Unit = Mocha.enjoy("reader")(Mocha.pref)
}
}
Резюме
Контекстные параметры могут сделать сигнатуры ваших функций более лег
кими для восприятия: вместо того чтобы пытаться разобраться в шаблонных аргументах, читатель может сосредоточиться на том, для чего в действи
тельности предназначена ваша функция, делегируя предоставление этих шаблонных аргументов ее контексту. Поиск этих контекстных параметров тоже происходит во время компиляции, что гарантирует доступность их значений на этапе выполнения.
В этой главе был описан механизм гивенов, способный неявно передавать аргументы функциям. Это уменьшает количество шаблонного кода и при этом позволяет функциям потреблять все данные, с которыми они работа
ют, в виде аргументов. Как вы сами видели, поиск контекстных параметров основан на типе параметров функции: если для неявной передачи доступно значение подходящего типа, компилятор передаст его функции. Вы также видели несколько примеров использования этого механизма для реализации специального полиморфизма, как в случае с классом типов
Ordering
. В сле
дующей главе вы узнаете, как классы типов можно использовать в сочетании с методами расширения, а в главе 23 будет представлено несколько примеров использования заданных экземпляров для классов типов.
22
Методы расширения
Если вы пишете функцию, которая в основном работает с конкретным клас
сом объектов, у вас может возникнуть желание определить ее в качестве члена этого класса. В таком объектноориентированном языке, как Scala, программистам, которые будут вызывать эту функцию, данный подход мо
жет показаться наиболее естественным. Тем не менее в некоторых случаях класс нельзя изменять. Бывают также ситуации, когда функционал должен принадлежать givenэкземпляру класса типа (type class), определенному для класса. В связи с этим в Scala предусмотрен механизм, создающий видимость того, что функция определена как метод класса, хотя в реальности она ему не принадлежит.
В Scala 3 на смену подходу с неявными классами пришел новый механизм,
методы расширения. В этой главе вы узнаете, как создавать собственные и использовать чужие методы расширения.
22 .1 . Основы
Представьте, что вам нужно проверять строки на равенство, применяя два особых правила к пробельным символам. Вопервых, пробельные символы, находящиеся в начале или конце строки, нужно игнорировать. Вовторых, участки пробельных символов внутри строк должны совпадать по размеру и позиции, но могут отличаться по содержанию. Для этого вы могли бы об
резать пробельные символы в начале и в конце обеих строк, заменить любые внутренние последовательности пробельных символов одним пробелом и затем проверить получившиеся строки на равенство. Вот функция, которая выполняет это преобразование:
22 .1 . Основы 469
def singleSpace(s: String): String =
s.trim.split("\\s+").mkString(" ")
Функция singleSpace принимает строку и делает ее подходящей для сравне
ния с использованием
==
. Сначала она удаляет пробельные символы на обоих концах строки с помощью trim
. Затем она вызывает split
, чтобы разделить обрезанную строку по участкам последовательных пробельных символов.
В результате получается массив. И в завершение она использует mkString для объединения непробельных строк в массиве, разделяя каждую из них единственным символом пробела. Вот несколько примеров:
singleSpace("A Tale\tof Two Cities")
// "A Tale of Two Cities"
singleSpace(" It was the\t\tbest\nof times. ")
// "It was the best of times."
singleSpace можно использовать для проверки строк на равенство, игнори
руя различия в пробельных символах:
val s = "One Fish, Two\tFish "
val t = " One Fish, Two Fish"
singleSpace(s) == singleSpace(t) // true
Этот подход вполне логичен. singleSpace можно поместить в подходящий объектодиночку и перейти к следующей задаче. Но, если взглянуть на это с человеческой точки зрения, вам может показаться, что ваши пользователи предпочли бы вызывать этот метод напрямую из
String
, как показано ниже:
s.singleSpace == t.singleSpace // к сожалению, это выражение ложное
Данный синтаксис придал бы ощущение объектной ориентированности при работе с этой функцией. Но, поскольку класс
String входит в стандартную библиотеку, этот синтаксис было бы проще всего реализовать путем опреде
ления singleSpace в качестве метода расширения
1
. Это продемонстрировано в листинге 22.1.
Листинг 22.1. Метод расширения для String extension (s: String)
def singleSpace: String =
s.trim.split("\\s+").mkString(" ")
1
К числу других, более сложных вариантов относится добавление singleSpace в
String с использованием Java Community Process
SM
или Scala Improvement
Process.
470 Глава 22 • Методы расширения
Ключевое слово extension позволяет создать иллюзию того, что вы доба
вили в класс функциючлен, не меняя при этом сам класс. В скобках после extension указывается одна переменная того типа, к которому вы хотите добавить метод. Объект, на который ссылается эта переменная, называется
получателем метода расширения. В данном случае
(s:
String)
означает, что вы хотите добавить метод в
String
. Вслед за этой вводной частью идет самый обычный метод, единственная особенность которого в том, что в его теле используется получатель, s
Процесс использования метода расширения называется применением.
Например, здесь singleSpace применяется дважды для сравнения двух строк:
s.singleSpace == t.singleSpace // Возвращает true!
Определение метода расширения чемто напоминает определение аноним
ного класса, который принимает объектполучатель в качестве параметра своего конструктора и тем самым делает этот объект доступным для своих методов. Однако это впечатление обманчивое. Определение метода рас
ширения заменяется методом, который принимает получатель напрямую в качестве параметра. Например, компилятор перепишет определение ме
тода расширения из листинга 22.1, чтобы привести его к виду, показанному в листинге 22.2.
Листинг 22.2. Метод расширения после переписывания компилятором
// С внутренним обозначением расширения def singleSpace(s: String): String =
s.trim.split("\\s+").mkString(" ")
Единственной особенностью переписанного метода является то, что ком
пилятор присваивает ему внутреннее обозначение, сигнализирующее о том, что это метод расширения. Чтобы сделать этот метод доступным, проще всего разместить имя его переписанной версии в лексической области видимости. Вот пример в REPL:
scala> extension (s: String)
def singleSpace: String =
s.trim.split("\\s+").mkString(" ")
def singleSpace(s: String): String
Поскольку в этом сеансе REPL singleSpace находится в лексической области видимости и обозначен как метод расширения, его можно применить:
scala> s.singleSpace == t.singleSpace val res0: Boolean = true
22 .2 . Обобщенные расширения 471
Ввиду того что Scala заменяет методы расширения, при их применении не происходит ненужных преобразований. В Scala 2, где используется подход с неявными классами, это было не всегда так. Таким образом методы расши
рения предоставляют «синтаксический сахар без отрицательных эффектов».
Метод расширения, вызываемый из получателя, как в случае с s.singleSpace
, всегда имеет ту же производительность, что и передача получателя соответ
ствующему нерасширяющему методу — например, singleSpace(s)
22 .2 . Обобщенные расширения
Вы можете определять методы расширения, которые являются обобщенными.
В качестве примера возьмем метод head из класса
List
, который возвращает первый элемент списка, но генерирует исключение, если список пустой
1
:
List(1, 2, 3).head // 1
List.empty.head // генерирует NoSuchElementException
Если вы не уверены в том, что имеющийся у вас список чтото содержит, можете использовать вместо этого метод headOption
, который возвращает первый элемент, завернутый в
Some
. Если же список пустой, headOption воз
вращает
None
:
List(1, 2, 3).headOption // Some(1)
List.empty.headOption // None
List также предлагает метод tail
, который возвращает все, кроме первого элемента. Как и head
, он генерирует исключение, если список пустой:
List(1, 2, 3).tail // List(2, 3)
List.empty.tail // генерирует NoSuchElementException
Однако класс
List не предлагает безопасную альтернативу для получения оставшейся части списка, завернутой в
Option
. Если вам нужен такой метод, можете реализовать его в виде обобщенного расширения. Для этого нужно ука
зать один или несколько параметров типа после ключевого слова extension
, но перед скобками с получателем внутри. Пример показан в листинге 22.3.
Листинг 22.3. Обобщенный метод расширения extension [T](xs: List[T])
def tailOption: Option[List[T]] =
if xs.nonEmpty then Some(xs.tail) else None
1
Метод head из
List описан в разделе 14.4.
472 Глава 22 • Методы расширения
Метод расширения tailOption является обобщенным только для одного типа,
T
. Вот несколько примеров использования tailOption
, в которых из
T
создается экземпляр
Int и
String
:
List(1, 2, 3).tailOption // Some(List(2, 3))
List.empty[Int].tailOption // None
List("A", "B", "C").tailOption // Some(List(B, C))
List.empty[String].tailOption // None
Обычно имеет смысл разрешить автоматическое определение такого пара
метра типа, как это было сделано в предыдущих примерах, но вы можете указать его явно. Для этого метод необходимо вызвать напрямую, то есть не как метод расширения:
tailOption[Int](List(1, 2, 3)) // Some(List(2, 3))
22 .3 . Групповые расширения
Когда несколько методов нужно добавить в один и тот же тип, их можно определить вместе с помощью группового расширения. Например, поскольку многие операции с
Int могут вызывать переполнение буфера, вам, возможно, захочется определить несколько методов расширения для
Int
, способных распознать переполнение.
Чтобы представить значение
Int в дополнительном коде, нужно инвертиро
вать все его биты и прибавить единицу. Это представление позволяет реали
зовать вычитание в виде операции в дополнительном коде, вслед за которой идет сложение. Оно также имеет всего одно нулевое значение вместо двух: положительного и отрицательного
1
. К тому же, учитывая отсутствие отри
цательного нуля, у нас остается один разряд для дополнительного значения.
Это значение размещается в самом конце отрицательных целых чисел. Вот по
чему наименьшее отрицательное значение
Int
, взятое по модулю, на единицу меньше наибольшего положительного целого числа, которое можно выразить:
Int.MaxValue // 2147483647
Int.MinValue // -2147483648
Именно изза этой асимметричности между максимальным и минимальным значениями некоторые методы
Int могут переполняться. Например, метод
1
В обратном коде целые нулевые значения могут быть как положительными, так и отрицательными, по аналогии с форматом чисел с плавающей запятой IEEE 754, который используется в
Float и
Double
22 .3 . Групповые расширения 473
abs из
Int вычисляет абсолютное значение целого числа. В
Int абсолютное значение минимума составляет 2 147 483 648, однако это число невоз
можно выразить с помощью этого типа. Максимальное значение
Int равно
2 147 483 647, что на единицу меньше, поэтому вызов abs для
Int.MinValue приводит к переполнению и вы получаете исходное значение
MinValue
:
Int.MinValue.abs // -2147483648 (переполнение)
Если вам нужен метод, который возвращает абсолютное значение
Int
, но при этом распознает переполнение, вы можете определить метод расширения, как показано далее:
extension (n: Int)
def absOption: Option[Int] =
if n != Int.MinValue then Some(n.abs) else None
Для значения
Int.MinValue
, изза которого переполняется abs
, absOption возвращает
None
. Для остальных значений absOption возвращает результат работы abs
, завернутый в
Some
. Вот несколько примеров использования absOption
:
42.absOption // Some(42)
-42.absOption // Some(42)
Int.MaxValue.absOption // Some(2147483647)
Int.MinValue.absOption // None
Еще одной операцией, способной вызвать переполнение для минимального значения
Int
, является изменение знака числа на отрицательный. Операция unary_- с
Int.MinValue возвращает все то же
MinValue
1
:
-Int.MinValue // -2147483648 (переполнение)
Если вам нужен безопасный вариант unary_-
, вы можете определить его вместе с absOption в групповом расширении, как показано в листинге 22.4.
Листинг 22.4. Групповое расширение extension (n: Int)
def absOption: Option[Int] =
if n != Int.MinValue then Some(n.abs) else None def negateOption: Option[Int] =
if n != Int.MinValue then Some(-n) else None
1
Как было описано в разделе 5.4, компилятор Scala подставляет вместо
-Int.MinVa- lue метод unary_- для
Int.MinValue
— то есть
Int.MinValue.unary_-
474 Глава 22 • Методы расширения
Это расширение добавляет в
Int сразу два метода: absOption и negateOption
Вот несколько примеров использования последнего.
-42.negateOption // Some(42)
42.negateOption // Some(42)
Int.MaxValue.negateOption // Some(2147483647)
Int.MinValue.negateOption // None
Методы, определенные вместе в групповом расширении, называются ме-
тодами того же уровня. Из одного метода в групповом расширении можно вызывать другие, как если бы они были членами одного класса. Например, как показано в листинге 22.5, если вы решите добавить в
Int еще один метод расширения, isMinValue
, у вас будет возможность вызывать его напрямую из двух других методов, absOption и negateOption
1 ... 45 46 47 48 49 50 51 52 ... 64