scalabook
Трейты
Если провести аналогию с 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 Kindcase object Var extends Kindcase object Val extends Kindval 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 Strait Kindobject Var extends Kind, Sobject Val extends Kind, Sval 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
удаляются из пересечений, где это возможно.
Ссылки: