Трейты
Если провести аналогию с Java, то Scala trait
похож на интерфейс в Java 8+.
trait
-ы могут содержать:
- абстрактные методы и поля
- конкретные методы и поля
- могут иметь параметры конструктора, как и классы
В базовом использовании trait
может использоваться как интерфейс,
определяющий только абстрактные члены, которые будут реализованы другими классами:
trait Employee:
def id: Int
def firstName: String
def lastName: String
traits
также могут содержать определенные методы:
trait HasLegs:
def numLegs: Int
def walk(): Unit
def stop() = println("Stopped walking")
trait HasTail:
def tailColor: String
def wagTail() = println("Tail is wagging")
def stopTail() = println("Tail is stopped")
Классы и объекты могут расширять несколько traits
, что позволяет с их помощью создавать небольшие модули.
class IrishSetter(name: String) extends HasLegs, HasTail:
val numLegs = 4
val tailColor = "Red"
def walk() = println("I’m walking")
override def toString = s"$name is a Dog"
В классе IrishSetter
реализованы все абстрактные параметры и методы, поэтому можно создать его экземпляр:
val d = IrishSetter("Big Red")
// d: IrishSetter = Big Red is a Dog
Класс также может переопределять методы trait
-ов при необходимости.
Аргументы trait
оцениваются непосредственно перед его инициализацией.
Особенности при расширении trait с параметрами конструктора
Одна потенциальная проблема с параметрами trait
заключается в том, как предотвратить двусмысленность.
Например, можно попробовать расширить Greeting
дважды с разными параметрами:
trait Greeting(val name: String):
def msg = s"How are you, $name"
class C extends Greeting("Bob"):
println(msg)
class D extends C, Greeting("Bill")
// error:
// trait Greeting is already implemented by superclass C,
// its constructor cannot be called again
На самом деле эта программа не скомпилируется, потому что она нарушает второе правило для параметров trait
:
- Если класс
C
расширяет параметризованныйtrait
T
, а его суперкласс — нет, тоC
должен передать аргументы вT
. - Если класс
C
расширяет параметризованныйtrait
T
и его суперкласс тоже, тоC
не должен передавать аргументы вT
. trait
-ы никогда не должны передавать аргументы родительскимtrait
-ам.
Вот трейт, расширяющий параметризованный трейт Greeting
.
trait FormalGreeting extends Greeting:
override def msg = s"How do you do, $name"
Правильный способ создания класса E
, расширяющего оба - Greeting
и FormalGreeting
(в любом порядке) - такой:
class E extends Greeting("Bob"), FormalGreeting
(new C).msg
// How are you, Bob
// res1: String = "How are you, Bob"
(new E).msg
// res2: String = "How do you do, Bob"
Trait-ы с параметрами контекста
Правило "требуется явное расширение" ослабляется, если отсутствующий trait
содержит только параметры контекста.
В этом случае ссылка на трейт неявно вставляется как дополнительный родитель с выводимыми аргументами.
Например, вот вариант Greeting
, где адресат является параметром контекста типа ImpliedName
:
case class ImpliedName(name: String):
override def toString = name
trait ImpliedGreeting(using val iname: ImpliedName):
def msg = s"How are you, $iname"
trait ImpliedFormalGreeting extends ImpliedGreeting:
override def msg = s"How do you do, $iname"
class F(using iname: ImpliedName) extends ImpliedFormalGreeting
Определение F
в последней строке неявно расширяется до
class F(using iname: ImpliedName) extends
Object,
ImpliedGreeting(using iname),
ImpliedFormalGreeting(using iname)
Обратите внимание на вставленную ссылку на ImpliedFormalGreeting
- родительский trait ImpliedGreeting
,
которая не упоминалась явно.
given ImpliedName = ImpliedName("Bob")
(new F).msg
// res4: String = "How do you do, Bob"
Transparent traits
Trait-ы используются в двух случаях:
- как примеси для других классов и trait-ов
- как типы констант, определений или параметров
Некоторые trait-ы используются преимущественно в первой роли, и обычно их нежелательно видеть в выводимых типах.
Примером может служить trait Product
,
который компилятор добавляет в качестве примеси к каждому case class-у или case object-у.
В Scala 2 этот родительский trait иногда делает выводимые типы более сложными, чем они должны быть.
Пример:
trait Kind
case object Var extends Kind
case object Val extends Kind
val x = Set(if false then Val else Var)
Здесь предполагаемый тип x
равен Set[Kind & Product & Serializable]
,
тогда как можно было бы надеяться, что это будет Set[Kind]
.
Основания для выделения именно этого типа следующие:
- тип условного оператора, приведенного выше, является типом объединения
Val | Var
. - тип объединения расширяется в выводе типа до наименьшего супертипа, который не является типом объединения.
В примере - это тип Kind & Product & Serializable
, так как все три trait-а являются trait-ами обоих Val
и Var
.
Таким образом, этот тип становится предполагаемым типом элемента набора.
Scala 3 позволяет помечать trait примеси как transparent
, что означает, что он может быть подавлен при выводе типа.
Вот пример, который повторяет строки приведенного выше кода, но теперь с новым transparent trait S
вместо Product
:
transparent trait S
trait Kind
object Var extends Kind, S
object Val extends Kind, S
val x = Set(if true then Val else Var)
// x: Set[Kind] = ...
Теперь x
предположил тип Set[Kind]
. Общий transparent trait S
не появляется в выводимом типе.
Прозрачные trait-ы
Trait-ы scala.Product,
java.io.Serializable
и java.lang.Comparable
автоматически считаются transparent
.
Другие трейты превращаются в transparent trait
с помощью модификатора transparent
.
Как правило, transparent trait
— это trait-ы, влияющие на реализацию наследуемых классов,
и trait-ы, которые сами по себе обычно не используются как типы.
Два примера из стандартной библиотеки коллекций:
- IterableOps, который предоставляет реализации методов для Iterable.
- StrictOptimizedSeqOps, который оптимизирует некоторые из этих реализаций для последовательностей с эффективной индексацией.
Как правило, любой trait, расширяемый рекурсивно, является хорошим кандидатом на объявление transparent
.
Правила вывода типов говорят, что transparent trait
удаляются из пересечений, где это возможно.
Ссылки: