Это продолжение реализации и взаимодействия черт Scala. Часть I: Основы . Проблему ужасного алмаза можно решить, используя черты Scala и процесс, называемый линеаризацией . Возьмите следующий пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
|
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 упорядочивает их ( линеаризует ), так что всегда есть один путь от каждого класса к родительскому ( Base
). Порядок определяется обратным порядком смешанных черт (последний выигрывает и становится первым). Почему ты когда-либо …? Оказывается, составные черты отлично подходят для реализации нескольких слоев украшения вокруг реального объекта. Вы можете легко добавлять декораторы и перемещать их. У нас есть простая абстракция калькулятора и одна реализация:
01
02
03
04
05
06
07
08
09
10
|
trait Calculator { def increment(x: Int): Int } class RealCalculator extends Calculator { override def increment(x: Int) = { println(s "increment($x)" ) x + 1 } } |
Мы придумали три аспекта, которые мы хотели бы применять выборочно в зависимости от некоторых обстоятельств: регистрация всех вызовов increment()
, кэширование и проверка. Сначала давайте определим их всех:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
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) } |
Создание «сырого» калькулятора конечно возможно:
1
2
3
4
5
6
|
val calc = new RealCalculator calc: RealCalculator = RealCalculator @bbd9e6 scala> calc increment 17 increment( 17 ) res: Int = 18 |
Но мы можем добавлять столько миксинов, сколько захотим, в любом порядке:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
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
более динамичен и зависит от целевого использования, а не от объявления. Мы объясним это позже, но сначала еще один пример: давайте поместим ведение логов перед кэшированием, поэтому независимо от того, был ли кеш попаданием или пропуском, мы всегда получим логи. Более того, мы «отключаем» проверку, просто пропуская ее:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
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
инструкцию байт-кода, используемую с обычным super
. К сожалению, это сложно, но стоит знать и понимать, особенно когда укладка не работает должным образом. Calculator
и RealCalculator
компилируются в точности так, как вы могли ожидать:
1
2
3
4
5
6
7
8
9
|
public interface Calculator { int increment( int i); } public class RealCalculator implements Calculator { public int increment( int x) { return x + 1 ; } } |
Но как будет реализован следующий класс?
1
2
3
4
5
|
class FullBlownCalculator extends RealCalculator with Logging with Caching with Validating |
Давайте начнем с самого класса:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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$*
методов. Вот недостающий фрагмент, который позволит нам соединить все точки:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
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
вполне FullBlownCalculator
.
Теперь посмотрим на Validating$class.increment()
. Он только пересылает FullBlownCalculator.Validating$$super$increment(x)
. И когда мы снова вернемся к FullBlownCalculator
мы заметим, что этот метод делегирует статическое Caching$class.increment()
. Отсюда процесс похож. Почему дополнительное делегирование через static
метод? Миксины не знают, какой класс будет следующим в стеке («следующий super
»). Таким образом, они просто делегируют подходящее виртуальное семейство методов $$super$
. Каждый класс, использующий эти миксины, обязан их реализовывать, предоставляя правильный «супер».
Чтобы представить это в перспективе: компилятор не может просто делегировать прямо из Validating$class.increment()
в Caching$class.increment()
, даже если это FullBlowCalculator
процесс FullBlowCalculator
. Однако если мы создадим другой класс, который обращает эти миксины ( RealCalculator with Validating with Caching
), жестко закодированная зависимость между миксинами больше не будет действительной. За объявление заказа отвечает класс, а не миксин. Если вы все еще не FullBlownCalculator.increment()
, вот полный стек вызовов для FullBlownCalculator.increment()
:
01
02
03
04
05
06
07
08
09
10
11
12
|
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) |
Теперь вы понимаете, почему это называется « линеаризация »!