Хорошая или плохая практика? Инициализация объектов в геттере

У меня странная привычка, кажется... по крайней мере, по моему коллеге. Мы вместе работаем над небольшим проектом. Способ, которым я писал классы, (упрощенный пример):

[Serializable()]
public class Foo
{
    public Foo()
    { }

    private Bar _bar;

    public Bar Bar
    {
        get
        {
            if (_bar == null)
                _bar = new Bar();

            return _bar;
        }
        set { _bar = value; }
    }
}

Итак, в основном, я только инициализирую любое поле, когда вызывается геттер, и поле по-прежнему равно нулю. Я решил, что это уменьшит перегрузку, не инициализируя какие-либо свойства, которые нигде не используются.

ETA: Причина, по которой я сделал это, заключается в том, что мой класс имеет несколько свойств, которые возвращают экземпляр другого класса, который, в свою очередь, также имеет свойства с еще большим количеством классов и т.д. Вызов конструктора для верхнего класса впоследствии вызовет все конструкторы для всех этих классов, если они не всегда необходимы.

Есть ли возражения против этой практики, кроме личных предпочтений?

ОБНОВЛЕНИЕ: Я рассмотрел множество разных мнений по этому вопросу, и я буду придерживаться моего принятого ответа. Тем не менее, теперь я понял гораздо лучшее понимание концепции, и я могу решить, когда ее использовать, а когда нет.

Минусы:

  • Проблемы безопасности потока
  • Не подчиняется запросу "сеттер", когда переданное значение равно null
  • Micro-оптимизации
  • Обработка исключений должна выполняться в конструкторе
  • Нужно проверить значение null в классе

Плюсы:

  • Micro-оптимизации
  • Свойства никогда не возвращают null
  • Задержка или отказ от загрузки "тяжелых" объектов

Большинство минусов не применимы к моей текущей библиотеке, однако мне нужно будет проверить, действительно ли "микро-оптимизация" вообще что-то оптимизирует.

ПОСЛЕДНЕЕ ОБНОВЛЕНИЕ:

Хорошо, я изменил свой ответ. Мой первоначальный вопрос заключался в том, является ли это хорошей привычкой. И теперь я уверен, что это не так. Возможно, я все равно буду использовать его в некоторых частях моего текущего кода, но не безоговорочно и определенно не все время. Поэтому я буду потерять свою привычку и подумать об этом, прежде чем использовать ее. Спасибо всем!

Ответ 1

То, что у вас здесь, - наивное - реализация "ленивой инициализации".

Краткий ответ:

Использование ленивой инициализации безусловно не является хорошей идеей. Он имеет свои места, но нужно учитывать последствия этого решения.

Предыстория и пояснения:

Конкретная реализация:
Позвольте сначала взглянуть на ваш конкретный образец и почему я считаю его реализацию наивным:

  • Он нарушает Принцип наименьшего сюрприза (POLS). Когда значение присваивается свойству, ожидается, что это значение будет возвращено. В вашей реализации это не относится к null:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
    
  • Он представляет довольно много проблем с потоками: два вызывающих абонента foo.Bar для разных потоков могут потенциально получить два разных экземпляра Bar, и один из них будет без подключения к экземпляру Foo. Любые изменения, внесенные в этот экземпляр Bar, будут потеряны. Это еще один случай нарушения POLS. Когда доступно только сохраненное значение свойства, ожидается, что он будет потокобезопасным. Хотя вы можете утверждать, что класс просто не является потокобезопасным - в том числе и получателем вашего имущества - вам придется документировать это правильно, поскольку это не обычный случай. Кроме того, введение этой проблемы не требуется, как мы вскоре увидим.

В целом:
Теперь пришло время взглянуть на ленивую инициализацию вообще:
Ленивая инициализация обычно используется для отсрочки построения объектов, которые занимают много времени, или они занимают много памяти после полной сборки.
Это очень веская причина для использования ленивой инициализации.

Однако такие свойства обычно не имеют сеттеров, которые избавляются от первой проблемы, указанной выше.
Кроме того, будет использоваться поточно-безопасная реализация - например, Lazy<T> - во избежание второй проблемы.

Даже при рассмотрении этих двух точек при реализации ленивого свойства следующие моменты являются общими проблемами этого шаблона:

Заключение этих пунктов следующее:
Для каждого отдельного свойства, которое выполняется лениво, вы должны были рассмотреть эти моменты.
Это означает, что это решение в каждом конкретном случае и не может быть принято в качестве общей передовой практики.

Этот шаблон имеет свое место, но он не является общей лучшей практикой при реализации классов. Он не должен использоваться безоговорочно по причинам, указанным выше.


В этом разделе я хочу обсудить некоторые моменты, которые другие выдвинули в качестве аргументов для безусловной использования ленивой инициализации:

  • Сериализация:
    EricJ заявляет в одном комментарии:

    Объект, который может быть сериализован, не будет иметь его contructor, вызванный, когда он десериализуется (зависит от сериализатора, но многие обычные ведут себя так). Ввод кода инициализации в конструкторе означает, что вы должны предоставить дополнительную поддержку десериализации. Этот шаблон избегает этого специального кодирования.

    Есть несколько проблем с этим аргументом:

    • Большинство объектов никогда не будут сериализованы. Добавление некоторой поддержки для нее, когда она не нужна, нарушает YAGNI.
    • Когда класс должен поддерживать сериализацию, существуют способы его включения без обходного пути, который на первый взгляд не имеет ничего общего с сериализацией.
  • Micro-оптимизации: Ваш главный аргумент состоит в том, что вы хотите построить объекты только тогда, когда кто-то действительно обращается к ним. Таким образом, вы фактически говорите об оптимизации использования памяти.
    Я не согласен с этим аргументом по следующим причинам:

    • В большинстве случаев еще несколько объектов в памяти не влияют ни на что. У современных компьютеров достаточно памяти. Без случая фактических проблем, подтвержденных профилировщиком, это предварительная зрелость, и есть веские причины против этого.
    • Я подтверждаю тот факт, что иногда такая оптимизация оправдана. Но даже в этих случаях ленивая инициализация не кажется правильным решением. Существует две причины:

      • Ленивая инициализация потенциально может повредить производительность. Может быть, только незначительно, но, как показал ответ Билла, воздействие больше, чем можно было бы подумать на первый взгляд. Таким образом, этот подход в основном торгует производительностью и памятью.
      • Если у вас есть дизайн, в котором используется только часть класса, это указывает на проблему с самим дизайном: класс, о котором идет речь, скорее всего, несет более одной ответственности. Решение состоит в том, чтобы разбить класс на несколько более целенаправленных классов.

Ответ 2

Это хороший выбор дизайна. Настоятельно рекомендуется для кода библиотеки или основных классов.

Он вызывается некоторой "ленивой инициализацией" или "задержкой инициализации", и обычно считается, что это хороший выбор дизайна.

Во-первых, если вы инициализируете объявление переменных или конструктора уровня класса, тогда, когда ваш объект сконструирован, у вас есть накладные расходы на создание ресурса, который никогда не будет использоваться.

Во-вторых, ресурс создается только при необходимости.

В-третьих, вы избегаете сбора мусора, который не использовался.

Наконец, легче обрабатывать исключения инициализации, которые могут возникать в свойстве, а затем исключения, возникающие во время инициализации переменных уровня класса или конструктора.

Существуют исключения из этого правила.

Что касается аргумента производительности дополнительной проверки для инициализации в свойстве "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

Lazy<T> был представлен в .NET 4.0, поэтому, пожалуйста, не добавляйте еще один комментарий к этому классу.

Перед комментарием о микро-оптимизации

Когда вы строите библиотеки, вы должны учитывать все оптимизации. Например, в классах .NET вы увидите битовые массивы, используемые для переменных класса Boolean во всем коде, чтобы уменьшить потребление памяти и фрагментацию памяти, чтобы назвать две "микро-оптимизации".

Что касается пользовательских интерфейсов

Вы не собираетесь использовать ленивую инициализацию для классов, которые непосредственно используются пользовательским интерфейсом. На прошлой неделе я потратил большую часть дня, снимая ленивую загрузку восьми коллекций, используемых в виде-модели для комбинированных ящиков. У меня есть LookupManager, который обрабатывает ленивую загрузку и кеширование коллекций, необходимых любому элементу пользовательского интерфейса.

"сеттеры"

Я никогда не использовал set-property ( "seters" ) для любого ленивого загруженного свойства. Поэтому вы никогда не допустили бы foo.Bar = null;. Если вам нужно установить Bar, тогда я бы создал метод под названием SetBar(Bar value) и не использовал lazy-initialization

Коллекции

Свойства набора классов всегда инициализируются при объявлении, поскольку они никогда не должны быть пустыми.

Комплексные классы

Позвольте мне повторить это по-другому, вы используете ленивую инициализацию для сложных классов. Как правило, это плохо разработанные классы.

Наконец

Я никогда не говорил делать это для всех классов или во всех случаях. Это плохая привычка.

Ответ 3

Рассматриваете ли вы реализацию такого шаблона с помощью Lazy<T>?

В дополнение к простому созданию ленивых объектов вы получаете безопасность потока при инициализации объекта:

Как говорили другие, вы лениво загружаете объекты, если они действительно ресурсоемкие, или требуется некоторое время для их загрузки во время построения объекта.

Ответ 4

Недостаток, который я вижу, заключается в том, что если вы хотите спросить, имеет ли Bars значение null, это никогда не будет, и вы создадите там список.

Ответ 5

Я думаю, это зависит от того, что вы инициализируете. Я, вероятно, не сделал бы этого для списка, поскольку стоимость строительства довольно мала, поэтому он может идти в конструкторе. Но если это был предварительно заполненный список, я, вероятно, не стал бы, пока он не понадобился в первый раз.

В принципе, если стоимость строительства перевешивает стоимость выполнения условной проверки на каждый доступ, тогда ленивый ее создает. Если нет, сделайте это в конструкторе.

Ответ 6

Ленивая инстанция/инициализация - совершенно жизнеспособная модель. Имейте в виду, однако, что, как правило, потребители вашего API не ожидают, что геттеры и сеттеры будут получать заметное время от POV конечного пользователя (или сбой).

Ответ 7

Я просто собирался поставить комментарий к Дэниелу, но я честно не думаю, что это идет достаточно далеко.

Хотя это очень хороший шаблон для использования в определенных ситуациях (например, когда объект инициализируется из базы данных), это привычка HORRIBLE, чтобы войти.

Одна из лучших вещей об объекте заключается в том, что он предлагает безопасную, надежную среду. Самый лучший случай - создать как можно больше полей "Финал", заполнив их конструктором. Это делает ваш класс совершенно пуленепробиваемым. Разрешить изменение полей через сеттеры немного меньше, но не страшно. Например:

class SafeClass
{
    String name="";
    Integer age=0;

    public void setName(String newName)
    {
        assert(newName != null)
        name=newName;
    }// follow this pattern for age
    ...
    public String toString() {
        String s="Safe Class has name:"+name+" and age:"+age
    }
}

С вашим шаблоном метод toString будет выглядеть так:

    if(name == null)
        throw new IllegalStateException("SafeClass got into an illegal state! name is null")
    if(age == null)
        throw new IllegalStateException("SafeClass got into an illegal state! age is null")

    public String toString() {
        String s="Safe Class has name:"+name+" and age:"+age
    }

Не только это, но вам нужны нулевые проверки везде, где вы, возможно, можете использовать этот объект в своем классе (вне вашего класса является безопасным из-за нулевой проверки в getter, но вы должны в основном использовать своих членов классов внутри класса)

Также ваш класс постоянно находится в неопределенном состоянии - например, если вы решили сделать этот класс классом hibernate, добавив несколько аннотаций, как бы вы это сделали?

Если вы принимаете какое-либо решение, основанное на некоторой микрооптимизации без требований и тестирования, это почти наверняка неправильное решение. На самом деле существует действительно очень хороший шанс, что ваш шаблон фактически замедляет работу системы даже в самых идеальных обстоятельствах, потому что оператор if может вызвать отказ предсказания ветвления на процессоре, который замедлит работу во много раз больше, чем просто присваивая значение в конструкторе, если только созданный объект не является достаточно сложным или исходит из удаленного источника данных.

Для примера проблемы предсказания (который вы повторяете неоднократно, ность только один раз), см. первый ответ на этот удивительный вопрос: Почему быстрее обрабатывать отсортированные массив, чем несортированный массив?

Ответ 8

Позвольте мне добавить еще один момент ко многим хорошим моментам, сделанным другими...

Отладчик будет (по умолчанию) оценивать свойства при прохождении кода, что может потенциально создать экземпляр Bar раньше, чем обычно, просто выполнив код. Другими словами, простой акт отладки изменяет выполнение программы.

Это может быть или не быть проблемой (в зависимости от побочных эффектов), но это то, о чем нужно знать.

Ответ 9

Вы уверены, что Foo должен вообще что-то создавать?

Мне кажется, что вонючий (хотя и не обязательно ошибочный), чтобы Foo вообще ничего не создавал. Если целью Foo не является factory, он не должен создавать экземпляр собственных коллабораторов, а вместо этого вставлять их в свой конструктор.

Если, однако, целью Foo является создание экземпляров типа Bar, то я не вижу ничего плохого в этом лениво.