ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Carbon 语言 api 文件默认 public:库级可见性设计决策的演进与实现

Carbon 语言 api 文件默认 public:库级可见性设计决策的演进与实现 Carbon 语言 api 文件默认 public库级可见性设计决策的演进与实现【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-langapi 文件API file与 impl 文件implementation file的拆分是 Carbon 语言组织库代码的基石而“api 文件中声明的实体默认是否对外可见”直接决定了开发者书写库接口的心智模型。本文以提案 p000752-api-file-default-public.md 为主体系统梳理 Carbon 将 api 文件默认可见性从private调整为public的决策过程、备选方案取舍并结合当前仓库中的设计文档与 toolchain 源码说明该规则在编译器中的实际落地形态。读完本文你将掌握Carbon 库文件中可见性的完整规则api 文件默认 public、impl 文件默认 private、为什么单独定义separate definition禁止重复书写可见性关键字、以及private在文件作用域与类作用域中的使用边界并能在实际编写 Carbon 库时正确规划哪些实体可以暴露给外部调用方。问题的起点类成员默认 publicapi 文件是否应保持一致在 Carbon 项目早期#665 问题lead 决策议题private vs public 的语法策略以及 external/api 等其他可见性工具已经确定了“类class上的方法默认 public”这一方向。本提案PR #752要回答的正是紧随其后的问题既然类成员默认 public那么 api 文件的默认可见性是否应该采用同样的策略这个问题的背景有三层C 的历史对照在 C 中struct的成员默认是 public 的文档原文此处描述class成员也默认 public实际上 C 的class默认是 private提案文本此处存在笔误Carbon 需要在语言设计层面明确自己的选择提案 #107Code and name organization该提案最早引入api关键字用来在 api 文件中标记公开 API这一阶段的隐含语义是“显式写出api才公开”#665 的类成员决策既然类成员已经走向“默认 public”api 文件如果继续“默认私有、显式公开”两者语义就不一致。值得注意的是本提案并不是简单拍板而是重开了 #665 中关于备选方案的讨论对多种可见性策略做了完整对比后才得出结论。提案核心api 文件默认 publicimpl 文件默认 private提案给出的最终规则非常简洁包含两条互补的默认值api 文件中的实体默认 public无需额外书写api关键字如果需要标记“仅库内部可见、只对 impl 文件可见”的实体则显式书写private。impl 文件无需任何可见性标记其中声明的实体默认 private只有当某实体在 api 文件中被前置声明forward declared时才沿用声明处的可见性。这条规则的深层含义是可见性遵循“在给定上下文中取最大可见度”的原则——api 文件是库的门面其中的一切天然应该公开impl 文件是内部实现细节其中的一切天然应该私有。结合当前仓库的设计文档 docs/design/code_and_name_organization/README.md可以看到这条决策的最终形态文档中的“Exporting entities from an API file”一节package Geometry library Shapes; // Circle 是库公开 API 的一部分其他库可通过 Geometry.Circle 访问。 struct Circle { ... } // CircleHelper 是私有的其他库不可见。 private fn CircleHelper(circle: Circle) { ... } namespace Operations; // Operations.GetCircumference 是库公开 API 的一部分 // 其他库可通过 Geometry.Operations.GetCircumference 访问。 fn Operations.GetCircumference(circle: Circle) { ... }设计文档进一步强调了这种拆分的三个价值可读性api-only 文件便于读者快速扫描 API 文档编译性能减少 api 文件中的实现代码意味着更少的导入缩小了依赖该库的文件的传递编译闭包transitive compilation closure从而加速编译可维护性更小的文件更易维护。同时设计文档明确写下了与提案完全一致的 impl 文件规则Entities in an implementation file should never have visibility keywords. If they are forward declared in the API file, they use the declarations visibility; if they are only present in an implementation file, they are implicitlyprivate.实现文件中的实体不应书写可见性关键字若在 api 文件中被前置声明则沿用声明的可见性若只存在于实现文件中则隐式为private。设计依据与“代码易于阅读、理解、编写”目标的呼应提案将决策锚定在 Carbon 的官方目标 docs/project/goals.md 中的“Code that is easy to read, understand, and write”代码易于阅读、理解和编写之上当开发者比较类成员的可见性与库对其他包的可见性时如果两者语义相似都默认 public代码会更易于理解换句话说Carbon 希望在整个语言中建立一致的“默认公开”心智模型类成员默认 publicapi 文件实体默认 public只有需要隐藏时才显式private。备选方案分析三种被否决或调整的路线提案完整记录了三个备选方案及其论证这些讨论对理解最终规则的边界条件至关重要。备选一api 文件默认 private这是api关键字原本的隐含语义也是改动前的状态。优点降低开发者意外暴露 API 的概率因为“公开”变成了显式选择在 api 与 impl 文件之间移动函数时可见性不会因此改变。缺点api 文件的首要目的就是暴露 API开发者天然会认为其中的东西是公开的与类成员“默认 public”的行为不一致。最终结论为了与类行为保持一致改为 api 文件默认 public。备选二impl 文件默认 public既然 api 默认 public是否可以让 impl 也默认 public从而让函数在 api/impl 之间移动时可见性完全不变优点函数在 api 与 impl 之间移动时可见性不变。缺点在 impl 文件中一切实体必须是 private除非它是某个 api 声明的单独定义。因此如果 impl 默认 public那么 impl 文件中所有实体都需要显式书写private造成大量冗余劳动。为避免这种“显式声明一切为 private”的繁琐toilimpl 文件保持默认 private。作为默认行为的自然推论impl 文件中不应书写private关键字正如 api 文件中不应书写public关键字因为默认值已覆盖。这一推论在后面的“单独定义禁写关键字”规则中得到了强化。备选三单独定义时允许可选或强制可见性关键字当实体已有前置声明时其单独定义separate definition中当前禁止书写可见性关键字。本备选方案探讨是否放开这一限制让开发者可以在定义处直接看到可见性。“可选关键字”方案的问题——考虑以下代码api 文件class Foo { private fn Bar(); private fn Wiz(); };impl 文件fn Foo.Bar() { ...impl... } private fn Foo.Wiz() { ...impl... } fn Baz() { ...impl... }在“可选”设置下上述代码合法。但Foo.Bar的定义处没有private关键字开发者尤其是看到Foo.Wiz显式写了private后很容易误以为Foo.Bar是 public 的而实际上它继承自 api 文件中的private声明。这类误读同样可能发生在重构中——例如删除Foo.Wizimpl 版本上的关键字是合法操作但并不会让它变成 public。“强制关键字匹配”方案的问题——一种回应是强制关键字必须与声明一致让编译期保证正确性。再看类似例子api 文件class Foo { fn Bar(); private fn Wiz(); };impl 文件fn Foo.Bar() { ...impl... } private fn Foo.Wiz() { ...impl... } fn Baz() { ...impl... }Foo.Bar是 public 的这可能让开发者误以为同文件中的Baz也是 public 的。虽然可以通过强制给Baz写private来纠正但正如“默认 impl 为 public”备选方案中所述我们不愿意为此引入强制private的负担。同一文件内的前置声明场景——即便前置声明和单独定义都在 api 文件内风险依然存在private fn PrintLeaves(Node node); fn PrintNode(Node node) { Print(node.value); PrintLeaves(node); ); fn PrintLeaves(Node node) { for (Node leaf : node.leaves) { PrintNode(leaf); } }读者读到PrintLeaves的定义时可能因为“(a) 没有关键字 且 (b) 位于 api 文件”而错误推断它是隐式 public 的。提案指出这将在“Open question #472调用同文件中稍后定义的函数”的讨论中一并解决。最终决策单独定义上禁止书写可见性关键字。这样 impl 文件在文件作用域内不会有任何可见性关键字类内部仍可写既提升了可写性writability又让 api 文件始终作为 public 实体的单一事实来源single source of truth保障可读性。仓库中的实现证据编译器如何落地这套规则该提案的决策不仅停留在文档层面在 Carbon 的 toolchain 中已有对应的实现痕迹可以作为理解规则执行方式的参照。跨包导入时的可见性过滤在 toolchain/check/import.cpp 中可以看到导入器在处理来自其他库的实体时会检查访问级别if (import_scope_entry.result.access_kind() ! SemIR::AccessKind::Public) { // Ignore cross-package non-public names. return ...; }这段代码直接印证了提案的核心语义只有AccessKind::Public的实体才能跨越包的边界被导入使用api 文件中标记为private的实体在导入阶段就被过滤掉从而实现对库外部调用方“不可见”。private 关键字的作用域限制在 toolchain/check/modifiers.cpp 中可见性修饰符的使用范围被严格限定// Both private and protected allowed in a class definition. ... // Otherwise neither private nor protected allowed. CARBON_DIAGNOSTIC(ModifierPrivateNotAllowed, Error, private not allowed; requires class or file scope); ForbidModifiersOnDecl(context, ModifierPrivateNotAllowed, introducer, KeywordModifierSet::Private);也就是说private关键字只能在类定义内部或文件作用域中使用文件作用域即 api 文件中对库内部实体的标注这与提案中“api 文件中用private标记仅库内可见实体”的规则一一对应。成员访问时的权限检查在 toolchain/check/name_lookup.cpp 中当以“父作用域访问”方式访问私有/受保护成员时编译器会发出诊断信息if (access_kind SemIR::AccessKind::Private is_parent_access) { ... cannot access {0:private|protected} member {1} of type {2},从源码结构可以看出SemIR 中通过SemIR::AccessKind枚举Public/Private/Protected统一表达可见性private的过滤在导入边界import和成员访问name lookup两处分别实施形成双重校验。完整规则速览与重构指引综合提案 p000752 与当前设计文档Carbon 库文件可见性的完整规则可归纳如下场景默认可见性可写关键字说明api 文件中的实体publicprivateprivate标记仅库内impl 文件可见impl 文件中的实体private禁止仅被 api 前置声明的实体沿用声明可见性类成员publicprivate/protected类作用域内可写单独定义separate definition沿用声明禁止api 文件是 public 实体的单一事实来源命名空间namespace含至少一个 public 非命名空间实体才导出—详见设计文档设计文档 docs/design/code_and_name_organization/README.md 还给出了围绕该规则的一组重构操作指南其中最值得注意的几条把实体定义从 api 文件移到 impl 文件保留声明对调用方无影响是本地变更给声明添加private修饰符需要先搜索库外部调用方并修复删除private修饰符对调用方无影响的本地变更把private声明从 api 文件移到 impl 文件声明必须与定义位于同一文件且只能被该 impl 文件使用需先修复库内其他调用方。这些重构规则的共同前提正是“默认 public 显式 private”模型由于 impl 文件实体天然私有、api 文件实体默认公开开发者与自动化工具都可以基于文件类型快速判断实体的大致可见性而无需逐一检查关键字。总结p000752 提案确立了 Carbon 库级可见性的两条核心默认值——api 文件默认 public、impl 文件默认 private——并与类成员默认 public 的行为保持一致形成了贯穿整个语言的统一心智模型公开是默认状态隐藏才需要显式声明。围绕这一决策提案还论证了“单独定义禁止可见性关键字”的写实取舍确保 api 文件始终是 public 实体的单一事实来源。在当前仓库中该规则已从提案文本沉淀为设计文档 docs/design/code_and_name_organization/README.md 的正式内容并在 toolchain 的导入过滤toolchain/check/import.cpp、修饰符作用域检查toolchain/check/modifiers.cpp与成员访问校验toolchain/check/name_lookup.cpp中得到实现。对编写 Carbon 库的开发者而言理解这套规则即可准确回答一个最基础的问题我的哪些声明会被其他库看到哪些不会。【免费下载链接】carbon-langCarbon Languages main repository: documents, design, implementation, and related tools. (NOTE: Carbon Language is experimental; see README)项目地址: https://gitcode.com/GitHub_Trending/ca/carbon-lang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进