Saltar al contenido principal

Corrección de fallos de promoción de tipos

Soluciones para casos donde sabes más sobre el tipo de un campo de lo que Dart puede determinar.

La promoción de tipos ocurre cuando el análisis de flujo puede confirmar con certeza que una variable con un tipo anulable no es nula, y que no cambiará a partir de ese momento. Muchas circunstancias pueden debilitar la solidez del tipo, lo que hace que la promoción de tipos falle.

Esta página enumera las razones por las que ocurren fallos en la promoción de tipos, con consejos sobre cómo solucionarlos. Para obtener más información sobre el análisis de flujo y la promoción de tipos, consulta la página Entendiendo la seguridad de nulos (null safety).

Versión del lenguaje no compatible para la promoción de campos

#

La causa: Estás intentando promover un campo, pero la promoción de campos depende de la versión del lenguaje, y tu código está configurado para una versión del lenguaje anterior a la 3.2.

Si ya estás utilizando una versión del SDK >= Dart 3.2, es posible que tu código todavía esté configurado explícitamente para una versión del lenguaje anterior. Esto puede ocurrir por una de las siguientes razones:

  • Tu archivo pubspec.yaml declara una restricción de SDK con un límite inferior por debajo de 3.2, o
  • Tienes un comentario // @dart=version en la parte superior del archivo, donde la versión es inferior a 3.2.

Example:

mal dart
// @dart=3.1

class C {
  final int? _i;
  C(this._i);

  void f() {
    if (_i != null) {
      int i = _i;  // ERROR
    }
  }
}

Message:

'_i' refers to a field. It couldn't be promoted because field promotion is only available in Dart 3.2 and above.

Solution:

Asegúrate de que tu biblioteca no esté utilizando una versión del lenguaje anterior a la 3.2. Busca en la parte superior de tu archivo un comentario // @dart=version desactualizado, o en tu pubspec.yaml un límite inferior de la restricción del SDK desactualizado.

Solo las variables locales se pueden promover (antes de Dart 3.2)

#

La causa: Estás intentando promover una propiedad, pero solo las variables locales se pueden promover en las versiones de Dart anteriores a la 3.2, y estás utilizando una versión anterior a la 3.2.

Example:

mal dart
class C {
  int? i;
  void f() {
    if (i == null) return;
    print(i.isEven);       // ERROR
  }
}

Message:

'i' refers to a property so it couldn't be promoted.

Solution:

Si estás utilizando Dart 3.1 o anterior, actualiza a 3.2 o posterior.

Si necesitas seguir usando una versión anterior, lee la sección Otras causas y soluciones temporales.

Otras causas y soluciones temporales

#

Los ejemplos restantes en esta página documentan las razones de los fallos de promoción no relacionados con inconsistencias de versión, tanto para fallos de campos como de variables locales, con ejemplos y soluciones temporales.

En general, las soluciones habituales para los fallos de promoción son una o más de las siguientes:

  • Asignar el valor de la propiedad a una variable local con el tipo no anulable que necesitas.
  • Agregar una comprobación de nulo explícita (por ejemplo, i == null).
  • Usar ! o as como una comprobación redundante si estás seguro de que una expresión no puede ser null.

Aquí tienes un ejemplo de cómo crear una variable local (que puede llamarse i) que contiene el valor de i:

bien dart
class C {
  int? i;
  void f() {
    final i = this.i;
    if (i == null) return;
    print(i.isEven);
  }
}

Este ejemplo presenta un campo de instancia, pero en su lugar podría usar un getter de instancia, un campo o getter estático, una variable o getter de nivel superior, o this.

Y aquí tienes un ejemplo del uso de i!:

bien dart
print(i!.isEven);

No se puede promover this

#

La causa: Estás intentando promover this, pero la promoción de tipos para this aún no es compatible.

Un escenario común de promoción de this es al escribir métodos de extensión (extension methods). Si el tipo de extensión on es un tipo anulable, querrás hacer una comprobación de nulo para ver si this es null:

Example:

mal dart
extension on int? {
  int get valueOrZero {
    return this == null ? 0 : this; // ERROR
  }
}

Message:

`this` can't be promoted.

Solution:

Crea una variable local para contener el valor de this, luego realiza la comprobación de nulo.

bien dart
extension on int? {
  int get valueOrZero {
    final self = this;
    return self == null ? 0 : self;
  }
}

Solo los campos privados se pueden promover

#

La causa: Estás intentando promover un campo, pero el campo no es privado.

Es posible que otras bibliotecas de tu programa sobreescriban campos públicos con un getter. Dado que es posible que los getters no devuelvan un valor estable, y el compilador no puede saber qué están haciendo otras bibliotecas, los campos no privados no se pueden promover.

Example:

mal dart
class Example {
  final int? value;
  Example(this.value);
}

void test(Example x) {
  if (x.value != null) {
    print(x.value + 1); // ERROR
  }
}

Message:

'value' refers to a public property so it couldn't be promoted.

Solution:

Hacer que el campo sea privado le permite al compilador estar seguro de que ninguna biblioteca externa podría sobreescribir su valor, por lo que es seguro promoverlo.

bien dart
class Example {
  final int? _value;
  Example(this._value);
}

void test(Example x) {
  if (x._value != null) {
    print(x._value + 1);
  }
}

Solo los campos final se pueden promover

#

La causa: Estás intentando promover un campo, pero el campo no es final.

Para el compilador, los campos no finales podrían, en principio, ser modificados en cualquier momento entre el momento en que se prueban y el momento en que se usan. Por lo tanto, no es seguro para el compilador promover un tipo anulable no final a un tipo no anulable.

Example:

mal dart
class Example {
  int? _mutablePrivateField;
  Example(this._mutablePrivateField);

  void f() {
    if (_mutablePrivateField != null) {
      int i = _mutablePrivateField; // ERROR
    }
  }
}

Message:

'_mutablePrivateField' refers to a non-final field so it couldn't be promoted.

Solution:

Haz que el campo sea final:

bien dart
class Example {
  final int? _immutablePrivateField;
  Example(this._immutablePrivateField);

  void f() {
    if (_immutablePrivateField != null) {
      int i = _immutablePrivateField; // OK
    }
  }
}

Los getters no se pueden promover

#

La causa: Estás intentando promover un getter, pero solo los campos de instancia se pueden promover, no los getters de instancia.

El compilador no tiene forma de garantizar que un getter devuelva el mismo resultado cada vez. Dado que su estabilidad no se puede confirmar, no es seguro promover los getters.

Example:

mal dart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  if (x._value != null) {
    print(x._value.isEven); // ERROR
  }
}

Message:

'_value' refers to a getter so it couldn't be promoted.

Solution:

Asigna el getter a una variable local:

bien dart
import 'dart:math';

abstract class Example {
  int? get _value => Random().nextBool() ? 123 : null;
}

void f(Example x) {
  final value = x._value;
  if (value != null) {
    print(value.isEven); // OK
  }
}

Los campos externos (external) no se pueden promover

#

La causa: Estás intentando promover un campo, pero el campo está marcado como external.

Los campos externos no se promueven porque son esencialmente getters externos; su implementación es código externo a Dart, por lo que no hay garantía para el compilador de que un campo externo devuelva el mismo valor cada vez que se llama.

Example:

mal dart
class Example {
  external final int? _externalField;

  void f() {
    if (_externalField != null) {
      print(_externalField.isEven); // ERROR
    }
  }
}

Message:

'_externalField' refers to an external field so it couldn't be promoted.

Solution:

Asigna el valor del campo externo a una variable local:

bien dart
class Example {
  external final int? _externalField;

  void f() {
    final i = _externalField;
    if (i != null) {
      print(i.isEven); // OK
    }
  }
}

Conflicto con un getter en otro lugar de la biblioteca

#

La causa: Estás intentando promover un campo, pero otra clase en la misma biblioteca contiene un getter concreto con el mismo nombre.

Example:

mal dart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // ERROR
  }
}

Message:

'_overridden' couldn't be promoted because there is a conflicting getter in class 'Override'.

Solution:

Si el getter y el campo están relacionados y necesitan compartir su nombre (como cuando uno de ellos sobreescribe al otro, como en el ejemplo de arriba), entonces puedes habilitar la promoción de tipos asignando el valor a una variable local:

bien dart
import 'dart:math';

class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? get _overridden => Random().nextBool() ? 1 : null;
}

void testParity(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // OK
  }
}

Nota sobre clases no relacionadas

#

Ten en cuenta que en el ejemplo anterior está claro por qué no es seguro promover el campo _overridden: porque existe una relación de sobreescritura entre el campo y el getter. Sin embargo, un getter en conflicto evitará la promoción del campo incluso si las clases no están relacionadas. Por ejemplo:

mal dart
import 'dart:math';

class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _i => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // ERROR
  }
}

Otra biblioteca podría contener una clase que combine las dos clases no relacionadas en la misma jerarquía de clases, lo que provocaría que la referencia en la función f a x._i se despachara a Unrelated._i. Por ejemplo:

mal dart
class Surprise extends Unrelated implements Example {}

void main() {
  f(Surprise());
}

Solution:

Si el campo y la entidad en conflicto realmente no están relacionados, puedes solucionar el problema dándoles nombres diferentes:

bien dart
class Example {
  final int? _i;
  Example(this._i);
}

class Unrelated {
  int? get _j => Random().nextBool() ? 1 : null;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // OK
  }
}

Conflicto con un campo no promovible en otro lugar de la biblioteca

#

La causa: Estás intentando promover un campo, pero otra clase en la misma biblioteca contiene un campo con el mismo nombre que no es promovible (por cualquiera de las otras razones enumeradas en esta página).

Example:

mal dart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  if (x._overridden != null) {
    print(x._overridden.isEven); // ERROR
  }
}

Este ejemplo falla porque en tiempo de ejecución, x podría ser en realidad una instancia de Override, por lo que la promoción no sería sólida.

Message:

'overridden' couldn't be promoted because there is a conflicting non-promotable field in class 'Override'.

Solution:

Si los campos realmente están relacionados y necesitan compartir un nombre, entonces puedes habilitar la promoción de tipos asignando el valor a una variable local final para promover:

bien dart
class Example {
  final int? _overridden;
  Example(this._overridden);
}

class Override implements Example {
  @override
  int? _overridden;
}

void f(Example x) {
  final i = x._overridden;
  if (i != null) {
    print(i.isEven); // OK
  }
}

Si los campos no están relacionados, entonces renombra uno de los campos para que no entren en conflicto. Lee la Nota sobre clases no relacionadas.

Conflicto con el reenviador implícito noSuchMethod

#

La causa: Estás intentando promover un campo que es privado y final, pero otra clase en la misma biblioteca contiene un reenviador implícito noSuchMethod con el mismo nombre que el campo.

Esto no es seguro porque no hay garantía de que noSuchMethod devuelva un valor estable de una invocación a la siguiente.

Example:

mal dart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // ERROR
  }
}

En este ejemplo, _i no se puede promover porque podría resolverse al reenviador implícito no seguro noSuchMethod (también llamado _i) que el compilador genera dentro de MockExample.

El compilador crea esta implementación implícita de _i porque MockExample promete soportar un getter para _i cuando implementa Example en su declaración, pero no cumple esa promesa. Por lo tanto, la implementación del getter indefinido es manejada por la definición noSuchMethod de Mock, la cual crea un reenviador implícito noSuchMethod con el mismo nombre.

El fallo también puede ocurrir entre campos de clases no relacionadas.

Message:

'_i' couldn't be promoted because there is a conflicting noSuchMethod forwarder in class 'MockExample'.

Solution:

Define el getter en cuestión para que noSuchMethod no tenga que manejar implícitamente su implementación:

bien dart
import 'package:mockito/mockito.dart';

class Example {
  final int? _i;
  Example(this._i);
}

class MockExample extends Mock implements Example {
  @override
  late final int? _i;
}

void f(Example x) {
  if (x._i != null) {
    int i = x._i; // OK
  }
}

El getter se declara como late para ser consistente con cómo se usan generalmente los mocks; no es necesario declarar el getter como late para resolver este fallo de promoción de tipos en escenarios que no involucren mocks.

Posiblemente escrito después de la promoción

#

La causa: Estás intentando promover una variable que podría haber sido escrita desde que fue promovida.

Example:

mal dart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;           // (1)
  }
  if (!b) {
    print(i.isEven); // (2) ERROR
  }
}

Solution:

En este ejemplo, cuando el análisis de flujo llega a (1), degrada i de int no anulable de vuelta a int? anulable. Un humano puede notar que el acceso en (2) es seguro porque no hay ninguna ruta de código que incluya tanto (1) como (2), pero el análisis de flujo no es lo suficientemente inteligente como para ver eso, porque no rastrea correlaciones entre condiciones en declaraciones if separadas.

Podrías solucionar el problema combinando las dos declaraciones if:

bien dart
void f(bool b, int? i, int? j) {
  if (i == null) return;
  if (b) {
    i = j;
  } else {
    print(i.isEven);
  }
}

En casos de flujo de control lineal como estos (sin bucles), el análisis de flujo tiene en cuenta el lado derecho de la asignación al decidir si degradar. Como resultado, otra forma de corregir este código es cambiar el tipo de j a int.

bien dart
void f(bool b, int? i, int j) {
  if (i == null) return;
  if (b) {
    i = j;
  }
  if (!b) {
    print(i.isEven);
  }
}

Posiblemente escrito en una iteración previa del bucle

#

La causa: Estás intentando promover algo sobre lo que se podría haber escrito en una iteración anterior de un bucle, por lo que la promoción quedó invalidada.

Example:

mal dart
void f(Link? p) {
  if (p != null) return;
  while (true) {    // (1)
    print(p.value); // (2) ERROR
    var next = p.next;
    if (next == null) break;
    p = next;       // (3)
  }
}

Cuando el análisis de flujo llega a (1), mira hacia adelante y ve la escritura en p en (3). Pero debido a que está mirando hacia adelante, aún no ha determinado el tipo del lado derecho de la asignación, por lo que no sabe si es seguro mantener la promoción. Para estar seguro, invalida la promoción.

Solution:

Puedes solucionar este problema moviendo la comprobación de null al principio del bucle:

bien dart
void f(Link? p) {
  while (p != null) {
    print(p.value);
    p = p.next;
  }
}

Esta situación también puede surgir en declaraciones switch si un bloque case tiene una etiqueta, porque puedes usar declaraciones switch etiquetadas para construir bucles:

mal dart
void f(int i, int? j, int? k) {
  if (j == null) return;
  switch (i) {
    label:
    case 0:
      print(j.isEven); // ERROR
      j = k;
      continue label;
  }
}

Nuevamente, puedes solucionar el problema moviendo la comprobación de null al principio del bucle:

bien dart
void f(int i, int? j, int? k) {
  switch (i) {
    label:
    case 0:
      if (j == null) return;
      print(j.isEven);
      j = k;
      continue label;
  }
}

En catch después de una posible escritura en try

#

La causa: La variable podría haber sido escrita en un bloque try, y la ejecución ahora se encuentra en un bloque catch.

Example:

mal dart
void f(int? i, int? j) {
  if (i == null) return;
  try {
    i = j;                 // (1)
    // ... Additional code ...
    if (i == null) return; // (2)
    // ... Additional code ...
  } catch (e) {
    print(i.isEven);       // (3) ERROR
  }
}

En este caso, el análisis de flujo no considera seguro i.isEven (3), porque no tiene forma de saber en qué momento del bloque try podría haber ocurrido la excepción, por lo que asume de manera conservadora que podría haber sucedido entre (1) y (2), cuando i era potencialmente null.

Situaciones similares pueden ocurrir entre bloques try y finally, y entre bloques catch y finally. Debido a un artefacto histórico de cómo se realizó la implementación, estas situaciones de try/catch/finally no tienen en cuenta el lado derecho de la asignación, de manera similar a lo que sucede en los bucles.

Solution:

Para solucionar el problema, asegúrate de que el bloque catch no dependa de suposiciones sobre el estado de las variables que cambian dentro del bloque try. Recuerda, la excepción puede ocurrir en cualquier momento durante el bloque try, posiblemente cuando i es null.

La solución más segura es agregar una comprobación de nulo dentro del bloque catch:

bien dart
try {
  // ···
} catch (e) {
  if (i != null) {
    print(i.isEven); // (3) OK due to the null check in the line above.
  } else {
    // Handle the case where i is null.
  }
}

O, si estás seguro de que no puede ocurrir una excepción mientras i sea null, simplemente usa el operador !:

dart
try {
  // ···
} catch (e) {
  print(i!.isEven); // (3) OK because of the `!`.
}

Desajuste de subtipo (subtype mismatch)

#

La causa: Estás intentando promover a un tipo que no es un subtipo del tipo promovido actual de la variable (o no era un subtipo en el momento del intento de promoción).

Example:

mal dart
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is Pattern /* (2) */ ) {
      print(o.matchAsPrefix('foo')); // (3) ERROR
    }
  }
}

En este ejemplo, o se promueve a Comparable en (1), pero no se promueve a Pattern en (2), porque Pattern no es un subtipo de Comparable. (La lógica es que si se promoviera, entonces no podrías usar métodos en Comparable). Ten en cuenta que el hecho de que Pattern no sea un subtipo de Comparable no significa que el código en (3) esté muerto; o podría tener un tipo, como String, que implemente tanto Comparable como Pattern.

Solution:

Una solución posible es crear una nueva variable local de modo que la variable original se promueva a Comparable, y la nueva variable se promueva a Pattern:

dart
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    Object o2 = o;
    if (o2 is Pattern /* (2) */ ) {
      print(
        o2.matchAsPrefix('foo'),
      ); // (3) OK; o2 was promoted to `Pattern`.
    }
  }
}

Sin embargo, alguien que edite el código más tarde podría verse tentado a cambiar Object o2 por var o2. Ese cambio le da a o2 el tipo Comparable, lo que trae de vuelta el problema de que el objeto no sea promovible a Pattern.

Una comprobación de tipo redundante podría ser una mejor solución:

bien dart
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is Pattern /* (2) */ ) {
      print((o as Pattern).matchAsPrefix('foo')); // (3) OK
    }
  }
}

Otra solución que a veces funciona es cuando puedes usar un tipo más preciso. Si la línea 3 solo se interesa por cadenas, entonces puedes usar String en tu comprobación de tipo. Como String es un subtipo de Comparable, la promoción funciona:

bien dart
void f(Object o) {
  if (o is Comparable /* (1) */ ) {
    if (o is String /* (2) */ ) {
      print(o.matchAsPrefix('foo')); // (3) OK
    }
  }
}

Escritura capturada por una función local

#

La causa: La variable ha sido capturada para escritura (write-captured) por una función local o expresión de función.

Example:

mal dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ...
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven);       // (2) ERROR
}

El análisis de flujo deduce que tan pronto como se alcanza la definición de foo, esta podría ser llamada en cualquier momento, por lo tanto, ya no es seguro promover i en absoluto. Al igual que con los bucles, esta degradación ocurre independientemente del tipo del lado derecho de la asignación.

Solution:

A veces es posible reestructurar la lógica de manera que la promoción se realice antes de la captura por escritura:

bien dart
void f(int? i, int? j) {
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i.isEven); // (2) OK
  var foo = () {
    i = j;
  };
  // ... Use foo ...
}

Otra opción es crear una variable local, para que no sea capturada por escritura:

bien dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ...
  var i2 = i;
  if (i2 == null) return; // (1)
  // ... Additional code ...
  print(i2.isEven); // (2) OK because `i2` isn't write captured.
}

O puedes hacer una comprobación redundante:

dart
void f(int? i, int? j) {
  var foo = () {
    i = j;
  };
  // ... Use foo ...
  if (i == null) return; // (1)
  // ... Additional code ...
  print(i!.isEven); // (2) OK due to `!` check.
}

Escrito fuera de la clausura o expresión de función actual

#

La causa: La variable se escribe fuera de una clausura (closure) o expresión de función, y la ubicación de la promoción de tipos está dentro de la clausura o expresión de función.

Example:

mal dart
void f(int? i, int? j) {
  if (i == null) return;
  var foo = () {
    print(i.isEven); // (1) ERROR
  };
  i = j;             // (2)
}

El análisis de flujo deduce que no hay forma de determinar cuándo podría llamarse a foo, por lo que podría llamarse después de la asignación en (2), y por lo tanto, la promoción podría dejar de ser válida. Al igual que con los bucles, esta degradación ocurre independientemente del tipo del lado derecho de la asignación.

Solution:

Una solución es crear una variable local:

bien dart
void f(int? i, int? j) {
  if (i == null) return;
  var i2 = i;
  var foo = () {
    print(i2.isEven); // (1) OK because `i2` isn't changed later.
  };
  i = j; // (2)
}

Example:

Un caso particularmente desagradable se ve así:

mal dart
void f(int? i) {
  i ??= 0;
  var foo = () {
    print(i.isEven); // ERROR
  };
}

En este caso, un humano puede ver que la promoción es segura porque la única escritura en i utiliza un valor no nulo y ocurre antes de que se cree foo. Pero el análisis de flujo no es tan inteligente.

Solution:

Nuevamente, una solución es crear una variable local:

bien dart
void f(int? i) {
  var j = i ?? 0;
  var foo = () {
    print(j.isEven); // OK
  };
}

Esta solución funciona porque se infiere que j tiene un tipo no anulable (int) debido a su valor inicial (i ?? 0). Debido a que j tiene un tipo no anulable, independientemente de si se asigna más tarde o no, j nunca puede tener un valor no nulo.

Escritura capturada fuera de la clausura o expresión de función actual

#

La causa: La variable que estás intentando promover está capturada para escritura fuera de una clausura (closure) o expresión de función, pero este uso de la variable está dentro de la clausura o expresión de función que está intentando promoverla.

Example:

mal dart
void f(int? i, int? j) {
  var foo = () {
    if (i == null) return;
    print(i.isEven); // ERROR
  };
  var bar = () {
    i = j;
  };
}

El análisis de flujo deduce que no hay forma de saber en qué orden podrían ejecutarse foo y bar; de hecho, bar podría incluso ejecutarse a la mitad de la ejecución de foo (debido a que foo llama a algo que llama a bar). Por lo tanto, no es seguro promover i en absoluto dentro de foo.

Solution:

La mejor solución es probablemente crear una variable local:

bien dart
void f(int? i, int? j) {
  var foo = () {
    var i2 = i;
    if (i2 == null) return;
    print(i2.isEven); // OK because i2 is local to this closure.
  };
  var bar = () {
    i = j;
  };
}