Чем отличаются upper bound и lower bound?
В Scala upper bound и lower bound — это ограничения на типовые параметры, которые позволяют контролировать допустимые типы в обобщениях. Они широко используются для ограничения области значений параметризованных типов и обеспечения типобезопасности. Синтаксически эти ограничения указываются с помощью <: (upper bound) и >: (lower bound).
Upper Bound (<:)
Определение: Upper bound (T <: U) указывает, что тип T должен быть подтипом типа U.
Пример:
class Animal
class Dog extends Animal
class Cat extends Animal
def printAnimalInfo\[T <: Animal\](animal: T): Unit = {
println(animal.getClass.getSimpleName)
}
В этом примере T должен быть подтипом Animal. Соответственно, printAnimalInfo(new Dog) и printAnimalInfo(new Cat) допустимы, а printAnimalInfo("String") вызовет ошибку компиляции.
Зачем нужен upper bound:
-
Чтобы ограничить использование метода или класса определённой иерархией типов.
-
Для доступа к членам типа U внутри обобщённого кода, так как T гарантированно имеет свойства U.
Lower Bound (>:)
Определение: Lower bound (T >: U) указывает, что тип T должен быть супертипом типа U.
Пример:
class Animal
class Dog extends Animal
class Cat extends Animal
def addAnimalToList\[T >: Dog\](list: List\[T\], dog: Dog): List\[T\] = {
dog :: list
}
В этом случае T должен быть супертипом Dog, то есть T может быть Animal, AnyRef или Any.
Зачем нужен lower bound:
-
Когда нужно безопасно добавлять значения в коллекцию, сохраняя корректную типизацию.
-
Позволяет гарантировать, что тип параметра допускает определённый подтип.
Комбинация Upper и Lower Bounds
Можно использовать и верхнюю, и нижнюю границу одновременно:
def process\[T >: Dog <: Animal\](value: T): Unit = {
// T — это тип от Dog до Animal (включительно)
}
Этот код означает, что T должен быть супертипом Dog, но при этом подтипом Animal, то есть либо Dog, либо Animal.
Пример с ковариантными контейнерами
abstract class Animal {
def speak(): Unit
}
class Dog extends Animal {
def speak(): Unit = println("Woof")
}
class Cat extends Animal {
def speak(): Unit = println("Meow")
}
def makeAnimalSpeak\[T <: Animal\](animal: T): Unit = {
animal.speak()
}
Только те типы, которые являются подтипами Animal, смогут быть переданы. Это позволяет использовать speak() без дополнительных проверок типов.
Почему это важно: PECS (Producer Extends, Consumer Super)
Этот принцип (из Java, но применим и в Scala):
-
Producer Extends (<:) — если вы чтo-тo извлекаете из обобщённого объекта, используйте upper bound.
-
Consumer Super (>:) — если вы что-то добавляете в обобщённый объект, используйте lower bound.
Пример:
def copyElements\[S, D >: S\](from: List\[S\], to: List\[D\]): List\[D\] = {
from ::: to
}
D >: S гарантирует, что to может принять элементы из from, потому что каждый S можно безопасно привести к D.
Bounds в контексте типа параметра класса
class Cage\[T <: Animal\](val animal: T)
Здесь Cage может содержать Dog, Cat, но не String.
Пример с коллекциями
def prependToList\[T >: Int\](list: List\[T\], element: Int): List\[T\] = {
element :: list
}
Вы можете добавлять Int в список любого типа, который является супертипом Int: Int, AnyVal, Any.
Пример с максимальным элементом
def max\[T <: Ordered\[T\]\](a: T, b: T): T = if (a > b) a else b
Здесь T ограничен типами, реализующими Ordered[T]. Это позволяет использовать оператор >.
Типичные ошибки
- **Неправильная граница:
**
// Ошибка: компилятор не сможет вывести типы
def wrong\[T >: Animal\](a: T): T = a
- **Путаница в направлении:
**
// Неверно, если хотите вызывать методы Dog
def handleAnimal\[T >: Dog\](animal: T): Unit = {
// animal.bark() — ошибка
}
Нужно использовать <: Dog, если нужно вызвать методы Dog.
Вывод из границ при сопоставлении типов
Если определён T >: Dog <: Animal, то возможны следующие варианты для T:
-
Dog
-
Animal
T не может быть Cat, потому что Cat <: Animal, но Cat не является > Dog.
Таким образом, upper и lower bounds в Scala дают точный контроль над допустимыми типами в обобщённом программировании и позволяют писать безопасный, выразительный и гибкий код.