Newtype-типы
Типобезопасные обёртки
«Newtype-типы» — бесплатный урок Scala for Backend Engineering & Functional Programming на CoddyKit. Это урок 3 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Scala for Backend Engineering & Functional Programming, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.
Типобезопасные обёртки
Новый тип — это отдельный тип, оборачивающий одно базовое значение. Он предотвращает смешивание значений с одинаковым примитивным представлением, например UserId и ProductId, которые оба имеют тип Int.
case class UserId(value: Int)
case class ProductId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val u = UserId(1)
val p = ProductId(1)
println(u.value == p.value) // values equal, types differПроблема чрезмерной зависимости от примитивов
Чрезмерная зависимость от примитивов — это чрезмерное использование String и Int для понятий предметной области. Из-за этого можно передать email туда, где требуется имя. Новые типы исправляют проблему, предоставляя каждому понятию собственный тип.
case class Email(value: String)
case class City(value: String)
object Main:
def greet(c: City): String = s"Welcome to ${c.value}"
def main(args: Array[String]): Unit =
println(greet(City("Berlin")))Новые типы на основе case class
Самый простой новый тип — однопольный case class. Он изначально предоставляет сравнение, удобный toString и сопоставление с образцом. Недостаток — выделение одного объекта для каждого значения.
case class OrderId(value: String)
object Main:
def main(args: Array[String]): Unit =
val id = OrderId("ORD-42")
println(id)
println(id.value)Новые типы на основе непрозрачных типов
Чтобы избежать выделения памяти, реализуйте новый тип с помощью непрозрачного типа. Во время выполнения он ведёт себя как базовый тип, но во время компиляции является отдельным.
object Ids:
opaque type UserId = Int
def apply(i: Int): UserId = i
extension (u: UserId) def value: Int = u
object Main:
def main(args: Array[String]): Unit =
val u = Ids(7)
println(u.value)Добавление поведения с помощью расширений
Добавляйте операции к новому типу с помощью методов extension, открывая только те возможности, которые имеют смысл для понятия предметной области.
object Money:
opaque type Cents = Long
def apply(n: Long): Cents = n
extension (c: Cents)
def +(o: Cents): Cents = c + o
def toDollars: Double = c / 100.0
object Main:
def main(args: Array[String]): Unit =
val total = Money(250) + Money(750)
println(total.toDollars)Новые типы в сигнатурах функций
Новые типы делают сигнатуры самодокументируемыми и обнаруживают ошибки в порядке аргументов во время компиляции. Вы не сможете случайно поменять местами два параметра разных новых типов.
case class Width(value: Int)
case class Height(value: Int)
object Main:
def area(w: Width, h: Height): Int = w.value * h.value
def main(args: Array[String]): Unit =
println(area(Width(4), Height(5)))Новые типы и коллекции
Map с ключом-новым типом понятнее и безопаснее, чем карта с ключом типа Int. Тип не позволяет использовать ключ неправильного вида.
case class UserId(value: Int)
object Main:
def main(args: Array[String]): Unit =
val names = Map(UserId(1) -> "Ada", UserId(2) -> "Bob")
println(names(UserId(2)))Проверяемые новые типы
Объедините новый тип с умным конструктором, чтобы обёртка также обеспечивала инварианты. Например, NonEmptyString никогда не может быть пустой.
case class NonEmptyString private (value: String)
object NonEmptyString:
def of(s: String): Option[NonEmptyString] =
if s.nonEmpty then Some(NonEmptyString(s)) else None
object Main:
def main(args: Array[String]): Unit =
println(NonEmptyString.of("hi"))
println(NonEmptyString.of(""))Выбор представления
Выбирайте представление в зависимости от потребностей.
- case class: проще всего, поддерживает сопоставление с образцом, выделяет объект.
- opaque type: не требует затрат, не использует упаковку, идеально подходит для критичных по производительности участков и больших коллекций.
object Temp:
opaque type Kelvin = Double
def apply(d: Double): Kelvin = d
extension (k: Kelvin) def value: Double = k
object Main:
def main(args: Array[String]): Unit =
val readings = List(Temp(300.0), Temp(310.5))
println(readings.map(_.value).sum)Предотвращение случайных преобразований
Два новых типа на основе одной и той же базы не преобразуются неявно. Для перехода между ними нужно написать явную функцию, документирующую намерение.
case class Meters(value: Double)
case class Feet(value: Double)
object Main:
def toFeet(m: Meters): Feet = Feet(m.value * 3.281)
def main(args: Array[String]): Unit =
println(toFeet(Meters(2.0)))Когда использовать новые типы
Используйте новые типы всегда, когда примитив несёт смысл предметной области.
- Идентификаторы, единицы измерения, строки заданного формата.
- Используйте case class для простоты, а непрозрачный тип — для производительности.
- Добавляйте умный конструктор, если существуют инварианты.
object Domain:
opaque type Sku = String
def of(s: String): Option[Sku] =
if s.startsWith("SKU-") then Some(s) else None
extension (k: Sku) def raw: String = k
object Main:
def main(args: Array[String]): Unit =
println(Domain.of("SKU-9").map(_.raw))Быстрая проверка
Проверьте, насколько хорошо Вы поняли новые типы.
Итоги
Вы изучили новые типы.
- Новые типы оборачивают одно базовое значение в отдельный тип.
- Они устраняют чрезмерную зависимость от примитивов и предотвращают путаницу аргументов.
- Используйте
case classдля простоты, аopaque type— для отсутствия затрат. - Добавляйте умный конструктор для проверяемых оболочек.
- Преобразования между новыми типами всегда должны быть явными.
object Ids:
opaque type AccountId = Long
def apply(n: Long): AccountId = n
extension (a: AccountId) def value: Long = a
object Main:
def main(args: Array[String]): Unit =
val acc = Ids(1001L)
println(acc.value)Часто задаваемые вопросы
Урок «Newtype-типы» бесплатный?
Да — полный текст урока «Newtype-типы» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Scala for Backend Engineering & Functional Programming, подпишись на CoddyKit PRO. Курс Scala for Backend Engineering & Functional Programming содержит 4 уроков всего.
Чему я научусь в уроке «Newtype-типы»?
Типобезопасные обёртки Ты практикуешь Scala for Backend Engineering & Functional Programming с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Scala for Backend Engineering & Functional Programming?
Предыдущий опыт не требуется. Scala for Backend Engineering & Functional Programming на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 3 из 4.
Сколько времени занимает урок «Newtype-типы»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Scala for Backend Engineering & Functional Programming?
Да. Каждый урок Scala for Backend Engineering & Functional Programming включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.