За исключением добавленной многословности, есть ли другие веские причины, почему нельзя утверждать, что каждая переменная экземпляра должна быть лениво инициализирована?Почему бы не сделать каждую переменную экземпляра Scala лениво инициализированной?
ответ
Прежде всего: если что-то пойдет не так в инициализации ленивого val (например, доступ к внешнему ресурсу, которого не существует), вы заметите его только при первом доступе к val, тогда как с нормальным val вы заметят, как только объект будет построен. Вы также можете иметь циклические зависимости в ленивых vals, которые приведут к тому, что класс не работает вообще (один из страшных NullPointerExceptions), но вы узнаете только при первом доступе к одному из подключенных ленивых vals.
Так ленивые деньги делают программу менее детерминированной, что всегда плохо.
Во-вторых: накладные расходы во время выполнения связаны с ленивым значением. Lazy val в настоящее время реализуется частной битовой маской (int) в классе с использованием lazy vals (один бит для каждого ленивого val, поэтому, если у вас более 32 ленивых vals будут два битмакса и т. Д.)
Чтобы сделать убедитесь, что инициализатор ленивого значения будет выполняться ровно один раз, при инициализации поля будет синхронизирована запись в битовую маску, а волатильное чтение будет выполняться каждый раз, когда к нему обращаются. Теперь волатильное чтение довольно дешево в архитектуре x86, но волатильная запись может быть очень дорогостоящей.
Насколько я знаю, в настоящее время предпринимаются попытки оптимизировать это в будущей версии scala, но всегда будет накладные расходы, чтобы проверить, инициализировано ли поле по сравнению с прямым доступом к воротам. Например, дополнительный код для доступа к ленивому валу может препятствовать тому, чтобы метод был встроен.
Конечно, для очень маленького класса также могут быть важны служебные данные памяти битмаски.
Но даже если у вас нет проблем с производительностью, хорошо выяснить порядок, в котором валы зависят друг от друга и просто сортировать их в этом порядке и использовать обычные валы.
Edit: вот пример кода, который иллюстрирует недетерминизм вы можете получить, если вы используете ленивые Vals:
class Test {
lazy val x:Int = y
lazy val y:Int = x
}
Вы можете создать экземпляр этого класса без каких-либо проблем, но как только вы получаете доступ либо x или y вы получите StackOverflow. Это, конечно, искусственный пример. В реальном мире у вас гораздо более длительные и неочевидные циклы зависимостей.
Это сеанс консоли scala, используя: javap, который иллюстрирует накладные расходы времени выполнения ленивого val. Первый нормальный вал:
scala> class Test { val x = 0 }
defined class Test
scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public int x();
Code:
0: aload_0
1: getfield #11; //Field x:I
4: ireturn
public Test();
Code:
0: aload_0
1: invokespecial #17; //Method java/lang/Object."<init>":()V
4: aload_0
5: iconst_0
6: putfield #11; //Field x:I
9: return
}
А теперь ленивый вал:
scala> :javap -c Test
Compiled from "<console>"
public class Test extends java.lang.Object implements scala.ScalaObject{
public volatile int bitmap$0;
public int x();
Code:
0: aload_0
1: getfield #12; //Field bitmap$0:I
4: iconst_1
5: iand
6: iconst_0
7: if_icmpne 45
10: aload_0
11: dup
12: astore_1
13: monitorenter
14: aload_0
15: getfield #12; //Field bitmap$0:I
18: iconst_1
19: iand
20: iconst_0
21: if_icmpne 39
24: aload_0
25: iconst_0
26: putfield #14; //Field x:I
29: aload_0
30: aload_0
31: getfield #12; //Field bitmap$0:I
34: iconst_1
35: ior
36: putfield #12; //Field bitmap$0:I
39: getstatic #20; //Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit;
42: pop
43: aload_1
44: monitorexit
45: aload_0
46: getfield #14; //Field x:I
49: ireturn
50: aload_1
51: monitorexit
52: athrow
Exception table:
from to target type
14 45 50 any
public Test();
Code:
0: aload_0
1: invokespecial #26; //Method java/lang/Object."<init>":()V
4: return
}
Как вы можете видеть, нормальный вал аксессор очень короткий и, безусловно, будет встраиваемый, в то время как ленивый вал аксессор довольно сложный и (что наиболее важно для параллелизма) включает в себя синхронизированный блок (команды monitorenter/monitorexit). Вы также можете увидеть дополнительное поле, генерируемое компилятором.
Во-первых, речь идет о lazy val
s («константы» Скалы), а не ленивые переменные (которых я не думаю о существовании).
Две причины были бы ремонтопригодность и эффективность, особенно в контексте полей класса:
Эффективность: выгода, не ленивой инициализации является то, что вы контролируете, где это происходит. Представьте себе структуру типа fork-join, в которой вы создаете несколько объектов в рабочих потоках, а затем передаете их в центральную обработку. С нетерпением eval инициализация выполняется на рабочих потоках. С ленивым eval это делается на основной теме, потенциально создавая узкое место.
ремонтопригодность: если все значения лениво инициализированы, и ваша программа взрывает, вы получаете трассировки стеки, локализованные в совершенно ином контексте, чем инициализация экземпляра, возможно, в другом потоке.
Есть также, конечно, расходы, связанные с использованием языка (я вижу, что @Beryllium опубликовал один пример), но я не достаточно компетентен, чтобы обсудить их.
Wouldn «Имеет ли смысл делать поколение, а также обрабатывать рабочие потоки, а затем возвращать их в главный поток? Или, если требуется взаимодействие с объектом, рабочие потоки возвращают свои объекты другому рабочему потоку, который будет выполнять обработку, а не основной поток, так что главный поток не должен беспокоиться об управлении большей частью рабочих потоков, средний менеджмент (так сказать). – JAB
Вот что я имею в виду. Чтобы разработать, большое количество алгоритмов имеет распределенный обрабатывающий элемент ** и ** централизованный обрабатывающий элемент - иначе * закон Амдаля * не будет существовать :). Одним из тривиальных примеров является этап слияния Mergesort. Конечно, это может показаться очевидным, но когда вы потенциально находитесь в мышлении «все, что у меня есть, это молот», это реальная ошибка, с которой вы можете попасть. –
Если я прочитал ваш код, и вы использовали ленивый, я бы сжег время, спрашивая, ПОЧЕМУ ВЫ ИСПОЛЬЗУЕТЕ ленивую инициализацию, которая, вероятно, является самой дорогой ценой ленивых в дополнение к штрафам за производительность.
Теперь, когда вы должны думать о отложенной инициализации (и аналогично Streams, которые я причисляю) это:
Циклическая зависимость: где одна переменная зависит от другой инициализации и/или наоборот. Бесконечные наборы: потоки позволяют вам найти первые 1000 простых чисел, не требуя знать, сколько действительных чисел может отображаться.
Я уверен, что есть пара других - это большие, которые я вижу.
Просто помните, что ленивый val - это как def, который оценивается ровно один раз и знает, что вы должны использовать его только тогда, когда вам это действительно нужно, иначе он запутает другого разработчика, когда спросит, почему он ленив?
Что касается скрытых расходов: http://stackoverflow.com/q/3041253/2390083 – Beryllium
И скрытые расходы будут все хуже: http://stackoverflow.com/a/17329465/175251 –