将主构造函数引入 Dart

Lobsters Hottest 新闻

摘要

这篇博客文章讨论了在 Dart 3.13 中添加主构造函数,这是一种语法糖特性,旨在基于用户反馈和设计迭代来增强代码的简洁性和可读性。

<p><a href="https://lobste.rs/s/khphqe/bringing_primary_constructors_dart">评论</a></p>
查看原文
查看缓存全文

缓存时间: 2026/08/21 06:52

# 为 Dart 源代码引入主构造函数:https://dart.dev/blog/bringing-primary-constructors-to-dart 展示 Dash 吉祥物及主构造函数语法蓝图示意图。 为 Dart 引入主构造函数(AI 声明:本文每一句话均由我本人撰写——包括破折号。) 我最喜欢的 Dart 3.13 特性是主构造函数 (https://dart.dev/language/primary-constructors)。在语言团队确定可靠的设计方案之前,该特性经历了漫长的时间和反复迭代。由于许多开发者一直在耐心等待这个特性,我认为值得撰写文章介绍我们在将这一重大语法变更引入 Dart 过程中克服的挑战。 ## 关于语法糖 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#on-syntactic-sugar) 多年来,用户一直要求提供类似主构造函数的功能。这是一个备受期待的特性,仔细想想会觉得有点奇怪。主构造函数并不能让你做到 Dart 中原本无法实现的事情。它只是为你已经能够表达的内容提供了一种不同的——希望是更好的——语法。 1960 年代,Peter Landin 创造了“语法糖化”一词,指的是在更基础但令人不悦的语言之上添加一些文本修饰。如今,我们更倾向于将这个术语用作名词,并将这类特性称为“语法糖”。 每种编程语言的不朽敌人都是复杂性。即使是最微小的特性也必须经过设计、规范、实现、测试和文档化。成本是巨大的。我认为语言中的复杂性就像飞机上的重量。一定量的复杂性是机器正常运转所必需的,但必须小心避免不必要地增加重量,否则可能导致整个装置无法起飞。从这个角度看,语法糖似乎是个坏主意。它是额外的复杂性,却没有带来额外的效用。更糟糕的是,一旦我们添加了它,也会把复杂性传递给用户。现在他们每次表达某个概念时,都必须选择使用哪种语法。这类特性何时才是个好主意?(我承认我觉得有必要为此辩解,因为过去几年我的大部分工作都是在向 Dart 添加这类特性。) 我认为语法糖可以通过以下几种方式证明其价值: ### 新方式确实更好 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#the-new-way-is-simply-better) 尽管我们语言设计师有些机械化的风格和对EBNF (https://en.wikipedia.org/wiki/Extended_Backus%E2%80%93Naur_form) 的偏爱,但我们也是人,会犯错。此外,我们一直在学习,我们服务的生态系统不断发现新的软件构建方式,用户的期望也随时间推移而变化。Dart 最初设计时,必须使用显式 `new` 关键字来调用构造函数。这样做是为了让来自 C++、Java、JavaScript 和其他语言的用户感到熟悉。意图是让代码中分配新对象的调用更加清晰。 随着垃圾收集器的改进以及用户对自动内存管理越来越适应,大多数用户发现 `new` 关键字弊大于利。(而且,老实说,Dart 通过支持工厂构造函数 (https://dart.dev/language/constructors#factory-constructors) 一直削弱了这个信号。即使使用 `new` 调用,工厂构造函数也可能返回之前创建的对象。) 在 Dart 2.0 中,我们发布了一项语言变更,允许在调用构造函数时省略 `new` 关键字(在许多地方也省略 `const`)。我们仍然支持旧语法,因此这项语言变更本质上是语法糖,但我们保留旧语法主要是为了向后兼容。我们始终希望你使用新的更简洁的语法。我们提供了工具 (https://dart.dev/tools/dart-fix) 来自动移除不必要的 `new` 关键字,并有一个代码检查 (https://dart.dev/tools/linter-rules/unnecessary_new) 在你忘记时提醒你。旧语法实际上已被弃用,随着时间的推移,你会越来越少见到它。如果你是 Dart 新手,你可能甚至没意识到我们支持在构造函数调用中使用 `new`。这意味着支持带和不带 `new` 的构造函数调用所增加的复杂性很低。 现有用户学习新语法需要一定的转换成本。但新用户大多只会学习新方式,永远不会遇到旧方式。在选择两种语法时几乎没有认知负担,因为你只需始终使用新语法(如果你不这样做,我们的工具会温和地提醒你)。除了拥有时间机器回到过去第一次就做对之外,这是我们在语言中纠正错误时所能做的最佳选择。 ### 语法可以显著改善常见用例 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#the-syntax-can-be-much-better-for-a-common-use-case) Dart 在公开存在的最初几年里不支持枚举声明。语言不允许你这样写: dart ```dart enum Color { red, blue, yellow } ``` 相反,你必须写类似这样的代码: dart ```dart class Color { static const Color red = Color._(0, 'red'); static const Color blue = Color._(1, 'blue'); static const Color yellow = Color._(2, 'yellow'); const Color._(this.index, this.name); final int index; final String name; } ``` 老 Java 开发者会记得这是 Josh Bloch 的“类型安全枚举模式”。在底层,这种更冗长的类声明做的事情几乎与当今 Dart 中的枚举声明完全相同。Dart 枚举声明几乎完全是语法糖。(我说“几乎”是因为枚举声明在 switch 语句中提供了穷尽性检查 (https://dart.dev/language/branches#exhaustiveness-checking)。) 然而,正如你从这两个示例中看到的,枚举声明是*非常棒*的语法糖。一个简单的枚举声明会展开成大量的 Dart 代码。现在,如果几乎没有人编写枚举类型,那么为这种用例优化而添加语法可能仍然不值得。但在一种优先考虑类型安全和数据验证的语言中,枚举非常常见。仅 Flutter 框架就定义了数十种。相对少量的语法糖有时可以极大地缩短和简化*大量*用户代码。 ### 语法可以使意图更清晰 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#the-syntax-can-make-the-intent-clearer) 上一节听起来好像简洁就是全部目的。我猜在一个我们越来越多地按代币成本支付 AI 代理读写代码的世界里,存在直接的财务激励。但这不仅仅关乎字符数。考虑: dart ```dart class Color { static const Color red = Color('red', 0xff0000); static const Color blue = Color('blue', 0x0000ff); static const Color yellow = Color('yellow', 0xff00ff); const Color(this.name, this.rgb); final String name; final int rgb; } ``` 这是一个枚举类型吗?我的意思是*枚举的*:使用这个类的人是否应该假设他们需要处理的 `Color` 的*唯一*实例是 `red`、`blue` 或 `yellow`?注意构造函数是*公共的*,所以其他库可以自由调用构造函数创建其他颜色。这个类的意图是一个*封闭的*颜色列表,还是一个带有少量预定义值的开放工厂? 阅读代码,*我们不知道*。代码是大量定义了一个类型和一些常量的机制。它*看起来像*你想要枚举时会写的代码,但这些机制没有揭示意图。代码告诉编译器代码的含义,但没有告诉读者如何使用它。如果我们将此改为枚举声明,那么它是一个封闭值集的策略就变得显而易见了。(而且,既然 Dart 有了真正的枚举,选择*不*将此代码更改为枚举声明很可能发出一个信号:它*不是*一个封闭集。) 对我来说,这是添加语法糖的一个令人信服的理由。代码作为语法被编写和执行,但每个使用代码的用户关心的是它的*含义*——它的语义。为了正确维护代码,我们需要理解其意图和策略。在 AI 生成代码的速度往往快于我们仔细审查代码的时间的世界里,这一点越来越真实。即使可以通过拼凑几种现有语言特性的机制让编译器做你想做的事,拥有语法糖来产生相同行为也是值得的,因为更好的语法将该行为提升到更高的抽象层次,使预期语义更明显。这可以*减少*理解代码所需的认知工作,即使整个语言变得更复杂。 ## 为何需要主构造函数 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#why-primary-constructors) 好吧,我应该讨论主构造函数,而不是枚举和 `new` 关键字。(虽然——伏笔!——我也会谈论 `new`。) 多年来,Dart 语言仓库的 #1 开放问题 (https://github.com/dart-lang/language/issues/314) 一直是关于数据类的功能请求。如果你不知道,数据类是 Kotlin (https://kotlinlang.org/docs/data-classes.html) 中的一个特性,它允许你定义一个带有一些字段的类,编译器会免费为你提供相等性、哈希码等。如果你阅读那个问题上的数百条评论,你会看到大多数用户对值语义部分——相等性和哈希码部分——兴趣不大。主要是关于有一种更简单的方式来定义一个具有构造函数并存储一些状态的类。 该功能实际上来自 Kotlin 中另一个更基础的特性:主构造函数 (https://kotlinlang.org/docs/classes.html#primary-constructor)。我相信 Kotlin 是从Scala (https://docs.scala-lang.org/scala3/book/domain-modeling-tools.html#classes) 那里得到这个想法的。此后,C# (https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors) 和 Java (https://openjdk.org/jeps/395) 也添加了它们自己对这个概念的实现。(数据类的值语义部分也很有用。我们正在单独探索那部分 (https://github.com/dart-lang/language/blob/main/working/value-classes/feature-specification.md)。) 主构造函数有两个关键部分: 1. 你可以通过直接在类头中编写参数列表来定义构造函数。这避免了需要编写关键字或重复类名来声明构造函数。它也避免了在非常简单的只包含一些状态的类中需要两级嵌套和缩进,一级用于类体,一级用于构造函数参数列表。 2. 在该参数列表中,你可以指示某些参数应声明对应的实例字段,这些字段会自动从参数初始化。 没有主构造函数或任何其他语法糖,我们必须在 Dart 中这样写: dart ```dart class Point { final int x; final int y; Point(int x, int y) : x = x, y = y; } ``` 在这个例子中,我们不得不写两次类名。对于每一部分状态,我们写了两次类型和*四次*名称。在这个例子中还不算太糟糕,因为只有两个字段且名称都很短。一旦你开始处理具有长名称和大量状态的复杂领域特定内容,它就会变得丑陋。这不是一个新问题,Dart 很早就有一种叫做“初始化形式参数 (https://dart.dev/language/constructors#use-initializing-formal-parameters)”的语法糖来帮助: dart ```dart class Point { final int x; final int y; Point(this.x, this.y); } ``` 在构造函数参数上使用 `this.` 意味着你只需写一次每个字段的类型,写两次名称。好多了!但你仍然需要写两次类名,每个字段名两次。初始化形式参数很不错,但用户仍然告诉我们它们不够用。 ## 关于从其他语言借鉴特性 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#on-borrowing-features-from-other-languages) 那么,一些从其他语言来到 Dart 的用户告诉我们他们怀念某个特性。我们如何处理这种反馈?就个人而言,我喜欢从其他语言借鉴特性。那些语言的创造者已经在设计和验证该特性上投入了大量工作。我们可以从他们那里学到很多,而且那种语言的存在本身证明了该特性在概念上是连贯且可实现的。从其他语言汲取灵感也可以让我们的语言更容易学习。除非用户是编程新手,否则他们学习 Dart 不是从零开始。他们带着从其他语言学到的所有知识来到这里。他们需要学习的是他们已知内容与 Dart 内容之间的*差异*。 当我们从其他语言借鉴语法和语义时,我们减少了该差异的规模,降低了学习 Dart 的难度。这种哲学一直是 Dart 成功的关键。从小小的分号一直到类,Dart 从头到尾的设计都是为了让其他主流语言(如 JavaScript、Java 和 C#)的用户感到熟悉和易于学习。同时,良好的语言设计是具有情境性和整体性的。“什么是一双好鞋?”当你站在北极苔原和夏威夷海滩上时,答案会大相径庭。一个在 Rust 中运行得非常好的语言特性可能无法优雅地融入 Dart 独特的语法、语义、历史、用户群和生态系统中。我不希望 Dart 感觉像一个从其他语言身上扯下肢体缝合成的科学怪人。因此,当 Dart 语言团队审视其他语言的特性时,我们同时关注该特性如何在该语言的上下文中解决问题,也关注该上下文与 Dart 自身的匹配程度。 ## 为 Dart 添加主构造函数 \# (https://dart.dev/blog/bringing-primary-constructors-to-dart#adding-primary-constructors-to-dart) 我们知道用户想要一种更简洁的记法来定义从构造函数参数初始化某些字段的类。有了主构造函数,你编写构造函数,编译器合成字段。语言也可以走相反的路:你编写字段声明,编译器免费给你构造函数。Swift 通过成员逐个初始化器 (https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/#Memberwise-Initializers-for-Structure-Types) 就是这样做的。 任何时候你的语言从一段语法推导出两种声明时,一个挑战在于一种语法需要处理你可能配置这两种声明的所有方式。在我们这里的情况下,实例字段可能是 final 的也可能不是。它可能有像 `@override` 这样的元数据或文档注释。构造函数可以是命名的或未命名的,`const` 或不是。构造函数参数可以是位置的或命名的,可选的或必需的。如果是可选的,它可能需要指定默认值。 我们花了一些时间研究从字段声明推断构造函数 (https://github.com/dart-lang/language/issues/698),但最终决定让参数成为用户手写时更有用的声明。由于构造函数通常是公共 API,完全控制其签名很重要:构造函数的名称和 `const` 性质、哪些参数是命名的或位置的、位置参数的顺序及其默认值。为了从构造函数参数推断实例字段,用户唯一需要提供的缺失部分是字段是否应该是 final 的。这相当自然……

相似文章

就地初始化的四个层次

Lobsters Hottest

文章讨论了 Rust 中就地初始化的四个层次,从原始指针到语言提案如 &uninit/&own,以提高编程中的内存效率和安全性。

在C++中模拟函数的关键字参数

Hacker News Top

本文演示了一种通过使用带有指定初始化器的结构体来模拟Python风格关键字参数的技术,无需宏或模板魔法即可提高代码可读性。