Кажется, у меня странная привычка ... по крайней мере, по словам моего сотрудника. Мы вместе работали над небольшим проектом. Я написал классы так (упрощенный пример):
[Serializable()]
public class Foo
{
public Foo()
{ }
private Bar _bar;
public Bar Bar
{
get
{
if (_bar == null)
_bar = new Bar();
return _bar;
}
set { _bar = value; }
}
}
Так что, в основном, я инициализирую любое поле только тогда, когда вызывается метод get, а поле все еще пустое. Я полагал, что это уменьшит перегрузку, не инициализируя никакие свойства, которые нигде не используются.
ETA: причина, по которой я это сделал, состоит в том, что у моего класса есть несколько свойств, которые возвращают экземпляр другого класса, которые, в свою очередь, также имеют свойства с еще большим количеством классов и так далее. Вызов конструктора для высшего класса впоследствии вызовет все конструкторы для всех этих классов, когда они не всегда все необходимы.
Есть ли какие-либо возражения против этой практики, кроме личных предпочтений?
ОБНОВЛЕНИЕ: я рассмотрел много различных мнений относительно этого вопроса, и я буду стоять на своем принятом ответе. Тем не менее, теперь я пришел к гораздо лучшему пониманию концепции, и я могу решить, когда использовать ее, а когда нет.
Минусы:
- Проблемы безопасности потоков
- Не подчиняется запросу "setter", когда переданное значение является нулевым
- Микро-оптимизация
- Обработка исключений должна происходить в конструкторе
- Необходимо проверить на нулевое значение в коде класса
Плюсы:
- Микро-оптимизация
- Свойства никогда не возвращают ноль
- Задержка или избежание загрузки "тяжелых" предметов
Большинство минусов не применимо к моей текущей библиотеке, однако мне придется проверить, действительно ли «микрооптимизации» вообще что-то оптимизируют.
ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ:
Хорошо, я изменил свой ответ. Мой оригинальный вопрос был или нет, это хорошая привычка. И теперь я убежден, что это не так. Возможно, я все еще буду использовать его в некоторых частях моего текущего кода, но не безоговорочно и определенно не всегда. Так что я собираюсь потерять свою привычку и подумать об этом, прежде чем использовать его. Спасибо всем!
источник
Ответы:
Здесь у вас есть наивная реализация "ленивой инициализации".
Короткий ответ:
Безусловно, ленивая инициализация не очень хорошая идея. У него есть свои места, но нужно учитывать, какое влияние оказывает это решение.
Предпосылки и объяснение:
Конкретная реализация:
давайте сначала посмотрим на ваш конкретный пример и почему я считаю его реализацию наивной:
Это нарушает принцип наименьшего сюрприза (POLS) . Когда значение присваивается свойству, ожидается, что это значение будет возвращено. В вашей реализации это не относится к
null
:foo.Bar
с потоками : два вызывающих в разных потоках потенциально могут получить два разных экземпляра,Bar
и один из них будет без связи сFoo
экземпляром. Любые изменения, внесенные в этотBar
экземпляр, молча теряются.Это еще один случай нарушения ПОЛ. Когда осуществляется доступ только к сохраненному значению свойства, ожидается, что он будет поточно-ориентированным. Хотя вы можете утверждать, что этот класс просто не является потокобезопасным, включая получатель вашего свойства, вам придется правильно документировать это, поскольку это не является нормальным случаем. Кроме того, введение этой проблемы не является необходимым, как мы скоро увидим.
В общем:
сейчас пришло время взглянуть на ленивую инициализацию в целом:
ленивая инициализация обычно используется для задержки создания объектов, которые требуют много времени для создания или которые занимают много памяти после полной сборки.
Это очень веская причина для использования отложенной инициализации.
Однако такие свойства обычно не имеют сеттеров, что избавляет от первой проблемы, указанной выше.
Кроме того,
Lazy<T>
для избежания второй проблемы будет использоваться поточно-ориентированная реализация .Даже при рассмотрении этих двух моментов в реализации ленивого свойства, следующие моменты являются общими проблемами этого шаблона:
Строительство объекта может быть неудачным, что приведет к исключению из метода получения свойства. Это еще одно нарушение ПОЛ, и поэтому его следует избегать. Даже раздел о свойствах в «Руководстве по проектированию для разработки библиотек классов» прямо заявляет, что получатели свойств не должны генерировать исключения:
Вредна автоматическая оптимизация компилятором, а именно встраивание и предсказание переходов. Пожалуйста, смотрите ответ Билла К. для подробного объяснения.
Вывод из этих пунктов следующий:
для каждого отдельного свойства, которое реализуется лениво, вы должны были рассмотреть эти пункты.
Это означает, что это индивидуальное решение и его нельзя воспринимать как лучшую практику.
Этот шаблон имеет свое место, но он не является общей передовой практикой при реализации классов. Он не должен использоваться безоговорочно по причинам, указанным выше.
В этом разделе я хочу обсудить некоторые моменты, которые другие выдвинули в качестве аргументов для безоговорочного использования ленивой инициализации:
Сериализация:
EricJ заявляет в одном комментарии:
Есть несколько проблем с этим аргументом:
Микрооптимизация. Ваш главный аргумент заключается в том, что вы хотите создавать объекты только тогда, когда кто-то действительно обращается к ним. Итак, вы на самом деле говорите об оптимизации использования памяти.
Я не согласен с этим аргументом по следующим причинам:
Я признаю тот факт, что иногда такая оптимизация оправдана. Но даже в этих случаях ленивая инициализация не кажется правильным решением. На это есть две причины:
источник
null
деле стал намного сильнее. :-)Это хороший выбор дизайна. Настоятельно рекомендуется для библиотечного кода или основных классов.
Он вызывается некоторой «отложенной инициализацией» или «отложенной инициализацией» и, как правило, считается хорошим выбором при проектировании.
Во-первых, если вы инициализируете в объявлении переменных уровня класса или конструктора, то когда ваш объект создается, у вас есть накладные расходы на создание ресурса, который никогда не будет использоваться.
Во-вторых, ресурс создается только при необходимости.
В-третьих, вы избегаете сбора мусора неиспользованным объектом.
Наконец, легче обрабатывать исключения инициализации, которые могут возникать в свойстве, чем исключения, возникающие при инициализации переменных уровня класса или конструктора.
Есть исключения из этого правила.
Что касается аргумента производительности дополнительной проверки инициализации в свойстве get, то он незначителен. Инициализация и удаление объекта являются более значительным ударом по производительности, чем простая проверка нулевого указателя с помощью прыжка.
Рекомендации по разработке библиотек классов по адресу http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx
относительно
Lazy<T>
Универсальный
Lazy<T>
класс был создан именно для того, что хочет плакат, см. Ленивая инициализация на http://msdn.microsoft.com/en-us/library/dd997286(v=vs.100).aspx . Если у вас более старые версии .NET, вы должны использовать шаблон кода, показанный в вопросе. Этот шаблон кода стал настолько распространенным, что Microsoft посчитала необходимым включить класс в новейшие библиотеки .NET, чтобы упростить реализацию шаблона. Кроме того, если вашей реализации нужна безопасность потоков, вы должны добавить ее.Примитивные типы данных и простые классы
Obvioulsy, вы не собираетесь использовать ленивую инициализацию для примитивного типа данных или простого использования класса, как
List<string>
.Прежде чем комментировать ленивый
Lazy<T>
была введена в .NET 4.0, поэтому, пожалуйста, не добавляйте еще один комментарий относительно этого класса.Прежде чем комментировать о микрооптимизации
Когда вы создаете библиотеки, вы должны учитывать все оптимизации. Например, в классах .NET вы увидите битовые массивы, используемые для переменных логического класса по всему коду для уменьшения потребления памяти и фрагментации памяти, просто чтобы назвать две «микрооптимизации».
Что касается пользовательских интерфейсов
Вы не собираетесь использовать отложенную инициализацию для классов, которые непосредственно используются пользовательским интерфейсом. На прошлой неделе я потратил большую часть дня, снимая ленивую загрузку восьми коллекций, используемых в модели представления для комбо-боксов. У меня есть,
LookupManager
который обрабатывает ленивую загрузку и кэширование коллекций, необходимых любому элементу пользовательского интерфейса.«сеттер»
Я никогда не использовал set-property ("setters") для каких-либо отложенных загруженных свойств. Поэтому вы никогда не позволите
foo.Bar = null;
. Если вам нужно установить,Bar
то я бы создал метод с именемSetBar(Bar value)
и не использовать ленивую инициализациюКоллекции
Свойства коллекции классов всегда инициализируются при объявлении, потому что они никогда не должны быть нулевыми.
Сложные классы
Позвольте мне повторить это по-другому, вы используете ленивую инициализацию для сложных классов. Которые обычно плохо спланированы.
наконец
Я никогда не говорил делать это для всех классов или во всех случаях. Это плохая привычка.
источник
Считаете ли вы реализацию такого шаблона с помощью
Lazy<T>
?В дополнение к простому созданию ленивых загруженных объектов, вы получаете безопасность потока, пока объект инициализируется:
Как говорили другие, вы лениво загружаете объекты, если они действительно ресурсоемки или для их загрузки требуется некоторое время во время создания объекта.
источник
Lazy<T>
сейчас и воздержусь от использования, как я всегда делалMaking the Lazy<T> object thread safe does not protect the lazily initialized object. If multiple threads can access the lazily initialized object, you must make its properties and methods safe for multithreaded access.
Я думаю, это зависит от того, что вы инициализируете. Я, вероятно, не сделал бы это для списка, так как стоимость строительства довольно мала, поэтому он может идти в конструкторе. Но если бы это был предварительно заполненный список, то, вероятно, я бы не стал, пока он не понадобился впервые.
По сути, если стоимость строительства превышает стоимость выполнения условной проверки при каждом доступе, создайте его ленивым образом. Если нет, сделайте это в конструкторе.
источник
Недостаток, который я вижу, состоит в том, что если вы захотите спросить, является ли Bars нулевым, это никогда не произойдет, и вы создадите список там.
источник
null
, но не возвращаете это. Обычно вы предполагаете, что получите то же значение, которое вы указали в свойстве.Ленивая инстанциация / инициализация - вполне жизнеспособный паттерн. Имейте в виду, однако, что, как правило, потребители вашего API не ожидают, что методы получения и установки получат заметное время от POV конечного пользователя (или потерпят неудачу).
источник
Я просто собирался комментировать ответ Даниэля, но, честно говоря, не думаю, что он заходит достаточно далеко.
Хотя это очень хороший шаблон для использования в определенных ситуациях (например, когда объект инициализируется из базы данных), это ужасная привычка.
Одна из лучших вещей об объекте - то, что он предлагает безопасную, доверенную среду. Самый лучший случай, если вы сделаете как можно больше полей "Final", заполнив их все конструктором. Это делает ваш класс довольно пуленепробиваемым. Позволять менять поля через сеттеры немного меньше, но не страшно. Например:
С вашим шаблоном метод toString будет выглядеть так:
Не только это, но вам нужны нулевые проверки везде, где вы, возможно, используете этот объект в своем классе (Вне класса ваш класс безопасен из-за нулевой проверки в получателе, но вы должны в основном использовать членов классов внутри класса)
Кроме того, ваш класс постоянно находится в неопределенном состоянии - например, если вы решили сделать этот класс спящим, добавив несколько аннотаций, как бы вы это сделали?
Если вы принимаете какое-либо решение на основе микрооптомизации без требований и тестирования, это почти наверняка будет неправильным решением. На самом деле, есть действительно очень хороший шанс, что ваш паттерн на самом деле замедляет работу системы даже в самых идеальных условиях, потому что оператор if может вызвать сбой предсказания ветвления в ЦП, который будет тормозить вещи намного, намного, намного больше, чем просто присваивая значение в конструкторе, если создаваемый вами объект не является достаточно сложным или не поступает из удаленного источника данных.
В качестве примера проблемы прогнозирования Brance (которую вы повторяете много раз, чаще всего один раз), смотрите первый ответ на этот удивительный вопрос: почему быстрее обрабатывать отсортированный массив, чем несортированный массив?
источник
toString()
будет вызыватьgetName()
, а не использоватьname
напрямую.Позвольте мне добавить еще один момент ко многим хорошим моментам, высказанным другими ...
Отладчик будет ( по умолчанию ) оценивать свойства при пошаговом выполнении кода, что потенциально может привести к созданию экземпляра
Bar
быстрее, чем обычно, если просто выполнить код. Другими словами, простое действие отладки изменяет выполнение программы.Это может или не может быть проблемой (в зависимости от побочных эффектов), но это то, что нужно знать.
источник
Вы уверены, что Фу вообще должен что-то создавать?
Мне кажется вонючим (хотя и не обязательно неправильным ) позволять Фу создавать что-либо вообще. Если Foo не является явной целью быть фабрикой, он не должен создавать экземпляры своих собственных сотрудников, а вместо этого вводить их в свой конструктор .
Однако, если цель существования Foo - создавать экземпляры типа Bar, то я не вижу ничего плохого в том, чтобы делать это лениво.
источник