Файл: Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 09.12.2023
Просмотров: 777
Скачиваний: 11
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
500 Глава 23 • Классы типов
И у вас имеется экземпляр этого класса:
val street = Street("123 Main St")
В этом случае вы не сможете инициализировать переменную типа
String с помощью
Street
:
scala> val streetStr: String = street
1 |val streetStr: String = street
| ˆˆˆˆˆˆ
| Found: (street : Street)
| Required: String
Вместо этого вам придется вручную привести
Street к
String путем вызова street.value
:
val streetStr: String = street.value // 123 Main St
Этот код легко понять, но у вас может быть ощущение того, что вызов value из
String для преобразования значения в
String является шаблонным и мало
информативным кодом. Поскольку тип
Street всегда можно безопасно при
вести к типу
String
, который лежит в его основе, вам, возможно, захочется предоставить неявное преобразование из
Street в
String
. В Scala 3 для этого нужно определить givenэкземпляр типа
Conversion[Street,
String]
1
, кото
рый является потомком типа функции
Street
=>
String
. Вот его определение:
abstract class Conversion[-T, +U] extends (T => U):
def apply(x: T): U
Поскольку у трейта
Conversion есть всего один абстрактный метод, для определения экземпляра зачастую можно использовать функциональный литерал SAM
2
. Следовательно, неявное приведение
Street к
String можно определить так:
given streetToString: Conversion[Street, String] = _.value
Чтобы не получать предупреждения во время компиляции при использова
нии неявных преобразований, для их включения следует либо передать ком
пилятору параметр
-language:implicitConversions
, либо локально указать следующую инструкцию импорта:
import scala.language.implicitConversions
1
В Scala 3 неявное преобразование можно также определить с помощью i mplicit def
, чтобы сохранить совместимость со Scala 2. В будущем этот подход может быть признан устаревшим.
2
Методы SAM были описаны в разделе 8.9.
23 .5 . Неявные преобразования
1 ... 48 49 50 51 52 53 54 55 ... 64
501
Включив неявные преобразования, вы можете написать следующее (при условии, что гивен streetToString находится в области видимости в качестве единого идентификатора):
val streetStr: String = street
Здесь компилятор встречает
Street в контексте, в котором должен быть указан тип
String
, и воспринимает это как обычную ошибку типизации.
Но прежде, чем сдаваться, он ищет неявное преобразование
Street в
String
В данном случае он находит streetToString
. После этого компилятор авто
матически вставляет streetToString в приложение. При этом внутри код принимает следующий вид:
val streetStr: String = streetToString(street)
Это неявное преобразование в буквальном смысле этого слова. Здесь нет явного запроса на приведение типов. Вместо этого вы пометили метод streetToString как доступное неявное преобразование, разместив его в об
ласти видимости и определив его как given
. В результате компилятор будет автоматически использовать его каждый раз, когда
Street необходимо при
вести к
String
Если же вы хотите определить неявное преобразование, позаботьтесь о том, чтобы оно всегда было подходящим. Например, приведение
Double к
Int неявным образом может вызвать недоумение, поскольку автомати
ческая потеря точности значения является сомнительной идеей. Поэтому мы на самом деле рекомендуем не преобразование. Намного логичнее двигаться в обратном направлении: от более конкретного типа к более общему. Например,
Int можно преобразовать в
Double без потери точно
сти, поэтому неявное приведение
Int к
Double имеет смысл. На самом деле именно это и происходит. Объект scala.Predef
, который автоматически импортируется любой программой на Scala, определяет неявные преоб
разования «меньших» числовых типов в «большие», в том числе и при
ведение
Int к
Double
Вот почему в Scala значения
Int можно хранить в переменных типа
Double
В системе типов для этого не предусмотрено отдельного правила; это просто применение неявного преобразования
1 1
Тем не менее компилятор Scala относится к этому преобразованию поособому, переводя его в специальный байткод i2d
. Благодаря этому скомпилированный образ получается таким же, как и в Java.
502 Глава 23 • Классы типов
23 .6 . Пример использования класса типов: сериализация JSON
В разделе 23.1 мы упоминали сериализацию в качестве примера поведения, применимого к типам, которые в остальном не имеют ничего общего и, сле
довательно, являются хорошими кандидатами на попадание в класс типов.
Напоследок в этой главе мы хотели бы проиллюстрировать использование класса типов для поддержки сериализации в JSON. Чтобы не усложнять этот пример, мы проигнорируем десериализацию, хотя обычно оба эти процесса реализуются в одной и той же библиотеке.
JSON — широко используемый формат обмена данными между клиентами на JavaScript и серверными приложениями
1
. Он определяет форматы для представления строк, чисел, логических значений, массивов и объектов.
Таким образом, все, что вы хотите сериализовать в JSON, должно быть вы
ражено в одном из этих пяти типов данных. Строки JSON выглядят как строковые литералы Scala, такие как "tennis"
. Числа JSON, представля
ющие целочисленные значения, аналогичны литералам
Int в Scala, таким как
10
. Логическое значение JSON может быть равно либо true
, либо false
Объект JSON — это набор пар «ключ — значение», разделенных запятыми и заключенных в фигурные скобки; ключ представляет собой строковое имя.
Массив JSON — это список типов данных JSON, разделенных запятыми и за
ключенных в квадратные скобки. В JSON также определено значение null
Вот пример объекта, содержащего по одному члену каждого из остальных четырех типов JSON, плюс член null
:
{
"style": "tennis",
"size": 10,
"inStock": true,
"colors": ["beige", "white", "blue"],
"humor": null
}
В этом примере мы сериализуем значения
String
Scala в строки JSON,
Int и
Long в числа JSON,
Boolean в логические значения JSON,
List в массивы
JSON и несколько других типов в объекты JSON. Необходимость сериализа
ции типов из стандартной библиотеки Scala, таких как
Int
, подчеркивает то, насколько трудно было бы решить эту задачу путем примеси трейта в класс,
1
JSON расшифровывается как JavaScript Object Notation.
23 .6 . Пример использования класса типов: сериализация JSON 503
который вы хотите сериализовать. Вы можете определить такой трейт и на
звать его, скажем,
JsonSerializable
. Он может предоставлять метод toJson
, который генерирует текст JSON для этого объекта. Затем вы могли бы при
мешивать
JsonSerializable в свои собственные классы и реализовывать метод toJson
. Однако с такими типами, как
String
,
Int
,
Long
,
Boolean или
List
, это не сработает, так как их нельзя изменять.
Подход на основе класса типов лишен этой проблемы. Вы можете опре
делить иерархию классов, целиком ориентированную на сериализацию объектов абстрактного типа
T
в JSON, не требуя при этом, чтобы классы, которые вы хотите сериализовать, наследовали общий супертрейт. Вместо этого можно определить givenэкземпляр трейта класса типов для каждого типа, предназначенного для сериализации в JSON. Такой трейт, с именем
JsonSerializer
, показан в листинге 23.13. Он принимает один параметр типа,
T
, и предлагает метод serialize
, который берет экземпляр
T
и преоб
разует его в строку JSON.
Листинг 23.13. Класс типов для сериализации в JSON
trait JsonSerializer[T]:
def serialize(o: T): String
Чтобы у ваших пользователей была возможность вызывать метод toJson из сериализуемых классов, можно определить метод расширения. Как уже об
суждалось в разделе 22.5, подходящим местом для размещения этого метода является трейт самого класса типов. Если вы выберете этот вариант, метод toJson будет доступен в типе
T
всегда, когда
JsonSerializer[T]
находится в области видимости. Трейт
JsonSerializer
, улучшенный с помощью этого метода расширения, показан в листинге 23.14.
Листинг 23.14. Класс типов для сериализации в JSON с методом расширения trait JsonSerializer[T]:
def serialize(o: T): String extension (a: T)
def toJson: String = serialize(a)
Следующим шагом было бы логично определить givenэкземпляры класса типов для
String
,
Int
,
Long и
Boolean
. Подходящим местом для их размеще
ния будет объекткомпаньон
JsonSerializer
, поскольку, как описывалось в разделе 21.2, компилятор ищет в нем нужный givenэкземпляр, если его не удалось найти в области видимости. Эти гивены можно определить так, как показано в листинге 23.15.
504 Глава 23 • Классы типов
Листинг 23.15. Объект-компаньон сериализатора JSON с гивенами object JsonSerializer:
given stringSerializer: JsonSerializer[String] with def serialize(s: String) = s"\"$s\""
given intSerializer: JsonSerializer[Int] with def serialize(n: Int) = n.toString given longSerializer: JsonSerializer[Long] with def serialize(n: Long) = n.toString given booleanSerializer: JsonSerializer[Boolean] with def serialize(b: Boolean) = b.toString
Импорт метода расширения
Вам может пригодиться возможность импортировать метод расшире
ния, добавляющий метод toJson в любые типы
T
, для которых доступен
JsonSerializer[T]
. Метод расширения, определенный в листинге 23.14, на это не способен, так как он делает toJson доступным для
T
только в случае, если
JsonSerializer[T]
находится в области видимости. В про
тивном случае он не сработает, даже если
JsonSerializer[T]
присутствует в объектекомпаньоне для
T
. Чтобы упростить импорт метода расширения, можете поместить его в объектодиночку, такой как показан в листин
ге 23.16. Этот метод содержит инструкцию using
, которая требует, чтобы гивен
JsonSerializer[T]
был доступен для типа
T
, к которому этот метод применяется.
Листинг 23.16. Метод расширения для удобного импорта object ToJsonMethods:
extension [T](a: T)(using jser: JsonSerializer[T])
def toJson: String = jser.serialize(a)
Имея в своем распоряжении объект
ToJsonMethods
, вы можете поэкспери
ментировать с сериализаторами в REPL. Вот несколько примеров их ис
пользования:
import ToJsonMethods.*
"tennis".toJson // "tennis"
10.toJson // 10
true.toJson // true
Будет полезно сравнить два метода расширения: один в объекте
ToJsonMethods из листинга 23.16, а другой — в трейте
JsonSerializer из ли
стинга 23.14. Метод расширения
ToJsonMethods принимает
JsonSerializer[T]
в качестве параметра using
, а метод расширения в
JsonSerializer этого не делает, так как он по определению является членом
JsonSerializer[T]
Таким образом, если toJson в
ToJsonMethods вызывает serialize из пере
23 .6 . Пример использования класса типов: сериализация JSON 505
данной ссылки
JsonSerializer с именем jser
, то метод toJson в трейте
JsonSerializer вызывает serialize из this
Сериализация объектов предметной области
Теперь представьте, что вам нужно сериализовать в JSON экземпляры опре
деленных классов в модели вашей предметной области, включая адресную книгу, показанную в листинге 23.17. Эта книга содержит список контактов, у каждого из которых есть произвольное количество адресов и телефонных номеров (от 0 и больше)
1
Листинг 23.17. Классы-образцы для адресной книги case class Address(
street: String,
city: String,
state: String,
zip: Int
)
case class Phone(
countryCode: Int,
phoneNumber: Long
)
case class Contact(
name: String,
addresses: List[Address],
phones: List[Phone]
)
case class AddressBook(contacts: List[Contact])
Строка JSON для адресной книги формируется из строк JSON ее вложенных объектов. Таким образом, чтобы сгенерировать строку JSON для адресной книги, каждый из ее вложенных объектов должен поддерживать преоб
разование в формат JSON. Например, каждый экземпляр
Contact в поле contacts должен быть представлен в формате JSON этого контакта. Каждый экземпляр
Address контакта должен быть преобразован в JSON этого адреса.
Следовательно, для сериализации
AddressBook необходимо сериализовать в JSON каждый объект, из которого состоит адресная книга. Поэтому будет логично определить сериализаторы для всех объектов предметной области.
1
Для атрибутов этих классов было бы лучше определить крошечные типы, как описывалось в разделе 17.4. Но, чтобы не усложнять этот пример, мы будем ис
пользовать типы
String и
Int
506 Глава 23 • Классы типов
Хорошим местом размещения givenэкземпляров
JsonSerializer для ваших объектов предметной области являются их объектыкомпаньоны. В листин
ге 23.18 показано, как вы можете, к примеру, определить сериализаторы для
Address и
Phone
. В методах serialize мы импортируем и используем метод расширения toJson из объекта
ToJsonMethods
, показанного в листинге 23.16, но переименовываем его в asJson
. Это необходимо, чтобы избежать кон
фликта с одноименным методом расширения toJson
, унаследованным от
JsonSerializer
(см. листинг 23.14).
Листинг 23.18. Сериализаторы JSON для Address и Phone object Address:
given addressSerializer: JsonSerializer[Address] with def serialize(a: Address) =
import ToJsonMethods.{toJson as asJson}
s"""|{
| "street": ${a.street.asJson},
| "city": ${a.city.asJson},
| "state": ${a.state.asJson},
| "zip": ${a.zip.asJson}
|}""".stripMargin object Phone:
given phoneSerializer: JsonSerializer[Phone] with def serialize(p: Phone) =
import ToJsonMethods.{toJson as asJson}
s"""|{
| "countryCode": ${p.countryCode.asJson},
| "phoneNumber": ${p.phoneNumber.asJson}
|}""".stripMargin
Сериализация списков
Два других объекта предметной области,
Contact и
AddressBook
, содержат списки. Поэтому для их сериализации было бы полезно иметь общую про
цедуру преобразования типов
List
Scala в массивы JSON. Массив JSON представляет собой список типов данных JSON, разделенных запятыми и заключенных в квадратные скобки, поэтому
List[T]
можно сериализо
вать для любого типа
T
, при условии существования
JsonSerializer[T]
В листинге 23.19 показан гивен
JsonSerializer для списков, который будет генерировать массив JSON из
List
, если для типа элементов списка существу
ет
JsonSerializer
Листинг 23.19. Given-сериализатор JSON для списков object JsonSerializer:
// гивены для строк, целых чисел и логических значений…
given listSerializer[T](using
23 .6 . Пример использования класса типов: сериализация JSON 507
JsonSerializer[T]): JsonSerializer[List[T]] with def serialize(ts: List[T]) =
s"[${ts.map(t => t.toJson).mkString(", ")}]"
Чтобы выразить зависимость от сериализатора для типа элементов спи
ска, гивен listSerializer принимает в качестве параметра using сериали
затор, способный сгенерировать JSON для элементов этого типа. Напри
мер, чтобы преобразовать
List[Address]
в массив JSON, необходимо иметь givenсериализатор для самого типа
Address
. Если сериализатор
Address недоступен, программа не скомпилируется. Например, поскольку гивен
JsonSerializer[Int]
находится в объектекомпаньоне
JsonSerializer
, вы можете сериализовать
List[Int]
в JSON, как показано ниже:
import ToJsonMethods.*
List(1, 2, 3).toJson // [1, 2, 3]
С другой стороны, мы еще не определили
JsonSerializer[Double]
, поэтому попытка сериализовать
List[Double]
в JSON приведет к ошибке компиляции:
scala> List(1.0, 2.0, 3.0).toJson
1 |List(1.0, 2.0, 3.0).toJson
|ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ
|value toJson is not a member of List[Double].
|An extension method was tried, but could not be fully
|constructed:
|
| ToJsonMethods.toJson[List[Double]](
| List.apply[Double]([1.0d,2.0d,3.0d : Double]*)
| )(JsonSerializer.listSerializer[T](
| /* missing */summon[JsonSerializer[Double]]))
| failed with
|
| no implicit argument of type JsonSerializer[List[Double]]
| was found for parameter json of method toJson in
| object ToJsonMethods.
| I found:
|
| JsonSerializer.listSerializer[T](
| /* missing */summon[JsonSerializer[Double]])
|
| But no implicit values were found that match type
| JsonSerializer[Double].
Этот пример иллюстрирует важное преимущество использования классов типов для сериализации объектов Scala: классы типов позволяют компиля
тору убедиться в том, что все классы, из которых состоит
AddressBook
, можно преобразовать в JSON. Например, если не предоставить givenэкземпляр
Address
, программа не скомпилируется. Для сравнения: если в Java глубоко
508 Глава 23 • Классы типов вложенный объект не реализует
Serializable
, вы получите исключение на этапе выполнения. Эта ошибка может произойти в обоих языках, но если в Java она происходит во время работы программы, то в Scala благодаря классам типов она проявляется на этапе компиляции.
Напоследок стоит отметить, что возможность вызова toJson в теле функции, переданной в map
(
"toJson"
в t
=>
t.toJson
), объясняется наличием в об
ласти видимости гивена
JsonSerializer[T]
: анонимного параметра using
, переданного в listSerializer
. Метод расширения, который используется в этом случае, объявлен в самом трейте
JsonSerializer
, представленном в листинге 23.14.
Собираем все вместе
Теперь, имея в своем распоряжении способ сериализации списков, вы може
те применить его в сериализаторах для
Contact и
AddressBook
. Это показано в листинге 23.20. Как и прежде, при импорте метода расширения toJson нужно переименовать в asJson
, чтобы избежать конфликта имен.
Листинг 23.20. Given-сериализаторы JSON для Contact и AddressBook object Contact:
given contactSerializer: JsonSerializer[Contact] with def serialize(c: Contact) =
import ToJsonMethods.{toJson as asJson}
s"""|{
| "name": ${c.name.asJson},
| "addresses": ${c.addresses.asJson},
| "phones": ${c.phones.asJson}
|}""".stripMargin object AddressBook:
given addressBookSerializer: JsonSerializer[AddressBook] with def serialize(a: AddressBook) =
import ToJsonMethods.{toJson as asJson}
s"""|{
| "contacts": ${a.contacts.asJson}
|}""".stripMargin
У нас все готово для сериализации адресной книги в JSON. В качестве при
мера возьмем экземпляр
AddressBook
, показанный в листинге 23.21, на ко
торый ссыла ется переменная addressBook
. Импортировав из
ToJsonMethods метод расширения toJson
, вы сможете сериализовать эту адресную книгу с помощью вызова:
addressBook.toJson