Хороша чи погана практика? Ініціалізація об'єктів у геттері


167

У мене є дивна звичка, здається ... щонайменше, за словами мого колеги. Ми разом працювали над невеликим проектом. Я написав класи (спрощений приклад):

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

Чи є заперечення проти цієї практики, окрім особистих уподобань?

ОНОВЛЕННЯ: Я розглянув багато різних думок щодо цього питання і буду стояти за свою прийняту відповідь. Однак зараз я прийшов до набагато кращого розуміння концепції і я в змозі вирішити, коли її використовувати, а коли ні.

Мінуси:

  • Питання безпеки нитки
  • Не підпорядковується запиту "setter", коли передане значення є нульовим
  • Мікрооптимізація
  • Обробка винятків повинна відбуватися в конструкторі
  • Потрібно перевірити на предмет null в класі класу

Плюси:

  • Мікрооптимізація
  • Властивості ніколи не повертаються до нуля
  • Затримати або уникати завантаження "важких" предметів

Більшість мінусів не застосовні до моєї поточної бібліотеки, проте мені доведеться перевірити, чи "мікрооптимізація" насправді щось оптимізує.

ОСТАННЄ ОНОВЛЕННЯ:

Гаразд, я змінив свою відповідь. Моє первісне питання полягав у тому, чи це хороша звичка чи ні. І я зараз переконаний, що це не так. Можливо, я все-таки буду використовувати його в деяких частинах свого поточного коду, але не безумовно і точно не весь час. Тому я втрачу звичку і подумаю про це, перш ніж її використовувати. Дякую всім!


14
Це ледачий навантажувальний малюнок, він не зовсім дає вам чудову користь, але все одно це добре.
Махінарій

28
Ледача інстанція має сенс, якщо у вас є вимірюваний вплив на продуктивність, або якщо ці члени рідко використовуються і споживають непомірний об'єм пам'яті, або якщо потрібно тривалий час, щоб їх інстанціювати і хочете робити це лише на вимогу. У будь-якому випадку не забудьте врахувати проблеми безпеки потоку (ваш поточний код - ні ) і подумайте про використання наданого класу Lazy <T> .
Кріс Сінклер

10
Я думаю, що це питання краще підходить для codereview.stackexchange.com
Eduardo Brites

7
@PLB це не однотонний шаблон.
Колін Макей

30
Я здивований, що ніхто не згадав про серйозну помилку з цим кодом. У вас є державна власність, яку я можу встановити ззовні. Якщо я встановлю його на NULL, ви завжди створите новий об’єкт і проігноруєте мій доступ до налаштування. Це може бути дуже серйозною помилкою. Для приватних об'єктів це може бути добре. Особисто мені не подобається робити такі передчасні оптимізації. Додає складність без додаткової вигоди.
SolutionYogi

Відповіді:


170

У вас тут є наївна реалізація "лінивої ініціалізації".

Коротка відповідь:

Використання лінивої ініціалізації беззастережно - це не дуже гарна ідея. Він має свої місця, але потрібно враховувати наслідки, які має це рішення.

Передумови та пояснення:

Конкретна реалізація:
Давайте спочатку розглянемо ваш конкретний зразок, і чому я вважаю його реалізацію наївною:

  1. Це порушує Принцип найменшого сюрпризу (POLS) . Коли значення присвоюється властивості, очікується, що це значення повернеться. У вашому впровадженні це не так null:

    foo.Bar = null;
    Assert.Null(foo.Bar); // This will fail
  2. У ньому представлено досить багато проблем із потоком: два абонента foo.Barна різних потоках потенційно можуть отримати два різні екземпляри, Barі один з них буде без з'єднання з Fooекземпляром. Будь-які зміни, внесені до цього Barпримірника, мовчки втрачаються.
    Це ще один випадок порушення ПОЛС. Коли доступ до лише збереженої вартості властивості, очікується, що він є безпечним для потоків. Хоча ви можете стверджувати, що клас просто не є безпечним для потоків - включаючи одержувач вашої власності - вам доведеться документувати це належним чином, оскільки це не нормальний випадок. Крім того, вступ цього питання є непотрібним, як ми побачимо найближчим часом.

Взагалі:
зараз час розглянути ледачу ініціалізацію в цілому:
Ініціалізація лінь зазвичай використовується для затримки побудови об'єктів, які потребують тривалого часу для побудови, або які займають багато пам’яті, коли повністю побудовані.
Це дуже вагома причина використання лінивої ініціалізації.

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

Навіть при розгляді цих двох моментів у здійсненні лінивої властивості наступні моменти є загальними проблемами цієї закономірності:

  1. Будівництво об'єкта може бути невдалим, що спричинить виняток із власника нерухомості. Це ще одне порушення ПОЛС, і тому його слід уникати. Навіть у розділі про властивості в "Посібнику з дизайну для розробки бібліотек класів" чітко зазначено, що особи, які отримують властивості, не повинні кидати винятки:

    Уникайте викидів винятків із власників нерухомості.

    Отримати власність мають бути простими операціями без будь-яких передумов. Якщо отримувач може кинути виняток, розгляньте, як перестановка властивості є методом.

  2. Автоматичні оптимізації компілятором завдають шкоди, а саме вставки та передбачення гілок. Будь ласка, дивіться відповідь Білла К. для детального пояснення.

Висновок цих пунктів такий:
Для кожної окремої властивості, яка реалізується ліниво, ви повинні були враховувати ці моменти.
Це означає, що це рішення, що стосується конкретного випадку, і не може сприйматися як загальна найкраща практика.

Ця закономірність має своє місце, але вона не є загальною найкращою практикою при проведенні занять. Його не слід використовувати беззастережно через вказані вище причини.


У цьому розділі я хочу обговорити деякі моменти, які інші висунули як аргументи для використання безладної ініціалізації:

  1. Серіалізація:
    EricJ заявляє в одному коментарі:

    Об'єкт, який може бути серіалізований, не буде викликати його кондуктора, коли він буде деріаріалізований (залежить від серіалізатора, але багато поширених поводяться так). Введення коду ініціалізації в конструктор означає, що вам потрібно надати додаткову підтримку для дезаріалізації. Ця закономірність дозволяє уникнути спеціального кодування.

    У цьому аргументі є кілька проблем:

    1. Більшість об’єктів ніколи не будуть серіалізовані. Додавання якоїсь підтримки для неї, коли вона не потрібна, порушує YAGNI .
    2. Коли клас повинен підтримувати серіалізацію, існують способи включити його без вирішення, яке не має нічого спільного з серіалізацією на перший погляд.
  2. Мікрооптимізація: Ваш головний аргумент полягає в тому, що ви хочете сконструювати об'єкти лише тоді, коли хтось дійсно звертається до них. Отже, ви насправді говорите про оптимізацію використання пам'яті.
    Я не згоден з цим аргументом з наступних причин:

    1. У більшості випадків ще кілька об'єктів у пам'яті не впливають ні на що. Сучасні комп'ютери мають достатньо способу пам'яті. Без випадків фактичних проблем, підтверджених профілером, це переддозріла оптимізація і є вагомі причини для цього.
    2. Я визнаю той факт, що іноді такий вид оптимізації виправданий. Але навіть у цих випадках лінива ініціалізація не здається правильним рішенням. Проти цього говорять дві причини:

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

4
@JohnWillemse: Це проблема вашої архітектури. Вам слід переробляти свої заняття таким чином, щоб вони були меншими та більш зосередженими. Не створюйте один клас для 5 різних речей / завдань. Створіть замість цього 5 класів.
Даніель Гільгарт

26
@JohnWillemse Можливо, тоді вважаємо це випадком передчасної оптимізації. Якщо у вас є мірне місце продуктивності / пам’яті, я б рекомендував проти цього, оскільки це збільшує складність і вводить проблеми з ниткою.
Кріс Сінклер

2
+1, це не гарний вибір дизайну для 95% класів. Ледача ініціалізація має свої плюси, але не повинна бути узагальнена для ВСІХ властивостей. Це додає складності, труднощів з читанням коду, питань безпеки потоку ... без оптимізації в 99% випадків. Крім того, як в коментарі сказав SolutionYogi, код ОП є помилковим, що підтверджує, що ця модель не є тривіальною для реалізації та її слід уникати, якщо насправді не потрібна лінива ініціалізація.
ken2k

2
@DanielHilgarth Дякую за те, що ви пройшли весь шлях, щоб записати (майже) все неправильно з використанням цього шаблону беззастережно. Чудова робота!
Олексій

1
@DanielHilgarth Ну так і ні. Порушення тут питання, так що так. Але також "ні", тому що POLS - це суто принцип, який ви, мабуть, не здивуєте кодом . Якщо Foo не виявився поза вашою програмою, це ризик ви можете взяти чи ні. У цьому випадку я майже можу гарантувати, що ви зрештою здивуєтесь , оскільки ви не контролюєте спосіб доступу до властивостей. Ризик просто став помилкою, і ваш аргумент щодо nullсправи став набагато сильнішим. :-)
атлас

49

Це хороший вибір дизайну. Настійно рекомендується для бібліотечного коду або основних класів.

Це називається деякою "ледачою ініціалізацією" або "затримкою ініціалізації", і, як правило, всі вважають хорошим вибором дизайну.

По-перше, якщо ви ініціалізуєте в декларації змінні рівня класу або конструктор, тоді, коли ваш об’єкт побудований, ви маєте накладні витрати на створення ресурсу, який ніколи не буде використаний.

По-друге, ресурс створюється лише за потреби.

По-третє, ви уникаєте збору сміття, який не був використаний.

Нарешті, простіше обробляти винятки ініціалізації, які можуть траплятися у властивості, а потім винятки, які виникають під час ініціалізації змінних рівня класу чи конструктора.

З цього правила є винятки.

Що стосується аргументу продуктивності додаткової перевірки на ініціалізацію у властивості "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, щоб полегшити реалізацію шаблону. Крім того, якщо для вашої реалізації потрібна безпека потоку, вам доведеться додати її.

Примітивні типи даних та прості класи

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

Перш ніж коментувати про Ледачий

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

Перш ніж коментувати мікро-оптимізацію

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

Щодо користувальницьких інтерфейсів

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

"Встановлювачі"

Я ніколи не використовував властивість set ("setters") для будь-якого ледачого завантаженого майна. Тому ти ніколи не дозволиш foo.Bar = null;. Якщо вам потрібно встановити, Barя створив би метод, який називається, SetBar(Bar value)а не використовувати ініціалізацію лінь

Колекції

Властивості колекції класів завжди ініціалізуються при оголошенні, оскільки вони ніколи не повинні бути нульовими.

Комплексні заняття

Дозвольте повторити це по-іншому, ви використовуєте ледачу ініціалізацію для складних класів. Які зазвичай, погано розроблені класи.

Нарешті

Я ніколи не казав робити це для всіх класів або у всіх випадках. Це шкідлива звичка.


6
Якщо ви можете зателефонувати foo.Bar кілька разів у різні потоки без будь-якого втручання значення, але отримаєте різні значення, то у вас є хижий бідний клас.
Лже Раян

25
Я думаю, що це неправильне правило, без багатьох міркувань. Якщо Bar не є знаряддям із знань, це зайва мікрооптимізація. І якщо Bar є ресурсомістким, в .net вбудований безпечний потік Lazy <T>.
Ендрю Ханлон

10
"легше обробляти винятки ініціалізації, які можуть виникати у властивості, а потім винятки, які виникають під час ініціалізації змінних рівня класу або конструктора." - гаразд, це нерозумно. Якщо об'єкт не може бути ініціалізований з якихось причин, я хочу знати якнайшвидше. Тобто негайно, коли вона побудована. Існують хороші аргументи для використання лінивої ініціалізації, але я не думаю, що це корисна ідея для повного використання .
мільйоз

20
Я дуже переживаю, що інші розробники побачать цю відповідь і вважають, що це справді хороша практика (о, хлопче). Це дуже погана практика, якщо ви використовуєте її беззастережно. Окрім того, що вже було сказано, ви ускладнюєте життя кожного (для розробників клієнтів та для розробників технічного обслуговування) настільки малий прибуток (якщо взагалі якийсь виграш). Вам слід почути з плюсів: Дональд Кнут у серії «Мистецтво комп’ютерного програмування», як відомо, сказав, що «передчасна оптимізація - корінь усього зла». Те, що ти робиш, - це не тільки зло, це дьявольське!
Олексій

4
Є так багато індикаторів, що ви вибираєте неправильну відповідь (і неправильне рішення програмування). У вашому списку більше мінусів, ніж плюсів. У вас більше людей, які проти нього проти, ніж за це. Більш досвідчені члени цього веб-сайту (@BillK та @DanielHilgarth), які розмістили в цьому запитанні, проти. Ваш колега вже сказав вам, що це неправильно. Серйозно, це неправильно! Якщо я зловив когось із розробників моєї команди (я керівник команди), що робить це, він візьме 5-хвилинний тайм-аут, а потім прочитає лекції, чому він ніколи цього не повинен робити.
Олексій

17

Чи вважаєте ви реалізувати таку модель, використовуючи Lazy<T>?

На додаток до простого створення об'єктів з ледачим завантаженням, ви отримуєте безпеку потоку під час ініціалізації об'єкта:

Як говорили інші, ви ледаче завантажуєте об'єкти, якщо вони дуже важкі для ресурсів або потрібен певний час, щоб завантажити їх під час побудови об'єкта.


Дякую, я зараз це розумію, і я обов'язково буду вивчати Lazy<T>зараз, і утриматися від використання способу, який я завжди робив.
Джон Віллемс

1
Ви не отримаєте безпеки магічної нитки ... вам все одно доведеться думати про це. Від MSDN: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.
Ерік Дж.

@EricJ. Звичайно, звичайно. Ви отримуєте безпеку потоків лише при ініціалізації об'єкта, але пізніше вам потрібно мати справу з синхронізацією, як і будь-який інший об’єкт.
Matías Fidemraizer

9

Я думаю, це залежить від того, що ви ініціалізуєте. Я, мабуть, не зробив би це для списку, оскільки вартість будівництва досить мала, тому вона може йти в конструктор. Але якби це був попередньо заповнений список, я, мабуть, не став би, поки це було потрібно вперше.

В основному, якщо вартість будівництва перевищує вартість виконання умовної перевірки кожного доступу, тоді ліниві створюють її. Якщо ні, зробіть це в конструкторі.


Дякую! Що має сенс.
Джон Віллемс

9

Мінус, який я бачу, полягає в тому, що якщо ви хочете запитати, чи є Барс недійсним, цього б ніколи не було, і ви створили б там список.


Я не думаю, що це недолік.
Пітер Порфі

Чому це мінус? Просто перевірте з будь-яким замість нуля. if (! Foo.Bars.Any ())
s.meijer

6
@PeterPorfy: Це порушує POLS . Ви ставите null, але не повертайте його назад. Зазвичай ви припускаєте, що отримаєте ту саму цінність, яку ви ввели у власність.
Даніель Гільгарт

@DanielHilgarth Ще раз спасибі Це дуже вагомий аргумент, який я раніше не розглядав.
Джон Віллемс

6
@AMissico: Це не складена концепція. Так само, як очікується, що натискання кнопки поруч із вхідними дверима задзвонить у дверний дзвінок, очікується, що речі, схожі на властивості, ведуть себе як властивості. Відкривання дверцят для пасток під ногами - дивовижна поведінка, особливо якщо кнопка не позначена як такою.
Брайан Боттчер

8

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


1
Я згоден, і своє питання я трохи відредагував. Я очікую, що повний ланцюжок базових конструкторів займе більше часу, ніж інстанціювання класів, лише коли вони потрібні.
Джон Віллемс

8

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

Хоча це дуже хороший зразок, який потрібно використовувати в певних ситуаціях (наприклад, коли об'єкт ініціалізується з бази даних), це ВІДПОВІДНА звичка потрапляти.

Однією з найкращих речей щодо об’єкта є те, що він ображає безпечне середовище, на яке довіряють. Найкращий випадок, якщо ви зробите якомога більше полів «Фінальні», заповнивши їх усіма конструкторами. Це робить ваш клас досить бездоганним. Дозволити змінити поля через сетери трохи менше, але не страшно. Наприклад:

клас SafeClass
{
    Ім'я рядка = "";
    Цілий вік = 0;

    public void setName (Рядок newName)
    {
        assert (newName! = null)
        name = newName;
    } // дотримуйтесь цієї моделі за віком
    ...
    public String toString () {
        Рядок s = "Безпечний клас має ім'я:" + ім'я + "та вік:" + вік
    }
}

З вашим шаблоном метод toString виглядатиме так:

    if (ім'я == null)
        киньте новий IllegalStateException ("SafeClass потрапив у незаконне стан! ім'я недійсне")
    якщо (вік == нуль)
        кинути нову IllegalStateException ("SafeClass потрапив у незаконне стан! вік є нульовим")

    public String toString () {
        Рядок s = "Безпечний клас має ім'я:" + ім'я + "та вік:" + вік
    }

Не тільки це, але вам потрібні нульові перевірки скрізь, де ви, можливо, будете використовувати цей об'єкт у своєму класі (Поза межами вашого класу є безпечним через нульову перевірку в getter, але вам слід в основному використовувати членів класів всередині класу)

Також ваш клас постійно знаходиться в невизначеному стані - наприклад, якщо ви вирішили перетворити цей клас у сплячий клас, додавши кілька анотацій, як би це зробити?

Якщо ви приймаєте будь-яке рішення на основі мікрооптимізації без вимог і тестування, це майже напевно неправильне рішення. Насправді, є дійсно хороший шанс, що ваша модель насправді сповільнює систему навіть за найідеальніших обставин, оскільки оператор if може спричинити збій передбачення гілки в процесорі, що сповільнить все набагато набагато більше разів, ніж просто призначити значення в конструкторі, якщо об'єкт, який ви створюєте, не є досить складним або не надходить із віддаленого джерела даних.

Для прикладу проблеми передбачення brance (яку ви виникаєте неодноразово, ні раз один раз), дивіться першу відповідь на це дивовижне запитання: Чому швидше обробляти відсортований масив, ніж несортований масив?


Дякуємо за ваш внесок У моєму випадку в жодному з класів немає методів, які можуть знадобитися для перевірки нуля, тому це не проблема. Я візьму до уваги ваші інші заперечення.
Джон Віллемс

Я насправді цього не розумію. Це означає, що ви не використовуєте своїх членів у класах, де вони зберігаються, - що ви просто використовуєте класи як структури даних. Якщо це так, ви можете прочитати javaworld.com/javaworld/jw-01-2004/jw-0102-toolbox.html, який містить хороший опис того, як ваш код можна покращити, уникаючи зовнішнього маніпулювання станом об’єкта. Якщо ви маніпулюєте ними внутрішньо, як це зробити, не перевіряючи нуль все повторно?
Білл К

Частини цієї відповіді хороші, але частини здаються надуманими. Зазвичай при використанні цього шаблону toString()дзвінки getName(), а не використання nameбезпосередньо.
Ізката

@BillK Так, класи - це величезна структура даних. Вся робота виконується в статичних класах. Я перевірю статтю за посиланням. Дякую!
Джон Віллемс

1
@izkata Насправді всередині класу здається, що це неприємність, коли ви використовуєте геттер чи ні, більшість місць, в яких я працював, використовували учасника безпосередньо. Це в стороні, якщо ви завжди використовуєте геттер, метод if () є ще шкідливішим, оскільки збої в прогнозуванні гілок трапляться частіше І через розгалуження час виконання, швидше за все, матиме більше проблем із введенням геттера. Проте, все це суперечить одкровенням Джона про те, що вони є структурами даних та статичними класами, і саме це я б найбільше хвилював.
Білл К

4

Дозвольте лише додати ще одну точку до багатьох хороших моментів, зроблених іншими ...

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

Це може бути або не бути проблемою (залежно від побічних ефектів), але слід пам’ятати про щось.


2

Ви впевнені, що Фоо повинен взагалі щось створювати?

Мені здається смердючим (хоча і не обов’язково неправильним ), щоб дозволити Фоо взагалі щось інстанціювати. Якщо тільки Foo не є виразним призначенням бути заводом, він не повинен створювати власних співробітників, а натомість вводити їх у свій конструктор .

Якщо ж метою Фоо було створити екземпляри типу Bar, то я не бачу нічого поганого в тому, щоб це робити ліниво.


4
@BenjaminGruenbaum Ні, не дуже. І з повагою, навіть якби це було, який саме пункт ви намагалися зробити?
KaptajnKold
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.