Dart 空安全不适用于 class 字段

Dart null safety doesn't work with class fields

我已将我的 Dart 代码迁移到 NNBD / Null Safety。其中一些看起来像这样:

class Foo {
  String? _a;
  void foo() {
    if (_a != null) {
      _a += 'a';
    }
  }
}

class Bar {
  Bar() {
    _a = 'a';
  }
  String _a;
}

这会导致两个分析错误。对于 _a += 'a';:

An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.

对于Bar() {

Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.

在这两种情况下,我都完全按照错误提示做了!这是怎么回事?

我正在使用 Dart 2.12.0-133。2.beta(12 月 15 日星期二)。

编辑:我发现 this page 上面写着:

The analyzer can’t model the flow of your whole application, so it can’t predict the values of global variables or class fields.

但这对我来说没有意义 - 在这种情况下,从 if (_a != null)_a += 'a'; 只有一种可能的流量控制路径 - 没有异步代码并且 Dart 是单线程的 - 所以_a 不是本地的并不重要。

Bar() 的错误消息明确说明了在构造函数中初始化字段的可能性。

An expression whose value can be 'null' must be null-checked before it can be dereferenced. Try checking that the value isn't 'null' before dereferencing it.

看来这真的只对局部变量有效。此代码没有错误:

class Foo {
  String? _a;
  void foo() {
    final a = _a;
    if (a != null) {
      a += 'a';
      _a = a;
    }
  }
}

虽然有点糟糕。我的代码现在充满了将 class 成员复制到局部变量并再次复制回来的代码。 :-/

Non-nullable instance field '_a' must be initialized. Try adding an initializer expression, or add a field initializer in this constructor, or mark it 'late'.

啊原来“字段初始化器”实际上是这样的:

class Bar {
  Bar() : _a = 'a';
  String _a;
}

问题是 class 字段可以被覆盖,即使它被标记为 final。下面的例子说明了这个问题:

class A {
  final String? text = 'hello';

  String? getText() {
    if (text != null) {
      return text;
    } else {
      return 'WAS NULL!';
    }
  }
}

class B extends A {
  bool first = true;

  @override
  String? get text {
    if (first) {
      first = false;
      return 'world';
    } else {
      return null;
    }
  }
}

void main() {
  print(A().getText()); // hello
  print(B().getText()); // null
}

B class 覆盖 text final 字段,因此它 return 是第一次被询问时的值,但 return 是 null 在此之后。您不能以可以阻止允许这种形式的覆盖的方式编写 A class。

所以我们无法将 getText 的 return 值从 String? 更改为 String,即使看起来我们检查了 text 字段中的 null 在 return 之前。

处理这种情况的方法很少。我给了一个 所以我只写它的解决方案:

  • 使用局部变量(推荐)

    void foo() {
      var a = this.a; // <-- Local variable
      if (a != null) {
        a += 'a';
        this.a = a;
      }
    }
    
  • 使用??

    void foo() {
      var a = (this.a ?? '') + 'a';
      this.a = a;
    }
    
  • 使用 Bang 运算符 (!)

    只有当您 100% 确定变量 (a) 在您使用时不是 null 时,才应使用此解决方案。

    void foo() {
      a = a! + 'a'; // <-- Bang operator
    }
    

回答你的第二个问题:

不可为 null 的字段应始终进行初始化。一般有3种初始化方式:

  • 声明中:

    class Bar {
      String a = 'a';
    }
    
  • 初始化中

    class Bar {
      String a;
    
      Bar({required this.a});
    }
    
  • 在初始化列表中:

    class Bar {
      String a;
    
      Bar(String b) : a = b;
    }
    

您可以像这样在 null-safety 中创建您的 类

    class JobDoc {
  File? docCam1;
  File? docCam2;
  File? docBarcode;
  File? docSignature;

  JobDoc({this.docCam1, this.docCam2, this.docBarcode, this.docSignature});

  JobDoc.fromJson(Map<String, dynamic> json) {
    docCam1 = json['docCam1'] ?? null;
    docCam2 = json['docCam2'] ?? null;
    docBarcode = json['docBarcode'] ?? null;
    docSignature = json['docSignature'] ?? null;
  }
}