多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Swift 导入声明访问级别修饰符(SE-0409)完全指南:从 `public import` 到隐藏传递依赖

Swift 导入声明访问级别修饰符(SE-0409)完全指南:从 `public import` 到隐藏传递依赖 文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载SE-0409 为 Swift 语言引入了在import声明上使用访问级别修饰符的能力让开发者可以精确声明一个依赖模块对源文件、模块、包package或全部客户端可见的范围并借此在编译期强制约束哪些声明可以引用被导入的模块。本文以该提案文档为主体结合仓库中 SE-0486 迁移工具与 SE-0540 默认 target 设置等关联提案完整讲解语法、类型检查规则、传递依赖加载逻辑、默认访问级别、与其他属性_exported、testable、_implementationOnly、scoped imports的交互以及迁移与采用注意事项。读完本文你将掌握如何用internal import/package import/fileprivate import隐藏实现细节、抑制依赖蔓延dependency creep并理解未来语言模式下import默认访问级别变更带来的影响。背景与动机为什么导入声明需要访问级别Swift 早已为声明提供public、package、internal、fileprivate、private等访问级别修饰符并在类型检查type-checking阶段强制执行一个可见性更高的声明不能引用可见性更低的声明。然而在 SE-0409 之前依赖dependencies没有任何等价的官方机制。库作者对每个依赖往往有不同的意图有些依赖期望被客户端知晓有些则纯粹是包、模块或源文件内部的实现细节。没有强制手段时很容易在无意间把本应保密的依赖暴露出去——例如在某个public声明中引用了本应仅供内部使用的模块。这带来两个实际问题API 泄漏客户端可能无意中依赖库的内部实现细节阻碍库后续演进不必要的编译负担所有库依赖对客户端可见意味着编译器在构建客户端时必须加载库的全部依赖即使其中大部分与客户端无关。SE-0409 的目标正是把声明访问级别带来的熟悉行为延伸到依赖与导入声明上用于隐藏实现细节并帮助管理依赖蔓延。该提案状态为Implemented (Swift 6.0)实现位于 main 与 release/5.9 分支通过前端标志-enable-experimental-feature AccessLevelOnImport门控并配套 upcoming feature flagInternalImportsByDefault见 0409-access-level-on-imports.md 头部元数据。解决方案概览SE-0409 的核心思路是扩展现有访问级别逻辑允许在 import 声明上声明现有修饰符open除外并将该访问级别应用到被导入的声明上。一个典型场景模块DatabaseAdapter只是当前模块的实现细节不希望暴露给客户端因此将导入标记为internal。编译器随后允许在internal函数中引用它但会在public函数签名中出现该模块类型时报错internal import DatabaseAdapter internal func internalFunc() - DatabaseAdapter.Entry {...} // Ok public func publicFunc() - DatabaseAdapter.Entry {...} // error: function cannot be declared public because its result uses an internal type此外该提案会汇总构成一个模块的所有源文件中的 import 访问级别决定库的客户端是否需要加载库的依赖还是可以跳过。为兼顾源码兼容与最佳实践Swift 5 与 Swift 6 中不带显式访问级别的 import 隐含为public未来语言模式将改为internal。usableFromInline属性应用于 import 时允许从可内联代码中引用该依赖。声明导入模块的访问级别访问级别修饰符写在import声明之前可用的包括public、package、internal、fileprivate和private。public import对全部客户端可见public依赖可被任何声明引用对所有客户端可见public import PublicDependencypackage import仅同包模块可见package依赖只对同一 package 内的模块可见。只有package、internal、fileprivate与private声明的签名可以引用该模块package import PackageDependencyinternal import仅模块内部可见internal依赖对当前模块内部可见。只有internal、fileprivate与private声明的签名可以引用该模块internal import InternalDependencyfileprivate / private import仅源文件可见fileprivate与private都表示依赖仅对声明该 import 的源文件可见private的语义在此场景下与fileprivate一致。只有fileprivate与private声明的签名可以引用该模块fileprivate import DependencyPrivateToThisFile private import OtherDependencyPrivateToThisFile禁止open支持usableFromInlineopen访问级别修饰符在 import 声明上会被拒绝。提案在“备选方案”一节专门讨论了open import见下文最终未赋予其含义。usableFromInline属性可应用于 import 声明允许从可内联代码引用该依赖同时仍限制哪些声明的签名可以引用它。该属性只能用于package与internalimport标记后依赖对客户端可见usableFromInline package import UsableFromInlinePackageDependency usableFromInline internal import UsableFromInlineInternalDependency注意提案明确注明“Support for usableFromInline on imports has yet to be implemented”即截至提案文本写作时该属性在 import 上的支持尚未实现使用前请确认编译器版本的实际支持状态。类型检查访问级别成为导入声明的可见性上限现有类型检查会强制声明遵守各自的访问级别——例如public函数签名使用了internal类型会报错。SE-0409 扩展了这一逻辑import 声明上的访问级别作为该源文件中被导入声明的可见性上限upper bound。具体而言当类型检查一个含有internal import SomeModule的源文件时从SomeModule导入的所有声明在该文件上下文中都被视为internal访问级别。此时类型检查会强制这些声明只能出现在internal及更低级别的声明签名、以及普通函数体中它们不能出现在 public 声明签名、usableFromInline声明签名或可内联代码中。诊断信息复用现有访问级别修饰符与内联代码检查的报错。同一逻辑应用于package、fileprivate、privateimport。对于publicimport除了对导入的package声明“不能从 public 声明签名引用”的既有限制外没有额外约束。usableFromInline对 import 的作用域是可内联代码inlinable与backDeployed函数体、参数的默认初始化器、以及frozen结构体的属性。被标记的依赖可以从可内联代码中引用但不影响声明签名的类型检查——签名部分只依据访问级别判断。以下是提案给出的fileprivateimport 典型诊断示例近似输出fileprivate import DatabaseAdapter fileprivate func fileprivateFunc() - DatabaseAdapter.Entry { ... } // Ok internal func internalFunc() - DatabaseAdapter.Entry { ... } // error: function cannot be declared internal because its return uses a fileprivate type public func publicFunc(entry: DatabaseAdapter.Entry) { ... } // error: function cannot be declared public because its parameter uses a fileprivate type public func useInBody() { DatabaseAdapter.create() // Ok } inlinable public func useInInlinableBody() { DatabaseAdapter.create() // error: global function create() is fileprivate and cannot be referenced from an inlinable function }注意函数体body内引用不受签名限制因此useInBody中调用DatabaseAdapter.create()是合法的但inlinable函数体因代码会被内联到客户端仍然受限。传递依赖加载何时可以隐藏依赖在模块级别使用访问级别信息后如果某个依赖从未被 public 导入且满足其他条件就可以对客户端隐藏它——客户端构建时无需加载该传递依赖从而加快构建速度、避免分发纯实现细节的模块。同一模块的不同文件可以用不同访问级别导入同一个依赖在模块级别只取最宽松most permissive的访问级别。例如一个依赖分别被两个文件以package和internal导入则在模块级别视为package可见性。传递客户端transitive client是指通过中间模块间接依赖另一个模块的模块。如下场景中TransitiveClient经由MiddleModule成为IndirectDependency的传递客户端module IndirectDependency ↑ module MiddleModule ↑ module TransitiveClient中间模块如何导入间接依赖决定了传递客户端在编译时是否需要加载它。有四个因素会强制要求加载传递依赖如果都不满足依赖可以被隐藏public或usableFromInline依赖必须总是由传递客户端加载非弹性non-resilient模块的所有依赖都必须由传递客户端加载——因为模块中的类型可能在其存储中使用这些依赖的类型编译器要正确生成代码就必须掌握非弹性类型的完整存储信息该限制在 Future Directions 中有进一步讨论若模块与传递客户端属于同一 package则该模块的package依赖必须加载——因为模块中的package声明可能在签名中使用该依赖的类型。“同一 package”的判断依据是 package 名称匹配与 package 声明使用的逻辑一致传递客户端对模块使用testable导入时其所有依赖都必须加载——因为 testable 客户端可以使用internal声明而后者可能依赖任意可见性级别甚至private/fileprivate导入的依赖。其余情况下依赖被隐藏传递客户端无需加载。注意一条导入路径上被隐藏的依赖可能因另一条导入路径而仍需加载。与隐藏依赖对应的模块接口module interface无需分发给客户端但该模块关联的二进制仍需分发以执行最终程序。默认导入访问级别语言模式与InternalImportsByDefault不带显式访问级别的 import 的默认访问级别取决于语言版本Swift 6 及之前的语言模式下import 默认为public这是为保持源码兼容性——Swift 5 中唯一的官方 import 行为等价于本提案的 public import未来语言模式下import 默认为internal使 import 行为与声明默认 internal对齐同时有助于限制无意间的依赖蔓延——将依赖标记为 public 将需要显式修饰符。因此下面的 import 在 Swift 6 及之前是public在未来语言模式下将变成internalimport ADependency未来的语言变更很可能要求采用新语言模式的代码做源码修改但不会破坏停留在当前语言模式的代码。迁移工具可以自动插入public修饰符在工具不可用时一个简单脚本即可在所有 import 前插入public以保持 Swift 5 行为。Upcoming feature flagInternalImportsByDefault允许在 Swift 5 或 6 模式下提前启用这一未来行为。仓库中的相关提案佐证了该标志的实际应用方式SE-0486 迁移工具0486-adoption-tooling-for-swift-features.md 将InternalImportsByDefault列为具备机械化迁移mechanical migration的 upcoming feature其迁移规则就是import X→public import X并可通过-enable-upcoming-feature InternalImportsByDefault:migrate让编译器输出带 fix-it 的警告SE-0540 默认 target 设置0540-default-target-settings.md 展示了在 Package.swift 中通过settings.append(.enableUpcomingFeature(InternalImportsByDefault))或defaultSwiftSettings批量启用的实战写法该文件还提及 swift-configuration 包的真实清单即采用此方式。与其他导入属性的交互_exported仅限 public import_exported是比publicimport 更高一级的机制客户端看到被导入模块的声明时如同它们是本地模块的一部分。按本提案_exported只接受出现在 public import 声明上——无论是显式public修饰符还是当前语言模式下默认的 public 可见性。testable访问级别边界同样生效testable允许本地模块引用被导入模块的 internal 声明现有设计甚至允许在 public 声明中使用导入的 internal 或 package 类型。访问级别行为与普通 import 一致所有导入声明的可见性上限是 import 声明上的访问级别。在testableimport 情况下连被导入的 internal 声明也受该上限约束。_implementationOnly被 internal import 取代现有_implementationOnly import的使用应替换为 internal 或更低级别的 import。相比之下新特性启用了更严格的类型检查、产生更少的冗余警告。替换为 internal import 后弹性resilient模块的传递依赖加载要求保持不变但非弹性模块会改变——其传递依赖必须总是加载。提案强烈建议所有依赖_implementationOnly的模块改用 internal import。另可参考 SE-0497 定义可见性0497-definition-visibility.md 中_implementationOnly internal import A的用法示例以及其对实现隐藏implementation hiding与传递依赖关系的讨论。Scoped imports正交不用于限制单个声明scoped imports 特性与同一条 import 上声明的访问级别相互独立。下例中模块Foo在模块级是 public 依赖可从本地源文件的 public 声明签名引用scoped 部分struct Foo.Bar只限制查找范围——本文件只能引用Bar并在存在其他Bar声明时优先解析到它。scoped imports 不能用来限制单个声明的访问级别public import struct Foo.Bar兼容性、采用影响与未来方向源码兼容性为保持源码兼容当前语言模式含 Swift 6下 import 默认 public保留 Swift 5 行为。未来语言模式将改变默认值并要求代码修改。ABI 兼容性本提案不影响 ABI 兼容性——它是纯编译期type-checking层面的变更。采用影响谨慎使用下采用或回退本特性不会影响客户端非弹性模块中采用变更只发生在模块源文件的类型检查中改变不同依赖的访问级别不会影响客户端弹性模块中采用将既有 import 标记为低于 public 会影响客户端的构建方式——编译器可以加载更少的传递依赖。理论上不影响客户端但可能导致不同的编译行为实践中的泄漏理论上这些传递依赖不可能被客户端使用因此隐藏它们不影响客户端但实践中存在允许使用传递依赖的扩展成员的泄漏路径。采用本特性可能跳过传递依赖加载并阻断这些泄漏从而破坏依赖这些行为的源码兼容性。未来方向为非弹性模块隐藏依赖理论上非弹性模块也能隐藏依赖但需要重新思考编译流程中的若干限制。主要限制在于编译器需要知道导入类型的内存布局而这可能依赖传递依赖弹性模块能在运行时提供这些信息因此构建时不需要传递模块非弹性模块不提供运行时信息编译器必须在构建时加载传递依赖以获取布局。可能的解决方案包括在每个模块中复制所需信息或进一步限制依赖的引用方式。提案明确表示这本身是一个独立于本提案的特性。备选方案回顾方案一_implementationOnly import非官方的_implementationOnly属性提供了类似功能类型检查 隐藏传递依赖但存在明显问题在非弹性模块中使用或与testable组合时曾导致不稳定与运行时崩溃语义与本提案略有不同且类型检查不够严格——它依赖自有的类型检查逻辑来报告从 public 声明引用 implementation-only 导入模块的问题相比之下本提案复用既有访问级别检查逻辑与语义更易学习并全新引入了packageimport 与private/fileprivate文件级导入能力。方案二用open import作为官方_exported importopen修饰符在本提案中未赋予 import 上的含义因此仍可保留给未来使用。有人建议把它用作官方_exported——即标记一个对模块内所有源文件可见、并像本地模块一部分那样展示给客户端的 import常用于 Swift 对 clang module 的 overlay两个模块同名且意图统一呈现。提案作者基于两点拒绝纳入声明上的open表示可被模块外重写override这与_exported行为毫无关联其他访问级别在声明与 import 之间有对应含义open没有本提案的动机是隐藏实现细节、限制依赖蔓延鼓励open import或_exported与之背道而驰属于另一类问题应在独立提案中讨论。方案三从 API 使用中推断依赖可见性通过分析模块编译器可以判断哪些依赖被 public 声明使用、需要暴露给客户端从而自动把其余依赖视为 internal 并在条件满足时对间接客户端隐藏。该方案缺少“import 声明上的访问级别 声明签名的引用”这种信息冗余——正是这种冗余使编译器能对比 import 上标记的意图与声明签名中的实际使用从而在依赖不对外分发时提供关键检查若依赖从隐藏变为 public可能使模块依赖第三方不可用的依赖而无法分发。结语与上手建议SE-0409 把声明访问级别的成熟心智模型扩展到依赖管理用internal import/fileprivate import收紧实现细节用package import服务同包协作用public import或未来语言模式下的显式声明明确 API 契约同时让编译器在满足条件时跳过传递依赖加载以加速构建。实际采用时建议新代码直接采用从源头标记依赖意图配合类型检查防止 API 泄漏存量代码渐进迁移可先用-enable-upcoming-feature InternalImportsByDefault:migrateSwift 6.2 起的迁移模式见 SE-0486自动将import X迁移为public import X再逐步把确属内部实现的依赖降级为internal/package/fileprivateSwiftPM 项目中集中配置通过 SE-0540 的defaultSwiftSettings或逐 target 的.enableUpcomingFeature(InternalImportsByDefault)批量启用留意弹性模块差异非弹性模块的传递依赖目前仍必须加载隐藏依赖的收益主要体现在弹性模块与未来语言模式中。核心语法与规则速查全部以 0409-access-level-on-imports.md 为准可用修饰符public/package/internal/fileprivate/private禁止openusableFromInline仅限package/internalimport_exported仅限 public importtestable、scoped imports 与访问级别相互独立Swift 6 及之前默认public未来语言模式默认internal。赞分享文档【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址https://gitcode.com/gh_mirrors/sw/swift-evolution点击查看免费下载相关推荐重新审视 Swift 扩展访问修饰符SE-0119「从扩展中移除访问修饰符」提案深度解析重新审视 Swift 扩展访问修饰符SE 0119「从扩展中移除访问修饰符」提案深度解析 本文基于 swift evolution 仓库中的提案文档 prop文档Swift Package Manager 资源支持SE-0271完全指南从 manifest 声明到 Bundle.module 运行时访问Swift Package Manager 资源支持SE 0271完全指南从 manifest 声明到 Bundle.module 运行时访问 本篇技术指文档Swift SE-0386 全面解读package 访问修饰符与 SwiftPM 包级 API 可见性控制Swift SE 0386 全面解读 package 访问修饰符与 SwiftPM 包级 API 可见性控制 package 是 Swift 5.9 正式引入文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表