Инициализировать поля класса в конструкторе или при объявлении?


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
Обратите внимание, что для структур у вас не может быть инициализаторов полей экземпляра, поэтому у вас нет выбора, кроме как использовать конструктор.
йойо

Так как вы воспитали «лучшую практику»: assignice.com/blog/archives/5164 , forbes.com/sites/mikemyatt/2012/08/15/best-practices-arent/…
Стивен С.

Ответы:


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

36
Я не согласен с теми, кто не согласен с правилом 1. Можно ожидать, что другие выучат язык C #. Точно так же, как мы не комментируем каждый foreachцикл словами «это повторяет следующее для всех элементов в списке», нам не нужно постоянно пересчитывать значения по умолчанию в C #. Нам также не нужно делать вид, что C # имеет неинициализированную семантику. Поскольку отсутствие значения имеет четкое значение, его можно не указывать. Если это идеально, чтобы быть явным, вы всегда должны использовать newпри создании новых делегатов (как это требовалось в C # 1). Но кто это делает? Язык предназначен для добросовестных программистов.
Эдвард Брей,

149

В C # это не имеет значения. Два примера кода, которые вы даете, абсолютно эквивалентны. В первом примере компилятор C # (или это CLR?) Создаст пустой конструктор и инициализирует переменные, как если бы они были в конструкторе (в этом есть небольшой нюанс, который объясняет Джон Скит в комментариях ниже). Если конструктор уже существует, любая инициализация «выше» будет перемещена в его верхнюю часть.

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


4
Это не правильно, если вы решите инициализировать класс с помощью GetUninitializedObject. Все, что находится в ctor, не будет затронуто, но объявления полей будут запущены.
Wolf5

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

2
В самом деле? Клянусь, я получил эту информацию из CLR Рихтера через C # (второе издание, я думаю), и суть его заключалась в том, что это был синтаксический сахар (возможно, я прочитал его неправильно?), А CLR просто заклинило переменные в конструкторе. Но вы утверждаете, что это не так, то есть инициализация члена может произойти до инициализации ctor в безумном сценарии вызова виртуального базового ctor и переопределения в рассматриваемом классе. Правильно ли я понял? Вы только что узнали это? Озадаченный этим актуальным комментарием к 5-летнему сообщению (OMG это было 5 лет?).
Ужасно

@Quibblesome: дочерний конструктор класса будет содержать цепочечный вызов родительского конструктора. Языковой компилятор может включать в себя столько кода или меньше, сколько ему угодно, при условии, что родительский конструктор вызывается ровно один раз во всех путях кода, и до этого вызова строится ограниченный объект, находящийся в процессе конструирования. Одна из моих неприятностей с C # заключается в том, что, хотя он может генерировать код, который инициализирует поля, и код, который использует параметры конструктора, он не предлагает механизма для инициализации полей на основе параметров конструктора.
Суперкат

1
@Quibblesome, прежний будет проблемой, если значение назначено и передано методу WCF. Который сбросит данные к типу данных модели по умолчанию. Лучше всего выполнить инициализацию в конструкторе (позже).
Пранеш Джанартанан

16

Семантика C # здесь немного отличается от Java. В C # присваивание в объявлении выполняется перед вызовом конструктора суперкласса. В Java это делается сразу же после того, как это позволяет использовать «this» (особенно полезно для анонимных внутренних классов), и означает, что семантика двух форм действительно совпадает.

Если можете, сделайте поля окончательными.


15

Я думаю, что есть одна оговорка. Однажды я допустил такую ​​ошибку: внутри производного класса я пытался «инициализировать при объявлении» поля, унаследованные от абстрактного базового класса. В результате было создано два набора полей, одно из которых является «базовым», а другое - только что объявленными, а отладка заняла у меня довольно много времени.

Урок: чтобы инициализировать унаследованные поля, вы должны сделать это внутри конструктора.


Итак, если вы ссылаетесь на поле derivedObject.InheritedField, ссылается ли оно на ваш базовый или производный?
RayLuo

6

Предполагая тип в вашем примере, определенно предпочтите инициализировать поля в конструкторе. Исключительные случаи:

  • Поля в статических классах / методах
  • Поля, типизированные как static / final / et al

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


Почему это "определенно" так? Вы предоставили только предпочтение стиля без объяснения причин. Ой , подождите, не говоря уже , по @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.