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


413

Нещодавно я програмував на C # та Java, і мені цікаво, де найкраще місце ініціалізувати поля мого класу.

Чи потрібно це робити при декларації ?:

public class Dice
{
    private int topFace = 1;
    private Random myRand = new Random();

    public void Roll()
    {
       // ......
    }
}

чи в конструкторі ?:

public class Dice
{
    private int topFace;
    private Random myRand;

    public Dice()
    {
        topFace = 1;
        myRand = new Random();
    }

    public void Roll()
    {
        // .....
    }
}

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


3
Зауважте, що для структур у вас не може бути ініціалізаторів польових примірників, тому у вас немає іншого вибору, крім використання конструктора.
yoyo

Оскільки ви виховували "найкращу практику": satisfice.com/blog/archives/5164 , forbes.com/sites/mikemyatt/2012/08/15/best-practices-arent/…
Стівен C

Відповіді:


310

Мої правила:

  1. Чи не форматувати значення за замовчуванням в оголошенні ( null, false, 0, 0.0...).
  2. Віддайте перевагу ініціалізації в декларації, якщо у вас немає параметра конструктора, який змінює значення поля.
  3. Якщо значення поля змінюється через параметр конструктора, ставлять ініціалізацію в конструктори.
  4. Будьте послідовними у своїй практиці (найважливіше правило).

4
Я очікую, що kokos означає, що ви не повинні ініціалізувати членів до їх значень за замовчуванням (0, false, null тощо), оскільки компілятор зробить це для вас (1.). Але якщо ви хочете ініціалізувати поле до чогось іншого, ніж його значення за замовчуванням, вам слід зробити це в декларації (2.). Я думаю, що саме слово "за замовчуванням" може бентежити вас.
Рікі Гельгессон

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

32
Типовим значенням типу default(T)завжди є значення, яке має внутрішнє бінарне подання 0.
Олів'є Якот-Дескомб

15
Наче вам подобається правило 1 чи ні, воно не може бути використане з полями для читання тільки, які мають бути явно ініціалізовані до моменту закінчення конструктора.
yoyo

36
Я не погоджуюся з тими, хто не згоден з правилом 1. Добре очікувати, що інші вивчать мову C #. Так само, як ми не коментуємо кожен foreachцикл із "це повторює наступне для всіх елементів у списку", нам не потрібно постійно перезавантажувати значення C # за замовчуванням. Нам також не потрібно робити вигляд, що C # має неініціалізовану семантику. Оскільки відсутність значення має чіткий сенс, добре його залишити. Якщо це ідеально, щоб бути явним, ви завжди повинні використовувати newдля створення нових делегатів (як це було потрібно в C # 1). Але хто це робить? Мова розрахована на сумлінні кодери.
Едвард Брей

149

У C # це не має значення. Два зразки коду, які ви даєте, абсолютно еквівалентні. У першому прикладі компілятор C # (чи це CLR?) Сконструює порожній конструктор та ініціалізує змінні так, як ніби вони були в конструкторі (є невеликий нюанс цього, що Джон Скіт пояснює в коментарях нижче). Якщо вже є конструктор, то будь-яка ініціалізація "вгорі" буде переміщена у верхній частині.

З точки зору найкращої практики, перша менш схильна до помилок, ніж друга, тому що хтось може легко додати інший конструктор і забути ланцюжок.


4
Це неправильно, якщо ви вирішили ініціалізувати клас із GetUninitializedObject. Те, що є в ctor, не буде торкатися, але декларації поля будуть запущені.
Вовк5

28
Насправді це має значення. Якщо конструктор базового класу викликає віртуальний метод (який, як правило, погана ідея, але може трапитися), який переосмислюється у похідному класі, то, використовуючи ініціалізатори змінних екземплярів, змінна буде ініціалізована, коли метод викликається - тоді як використання ініціалізації в конструктором, їх не буде. (Ініціалізатори змінних екземплярів виконуються до виклику конструктора базового класу.)
Джон Скіт

2
Дійсно? Я клянусь, що я схопив цю інформацію з CLR Ріхтера через C # (друге видання, я думаю), і суть її полягала в тому, що це синтаксичний цукор (я, можливо, прочитав його неправильно?), І CLR просто заклинив змінні в конструкторі. Але ви констатуєте, що це не так, тобто ініціалізація члена може запуститись перед ініціалізацією ctor у шаленому сценарії виклику віртуального в базовому ctor та переопрацюванні у відповідному класі. Я правильно зрозумів? Ви щойно це дізналися? Спантеличений цей останній коментар до 5-річної посади (OMG це було 5 років?).
Дивовижна

@Quibblesome: Конструктор дочірнього класу буде містити ланцюговий виклик батьківському конструктору. Компілятор мови може включати стільки або мало коду до цього, скільки йому подобається, за умови, що батьківський конструктор викликається рівно один раз у всіх кодових шляхах, і обмежене використання об'єкта, що будується до цього виклику. Одне з моїх неприємностей щодо C # - це те, що, хоча він може генерувати код, який ініціалізує поля, та код, що використовує параметри конструктора, він не пропонує механізму ініціалізації полів на основі параметрів конструктора.
supercat

1
@Quibblesome, колишній буде проблемою, якщо значення присвоєно та передано методу WCF. Що дозволить скинути дані до типового типу даних моделі. Найкраща практика - це зробити ініціалізацію в конструкторі (пізніше).
Пранеш Джартартанан

16

Семантика C # дещо відрізняється від Java. В C # призначення в декларації виконується перед викликом конструктора надкласового класу. У Java це робиться негайно, після чого дозволяє використовувати «це» (особливо корисно для анонімних внутрішніх класів), і означає, що семантика двох форм дійсно збігається.

Якщо можете, зробіть поля остаточними.


15

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

Урок: щоб ініціалізувати успадковані поля, ви зробите це всередині конструктора.


Отже, якщо ви посилаєте поле на derivedObject.InheritedField, чи воно посилається на ваше базове або похідне?
RayLuo

6

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

  • Поля в статичних класах / методах
  • Поля, введені як статичні / кінцеві / та ін

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


Чому це "точно" так? Ви надали лише перевагу стилю, не пояснюючи його міркувань. Ой , зачекайте, не кажучи вже , по @quibblesome «s відповідь , вони" абсолютно еквівалентні ", так що це дійсно просто особисті переваги стилю.
RayLuo

4

Що робити, якщо я вам сказав, це залежить?

Я взагалі все ініціалізую і роблю це послідовно. Так, це занадто явно, але це також трохи простіше в обслуговуванні.

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

У системі реального часу я запитую, чи мені взагалі потрібна змінна чи константа.

А в C ++ я часто роблю поруч із відсутністю ініціалізації в будь-якому місці та переміщую його у функцію Init (). Чому? Ну а в C ++, якщо ви ініціалізуєте щось, що може кинути виняток під час побудови об'єкта, ви відкриваєтесь для витоку пам'яті.


4

Існує багато і різноманітних ситуацій.

Мені просто потрібен порожній список

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

public class CsvFile
{
    private List<CsvRow> lines = new List<CsvRow>();

    public CsvFile()
    {
    }
}

Я знаю цінності

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

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = new List<string>() {"usernameA", "usernameB"};
    }
}

або

public class AdminTeam
{
    private List<string> usernames;

    public AdminTeam()
    {
         usernames = GetDefaultUsers(2);
    }
}

Порожній список з можливими значеннями

Іноді я очікую порожній список за замовчуванням з можливістю додавання значень через інший конструктор.

public class AdminTeam
{
    private List<string> usernames = new List<string>();

    public AdminTeam()
    {
    }

    public AdminTeam(List<string> admins)
    {
         admins.ForEach(x => usernames.Add(x));
    }
}

3

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


3

Конструкція C # дозволяє припустити, що вбудована ініціалізація є кращою, або вона не буде в мові. Кожен раз, коли ви можете уникнути перехресних посилань між різними місцями в коді, вам, як правило, краще.

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

✓ ВАЖИТИ ініціалізацію статичних полів в ряд, а не явно за допомогою статичних конструкторів, оскільки час виконання може оптимізувати продуктивність типів, які не мають чітко визначеного статичного конструктора.

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


2

Бути послідовним важливо, але це питання, щоб задати собі питання: "Чи є у мене конструктор для чого-небудь іншого?"

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

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

Отже, як багато хто сказав, це залежить від вашого використання. Зробіть це просто і не робіть нічого зайвого, чого вам не доведеться.


2

Розгляньте ситуацію, коли у вас є більше одного конструктора. Чи буде ініціалізація різною для різних конструкторів? Якщо вони будуть однакові, то навіщо повторювати для кожного конструктора? Це відповідає заяві kokos, але може не бути пов'язано з параметрами. Скажімо, наприклад, ви хочете зберегти прапор, який показує, як створений об’єкт. Тоді цей прапор буде ініціалізований по-різному для різних конструкторів незалежно від параметрів конструктора. З іншого боку, якщо ви повторите ту саму ініціалізацію для кожного конструктора, ви залишаєте можливість того, що ви (ненавмисно) змінюєте параметр ініціалізації в деяких конструкторах, але не в інших. Отже, основна концепція тут полягає в тому, що загальний код повинен мати спільне розташування і не може бути повторений в різних місцях.


1

Існує невелика користь від встановлення значення в декларації. Якщо встановити його в конструкторі, він фактично встановлюється двічі (спочатку до значення за замовчуванням, а потім скидає в ctor).


2
У полі C # поля завжди завжди встановлюють значення за замовчуванням. Наявність ініціалізатора не має значення.
Джефрі Л Уітлідж

0

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

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


0

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

class MyGeneric<T>
{
    T data;
    //T data = ""; // <-- ERROR
    //T data = 0; // <-- ERROR
    //T data = null; // <-- ERROR        

    public MyGeneric()
    {
        // All of the above errors would be errors here in constructor as well
    }
}

А спеціальним методом ініціалізації загального поля до його значення за замовчуванням є наступний:

class MyGeneric<T>
{
    T data = default(T);

    public MyGeneric()
    {           
        // The same method can be used here in constructor
    }
}

0

Коли вам не потрібна певна логіка чи помилки:

  • Ініціалізуйте поля класів при оголошенні

Коли вам потрібна певна логіка чи помилка:

  • Ініціалізуйте поля класу в конструкторі

Це добре працює, коли доступне значення ініціалізації та ініціалізація може бути розміщена в одному рядку. Однак ця форма ініціалізації має обмеження через свою простоту. Якщо для ініціалізації потрібна певна логіка (наприклад, обробка помилок або цикл for для заповнення складного масиву), просте призначення недостатнє. Змінні екземплярів можна ініціалізувати в конструкторах, де можна використовувати помилки чи іншу логіку.

З https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.