Inicializar campos de classe no construtor ou na declaração?

413

Estive programando em C # e Java recentemente e estou curioso para saber onde o melhor lugar é inicializar meus campos de classe.

Devo fazê-lo na declaração ?:

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

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

ou em um construtor ?:

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

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

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

Estou realmente curioso sobre o que alguns de vocês veteranos acham que é a melhor prática. Eu quero ser consistente e seguir uma abordagem.

mmcdole
fonte
3
Observe que para estruturas você não pode ter inicializadores de campo de instância; portanto, você não tem escolha a não ser usar o construtor.
yoyo

Respostas:

310

Minhas regras:

  1. Não inicializar com os valores padrão na declaração ( null, false, 0,0.0 ...).
  2. Prefira a inicialização na declaração se você não tiver um parâmetro construtor que altere o valor do campo.
  3. Se o valor do campo for alterado devido a um parâmetro do construtor, coloque a inicialização nos construtores.
  4. Seja consistente em sua prática (a regra mais importante).
kokos
fonte
4
Espero que kokos signifique que você não deve inicializar os membros com seus valores padrão (0, falso, nulo etc.), pois o compilador fará isso por você (1.). Mas se você deseja inicializar um campo para algo diferente de seu valor padrão, faça-o na declaração (2.). Eu acho que pode ser o uso da palavra "padrão" que confunde você.
Ricky Helgesson
95
Não concordo com a regra 1 - não especificando um valor padrão (independentemente de ter sido inicializado pelo compilador ou não), você está deixando o desenvolvedor adivinhar ou procurar a documentação sobre qual seria o valor padrão para esse idioma específico. Para fins de legibilidade, eu sempre especificaria o valor padrão.
James
32
O valor padrão de um tipo default(T)é sempre o valor que possui uma representação binária interna de 0.
Olivier Jacot-Descombes 13/03
15
Quer você goste ou não da regra 1, ela não pode ser usada com campos somente leitura, que devem ser explicitamente inicializados quando o construtor for concluído.
yoyo
36
Não concordo com aqueles que discordam da regra 1. Não há problema em esperar que outras pessoas aprendam a linguagem C #. Assim como não comentamos cada foreachloop com "isso repete o seguinte para todos os itens da lista", não precisamos reajustar constantemente os valores padrão do C #. Também não precisamos fingir que o C # possui semântica não inicializada. Como a ausência de um valor tem um significado claro, é bom deixar de fora. Se for ideal para ser explícito, você sempre deve usá-lo newao criar novos delegados (como era necessário em C # 1). Mas quem faz isso? A linguagem é projetada para codificadores conscienciosos.
Edward Brey 14/05
149

Em C #, isso não importa. Os dois exemplos de código que você fornece são totalmente equivalentes. No primeiro exemplo, o compilador C # (ou é o CLR?) Construirá um construtor vazio e inicializará as variáveis ​​como se estivessem no construtor (há uma leve nuance sobre isso que Jon Skeet explica nos comentários abaixo). Se já houver um construtor, qualquer inicialização "acima" será movida para a parte superior.

Em termos de melhores práticas, o primeiro é menos propenso a erros do que o último, pois alguém poderia facilmente adicionar outro construtor e esquecer de encadear.

Quibblesome
fonte
4
Isso não está correto se você optar por inicializar a classe com GetUninitializedObject. O que estiver no ctor não será tocado, mas as declarações de campo serão executadas.
Wolf5
28
Na verdade isso importa. Se o construtor da classe base chama um método virtual (que geralmente é uma má idéia, mas pode acontecer) que é substituído na classe derivada, usando inicializadores de variável de instância, a variável será inicializada quando o método for chamado - enquanto estiver usando inicialização no construtor, eles não serão. (Initializers variável de instância são executados antes de o construtor da classe base é chamado.)
Jon Skeet
2
Mesmo? Eu juro que peguei essas informações do CLR de Richter via C # (segunda edição, eu acho) e o essencial era que isso era açúcar sintático (eu posso ter lido errado?) E o CLR apenas inseriu as variáveis ​​no construtor. Mas você está afirmando que esse não é o caso, ou seja, a inicialização do membro pode ser disparada antes da inicialização do ctor no cenário louco de chamar um ctor virtual de base e ter uma substituição na classe em questão. Eu entendi corretamente? Você acabou de descobrir isso? Intrigado com este comentário atualizado em um post de 5 anos (OMG, já faz 5 anos?).
Quibblesome 13/08
@ Quibblesome: Um construtor de classe filho conterá uma chamada em cadeia para o construtor pai. Um compilador de linguagem é livre para incluir tanto ou pouco código antes como quiser, desde que o construtor pai seja chamado exatamente uma vez em todos os caminhos de código, e uso limitado seja feito do objeto em construção antes dessa chamada. Um dos meus aborrecimentos com o C # é que, embora possa gerar código que inicializa campos e código que usa parâmetros do construtor, ele não oferece nenhum mecanismo para inicializar campos com base nos parâmetros do construtor.
Supercat
1
@ Quibblesome, antigo será um problema, se o valor for atribuído e passado para um método WCF. O que redefinirá os dados para o tipo de dados padrão do modelo. A melhor prática é fazer a inicialização no construtor (mais tarde).
Pranesh Janarthanan
16

A semântica do C # difere um pouco do Java aqui. Em C #, a atribuição na declaração é realizada antes de chamar o construtor da superclasse. Em Java, isso é feito imediatamente após o qual permite que 'this' seja usado (particularmente útil para classes internas anônimas) e significa que a semântica das duas formas realmente corresponde.

Se puder, torne os campos finais.

Tom Hawtin - linha de orientação
fonte
15

Eu acho que há uma ressalva. Uma vez cometi esse erro: dentro de uma classe derivada, tentei "inicializar na declaração" os campos herdados de uma classe base abstrata. O resultado foi que existiam dois conjuntos de campos, um é "base" e outro são os recém-declarados, e me custou algum tempo para depurar.

A lição: para inicializar campos herdados , você faria isso dentro do construtor.

xji
fonte
Então, se você indicar o campo por derivedObject.InheritedField, ele está se referindo à sua base ou à derivada?
RayLuo 3/17/17
6

Assumindo o tipo no seu exemplo, definitivamente prefira inicializar os campos no construtor. Os casos excepcionais são:

  • Campos em classes / métodos estáticos
  • Campos digitados como static / final / et al

Eu sempre penso na lista de campos na parte superior de uma classe como o índice (o que está contido aqui, não como é usado) e o construtor como a introdução. Métodos, é claro, são capítulos.

Noel
fonte
Por que "definitivamente" é assim? Você forneceu apenas uma preferência de estilo, sem explicar seu raciocínio. Ah, espere, não se preocupe, de acordo com a resposta do @quibblesome , eles são "totalmente equivalentes", então é realmente apenas uma preferência de estilo pessoal.
RayLuo 3/17
4

E se eu te contar, depende?

Em geral, inicializo tudo e faço de maneira consistente. Sim, é excessivamente explícito, mas também é um pouco mais fácil de manter.

Se estamos preocupados com o desempenho, bem, eu inicializo apenas o que precisa ser feito e o coloco nas áreas que oferece mais retorno.

Em um sistema de tempo real, questiono se preciso mesmo da variável ou constante.

E em C ++, costumo fazer quase nenhuma inicialização em qualquer lugar e movo-a para uma função Init (). Por quê? Bem, em C ++, se você está inicializando algo que pode gerar uma exceção durante a construção do objeto, você se abre para vazamentos de memória.

Dan Blair
fonte
4

Existem muitas e várias situações.

Eu só preciso de uma lista vazia

A situação está clara. Eu só preciso preparar minha lista e impedir que uma exceção seja lançada quando alguém adicionar um item à lista.

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

    public CsvFile()
    {
    }
}

Eu sei os valores

Sei exatamente quais valores quero ter por padrão ou preciso usar outra lógica.

public class AdminTeam
{
    private List<string> usernames;

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

ou

public class AdminTeam
{
    private List<string> usernames;

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

Lista vazia com valores possíveis

Às vezes, espero uma lista vazia por padrão, com a possibilidade de adicionar valores por meio de outro construtor.

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

    public AdminTeam()
    {
    }

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

Em Java, um inicializador com a declaração significa que o campo é sempre inicializado da mesma maneira, independentemente de qual construtor é usado (se você tiver mais de um) ou dos parâmetros de seus construtores (se eles tiverem argumentos), embora um construtor possa posteriormente altere o valor (se não for final). Portanto, o uso de um inicializador com uma declaração sugere ao leitor que o valor inicial é o valor que o campo possui em todos os casos , independentemente de qual construtor é usado e independentemente dos parâmetros passados ​​para qualquer construtor. Portanto, use um inicializador com a declaração apenas se, e sempre se, o valor para todos os objetos construídos for o mesmo.

Raedwald
fonte
3

O design do C # sugere que a inicialização em linha é preferida ou não seria no idioma. Sempre que você pode evitar uma referência cruzada entre lugares diferentes no código, geralmente está melhor.

Há também a questão da consistência com a inicialização do campo estático, que precisa estar alinhado para obter o melhor desempenho. As diretrizes de design de estrutura para o design de construtores dizem o seguinte:

CONSIDERAR inicializando campos estáticos em linha, em vez de usar explicitamente construtores estáticos, porque o tempo de execução é capaz de otimizar o desempenho de tipos que não possuem um construtor estático definido explicitamente.

"Considerar" neste contexto significa fazê-lo, a menos que haja uma boa razão para não fazê-lo. No caso de campos estáticos do inicializador, um bom motivo seria se a inicialização fosse muito complexa para ser codificada em linha.

Edward Brey
fonte
2

Ser consistente é importante, mas esta é a pergunta que você deve fazer: "Tenho um construtor para mais alguma coisa?"

Normalmente, estou criando modelos para transferências de dados que a própria classe não faz nada além de funcionar como alojamento para variáveis.

Nesses cenários, geralmente não tenho métodos ou construtores. Seria tolo para mim criar um construtor com o objetivo exclusivo de inicializar minhas listas, especialmente porque posso inicializá-las de acordo com a declaração.

Então, como muitos outros disseram, isso depende do seu uso. Mantenha as coisas simples e não faça nada a mais que você não precise.

MeanJerry
fonte
2

Considere a situação em que você possui mais de um construtor. A inicialização será diferente para os diferentes construtores? Se eles serão os mesmos, por que repetir para cada construtor? Isso está de acordo com a instrução kokos, mas pode não estar relacionado aos parâmetros. Digamos, por exemplo, que você queira manter uma bandeira que mostre como o objeto foi criado. Em seguida, esse sinalizador seria inicializado de maneira diferente para diferentes construtores, independentemente dos parâmetros do construtor. Por outro lado, se você repetir a mesma inicialização para cada construtor, deixa a possibilidade de alterar (sem querer) o parâmetro de inicialização em alguns construtores, mas não em outros. Portanto, o conceito básico aqui é que o código comum deve ter um local comum e não ser potencialmente repetido em locais diferentes.

Joe Programmer
fonte
1

Há um pequeno benefício de desempenho ao definir o valor na declaração. Se você configurá-lo no construtor, ele está sendo configurado duas vezes (primeiro para o valor padrão e depois redefinido no ctor).

John Meagher
fonte
2
Em C #, os campos são sempre definidos primeiro como valor padrão. A presença de um inicializador não faz diferença.
Jeffrey L Whitledge
0

Normalmente, tento que o construtor não faça nada além de obter as dependências e inicializar os membros da instância relacionados com eles. Isso facilitará sua vida se você quiser testar suas aulas.

Se o valor que você irá atribuir a uma variável de instância não for influenciado por nenhum dos parâmetros que você passará para o construtor, atribua-o no momento da declaração.

Iker Jimenez
fonte
0

Não é uma resposta direta à sua pergunta sobre as melhores práticas, mas um ponto de atualização importante e relacionado é que, no caso de uma definição de classe genérica, deixe no compilador a inicialização com valores padrão ou temos que usar um método especial para inicializar campos aos valores padrão (se isso for absolutamente necessário para a legibilidade do código).

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
    }
}

E o método especial para inicializar um campo genérico com seu valor padrão é o seguinte:

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

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

Quando você não precisa de alguma lógica ou manipulação de erros:

  • Inicializar campos de classe na declaração

Quando você precisar de alguma lógica ou manipulação de erros:

  • Inicializar campos de classe no construtor

Isso funciona bem quando o valor de inicialização está disponível e a inicialização pode ser colocada em uma linha. No entanto, essa forma de inicialização possui limitações devido à sua simplicidade. Se a inicialização exigir alguma lógica (por exemplo, tratamento de erros ou um loop for para preencher uma matriz complexa), a atribuição simples será inadequada. Variáveis ​​de instância podem ser inicializadas em construtores, onde o tratamento de erros ou outra lógica pode ser usada.

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

Yousha Aleayoub
fonte