ужасного
алмаза можно решить, используя черты Scala и процесс, называемый
линеаризацией . Возьмите следующий пример:
trait Base { def msg = "Base" } trait Foo extends Base { abstract override def msg = "Foo -> " + super.msg } trait Bar extends Base { abstract override def msg = "Bar -> " + super.msg } trait Buzz extends Base { abstract override def msg = "Buzz -> " + super.msg } class Riddle extends Base with Foo with Bar with Buzz { override def msg = "Riddle -> " + super.msg }
Теперь позвольте мне задать вам небольшой вопрос: каков результат
(new Riddle).msg
?
Riddle -> Base
Riddle -> Buzz -> Base
Riddle -> Foo -> Base
Riddle -> Buzz -> Bar -> Foo -> Base
Это не (1), потому что
Base.msg
переопределяется всеми чертами, которые мы расширяем, так что это не должно быть сюрпризом. Но это также не (2) и (3). Можно ожидать, что либо
Buzz
или
Foo
напечатан, помня, что вы можете сложить черты и либо первый или последний (на самом деле: последний) побед. Так почему
Riddle -> Buzz -> Base
неверно? Разве не
Buzz.msg
звонит
super.msg
и
Buzz
явно заявляет,
Base
что это родитель? Здесь есть немного магии.
Когда вы складываете несколько признаков, как мы это делали (
extends Base with Foo with Bar with Buzz
), компилятор Scala упорядочивает их (
линеаризует ), так что всегда есть один путь от каждого класса до parent (
Base
). Порядок определяется обратным порядком смешанных черт (последний
выигрываети становится первым). Почему ты когда-либо …? Оказывается, составные черты отлично подходят для реализации нескольких слоев украшения вокруг реального объекта. Вы можете легко добавлять декораторы и перемещать их.
У нас есть простая абстракция калькулятора и одна реализация:
trait Calculator { def increment(x: Int): Int } class RealCalculator extends Calculator { override def increment(x: Int) = { println(s"increment($x)") x + 1 } }
Мы придумали три аспекта, которые мы хотели бы применять выборочно в зависимости от некоторых обстоятельств: регистрация всех
increment()
вызовов, кэширование и проверка. Сначала давайте определим их всех:
trait Logging extends Calculator { abstract override def increment(x: Int) = { println(s"Logging: $x") super.increment(x) } } trait Caching extends Calculator { abstract override def increment(x: Int) = if(x < 10) { //silly caching... println(s"Cache hit: $x") x + 1 } else { println(s"Cache miss: $x") super.increment(x) } } trait Validating extends Calculator { abstract override def increment(x: Int) = if(x >= 0) { println(s"Validation OK: $x") super.increment(x) } else throw new IllegalArgumentException(x.toString) }
Создание «сырого» калькулятора конечно возможно:
val calc = new RealCalculator calc: RealCalculator = RealCalculator@bbd9e6 scala> calc increment 17 increment(17) res: Int = 18
Но мы можем добавлять столько миксинов, сколько захотим, в любом порядке:
scala> val calc = new RealCalculator with Logging with Caching with Validating calc: RealCalculator with Logging with Caching with Validating = $anon$1@1aea543 scala> calc increment 17 Validation OK: 17 Cache miss: 17 Logging: 17 increment(17) res: Int = 18 scala> calc increment 9 Validation OK: 9 Cache hit: 9 res: Int = 10
Видите, как последующие миксины включаются? Конечно, каждый миксин может пропустить
super
вызов, например, при попадании в кеш или сбое проверки. Просто чтобы прояснить это — не имеет значения, что каждый из украшающих миксинов
Calculator
определен как базовая черта.
super.increment()
всегда направляется на следующую черту в стеке (предыдущую в объявлении класса). Это означает, что
super
он более динамичен и зависит от целевого использования, а не от объявления. Мы объясним это позже, но сначала еще один пример: давайте поместим ведение логов перед кэшированием, поэтому независимо от того, был ли кеш попаданием или пропуском, мы всегда получим логи. Более того, мы «отключаем» проверку, просто пропуская ее:
scala> class VerboseCalculator extends RealCalculator with Caching with Logging defined class VerboseCalculator scala> val calc = new VerboseCalculator calc: VerboseCalculator = VerboseCalculator@f64dcd scala> calc increment 42 Logging: 42 Cache miss: 42 increment(42) res: Int = 43 scala> calc increment 4 Logging: 4 Cache hit: 4 res: Int = 5
Я обещал объяснить, как работает укладка. Вам должно быть действительно любопытно, как super
реализован этот «фанк»,
поскольку он не может просто полагаться на
invokespecial
инструкцию байт-кода, используемую с normal
super
. К сожалению, это сложно, но стоит знать и понимать, особенно когда укладка не работает должным образом.
Calculator
и
RealCalculator
скомпилировать в точности то, что вы могли ожидать:
public interface Calculator { int increment(int i); } public class RealCalculator implements Calculator { public int increment(int x) { return x + 1; } }
Но как будет реализован следующий класс?
class FullBlownCalculator extends RealCalculator with Logging with Caching with Validating
Давайте начнем с самого класса:
public class FullBlownCalculator extends RealCalculator implements Logging, Caching, Validating { public int increment(int x) { return Validating$class.increment(this, x); } public int Validating$$super$increment(int x) { return Caching$class.increment(this, x); } public int Caching$$super$increment(int x) { return Logging$class.increment(this, x); } public int Logging$$super$increment(int x) { return super.increment(x); } }
Вы видите, что здесь происходит? Прежде чем я покажу реализации всех этих
*$class
классов, уделите немного времени рассмотрению объявлений классов (в частности, порядка черт) и этих неуклюжих
*$$super$*
методов. Вот недостающий фрагмент, который позволит нам соединить все точки:
public abstract class Logging$class { public static int increment(Logging that, int x) { return that.Logging$$super$increment(x); } } public abstract class Caching$class { public static int increment(Caching that, int x) { return that.Caching$$super$increment(x); } } public abstract class Validating$class { public static int increment(Validating that, int x) { return that.Validating$$super$increment(x); } }
Не полезно? Давайте медленно пройдем первый шаг. При звонке
FullBlownCalculator
, согласно чертам правил укладки,
RealBlownCalculator.increment()
следует звонить
Validating.increment()
. Как видите,
Validating.increment()
перенаправляет
this
(себя) в статический
Validating$class.increment()
скрытый класс. Этот класс ожидает экземпляр класса
Validating
, но, поскольку он
FullBlownCalculator
также расширяет эту черту, передача
this
в порядке.
Теперь посмотри на
Validating$class.increment()
. Это только вперед
FullBlownCalculator.Validating$$super$increment(x)
. И когда мы снова вернемся к,
FullBlownCalculator
мы заметим, что этот метод делегирует static
Caching$class.increment()
. Отсюда процесс похож. Почему дополнительное делегирование через
static
метод? Миксины не знают, какой класс будет следующим в стеке («следующий
super
»). Таким образом они просто делегируют соответствующие виртуальные
$$super$
семейство методов. Каждый класс, использующий эти миксины, обязан их реализовывать, предоставляя правильные «супер».
Чтобы представить это в перспективе: компилятор не может просто делегировать прямо из
Validating$class.increment()
в
Caching$class.increment()
, даже если это
FullBlowCalculator
рабочий процесс. Однако, если мы создадим другой класс, который обращает эти
RealCalculator with Validating with Caching
жестко закодированные зависимости mixins (), больше не будет действительным. За объявление заказа отвечает класс, а не миксин.
Если вы все еще не подписаны, вот полный стек вызовов для
FullBlownCalculator.increment()
:
val calc = new FullBlownCalculator calc increment 42 FullBlownCalculator.increment(42) `- Validating$class.increment(calc, 42) `- Validating.Validating$$super$increment(42) (on calc) `- Caching$class.increment(calc, 42) `- Caching.Caching$$super$increment(42) (on calc) `- Logging$class.increment(calc, 42) `- Logging.Logging$$super$increment(42) (on calc) `- super.increment(42) `- RealCalculator.increment(42) (on calc)
Теперь вы понимаете, почему это называется «
линеаризация »!