Saltar al contenido principal

Constructores principales

Crea clases de forma concisa declarando parámetros y campos en la cabecera de la clase.

Descripción general

#

Los constructores primarios proporcionan una forma concisa de declarar los campos de una clase y su constructor principal en una sola línea. Reducen el código repetitivo de declarar campos, pasar parámetros y asignarlos en el cuerpo del constructor. Esta sintaxis abreviada cambia la forma en que escribes la declaración, pero no cambia el comportamiento en tiempo de ejecución.

Antes y después

#

Considera esta clase tradicional con dos campos y un constructor:

dart
// Current syntax.
class Point {
  int x;
  int y;

  Point(this.x, this.y);
}

Usar un constructor primario hace que la misma clase sea mucho más concisa:

dart
// Using a primary constructor.
class Point(var int x, var int y);

Declarar un parámetro en el constructor primario con var o final induce implícitamente una variable de instancia para ese parámetro.

Cualquier otro constructor declarado dentro del cuerpo de la clase se denomina constructor en el cuerpo (in-body constructor).

Para garantizar que el constructor primario se ejecute en cada nueva instancia, una clase, mixin class o enum con un constructor primario no puede tener ningún otro constructor generativo en el cuerpo que no sea redireccionado.

Declaraciones de campos en parámetros

#

Los parámetros en los constructores primarios con el modificador var o final, llamados parámetros declarativos (declaring parameters), inducen implícitamente un campo.

Si omites el modificador, el parámetro no crea un campo. Se comporta exactamente como un parámetro en un constructor tradicional.

dart
// Declares both fields x and y.
class Point(var int x, var int y);

// Doesn't declare a field.
class User(String name);

Debido a que los modificadores final y var en parámetros están reservados exclusivamente para declarar parámetros en constructores primarios, no puedes usarlos en parámetros de otros tipos de funciones.

Para los tipos de extensión (extension types), el constructor primario debe tener exactamente un parámetro. Este parámetro es siempre un parámetro declarativo, incluso si omites el modificador. Puedes usar el modificador final, pero usar var es un error.

Las clases mixin (mixin classes) solo pueden tener un constructor primario sin parámetros, cuerpo ni lista de inicialización.

Ámbitos del constructor primario

#

Cuando utilizas un constructor primario, los parámetros que declaras en la cabecera de la clase están disponibles en diferentes partes de la declaración de la clase. Dart gestiona su visibilidad utilizando dos ámbitos distintos:

  • Ámbito de inicializador primario (Primary initializer scope): Se aplica a los inicializadores de campos no late en el cuerpo de la clase y a la lista de inicialización del constructor primario (después de this :). En este ámbito, un nombre de parámetro como x se refiere directamente al parámetro del constructor.
  • Ámbito de parámetro primario (Primary parameter scope): Se aplica al bloque del cuerpo del constructor primario (dentro de { ... }). En este ámbito, el nombre de un parámetro declarativo se refiere a la variable de instancia inducida (campo), mientras que el nombre de un parámetro no declarativo se sigue refiriendo al parámetro del constructor.

El ámbito de inicializador primario hace que los parámetros de la cabecera estén directamente disponibles para inicializar campos no late, lo que elimina la necesidad de una lista de inicialización separada. Funciona tal como si estuvieras inicializando variables en la lista de inicialización de un constructor tradicional:

dart
class DeltaPoint(final int x, int delta) {
  // Accesses 'x' and 'delta' parameters directly!
  final int y = x + delta;
}

El ámbito de parámetro primario garantiza que cualquier actualización de las variables de instancia se refleje correctamente en el cuerpo del constructor, mientras que los inicializadores sigan teniendo acceso a los parámetros originales. El siguiente ejemplo muestra cómo el mismo nombre x se resuelve de forma diferente en cada ámbito:

dart
class ScopingDemo(var String x, String suffix) {
  // In a non-late field initializer, 'x' refers to the parameter 'x'.
  final String fieldAtDeclaration = x;
  final String fieldInInitializer;

  // In the initializer list, 'x' refers to the parameter 'x'.
  this : fieldInInitializer = x {
    // Inside the body, 'x' refers to the induced instance variable,
    // so assigning to it updates the field.
    x = x.toUpperCase();
    // 'suffix' induces no field, so it still refers to the parameter.
    print('$x$suffix');
  }
}

Este comportamiento consistente hace que la refactorización entre constructores tradicionales y primarios sea más simple y segura.

Añadir cuerpos de constructores

#

Para validar la entrada o realizar una inicialización compleja, puedes añadir un cuerpo al constructor primario dentro de la definición de la clase. Este cuerpo utiliza la palabra clave this seguida de un bloque:

dart
class Point(var int x, var int y) {
  this : assert(x >= 0 && y >= 0) {
    print('Point initialized at ($x, $y)');
  }
}

El bloque puede especificar una lista de inicialización después de this y/o un cuerpo de función. Para proporcionar solo una lista de inicialización, finalízala con un punto y coma, como this : assert(x >= 0);. También puedes anotar el bloque con metadatos, como @metadata this;.

Inicializar campos privados

#

Para inicializar un campo privado utilizando un parámetro nombrado, puedes escribir código repetitivo de asignación manual en un constructor tradicional:

dart
// Variant not using a private named parameter.
class User({required String name}) {
  String _name = name;
}

Con los constructores primarios y la característica de parámetros nombrados privados, puedes declarar el campo privado directamente en la cabecera del constructor. Cuando utilizas un nombre privado (con un guion bajo al inicio) para un parámetro nombrado, el compilador hace automáticamente que el nombre del parámetro sea público para el llamador eliminando el guion bajo:

dart
// Variant using a private named parameter.
class User({required var String _name});

En ambos casos, el llamador usa el nombre público name en el sitio de la llamada: User(name: 'John Doe').

Cuerpos vacíos

#

Un cuerpo vacío de una clase, mixin class, extensión o tipo de extensión ({}) se puede reemplazar por un punto y coma (;). Aunque esto es cierto en general para estas declaraciones, es particularmente útil al usar un constructor primario para mantener toda la declaración en una sola línea.

dart
class Point(var int x, var int y);

Constructores primarios constantes

#

Al igual que los constructores tradicionales, un constructor primario puede ser constante si la clase y sus campos lo permiten. Para declarar un constructor primario constante, coloca el modificador const antes del nombre de la clase en la cabecera de la clase:

dart
class const ConstPoint(final int x, final int y) {
  final int z;
  // A constant primary constructor can have an initializer list,
  // but can't have a body block.
  this : z = x + y;
}

Los constructores primarios constantes tienen estas restricciones importantes:

  • Sin bloque de cuerpo: Tener un cuerpo { ... } es un error en tiempo de compilación, incluso si está vacío. Un constructor primario constante solo puede usar una lista de inicialización seguida de un punto y coma.
  • Campos final definitivamente inicializados: Como cualquier clase con un constructor constante generativo, cada variable de instancia debe ser final, no puede ser late, y debe ser definitivamente inicializada por un parámetro declarativo, un inicializador de campo o la lista de inicialización del constructor primario.
  • Potentially constantPotentially constantAn expression that can be evaluated at compile time if all constructor parameters of the enclosing constant constructor are replaced by constant values, and all type parameters are replaced by concrete types. Learn more initializers: The initializing expressions for each instance variable must be potentially constant. This includes field initializers and expressions in the primary constructor's initializer list.

Constructores primarios nombrados

#

También puedes declarar un constructor primario como un constructor nombrado (named constructor) agregando un punto (.) y un nombre después del nombre de la clase en la cabecera de la clase:

dart
// A named primary constructor.
class Point.custom(var int x, var int y);

Un patrón común es definir un constructor primario privado (como Point._) que restrinja la instanciación directa y obligue a los llamadores a usar métodos factory u otros constructores:

dart
// A private named primary constructor.
class Point._(var int x, var int y);

Parámetros super

#

Los superparámetros funcionan igual que en los constructores tradicionales, lo que te permite redirigir parámetros al constructor de la superclase:

dart
class Person(final String name, final int age);

class Employee(super.name, super.age, final String role) extends Person;

Esto reduce el código repetitivo en estructuras de clases jerárquicas, eliminando la necesidad de escribir manualmente listas de inicialización o duplicar declaraciones de parámetros.

Constructores primarios de enum

#

Puedes usar constructores primarios para declarar enums mejorados (enhanced enums) de manera mucho más concisa.

Al usar un constructor primario, puedes definir los campos del enum y su constructor en una sola línea, eliminando el código repetitivo habitual de declarar campos, parámetros e inicializarlos:

dart
enum Color(final String hex) {
  red('#FF0000'),
  green('#00FF00'),
  blue('#0000FF');
}

Los constructores primarios en los enums son implícitamente constantes. Aunque opcionalmente puedes escribir el modificador const antes del nombre del enum (como en enum const Color), es redundante y se puede omitir.

Restricciones y cambios disruptivos (breaking changes)

#

Ten en cuenta estas restricciones y posibles errores al usar constructores primarios:

  • Los parámetros declarativos no pueden ser late ni external: Los modificadores late y external no están permitidos en los parámetros en la cabecera del constructor primario. Para usar estos modificadores, declara los campos en el cuerpo de la clase como de costumbre.
  • Colisiones de nombres: Declarar un parámetro en el constructor primario con el mismo nombre que un método u otro campo en el cuerpo de la clase resulta en un error en tiempo de compilación.
  • Sin asignaciones a los parámetros del constructor primario: Los parámetros del constructor primario son de solo lectura dentro del ámbito de inicializador primario. Asignarles un valor (como con x = 5 o x++) en un inicializador de campo o la lista de inicialización del constructor primario es un error en tiempo de compilación.
  • Doble inicialización: No puedes inicializar una variable de instancia tanto en su declaración como en la lista de inicialización del constructor primario (o como un parámetro formal de inicialización), incluso si el campo es mutable. Hacerlo resulta en un error en tiempo de compilación.
  • Body part constraints:
    • Una parte del cuerpo del constructor primario (el bloque this) no puede usar los modificadores async, async* ni sync*, y no puede usar la sintaxis de flecha de cuerpo de expresión (=>).
    • No puedes escribir un bloque this si la cabecera de la clase no declara un constructor primario.
    • Una clase puede tener a lo sumo una parte del cuerpo del constructor primario.
  • Los constructores primarios de clases mixin deben ser triviales: Una clase mixin (mixin class) solo puede declarar un constructor primario si no tiene parámetros, ni lista de inicialización, ni cuerpo.
  • Parámetros covariantes: Solo puedes usar el modificador covariant en un parámetro del constructor primario si es un parámetro declarativo mutable (usa el modificador var). Usar covariant en parámetros final o no declarativos es un error en tiempo de compilación porque no inducen un setter.