Saltar al contenido principal

Constructores

Todo sobre el uso de constructores en Dart.

Los constructores son funciones especiales que crean instancias de clases.

Dart implementa muchos tipos de constructores. A excepción de los constructores por defecto, estas funciones usan el mismo nombre que su clase.

Constructores generativos

Crea nuevas instancias e inicializa variables de instancia.

Constructores por defecto

Se utiliza para crear una nueva instancia cuando no se ha especificado un constructor. No toma argumentos y no tiene nombre.

Constructores nombrados

Aclara el propósito de un constructor o permite la creación de múltiples constructores para la misma clase.

Constructores constantes

Crea instancias como constantes en tiempo de compilación.

Constructores de tipo factory

Crea una nueva instancia de un subtipo o devuelve una instancia existente desde la caché.

Constructor redireccionado

Reenvía llamadas a otro constructor de la misma clase.

Tipos de constructores

#

Constructores generativos

#

Para instanciar una clase, usa un constructor generativo.

dart
class Point {
  // Instance variables to hold the coordinates of the point.
  double x;
  double y;

  // Generative constructor with initializing formal parameters:
  Point(this.x, this.y);
}

Constructores por defecto

#

Si no declaras un constructor, Dart utiliza el constructor por defecto. El constructor por defecto es un constructor generativo sin argumentos ni nombre.

Constructores nombrados

#

Usa un constructor nombrado para implementar múltiples constructores para una clase o para proporcionar claridad adicional:

dart
const double xOrigin = 0;
const double yOrigin = 0;

class Point {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  Point(this.x, this.y);

  // Named constructor
  Point.origin() : x = xOrigin, y = yOrigin;
}

Una subclase no hereda el constructor nombrado de una superclase. Para crear una subclase con un constructor nombrado definido en la superclase, implementa ese constructor en la subclase.

Constructores constantes

#

Si tu clase produce objetos inmutables, haz que estos objetos sean constantes en tiempo de compilación. Para hacer que los objetos sean constantes en tiempo de compilación, define un constructor const con todas las variables de instancia configuradas como final.

dart
class ImmutablePoint {
  static const ImmutablePoint origin = ImmutablePoint(0, 0);

  final double x, y;

  const ImmutablePoint(this.x, this.y);
}

Los constructores constantes no siempre crean constantes. Podrían ser invocados en un contexto que no sea const. Para obtener más información, consulta la sección sobre usar constructores.

Constructores redireccionados

#

Un constructor podría redirigir a otro constructor en la misma clase. Un constructor redireccionado tiene un cuerpo vacío. El constructor usa this en lugar del nombre de la clase después de dos puntos (:).

dart
class Point {
  double x, y;

  // The main constructor for this class.
  Point(this.x, this.y);

  // Delegates to the main constructor.
  Point.alongXAxis(double x) : this(x, 0);
}

Constructores de tipo factory

#

Cuando te encuentres con uno de los siguientes dos casos al implementar un constructor, usa la palabra clave factory:

  • El constructor no siempre crea una nueva instancia de su clase. Aunque un constructor de tipo factory no puede devolver null, podría devolver:

    • una instancia existente de una caché en lugar de crear una nueva
    • una nueva instancia de un subtipo
  • Necesitas realizar un trabajo no trivial antes de construir una instancia. Esto puede incluir verificar argumentos o realizar cualquier otro procesamiento que no se pueda manejar en la lista de inicializadores.

El siguiente ejemplo incluye dos constructores de tipo factory.

  • El constructor de tipo factory Logger devuelve objetos desde una caché.
  • El constructor de tipo factory Logger.fromJson inicializa una variable final desde un objeto JSON.
dart
class Logger {
  final String name;
  bool mute = false;

  // _cache is library-private, thanks to
  // the _ in front of its name.
  static final Map<String, Logger> _cache = <String, Logger>{};

  factory Logger(String name) {
    return _cache.putIfAbsent(name, () => Logger._internal(name));
  }

  factory Logger.fromJson(Map<String, Object> json) {
    return Logger(json['name'].toString());
  }

  Logger._internal(this.name);

  void log(String msg) {
    if (!mute) print(msg);
  }
}

Usa un constructor de tipo factory como cualquier otro constructor:

dart
var logger = Logger('UI');
logger.log('Button clicked');

var logMap = {'name': 'UI'};
var loggerJson = Logger.fromJson(logMap);

Constructores de tipo factory redireccionados

#

Un constructor de tipo factory redireccionado especifica una llamada a un constructor de otra clase para usar siempre que alguien realice una llamada al constructor redireccionado.

dart
factory Listenable.merge(List<Listenable> listenables) = _MergingListenable

Dado que los constructores de tipo factory ordinarios también pueden crear y devolver instancias de otras clases, las fábricas redireccionadas parecen innecesarias. Sin embargo, las fábricas redireccionadas tienen varias ventajas:

  • Una clase abstracta podría proporcionar un constructor constante que use el constructor constante de otra clase.
  • Un constructor de tipo factory redireccionado evita la necesidad de que los reenviadores repitan los parámetros formales y sus valores por defecto.

Constructor tear-offs

#

Dart te permite proporcionar un constructor como parámetro sin llamarlo. Lo que se denomina tear-off (ya que "desprendes" o "tears off" los paréntesis) sirve como un closure que invoca al constructor con los mismos parámetros.

Si el tear-off es un constructor con la misma firma y tipo de retorno que acepta el método, puedes usar el tear-off como parámetro o variable.

Los tear-offs difieren de las lambdas o funciones anónimas. Las lambdas sirven como un contenedor para el constructor, mientras que un tear-off es el constructor mismo.

Use Tear-Offs

bien dart
// Use a tear-off for a named constructor:
var strings = charCodes.map(String.fromCharCode);

// Use a tear-off for an unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);

Not Lambdas

mal dart
// Instead of a lambda for a named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));

// Instead of a lambda for an unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

Para más discusión, mira este video de Decoding Flutter sobre tear-offs.

Ver en YouTube en una nueva pestaña: "Dart Tear-offs | Decoding Flutter"

Sintaxis de constructor concisa

#

En Dart 3.13 y versiones posteriores, puedes omitir el nombre de la clase al declarar un constructor generativo o de tipo factory dentro del cuerpo de la clase usando directamente el modificador new o factory:

  • new o new nombrado
  • factory o factory nombrado

A diferencia de los constructores nombrados tradicionales (como Point.origin()), los constructores nombrados concisos no usan un punto entre la palabra clave (modificador) y el nombre.

dart
class Point {
  double x, y;

  // Concise unnamed generative constructor.
  new(this.x, this.y);

  // Concise named generative constructor.
  new origin() : x = 0, y = 0;

  // Equivalent to `factory Point.clone(Point other)`.
  factory clone(Point other) => Point(other.x, other.y);
}

Esta sintaxis reduce la verbosidad y facilita la refactorización de los nombres de las clases.

La siguiente tabla muestra cómo mapear la sintaxis de constructor tradicional a la sintaxis de constructor concisa (para una clase llamada LongClassName):

Sintaxis original de DartSintaxis abreviada
LongClassName() {}new() {}
LongClassName.name() {}new name() {}
const LongClassName();const new();
const LongClassName.name();const new name();
LongClassName(): this.other();new(): this.other();
LongClassName.name(): this();new name(): this();
const LongClassName(): this.other(); const new(): this.other();
const LongClassName.name(): this(); const new name(): this();
factory LongClassName() { ... }factory() { ... }
factory LongClassName.name() { ... } factory name() { ... }
factory LongClassName() = D;factory() = D;
factory LongClassName.name() = D;factory name() = D;
const factory LongClassName() = D;const factory() = D;
const factory LongClassName.name() = D; const factory name() = D;

Para obtener una sintaxis aún más concisa en la que defines campos y constructores en una sola línea, consulta Constructores primarios (Primary constructors).

Inicialización de variables de instancia

#

Dart proporciona varias formas de inicializar variables de instancia. Puedes asignar valores en la declaración, usar parámetros formales de inicialización, o usar una lista de inicializadores.

Inicializar variables de instancia en la declaración

#

Inicializa las variables de instancia al declarar las variables.

dart
class PointA {
  double x = 1.0;
  double y = 2.0;

  // The implicit default constructor sets these variables to (1.0,2.0)
  // PointA();

  @override
  String toString() {
    return 'PointA($x,$y)';
  }
}

Usar parámetros formales de inicialización

#

Para simplificar el patrón común de asignar un argumento de constructor a una variable de instancia, Dart cuenta con parámetros formales de inicialización.

En la declaración del constructor, incluye this.<propertyName> y omite el cuerpo. La palabra clave this se refiere a la instancia actual.

Cuando existe un conflicto de nombres, usa this. De lo contrario, el estilo de Dart omite el this. Existe una excepción para el constructor generativo donde debes anteponer this al nombre del parámetro formal de inicialización.

Como se señaló anteriormente en esta guía, ciertos constructores y ciertas partes de los constructores no pueden acceder a this. Estos incluyen:

  • Constructores de tipo factory
  • El lado derecho de una lista de inicializadores
  • Argumentos para un constructor de la superclase

Los parámetros formales de inicialización también te permiten inicializar variables de instancia no anulables o final. Ambos tipos de variables requieren inicialización o un valor por defecto.

dart
class PointB {
  final double x;
  final double y;

  // Sets the x and y instance variables
  // before the constructor body runs.
  PointB(this.x, this.y);

  // Initializing formal parameters can also be optional.
  PointB.optional([this.x = 0.0, this.y = 0.0]);
}

Esto también funciona con variables nombradas.

dart
class PointC {
  double x; // must be set in constructor
  double y; // must be set in constructor

  // Generative constructor with initializing formal parameters
  // with default values
  PointC.named({this.x = 1.0, this.y = 1.0});

  @override
  String toString() {
    return 'PointC.named($x,$y)';
  }
}

// Constructor using named variables.
final pointC = PointC.named(x: 2.0, y: 2.0);

Todas las variables introducidas a partir de los parámetros formales de inicialización son tanto final como únicamente en el alcance (scope) de las variables inicializadas.

Para realizar una lógica que no puedes expresar en la lista de inicializadores, crea un constructor de tipo factory o un método estático con esa lógica. Luego puedes pasar los valores calculados a un constructor normal.

Puedes configurar los parámetros del constructor como anulables para evitar inicializarlos.

dart
class PointD {
  double? x; // null if not set in constructor
  double? y; // null if not set in constructor

  // Generative constructor with initializing formal parameters
  PointD(this.x, this.y);

  @override
  String toString() {
    return 'PointD($x,$y)';
  }
}

Private named parameters

#

En Dart, los campos que comienzan con un guion bajo son privados para su biblioteca. Para inicializar un campo privado usando un parámetro nombrado, puedes escribir el código repetitivo (boilerplate) de asignación manual en la lista de inicializadores:

dart
class Point {
  final double _x;
  Point({required double x}) : _x = x;
}

También puedes inicializar campos privados directamente en la lista de parámetros del constructor. Cuando antepones this._ al parámetro nombrado, el compilador elimina automáticamente el guion bajo para el llamador, lo que le permite usar un nombre público y limpio:

dart
class Point {
  final double _x;
  Point({required this._x});
}

En ambos casos, el llamador utiliza el nombre público x en el sitio de la llamada:

dart
var p = Point(x: 1.0);

Al igual que los parámetros nombrados regulares, puedes hacer que los parámetros nombrados privados sean opcionales o requeridos. También puedes proporcionar valores por defecto explícitos.

En el siguiente ejemplo, el parámetro _x es opcional y tiene como valor por defecto null. El parámetro _y también es opcional pero tiene un valor por defecto explícito de 0.0:

dart
class PointPrivate {
  final double? _x; // Nullable field
  final double _y; // Non-nullable field

  PointPrivate({this._x, this._y = 0.0});

  @override
  String toString() => 'PointPrivate($_x, $_y)';
}

void testPrivate() {
  var p = PointPrivate(x: 1.0, y: 2.0);
  print(p);
}

Restricciones

#
  • Sin conflictos: Ni el nombre privado ni el nombre público generado pueden coincidir con ningún otro nombre de parámetro en el mismo constructor.
  • Solo formales de inicialización: Los parámetros nombrados en Dart generalmente no pueden ser privados. Esta capacidad es una excepción que solo se aplica a los parámetros nombrados que son formales de inicialización (this._field). No puedes usar identificadores privados para parámetros nombrados normales.
  • Nombre público válido: El nombre privado debe mapearse a un identificador público válido. Por ejemplo, this._ o this._2x son inválidos porque no tienen equivalentes públicos válidos.

Uso en listas de inicializadores

#

Dentro de la lista de inicializadores del constructor, haz referencia al parámetro usando su nombre privado:

dart
class PointPrivateAssert {
  final double _x;

  PointPrivateAssert({required this._x}) : assert(_x >= 0);
}

Interacción con superparámetros

#

Al extender una clase que utiliza parámetros nombrados privados, las subclases utilizan el nombre público para los superparámetrosSuperparámetroUn parámetro del constructor que pasa automáticamente un argumento al constructor de la superclase. Obtén más información.

En el siguiente ejemplo, la clase Tool define el campo privado _price. Aunque el campo es privado, su parámetro nombrado correspondiente es público (price, no _price). Para pasar el valor, la subclase Hammer utiliza el identificador público price:

dart
class Tool {
  final int _price;
  Tool({required this._price});
}

class Hammer extends Tool {
  // Forwards to the public 'price' argument
  Hammer({required super.price});
}

Usar una lista de inicializadores

#

Antes de que se ejecute el cuerpo del constructor, puedes inicializar variables de instancia. Separa los inicializadores con comas.

dart
// Initializer list sets instance variables before
// the constructor body runs.
Point.fromJson(Map<String, double> json) : x = json['x']!, y = json['y']! {
  print('In Point.fromJson(): ($x, $y)');
}

Para validar las entradas durante el desarrollo, usa assert en la lista de inicializadores.

dart
Point.withAssert(this.x, this.y) : assert(x >= 0) {
  print('In Point.withAssert(): ($x, $y)');
}

Las listas de inicializadores ayudan a configurar campos final.

El siguiente ejemplo inicializa tres campos final en una lista de inicializadores. Para ejecutar el código, haz clic en Run.

Herencia de constructores

#

Las subclases, o clases hijas, no heredan los constructores de su superclase, o clase de la que heredan directamente. Si una clase no declara un constructor, solo puede usar el constructor por defecto.

Una clase puede heredar los parámetros de una superclase. Estos se denominan superparámetros

Los constructores funcionan de manera algo similar a cómo llamas a una cadena de métodos estáticos. Cada subclase puede llamar al constructor de su superclase para inicializar una instancia, al igual que una subclase puede llamar al método estático de una superclase. Este proceso no "hereda" los cuerpos ni las firmas de los constructores.

Constructores de superclase no predeterminados

#

Dart ejecuta los constructores en el siguiente orden:

  1. lista de inicializadores
  2. constructor sin nombre y sin argumentos de la superclase
  3. constructor sin argumentos de la clase principal

Si la superclase carece de un constructor sin nombre y sin argumentos, llama a uno de los constructores en la superclase. Antes del cuerpo del constructor (si lo hay), especifica el constructor de la superclase después de dos puntos (:).

En el siguiente ejemplo, el constructor de la clase Employee llama al constructor nombrado para su superclase, Person. Para ejecutar el siguiente código, haz clic en Run.

Como Dart evalúa los argumentos para el constructor de la superclase antes de invocar el constructor, un argumento puede ser una expresión como una llamada a una función.

dart
class Employee extends Person {
  Employee() : super.fromJson(fetchDefaultData());
  // ···
}

Parámetros super

#

Para evitar pasar cada parámetro en la invocación super de un constructor, usa parámetros de superinicialización para reenviar parámetros al constructor de la superclase especificado o predeterminado. No puedes usar esta característica con constructores redireccionados. Los parámetros de superinicialización tienen una sintaxis y semántica similar a los parámetros formales de inicialización.

Si la invocación del superconstructor incluye argumentos posicionales, los parámetros de superinicialización no pueden ser posicionales.

dart
class Vector2d {
  final double x;
  final double y;

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

class Vector3d extends Vector2d {
  final double z;

  // Forward the x and y parameters to the default super constructor like:
  // Vector3d(final double x, final double y, this.z) : super(x, y);
  Vector3d(super.x, super.y, this.z);
}

Para ilustrar mejor, considera el siguiente ejemplo.

dart
  // If you invoke the super constructor (`super(0)`) with any
  // positional arguments, using a super parameter (`super.x`)
  // results in an error.
  Vector3d.xAxisError(super.x): z = 0, super(0); // BAD

Este constructor nombrado intenta establecer el valor de x dos veces: una en el superconstructor y otra como un superparámetro posicional. Dado que ambos se refieren al parámetro posicional x, esto genera un error.

Cuando el superconstructor tiene argumentos nombrados, puedes dividirlos entre superparámetros nombrados (super.y en el siguiente ejemplo) y argumentos nombrados para la invocación del superconstructor (super.named(x: 0)).

dart
class Vector2d {
  // ...
  Vector2d.named({required this.x, required this.y});
}

class Vector3d extends Vector2d {
  final double z;

  // Forward the y parameter to the named super constructor like:
  // Vector3d.yzPlane({required double y, required this.z})
  //       : super.named(x: 0, y: y);
  Vector3d.yzPlane({required super.y, required this.z}) : super.named(x: 0);
}