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

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

[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: причина, по которой я это сделал, заключается в том, что мой класс имеет несколько свойств, которые возвращает экземпляр другого класса, который, в свою очередь, также имеет свойства с еще большим количеством классов и т. д. Вызов конструктора для верхнего класса впоследствии вызовет все конструкторы для всех этих классов, если они не являются всегда всем нужны.

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

плюсы:

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

плюсы:

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

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

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

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

9 ответов


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

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

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

фон и объяснение:

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

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

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

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

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

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

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

    избежать исключения из аксессоры.

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

  2. автоматическая оптимизация компилятором повреждена, а именно инлайнинг и прогнозирование ветвей. Пожалуйста, смотрите ответ Билла к подробное описание.

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

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


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

  1. сериализация:
    EricJ заявляет в одном комментарии:

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

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

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

    1. в большинстве случаев еще несколько объектов в памяти ни на что не влияют. У современных компьютеров достаточно памяти. Без случая фактических проблем, подтвержденных профилировщиком, это преждевременная оптимизация и есть веские причины против этого.
    2. Я признаю тот факт, что иногда такая оптимизация оправдана. Но даже в эти случаи ленивой инициализации не кажется правильным решением. Против этого есть две причины:--8-->

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

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

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

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

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

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

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

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

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

рекомендации по разработке библиотек классов at http://msdn.microsoft.com/en-US/library/vstudio/ms229042.aspx

о Lazy<T>

универсальный Lazy<T> класс был создан именно для того, что плакат хочет, вижу ленивый Инициализация at http://msdn.microsoft.com/en-us/library/dd997286 (v=против 100).aspx. Если у вас есть более старые версии .NET, вы должны использовать шаблон кода, проиллюстрированный в вопросе. Этот шаблон кода стал настолько распространенным, что Microsoft сочла нужным включить класс в последние библиотеки .NET, чтобы упростить его реализацию. Кроме того, если ваша реализация нуждается в потокобезопасности, вы должны добавить ее.

примитивные типы данных и простые Классы

очевидно, вы не собираетесь использовать ленивую инициализацию для примитивного типа данных или просто использовать класс List<string>.

прежде чем комментировать о Lazy

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

прежде чем комментировать микро-оптимизации

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

Относительно Пользовательских Интерфейсов

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

"сеттеры"

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

коллекции

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

Сложные Классы

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

и наконец

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


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

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

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


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

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


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


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


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

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

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

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
    }

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

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

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

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


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

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

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


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

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

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