Scala. Профессиональное программирование 2022. Одерски Мартин, Спун Лекс, Веннерс Билл, Соммерс ФрэнкО41 Scala. Профессиональное программирование. 5е изд спб. Питер, 2022. 608 с. ил. Серия Библиотека программиста
Скачать 6.24 Mb.
|
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 23 .6 . Пример использования класса типов: сериализация JSON 509 Результат в формате JSON показан в листинге 23.22. Листинг 23.21. AddressBook val addressBook = AddressBook( List( Contact( "Bob Smith", List( Address( "12345 Main Street", "San Francisco", "CA", 94105 ), Address( "500 State Street", "Los Angeles", "CA", 90007 ) ), List( Phone( 1, 5558881234 ), Phone( 49, 5558413323 ) ) ) ) ) Листинг 23.22. Адресная книга, представленная в формате JSON { "contacts": [{ "name": "Bob Smith", "addresses": [{ "street": "12345 Main Street", "city": "San Francisco", "state": "CA", "zip": 94105 }, { "street": "500 State Street", "city": "Los Angeles", "state": "CA", 510 Глава 23 • Классы типов "zip": 90007 }], "phones": [{ "countryCode": 1, "phoneNumber": 5558881234 }, { "countryCode": 49, "phoneNumber": 5558413323 }] }] } Конечно, настоящая библиотека для работы с JSON была бы намного слож нее того, что вы увидели в этом примере. Вам, скорее всего, следовало бы вос пользоваться средствами метапрограммирования Scala, чтобы автоматизи ровать генерацию экземпляров JsonSerializer за счет вывода класса типов. Резюме В этой главе вы познакомились с классами типов и рассмотрели несколько примеров. Классы типов — это основополагающий способ реализации спе циального полиморфизма в Scala. Тот факт, что для классов типов в Scala предусмотрен синтаксический сахар в виде границ контекста, говорит о том, насколько важным является данный подход к проектированию в этом языке. Вам встречалось несколько способов применения классов типов: для глав ных методов, безопасных проверок на равенство, неявных преобразований и сериализации JSON. Надеемся, эти примеры дали вам представление о том, в каких ситуациях классы типов являются подходящим архитектур ным решением. В следующей главе мы сменим тему и подробно рассмотрим библиотеку коллекций Scala. |