Dart null 安全性不适用于类字段

Posted

技术标签:

【中文标题】Dart null 安全性不适用于类字段【英文标题】:Dart null safety doesn't work with class fields [duplicate] 【发布时间】:2021-04-04 00:08:14 【问题描述】:

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

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


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

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

值可以为“null”的表达式必须先进行空值检查,然后才能取消引用。 在取消引用之前尝试检查该值是否不是“空”。

对于Bar()

必须初始化不可为空的实例字段“_a”。 尝试添加初始值设定项表达式,或在此构造函数中添加字段初始值设定项,或将其标记为“延迟”。

在这两种情况下,我已经完全按照错误提示进行了操作!这是怎么回事?

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

编辑:我找到了this page,上面写着:

分析器无法对整个应用程序的流程进行建模,因此无法预测全局变量或类字段的值。

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

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

【问题讨论】:

这能回答你的问题吗? Null check doesn't cause type promotion in Dart 【参考方案1】:

问题是类字段即使被标记为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 类覆盖 text 最终字段,因此它在第一次被询问时返回一个值,但在此之后返回 null。您不能以可以防止允许这种形式的覆盖的方式编写您的 A 类。

所以我们不能将getText 的返回值从String? 更改为String,即使看起来我们在返回之前检查了text 字段中的null

【讨论】:

我觉得奇怪的是,一个相当深奥的用例,比如在子类中覆盖 final,现在在 NNBD 实现中出现了巨大的缺陷。 Flutter 布局将充满!,每一个都会在未来产生潜在的错误,因为替代方案太冗长了。 @shawnblais 或者创建对成员值的本地引用。 谢谢@jamesdlin 我相信这实际上是唯一安全的方法。但我认为大多数开发人员只会使用 !运算符,然后基本上消除了 NNBD 对该变量的任何优势,并创建了将来很容易被破坏的脆弱方法。例如; if(index == null) return; index = index! + 1; 如果后来有人删除了这个空检查,编译器什么也没说,手榴弹就在那里。在这个看起来有点傻的例子中,但是对于超过几行的方法,这是一个重要的问题,那些! 运算符并没有完全跳到读者身上。 @Adnan 没错。 getter/setter 方法的目的是它们不与普通变量区分开来。从某种意义上说,final 变量是一个只有 getter 的字段,而var 变量既有 setter 又有 getter。这也意味着我们总是可以用新的例如覆盖一个变量。 getter 像我的例子,这是这个问题的核心。 (但这也是我们不创建像 Java 这样的 getSomethig()setSomehing() 方法的原因,因为如果需要,我们可以随时将逻辑引入现有字段,而无需更改 API。 @Adnan 在 Java 中,很少公开类变量是标准做法。相反,我们定义getVariable()setVariable() 来获取和设置一个私有变量。原因是 Java 没有 getter/setter 方法的概念(如 Dart 或 C#),所以如果我们稍后要引入例如字段验证,如果不更改 API,我们就无法做到这一点,除非我们从一开始就使用一种方法来访问变量。【参考方案2】:

值可以为“null”的表达式必须先进行空值检查,然后才能取消引用。在取消引用之前尝试检查该值是否为“null”。

这似乎真的只适用于局部变量。此代码没有错误:

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

虽然有点烂。我的代码现在充满了仅将类成员复制到局部变量并再次返回的代码。 :-/

必须初始化不可为空的实例字段“_a”。尝试添加初始化表达式,或在此构造函数中添加字段初始化,或将其标记为“延迟”。

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

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

【讨论】:

【参考方案3】:

处理这种情况的方法很少。我已经给出了detailed answer here,所以我只是从中编写解决方案:

使用局部变量(推荐)

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


回答你的第二个问题:

应始终初始化不可为空的字段。一般有三种初始化方式:

在声明中:

class Bar 
  String a = 'a';

在初始化形式中

class Bar 
  String a;

  Bar(required this.a);

在初始化列表中:

class Bar 
  String a;

  Bar(String b) : a = b;

【讨论】:

【参考方案4】:

您可以像这样在 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;
  

【讨论】:

以上是关于Dart null 安全性不适用于类字段的主要内容,如果未能解决你的问题,请参考以下文章

Dart null 安全性和使用 json_serializable 将 JSON 解析为模型

库 'package:flutter/material.dart' 是遗留的,不应导入到 null 安全库中

▩Dart-空安全(Null Safety)

使用 if 检查进行 Dart 空安全 [重复]

是否可以为依赖的 Dart 包禁用空安全性?

必须初始化不可为空的实例字段