¿Inicializar campos de clase en el constructor o en la declaración?

413

He estado programando en C # y Java recientemente y tengo curiosidad por saber dónde es el mejor lugar para inicializar mis campos de clase.

¿Debo hacerlo en la declaración ?:

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

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

o en un constructor ?:

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

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

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

Tengo mucha curiosidad por lo que algunos de ustedes veteranos piensan que es la mejor práctica. Quiero ser coherente y apegarme a un enfoque.

mmcdole
fuente
3
Tenga en cuenta que para las estructuras no puede tener inicializadores de campo de instancia, por lo que no tiene más remedio que usar el constructor.
yoyo

Respuestas:

310

Mis reglas:

  1. No inicialice por los valores establecidos en la declaración ( null, false, 0, 0.0...).
  2. Prefiera la inicialización en la declaración si no tiene un parámetro constructor que cambie el valor del campo.
  3. Si el valor del campo cambia debido a un parámetro de constructor, coloque la inicialización en los constructores.
  4. Sea consistente en su práctica (la regla más importante).
kokos
fuente
44
Espero que kokos signifique que no debe inicializar miembros a sus valores predeterminados (0, falso, nulo, etc.), ya que el compilador lo hará por usted (1.). Pero si desea inicializar un campo en cualquier cosa que no sea su valor predeterminado, debe hacerlo en la declaración (2.). Creo que puede ser el uso de la palabra "default" lo que te confunde.
Ricky Helgesson
95
No estoy de acuerdo con la regla 1: al no especificar un valor predeterminado (independientemente de si el compilador lo inicializa o no), deja que el desarrollador adivine o busque la documentación sobre cuál sería el valor predeterminado para ese idioma en particular. Para fines de legibilidad, siempre especificaría el valor predeterminado.
James
32
El valor predeterminado de un tipo default(T)es siempre el valor que tiene una representación binaria interna de 0.
Olivier Jacot-Descombes
15
Ya sea que le guste o no la regla 1, no se puede usar con campos de solo lectura, que deben inicializarse explícitamente cuando finalice el constructor.
yoyo
36
No estoy de acuerdo con aquellos que no están de acuerdo con la regla 1. Está bien esperar que otros aprendan el lenguaje C #. Del mismo modo que no comentamos cada foreachbucle con "esto repite lo siguiente para todos los elementos de la lista", no necesitamos reexpresar constantemente los valores predeterminados de C #. Tampoco necesitamos pretender que C # tenga una semántica no inicializada. Dado que la ausencia de un valor tiene un significado claro, está bien dejarlo fuera. Si es ideal ser explícito, siempre debe usarlo newal crear nuevos delegados (como se requería en C # 1). ¿Pero quién hace eso? El lenguaje está diseñado para codificadores concienzudos.
Edward Brey
149

En C # no importa. Los dos ejemplos de código que da son completamente equivalentes. En el primer ejemplo, el compilador de C # (¿o es el CLR?) Construirá un constructor vacío e inicializará las variables como si estuvieran en el constructor (hay un ligero matiz de esto que Jon Skeet explica en los comentarios a continuación). Si ya hay un constructor, cualquier inicialización "arriba" se moverá a la parte superior.

En términos de mejores prácticas, el primero es menos propenso a errores que el segundo, ya que alguien podría agregar fácilmente otro constructor y olvidarse de encadenarlo.

Molesto
fuente
44
Eso no es correcto si elige inicializar la clase con GetUninitializedObject. Lo que esté en el ctor no se tocará pero se ejecutarán las declaraciones de campo.
Wolf5
28
En realidad sí importa. Si el constructor de la clase base llama a un método virtual (que generalmente es una mala idea, pero puede suceder) que se anula en la clase derivada, entonces al usar inicializadores de variable de instancia, la variable se inicializará cuando se llame al método, mientras que se usa la inicialización en constructor, no lo serán. (Los inicializadores de variables de instancia se ejecutan antes de que se llame al constructor de la clase base).
Jon Skeet
2
De Verdad? Juro que obtuve esta información del CLR de Richter a través de C # (creo que la segunda edición) y lo esencial era que se trataba de azúcar sintáctico (¿puedo haberlo leído mal?) Y el CLR simplemente metió las variables en el constructor. Pero está afirmando que este no es el caso, es decir, la inicialización del miembro puede dispararse antes de la inicialización del ctor en el escenario loco de llamar a un ctor virtual en base y tener una anulación en la clase en cuestión. ¿Lo entendí correctamente? ¿Acabas de descubrir esto? Confundido con este comentario actualizado en una publicación de 5 años (OMG, ¿han pasado 5 años?).
Triste el
@Quibblesome: un constructor de clase hijo contendrá una llamada encadenada al constructor padre. Un compilador de lenguaje es libre de incluir tanto o menos código antes de lo que quiera, siempre que se llame al constructor principal exactamente una vez en todas las rutas de código, y se haga un uso limitado del objeto en construcción antes de esa llamada. Una de mis molestias con C # es que si bien puede generar código que inicializa campos y código que usa parámetros de constructor, no ofrece ningún mecanismo para inicializar campos basados ​​en parámetros de constructor.
supercat
1
@Quibblesome, el anterior será un problema, si el valor se asigna y se pasa a un método WCF. Lo que restablecerá los datos al tipo de datos predeterminado del modelo. La mejor práctica es hacer la inicialización en el constructor (más tarde).
Pranesh Janarthanan el
16

La semántica de C # difiere ligeramente de Java aquí. En C #, la asignación en la declaración se realiza antes de llamar al constructor de la superclase. En Java, se realiza inmediatamente después de lo cual permite que se use 'esto' (particularmente útil para clases internas anónimas), y significa que la semántica de las dos formas realmente coincide.

Si puede, haga que los campos sean definitivos.

Tom Hawtin - tackline
fuente
15

Creo que hay una advertencia. Una vez cometí tal error: dentro de una clase derivada, intenté "inicializar en la declaración" los campos heredados de una clase base abstracta. El resultado fue que existían dos conjuntos de campos, uno es "base" y otro son los recién declarados, y me costó bastante tiempo depurar.

La lección: para inicializar campos heredados , lo harías dentro del constructor.

xji
fuente
Entonces, si hace referencia al campo derivedObject.InheritedField, ¿se refiere a su base o derivada?
RayLuo
6

Suponiendo el tipo en su ejemplo, definitivamente prefiera inicializar campos en el constructor. Los casos excepcionales son:

  • Campos en clases / métodos estáticos
  • Campos escritos como static / final / et al.

Siempre pienso en la lista de campos en la parte superior de una clase como la tabla de contenido (lo que está contenido aquí, no cómo se usa), y el constructor como la introducción. Los métodos, por supuesto, son capítulos.

Navidad
fuente
¿Por qué es "definitivamente" así? Usted proporcionó solo una preferencia de estilo, sin explicar su razonamiento. Oh, espera, no importa, según la respuesta de @quibblesome , son "totalmente equivalentes", por lo que en realidad es solo una preferencia de estilo personal.
RayLuo
4

¿Qué pasa si te digo que depende?

En general, inicializo todo y lo hago de manera consistente. Sí, es demasiado explícito, pero también es un poco más fácil de mantener.

Si estamos preocupados por el rendimiento, bueno, entonces inicializo solo lo que hay que hacer y lo ubico en las áreas que dan más por el dinero.

En un sistema de tiempo real, me pregunto si incluso necesito la variable o constante.

Y en C ++ a menudo hago casi ninguna inicialización en cualquier lugar y lo muevo a una función Init (). ¿Por qué? Bueno, en C ++ si está inicializando algo que puede generar una excepción durante la construcción del objeto, se abre a las pérdidas de memoria.

Dan Blair
fuente
4

Hay muchas y diversas situaciones.

Solo necesito una lista vacia

La situación es clara. Solo necesito preparar mi lista y evitar que se produzca una excepción cuando alguien agrega un elemento a la lista.

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

    public CsvFile()
    {
    }
}

Yo se los valores

Sé exactamente qué valores quiero tener por defecto o necesito usar alguna otra lógica.

public class AdminTeam
{
    private List<string> usernames;

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

o

public class AdminTeam
{
    private List<string> usernames;

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

Lista vacía con posibles valores.

A veces espero una lista vacía por defecto con la posibilidad de agregar valores a través de otro constructor.

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
fuente
3

En Java, un inicializador con la declaración significa que el campo siempre se inicializa de la misma manera, independientemente de qué constructor se use (si tiene más de uno) o los parámetros de sus constructores (si tienen argumentos), aunque un constructor podría posteriormente cambiar el valor (si no es final). Por lo tanto, el uso de un inicializador con una declaración sugiere a un lector que el valor inicializado es el valor que tiene el campo en todos los casos , independientemente del constructor utilizado y de los parámetros pasados ​​a cualquier constructor. Por lo tanto, use un inicializador con la declaración solo si, y siempre si, el valor para todos los objetos construidos es el mismo.

Raedwald
fuente
3

El diseño de C # sugiere que se prefiere la inicialización en línea, o no estaría en el lenguaje. Cada vez que puede evitar una referencia cruzada entre diferentes lugares en el código, generalmente está mejor.

También está la cuestión de la coherencia con la inicialización de campo estático, que debe estar en línea para obtener el mejor rendimiento. Las Pautas de diseño del marco para el diseño de constructores dicen esto:

✓ CONSIDERE la inicialización de campos estáticos en línea en lugar de usar explícitamente constructores estáticos, porque el tiempo de ejecución puede optimizar el rendimiento de los tipos que no tienen un constructor estático definido explícitamente.

"Considerar" en este contexto significa hacerlo a menos que haya una buena razón para no hacerlo. En el caso de los campos de inicializador estático, una buena razón sería si la inicialización es demasiado compleja para codificarla en línea.

Edward Brey
fuente
2

Ser consistente es importante, pero esta es la pregunta que debe hacerse: "¿Tengo un constructor para otra cosa?"

Por lo general, estoy creando modelos para transferencias de datos que la clase en sí misma no hace nada excepto trabajar como alojamiento de variables.

En estos escenarios, generalmente no tengo ningún método o constructor. Para mí sería una tontería crear un constructor con el exclusivo propósito de inicializar mis listas, especialmente porque puedo inicializarlas en línea con la declaración.

Como muchos otros han dicho, depende de su uso. Hazlo simple y no hagas nada extra que no tengas que hacer.

MeanJerry
fuente
2

Considere la situación en la que tiene más de un constructor. ¿La inicialización será diferente para los diferentes constructores? Si serán iguales, ¿por qué repetir para cada constructor? Esto está en línea con la declaración de kokos, pero puede no estar relacionado con los parámetros. Digamos, por ejemplo, que desea mantener una bandera que muestre cómo se creó el objeto. Entonces esa bandera se inicializaría de manera diferente para diferentes constructores, independientemente de los parámetros del constructor. Por otro lado, si repite la misma inicialización para cada constructor, deja la posibilidad de que (sin querer) cambie el parámetro de inicialización en algunos de los constructores pero no en otros. Entonces, el concepto básico aquí es que el código común debe tener una ubicación común y no potencialmente repetirse en diferentes ubicaciones.

Programador Joe
fuente
1

Hay un ligero beneficio de rendimiento al establecer el valor en la declaración. Si lo configura en el constructor, en realidad se está configurando dos veces (primero al valor predeterminado, luego restablecer en el ctor).

John Meagher
fuente
2
En C #, los campos siempre establecen primero el valor predeterminado. La presencia de un inicializador no hace ninguna diferencia.
Jeffrey L Whitledge
0

Normalmente intento que el constructor no haga nada más que obtener las dependencias e inicializar los miembros de instancia relacionados con ellas. Esto te hará la vida más fácil si quieres poner a prueba tus clases.

Si el valor que va a asignar a una variable de instancia no se ve influenciado por ninguno de los parámetros que va a pasar al constructor, asígnelo en el momento de la declaración.

Iker Jiménez
fuente
0

No es una respuesta directa a su pregunta sobre las mejores prácticas, pero un punto de actualización importante y relacionado es que, en el caso de una definición de clase genérica, dejarla en el compilador para inicializar con valores predeterminados o tenemos que usar un método especial para inicializar campos a sus valores predeterminados (si eso es absolutamente necesario para la legibilidad del 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
    }
}

Y el método especial para inicializar un campo genérico a su valor predeterminado es el siguiente:

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

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

Cuando no necesita algo de lógica o manejo de errores:

  • Inicializar campos de clase en la declaración

Cuando necesita algo de lógica o manejo de errores:

  • Inicializar campos de clase en constructor

Esto funciona bien cuando el valor de inicialización está disponible y la inicialización se puede poner en una línea. Sin embargo, esta forma de inicialización tiene limitaciones debido a su simplicidad. Si la inicialización requiere algo de lógica (por ejemplo, manejo de errores o un bucle for para llenar una matriz compleja), la asignación simple es inadecuada. Las variables de instancia se pueden inicializar en constructores, donde se puede utilizar el manejo de errores u otra lógica.

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

Yousha Aleayoub
fuente