Структурные типы
Некоторые варианты использования, такие как моделирование доступа к базе данных,
более удобны в динамически типизированных языках, чем в статически типизированных языках.
С динамически типизированными языками естественно моделировать строку как запись или объект
и выбирать записи с помощью простых точечных обозначений, например row.columnName
.
Достижение того же результата в статически типизированном языке требует определения класса для каждой возможной строки, возникающей в результате манипуляций с базой данных, включая строки, возникающие в результате join и проектирования, и настройки схемы для сопоставления между строкой и представляющим ее классом.
Это требует большого количества шаблонов,
что заставляет разработчиков менять преимущества статической типизации на более простые схемы,
в которых имена столбцов представляются в виде строк и передаются другим операторам, например row.select("columnName")
.
Этот подход лишен преимуществ статической типизации и все еще не так естественен, как динамически типизируемая версия.
Структурные типы (structural types) помогают в ситуациях, когда желательно поддерживать простую точечную нотацию в динамических контекстах, не теряя преимуществ статической типизации. Они также позволяют разработчикам настраивать, как должны определяться поля и методы.
Пример
Вот пример структурного типа Person
:
class Record(elems: (String, Any)*) extends Selectable:
private val fields = elems.toMap
def selectDynamic(name: String): Any = fields(name)
type Person = Record {
val name: String
val age: Int
}
Тип Person
добавляет уточнение (refinement) к своему родительскому типу Record
,
которое определяет поля name
и age
.
Говорится, что уточнение носит структурный (structural) характер,
поскольку name
и age
не определены в родительском типе.
Но тем не менее они существуют как члены класса Person
.
Например, следующая программа напечатала бы "Emma is 42 years old.
":
val person = Record(
"name" -> "Emma",
"age" -> 42
).asInstanceOf[Person]
println(s"${person.name} is ${person.age} years old.")
Родительский тип Record
в этом примере представляет собой универсальный класс,
который может в своем аргументе elems
принимать произвольные записи.
Этот аргумент - последовательность пар ключей типа String
и значений типа Any
.
Когда создается Person
как Record
, необходимо с помощью приведения типов задать,
что запись определяет правильные поля правильных типов.
Сама Record
слишком слабо типизирована, поэтому компилятор не может знать об этом без помощи пользователя.
На практике связь между структурным типом и его базовым общим представлением, скорее всего,
будет выполняться на уровне базы данных и, следовательно, не будет беспокоить конечного пользователя.
Record
расширяет маркер trait scala.Selectable
и определяет метод selectDynamic
,
который сопоставляет имя поля с его значением.
Выбор элемента структурного типа выполняется путем вызова соответствующего метода.
person.name
и person.age
преобразуются компилятором Scala в:
person.selectDynamic("name").asInstanceOf[String]
person.selectDynamic("age").asInstanceOf[Int]
Второй пример
Чтобы закрепить сказанное, вот еще один структурный тип с именем Book
,
представляющий книгу, доступную в базе данных:
type Book = Record {
val title: String
val author: String
val year: Int
val rating: Double
}
Как и в случае с Person
, экземпляр Book
создается следующим образом:
val book = Record(
"title" -> "The Catcher in the Rye",
"author" -> "J. D. Salinger",
"year" -> 1951,
"rating" -> 4.5
).asInstanceOf[Book]
Класс Selectable
Помимо selectDynamic
класс Selectable
иногда также определяет метод applyDynamic
,
который можно использовать для замены вызовов функций на вызов структурных элементов.
Таким образом, если a
является экземпляром Selectable
, структурный вызов типа a.f(b, c)
преобразуется в:
a.applyDynamic("f")(b, c)
Ссылки: