Это продолжение реализации и взаимодействия черт 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 RealCalculatorcalc: RealCalculator = RealCalculator@bbd9e6 scala> calc increment 17increment(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 Validatingcalc: RealCalculator with Logging with Caching with Validating = $anon$1@1aea543 scala> calc increment 17Validation OK: 17Cache miss: 17Logging: 17increment(17)res: Int = 18 scala> calc increment 9Validation OK: 9Cache hit: 9res: 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 Loggingdefined class VerboseCalculator scala> val calc = new VerboseCalculatorcalc: VerboseCalculator = VerboseCalculator@f64dcd scala> calc increment 42Logging: 42Cache miss: 42increment(42)res: Int = 43 scala> calc increment 4Logging: 4Cache hit: 4res: 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 FullBlownCalculatorcalc 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) |
Теперь вы понимаете, почему это называется « линеаризация »!