Dart SDK 语言版本化与实验特性机制:版本选择模型、Experiment Flags 与发布流程全解析
编程语言编译器语言运行时标准库开发工具【免费下载链接】sdkThe Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.项目地址https://gitcode.com/gh_mirrors/sdk1/sdk点击查看免费下载本文深入解析 Dart SDK 的语言版本化Language Versioning与实验特性开关Experiment Flags机制为什么每个稳定版本都锁定一种独立的 Dart 语言、正在开发中的 in-progress 版本如何衍生出一族实验语言、库与包如何通过// dart注释、SDK 约束与package_config.json精确选中目标语言版本以及实验特性从flag 后面到正式发布的完整流程。读完本文你将能准确理解--enable-experiment的行为边界并掌握为包开启实验特性、随发布周期正确调整 SDK 约束的完整实操方案。核心模型每一次发布都在雕刻一门语言Dart SDK 对语言版本采取一种非常独特的心智模型理解它是掌握一切后续机制的前提。该模型在 docs/process/language-versions-and-experiments.md 中被正式阐述核心原则有三条每个 major 或 minor即 .0版本的 SDK 发布都会创建一门新的语言版本。一门已发布语言版本的语义被它在该版本 SDK 中的实际行为彻底固定。任何时刻都存在一个 in-progress进行中语言版本它挂着一族彼此相关的语言——每一种对应一组实验特性的不同组合。语言版本化、实验 flag以及处理核心库和 Flutter 等特殊包所用的魔法归根结底都是同一种机制决定某个库要瞄准众多 Dart 语言中的哪一门。换言之--enable-experiment不是打开一个功能开关而是把库切换到另一门语言。这一视角的转变是理解 Dart 演进方式的关键。语言版本一条被锁定的有序序列存在一条有序的已发布语言版本序列2.5、2.6、2.7……每一版都是一门不同的语言。它们之间可能存在不兼容实践中大体兼容。每个 Dart 库都瞄准即编写于单一语言版本。一个程序可以包含用多种语言版本编写的库而一个 Dart SDK 可以同时支持多个不同版本的 Dart 语言。每当 Dart SDK 发布一个 major 或 minor 稳定版本对应的语言版本就被刻进石头。例如发布 Dart 2.5.0 的那一天起Dart 2.5 就永远是第一个支持 constant update常量更新变更的 Dart 版本不可再改变。文档写作当时2.5、2.6、2.7 语言版本均已锁定。Patch 版本如 2.5.1不引入新的语言版本。Dart SDK 2.5.0 与 2.5.1 都包含语言版本 2.5。这意味着不能在 patch 版本中发布破坏性语言变更——否则任何已经瞄准该语言版本的用户库都会在升级后瞬间被破坏。In-progress 版本尚未定型的那一门任何时刻还存在一个in-progress 语言版本它对应当前的 dev 构建或等价地对应下一个将要发布的稳定版本。例如文档写作时由于已发布 2.7.1 而尚未发布 2.8.0当时的 in-progress 语言版本就是 2.8。与已锁定的稳定版本不同in-progress 版本没有刻进石头。随着 SDK 的开发它的行为可能随时变化。实验语言in-progress 版本的一族兄弟姐妹in-progress 版本并非孤身一人它挂着一族实验语言experimental languages每一种对应一组实验 flag 的特定组合。虽然只有一个 Dart 2.7却可以同时存在多个 Dart 2.82.8bleeding edge 上、未启用任何实验特性时的 Dart。2.8non-nullable同样基础上启用了 non-nullable 实验。2.8variance启用 variance 实验。2.8non-nullablevariance同时启用两个实验。所有这些语言同时、并行地存在。Dart 2.6、Dart 2.7、Dart 2.8、Dart 2.8non-nullable 等在某种意义上都是真实存在的它们有有时并不完整的规范有实现它们的工具。请把每一门都看作一门拥有自己名称、语法和语义的独立语言。因此不要认为实验 flag 是开启一个功能。功能一直在那里只是位于另一门语言之中。唯一的问题是哪座库想搬过去使用它。可以用如下示意图可视化不同 Dart 风味的空间结构┌──────── shipped ─────────┐ ┌─ in-progress ─────────────┐ older... ┄─ 2.5 ─ 2.6 ─ 2.7 ─ 2.8 │ ┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 2.8non-nullable ╎ ╎ │ ╎ no ╎ 2.8variance ╎ languages ╎ │ ╎ here... ╎ 2.8triple-shift ╎ ╎ │ ╎ ╎ 2.8non-nullablevariance └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ │ ┆ other experiment combinations...图中左侧是向历史深处延伸的、所有已发布稳定版本的数值语言numeric languages序列右侧则是唯一的 in-progress 版本及其旁的各种实验组合。除此之外不存在其他语言——特别地不存在已发布版本 实验的组合。你无法在瞄准已发布语言版本的库上启用某个实验。一旦某版本发布它周围的所有实验语言便随之蒸发并由围绕新 in-progress 版本的全新实验语言取代。今天variance 等特性位于当前 in-progress 版本仓库中tools/experimental_features.yaml的current-version字段显示为3.14.0周围而不是任何已发布的稳定版本周围。选择一门语言数值部分如何确定存在如此之多的 Dart 语言后其余一切都只是让用户选择哪一门的机制。选择的第一步是确定数值部分如 2.7 还是 2.8语言版本规范定义了其规则按优先级排列如下注释显式选择// dart2.5这样的注释为库选择对应数值版本。在 CFECommon Front End实现中这会通过SourceCompilationUnit.registerExplicitLanguageVersion注册为显式语言版本覆盖见 pkg/front_end/lib/src/builder/compilation_unit.dart。包内其他库package_config.json文件指定它们的语言版本。该文件由 pub 依据用户所用各包 pubspec 中的 SDK 约束生成。无 SDK 约束的包pub 不会为该包在package_config.json中写入语言版本此时库默认使用 current SDK 版本。不属于任何包的库同样默认使用 current SDK 版本。SDK 版本 → 语言版本当前 SDK 版本通常是运行各种 Dart 工具加--version得到的 semver 版本号。将三段式 semver SDK 版本换算为 major.minor 语言版本的规则是SDK 的语言版本 该 SDK 中工具报告的版本号的 major 和 minor 部分。由此得出两个推论稳定版Dart 2.5.3 的语言版本就是 2.5符合直觉。dev / bleeding edge 版语言版本是即将到来的稳定版本。例如dart --version报告 2.8.0-edge.a38…其语言版本就是 2.8。换言之非稳定版 SDK 的默认语言版本就是 in-progress 语言。在仓库内部SDK 版本号的唯一事实来源是 tools/VERSION它在发布流程切分支、发版时被更新当前为 CHANNEL main、MAJOR 3、MINOR 14。理论上可以从该文件按上述规则推算语言版本但团队担心语言版本会作为发布行政事务的连带后果被无意改变因此仓库把语言版本显式存放在tools/experimental_features.yaml的current-version字段中。这带来一个理论上的副作用SDK 报告的版本可能与语言版本失同步实践中这种偏差应当罕见且只有自行构建 bleeding edge SDK 的用户才可能察觉。SDK 约束 → 语言版本将 SDK 约束换算为语言的规则是包使用的默认语言版本 其 SDK 约束最低版本minimum version的语言版本。因此以下 SDK 约束分别得到对应的语言版本SDK 约束得到的语言版本2.6.0 3.0.02.62.6.3 3.0.02.6仍是 2.62.7.1 3.0.02.7仍是 2.72.8.0-dev.1 3.0.02.8in-progress 版本这条规则允许用户瞄准只存在于 dev 版本中的语言版本也允许用户把 patch 版本作为最低版本以获得 bug 修复或核心库变更。实验 Flag如何登上 in-progress 的实验支线语言版本化解决了让库落到某个数值版本含 in-progress 版本 2.8的问题。但如果想体验正在开发的实验特性呢那就需要登上 in-progress 版本的实验兄弟语言——通过向各种工具传入实验 flag并在analysis_options.yaml中配置来实现。这就是实验 flag 的全部作用向 Dart 工具传入一组实验 flag意味着把所有使用 in-progress 语言的用户库当作使用指定实验语言来对待。两个关键词需要仔细拆解用户库user library指 Dart 与 Flutter 团队之外的普通用户编写的库团队自身拥有下文将介绍的特殊权限。使用 in-progress 语言该规则只对 in-progress 语言生效。传 non-nullable flag 会把所有瞄准 2.8 的用户库挪到 2.8non-nullable但对瞄准 2.7 或更早版本的库毫无影响。不存在 2.7non-nullable 这种东西。2.7.0 发布那天2.7 被锁定它周围的实验版本全部蒸发被围绕新 in-progress 版本 2.8 的一批全新实验语言取代。发布 SDK 与语言并不等于自动打开当时存在的所有实验 flag。许多被 flag 门控的语言变更会随多个版本漂移最终才准备好发布。例如 non-nullable 和 variance 实验在 2.7.0 发布前就存在之后依然存在。当新版 SDK 发布时除非实验特性被刻意shipped行为默认开启、flag 消失否则 flag 会原样保留为下一个 in-progress 版本中的实验特性。所以发布 Dart 2.7.0 那天variance 不再是影响 Dart 2.7 的 flag而是变成了影响 Dart 2.8 的 flag。从当前仓库的 tools/experimental_features.yaml 可以看到non-nullable最终在 2.12.0 正式发布enabledIn: 2.12.0而variance至今仍是一个未默认启用的实验没有enabledIn字段。实验 flag 对所有用户库全局生效注意一个关键性质传入实验 flag 会把所有in-progress 版本的用户库都挪到该实验语言上。如果你传了 non-nullable你的所有 2.8 库以及你所依赖的每个包中的每个 2.8 库立刻全部开始瞄准 2.8non-nullable。Dart 支持由使用各种已发布版本如 2.7 和 2.6的库构成的混合模式mixed-mode程序也可以把它们与一个in-progress 版本如 2.8 或 2.8non-nullable混用。但 Dart不支持任意组合的实验语言混合。团队不愿定义或实现一个 2.8variance 库导入 2.8non-nullable 库并继承其泛型类这类场景——组合的组合是通往混乱之路。团队内部可能因核心库等原因允许少量混合见下节因为可以精心控制处于这种奇怪状态下的代码但不允许用户编写混合不同实验语言的程序。如果你有一个不希望被正在尝试的实验影响的用户库请确保该库不在 in-progress 版本上。SDK 核心库与其他特权朋友实验 flag 是把库从 in-progress 版本挪到其某个实验兄弟版本上的一种方式但并非唯一方式。再次强调in-progress 版本的所有实验风味同时存在实验 flag 主要供用户把自己的库选入这些实验语言。Dart 团队自身拥有特殊权力。已迁移的 SDK 核心库不需要用户传任何实验 flag就能进入 2.8non-nullable——工具在编译这些特定库时知道自动完成迁移。同样当 Flutter以及 vector_math 等少数从其 API 导出的包迁移时也可以用白名单或其他特殊手段把它们挪进 2.8non-nullable。但所有这些库仍需小心选择正确的数值版本——因为再次强调不存在 2.7non-nullable。如果一个核心库没有被标记为 2.8它就不可能成为 2.8non-nullable。Dart 的语言版本化支持正是库们完成这件事的方式。在实现层面这个特殊权力体现为sdk/lib/_internal/allowed_experiments.json中的白名单机制CFE 的isExperimentEnabledInLibrary见 pkg/front_end/lib/src/api_prototype/experimental_flags.dart会按库的 canonical URI 区分dart:库与package:包查表决定是否允许其在未传 flag 时启用实验。当前仓库的 allowed_experiments.json 即定义了sdkExperiments与macros两个实验集。实操如何让库用上实验特性以 null safety 为例把上述机制串起来看看当年要让自己库用上?和late需要做哪几步null safety 现已随 2.12.0 正式发布但该流程对当前仍在 flag 之后的任何实验特性——如仓库中的macros、variance、data-assets等——完全适用使用 dev 或 bleeding edge 构建的 SDK。当时的稳定版最高只支持语言 2.7不支持 null safety。告诉 Dart 你的库应被当作 2.8 处理。核心库用的是版本注释和/或某种硬编码包则通过 pubspec 的 SDK 约束environment: sdk: 2.8.0-dev.0 2.8.0SDK 最低约束可以按需提高 dev 版本要求关键是最低版本至少是2.8.0 的一个 dev 版本。也可以完全省略 SDK 约束——这对应用包可行但对库包不行因为 pub 不允许发布没有 SDK 约束的包。SDK 最高约束相对较低的上限2.8.0为 2.8.0 之前搞破坏留出了回旋余地。严格来说并非必须——写成3.0.0时本文所述的一切依然成立但当时并不明智地宣称现在发布的包能一路兼容到 3.0.0。让编译器把所有 2.8 用户库挪到 2.8non-nullable即在调用工具时传--enable-experimentnon-nullable并在analysis_options.yaml中做类似配置以获得 IDE 支持。完整细节见 docs/process/experimental-flags.md。完成这三步Dart 工具就知道该库瞄准的是 2.8non-nullable至少在当下如此。实验 Flag 的 CLI 与 IDE 使用规范上述第 3 步涉及的 flag 用法在 docs/process/experimental-flags.md 中有明确的工程规范。之所以需要这套规范是因为 Dart SDK 通过多条渠道发布独立 SDK、Flutter SDK、Google 内部渠道各渠道发布日历不同任何 dev channel 构建都有可能在某个渠道成为正式发布的 SDK因此必须保证 dev channel 质量与哪些功能完整可用的一致性同时又要允许不完整功能落地以支持跨组件特性组合、自动化测试、合作方与客户的预览等需求。结论是所有进行中的特性都必须放在同一组 flag 之后对 flag 背后特性的变更不视为破坏性变更即使该特性曾出现在稳定 SDK 中且随时可能被移除。关于破坏性变更的完整流程见 docs/process/breaking-changes.md。所有破坏性变更与所有语言变更必须在可能时放在 flag 之后开发较大、将跨越数周甚至多个版本处于中间状态的用户可见变更以及可能带来显著负面性能影响的变更也建议如此。CLI 工具的 flag 格式Flag 由一个或多个小写单词用连字符连接组成。其唯一事实来源是一个共享的 .dart 文件各工具提供查询框架以便轻松访问新 flag。flag 通过--enable-experiment传给 CLI 工具可传多个 flag逗号分隔或重复传参dart --enable-experimentsuper-mixins dart --enable-experimentsuper-mixins,no-slow-checks,preview-dart3 dart --enable-experimentsuper-mixins --enable-experiment no-slow-checks --enable-experiment preview-dart3若用户传入无法识别的 flag例如 flag 已不再受支持工具必须打印到 stderr 提示、但不失败dart --enable-experiment better-mixins Unknown experiment flag better-mixins.在 CFE 实现中--enable-experiment被定义为共享的 Option见 pkg/front_end/lib/src/base/command_line_options.dart生成的ExperimentalFlag常量类见 pkg/front_end/lib/src/api_prototype/experimental_flags_generated.dart为每个 flag 携带name、isEnabledByDefault、isExpired、experimentEnabledVersion、experimentReleasedVersion五个属性。UI 工具IDE / 编辑器等的 flag 格式能调用 Dart 工具的 IDE 与编辑器必须支持传递这些 flag且支持方式应通用灵活增删 flag 时无需改动 UI。通常有两种形式影响分析的实验在analysis_options.yaml的单个enable-experiment:键下启用例如启用super-mixins与no-slow-checksanalyzer: enable-experiment: - super-mixins - no-slow-checks影响启动/运行行为的实验在 IDE 特定的运行配置Run Configuration中传入与 CLI 相同的--enable-experimentflag。当前全部实验 flag 的定义集中在 tools/experimental_features.yaml各工具据此访问。从源码看实验 Flag 的生命周期tools/experimental_features.yaml是实验特性的唯一事实来源其features映射中的每个条目包含以下字段字段必填含义help是实验特性的人类可读描述enabledIn否特性正式发布的 SDK 版本major.minor。指定后该特性无视 SDK 实际版本默认启用省略则默认禁用但可通过--enable-experimentflag启用。低于该版本的语言版本如// dart2.12或 package config可关闭该特性experimentalReleaseVersion否可通过 flag 启用该实验的 SDK 版本major.minor。若sdk/lib/_internal/allowed_experiments.json被更新此字段指定对这些库与包启用实验的版本expired否为 true 时 flag 无法再通过命令行启用条目待从文件移除省略视为 falsevalidation否一个打印 feature enabled 的程序stdout特性启用时编译通过、否则抛错用于对每个实验运行通用测试category否指定实验影响的组件如CFE、vm、language默认language同时为 CFE 与 Analyzer 生成代码实验通过若干状态演进详见文件头注释 tools/experimental_features.yamlDisabled禁用刚加入时省略enabledIn与expired默认禁用但可通过 flag 启用实现团队在 flag 后构建功能用户可先行尝试。Experimental release实验发布想让特定库与包默认启用时向sdk/lib/_internal/allowed_experiments.json白名单添加条目并提升experimentalReleaseVersion其他库与包仍需显式传 flag。Shipped已发布添加enabledIn指明包含该特性的 SDK 版本此时传 flag 无效默认启用且无法关闭。Retired / Rejected退役/否决添加expired。若特性已发布则 flag 退役若未发布则整个实验被否决用户传该 flag 会收到警告但工具继续运行。另外current-version字段指定正在开发的 Dart 版本未声明自身版本的 Dart 源文件被默认视为该版本实验 flag 不影响声明了更早版本的文件。该 YAML 通过代码生成器驱动整个工具链tools/generate_experimental_flags.dart为 VM 生成 runtime/vm/experimental_features.h 与 runtime/vm/experimental_features.ccCFE 侧则由dart pkg/front_end/tool/cfe.dart generate-experimental-flags生成 experimental_flags_generated.dart。生成代码中每个 flag 的experimentEnabledVersion在未指定enabledIn时即为defaultLanguageVersion当前 in-progress 版本从数据上印证了实验语言只存在于 in-progress 版本周围这一核心模型。在 CFE 的查询框架中experimental_flags.dart 提供全局态GlobalFeatures与库级态LibraryFeatures两层接口全局态由explicitExperimentalFlags命令行传入与 flag 默认值共同决定库级态再叠加 canonical URIdart:/package:对应的 allowed 白名单这正是核心库不传 flag 也能进入实验语言的实现细节。语言版本本身则由 source_library_builder.dart 中的LanguageVersion/ImplicitLanguageVersion类表达——前者来自显式声明后者来自包的默认版本。发布推演实验特性随稳定版发布的三种命运理解上述机制后用两个假想场景演示语言版本锁定对生态的连锁影响。文档以 null safety 为例但结论对任何实验特性通用。场景一2.8.0 发布但不包含 null safety假设发布 Dart 2.8.0 稳定版而 null safety 仍处于 non-nullable flag 之后。发布当天2.8 不再是 in-progress 版本被刻进石头其语言版本严格对应发布 SDK 的行为所有 2.8 实验版本消失flag 不再影响使用语言 2.8 的库与此同时新的 in-progress 版本 2.9 出现所有未发布而结转的 flag 转而作用于它——于是出现 2.9non-nullable、2.9variance 等。2.8 关闭、2.9 开启意味着三件事任何瞄准 2.8 并使用 null safety 特性的库都必须把语言版本改为瞄准 2.9。这可能意味着修改// dart2.8注释或在 pubspec 中抬高最低 SDK 约束。刚发布的 2.8.0 稳定 SDK 中核心库的语言版本必须是 2.9。因为核心库使用了 null safety而该特性已无法对 2.8 库启用。这看起来很怪——2.8.0 如何能支持一个未来的 Dart 版本现实是 2.8.0 对 2.9 的某个子集拥有秘密的内部支持核心库恰好落在其中。这是实现细节核心库使用了 SDK 内尚未对外暴露的能力。虽显奇怪但应该可以接受。2.8.0 发布后Flutter 用户玩 null safety 所需的所有包其最低 SDK 约束必须高于 2.8.0。它们需要到达语言级别 2.9唯一途径是排除 2.8.0 的约束。但如果用户跑在 Dart 2.8.0 稳定版上Pub 不会选中这些包——因为 2.8.0 落在其 SDK 约束之外​这是一个真实的问题根源在于用 SDK 约束同时控制语言版本与包解析。为此稳定版发布后不久团队会再发布一个 Dart 2.9.0-dev.0 dev 版本并合入 Flutter 的 dev channel。想体验 non-nullability 的用户必须待在 dev channel——null safety 仍是实验特性而稳定版的意义就在于稳定想实验实验特性就去 dev channel。场景二2.10.0 正式发布 null safety接着假设 null safety 最终在 2.10.0 正式发布。所有在玩 null safety 实验的包早已把最低 SDK 约束抬到类似2.10.0-dev.0。此时各包命运分三种若 SDK 约束形如2.10.0-dev.0 2.10.0只需把上限抬到包含 2.10.0。这是安全的选择——假设下一个稳定版本与先前的 in-progress dev 构建兼容是有风险的dev 构建的意义就在于在变动中。因此多数使用 null safety 的包应处于此状态。发布 null safety 后验证包在稳定版下依然正常然后抬高上限即可。若 SDK 约束形如2.10.0-dev.0 3.0.0作者无需任何操作。该约束声称同时支持先前 dev 版 null safety 与已发布的稳定版。这种宽约束是可疑的因为团队保留对 flag 门控特性做任意破坏性变更的权利但如果作者确信没有也不会发生破坏典型情况是作者本身就是 Dart 团队成员宽约束可以合理。此时包继续工作——它已经在语言 2.10 上而 2.10 现在原生支持 null safety无需改动。若 SDK 约束形如2.8.0 3.0.0包停留在之前的 legacy 语言版本相当于退出了 NNBD像以前一样继续工作。场景中的 2.10.0 并无特殊之处无论何时、以何种版本发布某个实验特性围绕包的影响都应遵循上述规律。结语一个机制三种视角回顾全文Dart 的语言演进可以被概括为一句话已发布版本把语言钉死in-progress 版本不断衍生实验语言而语言版本化、实验 flag 与白名单全部只是库选择语言的机制。对 SDK 用户与包作者而言这意味着向稳定版发布破坏性语言变更永远不可能patch 版不引入新语言版本在 in-progress 版本上实验 flag 会全局性地影响所有瞄准该版本的库包括传递依赖一旦稳定版发布实验就随之迁移到下一个 in-progress 版本而 SDK 约束既是包解析的门槛也是包的语言版本声明——理解这双重身份才能安全地为包选择何时升高、何时收窄约束。本文涉及的全部配置源文件与实现源码均可在仓库的 tools/experimental_features.yaml、docs/process/experimental-flags.md 与 pkg/front_end/lib/src/api_prototype/ 中进一步查阅。赞分享编程语言编译器语言运行时标准库开发工具【免费下载链接】sdkThe Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.项目地址https://gitcode.com/gh_mirrors/sdk1/sdk点击查看免费下载相关推荐Dart语言版本管理与实验性功能机制详解Dart语言版本管理与实验性功能机制详解 前言 Dart作为一门现代化的编程语言其语言特性的演进和版本管理机制对于开发者来说至关重要。本文将深入解析Dart语编程语言编译器语言运行时标准库开发工具Open WebUI 本地部署完整指南3 种方式一次装好对话数据全留本机Open WebUI 本地部署完整指南3 种方式一次装好对话数据全留本机 Open WebUI 本地部署后是一个完全离线的自托管 AI 对话界面它负责对话人工智能大模型AI 应用RAGAI Agent本地部署交互助手后端前端Keyviz 版本控制策略语义化版本与发布流程解析Keyviz 版本控制策略语义化版本与发布流程解析 引言版本混乱的痛点与解决方案 在开源项目开发中版本号的混乱往往导致用户困惑、开发者协作困难以及部署风险桌面应用交互助手创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考