Dart 3.15 让 this 也能自动收窄类型

Dart 3.15 this 类型提升通常会被理解成“把 this 在运行时改成子类”,实际变化只发生在静态分析阶段:经过成功的 this is Child 类型测试后,分析器会在该控制流路径内把 this 提升为 Child。因此,旧代码中的 (this as Child).childOnly() 通常可以改为 this.childOnly(),但对象引用和运行时类型都没有改变。

这种提升只存在于分析器确认成功的路径中。泛型参数 T、普通参数、可空字段,以及另一个指向同一对象的 dynamic 或 Object? 变量,不会随之获得更精确的类型。掘金热榜文章用“this 终于支持类型提升了”概括了这次变化,实际使用时仍需关注这些作用域和兼容边界。

Dart 3.15 的 this 类型提升范围

Dart 本身已经具备强类型、空安全和模式匹配。 过去的类型提升主要作用于可提升的局部变量和参数,this 作为实例接收者不在其范围内。下面的代码包含运行时类型判断,但在旧行为中,分支内的 this 仍只有 Base 的静态类型:

if (this is Child) {
  // 旧行为中,这里的 this 仍然不能直接访问 Child 的成员。
}

Dart 3.15 改变的是分析器对成功路径的认识:在 this is Child 成立的范围内,this 既可按 Base 分析,也可按 Child 分析。它不是创建新对象,也不是修改对象的类,而是让后续成员访问获得足够精确的静态类型。

这项变化能改善 IDE 中的类型分析,代码补全、成员检查和重构分析因此都能识别 Child 的 API。静态类型越准确,编辑器和分析器提供的补全与警告通常也越有用。

为什么旧代码要写 as 强转?

旧版代码通常需要在类型判断后,再显式转换一次接收者:

class Base {
  void handle() {
    if (this is Child) {
      (this as Child).childOnly();
    }
  }
}

class Child extends Base {
  void childOnly() {}
}

成员调用所需的具体类型信息来自 this is Child。但旧分析器没有据此提升原始 this,所以调用成员时只能把 this 强制转换成 Child,再以转换结果作为接收者。

Dart 3.15 中可以写成:

class Base {
  void handle() {
    if (this is Child) {
      this.childOnly();
    }
  }
}

class Child extends Base {
  void childOnly() {}
}

两段代码的运行时分派没有变化:进入 handle 的仍然是同一个对象,运行时类型仍然是 Child。变化只是去掉了第二次显式类型转换,让代码直接表达“在这个分支里,this 可以当作 Child 使用”。

现有 (this as Child) 通常仍然合法,也没有必要为了升级而全仓删除。适合删除的是这种紧跟明确类型判断、仅用于访问成员或继续调用方法的重复转换;如果强转本身就是公开的运行时检查、代码生成结果,或者仍需支持旧语言版本,则可以继续保留。

is、is! 和复合条件怎么写?

正向 is 判断是触发这种提升最直接的条件:

class Base {
  void handle() {
    if (this is Child) {
      this.childOnly();
    }
  }

  bool get ready => true;

  void handleWithGuard() {
    if (this is! Child) {
      return;
    }

    this.childOnly();
  }

  void handleWithNestedCondition() {
    if (this is Child) {
      if (ready) {
        this.childOnly();
      }
    }
  }

  void handleWithCompoundCondition(bool enabled) {
    if (enabled && this is Child) {
      this.childOnly();
    }
  }
}

class Child extends Base {
  void childOnly() {}
}

is! 本身并不把 this 提升为 Child。真正起作用的是控制流:早返回之后,剩余代码只可能在 this is Child 为真时执行,因此分析器可以在这里提升 this。如果条件不成立就直接离开当前方法,这个边界尤其清晰。

复合条件同样按控制流判断。下面的 enabled && this is Child 只有在两个条件都成立时才进入分支,所以 this.childOnly() 可以通过分析:

if (enabled && this is Child) {
  this.childOnly();
}

但这次提升只作用于分支内,分支外的 this 仍按 Base 分析:

if (this is Child) {
  this.childOnly();
}

this.childOnly(); // Error:此处的 this 仍按 Base 分析。

else 分支只能说明 this 不是 Child。失败分支之后重新汇合的代码仍然按原来的 Base 分析。对于 ||、嵌套判断和提前返回,也应沿着实际可达路径判断,不能仅凭文件中出现过一次 this is Child 就假定后续都成立。

哪些继承和泛型代码最值得改?

应优先处理基类或接口实现中的类型分派代码。基类方法需要根据实际接收者调用子类专属成员时,旧写法经常形成重复模式:

if (this is Child) {
  (this as Child).childOnly();
}

删除转换后,成员调用更直接,也更容易让 IDE 展示完整的子类 API。这项收益在多层分派中更明显,例如基类先判断具体实现,再连续调用只属于该实现的成员。

is 的目标也可以是接口,而不必是具体父类:

abstract class JsonEncoder {
  void encodeJson();
}

class BaseEncoder {
  void encode() {
    if (this is JsonEncoder) {
      this.encodeJson();
    }
  }
}

class JsonEncoderImpl extends BaseEncoder implements JsonEncoder {
  @override
  void encodeJson() {}
}

在成功分支里,this 被提升为 JsonEncoder,因此可以直接调用接口方法。mixin 组合后的具体类也可以作为类型测试目标;原理仍然是检查实际接收者是否满足该接口或组合类型。

泛型类需要额外区分“接收者被提升”和“泛型参数变量被提升”:

class Cache<T> {
  void put(T value) {
    if (this is Cache<int>) {
      this.putInt(value as int);
    } else if (this is Cache<String>) {
      this.putText(value as String);
    }
  }

  void putInt(int value) {}

  void putText(String value) {}
}

this is Cache<int> 可以把接收者 this 提升为 Cache<int>,但不会同步改写方法参数 value 的声明类型 T,也不会让另一个普通参数自动变成 int。如果需要静态证明参数也成立,应分别测试:

if (this is Cache<int> && value is int) {
  this.putInt(value);
}

这里 this 和 value 分别沿自己的条件得到提升。泛型、顶层函数和实例方法都是 Dart 的基础能力,但类型提升不会绕过泛型参数的独立类型检查。

对于静态方法,则不存在可提升的 this。静态方法中的普通局部变量或参数仍可使用原有类型提升,实例接收者的这项新能力则不适用。

空安全、dynamic 和模式匹配会怎样影响它?

空安全下,普通实例成员中的 this 本身不是 nullable。this is PremiumAccount 只能证明接收者具有 PremiumAccount 的运行时类型,不能证明它的每个属性都非空:

if (this is PremiumAccount) {
  // this 已经提升为 PremiumAccount。
  // plan 是否为 null 仍取决于 plan 自己的声明和检查。
}

如果 plan 是 String?,仍然需要单独判断,或者先保存到一个不再变化的局部变量后再检查。接收者类型和属性值的空状态是两套独立条件。

dynamic 与 Object? 变量也不会因为 this 被提升而改变静态类型:

Object? snapshot = this;
dynamic loose = this;

if (this is Child) {
  // this 按 Child 分析。
  // snapshot 仍是 Object?,loose 仍是 dynamic。
}

dynamic 的成员调用会延迟检查,Object? 则允许任意对象或 null;它们都需要自己的 is 条件才能获得可靠的子类型信息。

闭包和重新赋值同样不能绕过流分析规则。this 本身不能重新绑定到另一个对象,嵌套函数词法捕获的仍然是这个接收者;但闭包捕获的普通变量如果可能被写入,或者在条件成立后又被重新赋值,就不能无条件沿用之前的提升。迁移时应检查共享变量、异步回调和延迟执行代码,而不只是同步分支。

模式匹配解决的是“把匹配值绑定成有类型的变量”,与直接提升裸 this 不是同一种写法:

class Base {
  void handleWithIfCase() {
    if (this case Child child) {
      child.childOnly();
    }
  }

  void handleWithSwitch() {
    switch (this) {
      case Child child:
        child.childOnly();
        break;
    }
  }
}

class Child extends Base {
  void childOnly() {}
}

这里真正获得 Child 类型的是模式变量 child。如果使用 case Child() 之类不绑定变量的对象模式,不应假定后面的裸 this 已经自动提升。模式匹配本身是 Dart 的既有能力,适合需要同时解构对象、匹配多个字段或绑定局部变量的情况。

升级项目要改 SDK 还是语言版本?

Dart 3.15 迁移首先需要确认实际运行的 SDK,而不只是修改配置文件。独立 Dart 项目可以检查:

dart --version

Flutter 项目还应确认当前 Flutter 工具链实际携带的 Dart 版本:

flutter --version

这个特性受包的 language version 控制。对发布到 pub.dev 的 Dart 包,语言版本通常由 pubspec.yaml 中 SDK 约束的下限决定,例如:

environment:
  sdk: '>=3.15.0 <4.0.0'

不要手工修改 .dart_tool/package_config.json 或其他生成文件来强行启用语言版本。依赖解析会根据各 package 的 SDK 约束生成对应配置:

dart pub get

如果某个库需要暂时保持旧语言行为,可以在采用 Dart 3.15 下限的包中使用语言版本注释:

// @dart=3.14

这种注释用于局部退出新语言行为,不能替代 package 的 SDK 兼容声明,也不能让一个声明支持旧 SDK 的包具备 Dart 3.15 能力。Monorepo 中每个具有独立 pubspec.yaml 的 package 也需要分别判断,不能只看仓库根目录。

Dart 应用完成依赖解析后执行:

dart analyze
dart test

Flutter 项目则使用与自身工具链一致的流程:

flutter pub get
flutter analyze
flutter test

对于对外发布的库,提高最低 Dart SDK 会直接改变下游解析条件。即使公共 API 和运行时代码完全没变,仍使用旧 Dart SDK 的调用方也可能无法解析该版本。因此,是否提高 SDK 下限必须结合库支持的 Dart 版本范围决定,不能只因本机升级成功就直接提高下限。

旧代码怎样逐步删掉 as?

先处理类型分支较多、重复转换最明显的类,而不是全仓搜索 as 后批量删除。优先处理满足三个条件的代码:已经存在明确的 this is T 或等价类型测试;转换结果只用于紧邻的成员访问;改写后不改变公共方法签名。

每改完一个分支就执行格式化和静态分析:

dart format .
dart analyze

随后运行对应测试:

dart test

Flutter 项目使用:

dart format .
flutter analyze
flutter test

测试至少应覆盖子类和基类两种接收者,确认成功分支确实调用子类成员,失败分支仍走原有逻辑。泛型库还应覆盖不同类型参数的实例,异步代码则要检查闭包捕获和重新赋值。

生成代码不要手工清理 as。应使用项目现有的生成流程重新生成,再审查差异,避免下一次生成时全部丢失。仍需支持旧 Dart 版本的发布分支、承担明确运行时检查的强转,以及不属于重复收窄的转换,也应继续保留。

如果暂时不能提高库的最低 SDK,可以保留旧写法和当前兼容范围,等计划停止支持旧调用方的版本发布后再做清理。也可以维护独立发布线或兼容层,但把依赖最低 Dart 3.15 的实现直接加入旧版本依赖图,对旧调用方并不是透明的降级方案。

迁移完成的验收标准

迁移完成不以“代码看起来少了一个 as”为准,而要同时满足以下条件:实际 Dart 或 Flutter 工具链达到 3.15,启用该特性的 package language version 正确,依赖解析和静态分析通过,单元测试或 Widget 测试覆盖成功与失败分支,公共 API 没有意外变化。

对于独立应用,应固定项目实际支持的 SDK 版本并让 CI 使用同一工具链。对于对外库,最低 SDK 和当前稳定 SDK 都应纳入测试范围;如果项目仍承诺支持旧调用方,还必须确认它们能够完成依赖解析并运行原有测试。这些结果应纳入兼容矩阵;所有仍受支持的调用方通过验证后,Dart 3.15 的 this 类型提升迁移才算完成。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容