ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Swift 非穷尽枚举(Non-Exhaustive Enums)深度解析:SE-0192 的 frozen 与 @unknown 机制

Swift 非穷尽枚举(Non-Exhaustive Enums)深度解析:SE-0192 的 frozen 与 @unknown 机制 Swift 非穷尽枚举Non-Exhaustive Enums深度解析SE-0192 的 frozen 与 unknown 机制【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolutionSE-0192proposals/0192-non-exhaustive-enums.md是 Swift 语言为 ABI 稳定性铺路的关键提案它正式把枚举划分为frozen冻结与non-frozen非冻结两类并引入unknown属性来兼顾穷尽性检查与未来新增 case 的源代码兼容性。本文以该提案为骨架结合仓库中的后续提案SE-0260 library evolution、SE-0487 extensible enums与 Swift 5.0 发布说明releases/swift-5_0.md完整还原该特性的设计动机、语言规则、C 互操作细节、ABI 影响与实战写法帮助你理解为什么你写的unknown default:长这样、它在 Swift 5 之后扮演什么角色。一、为什么要区分 frozen 与 non-frozen 枚举在 SE-0192 之前给一个枚举新增 case 必然破坏源代码兼容性所有穷尽匹配该枚举的switch都会因为缺少新 case 而编译失败。这显然与 Apple 持续演进 SDK API 的流程相悖。例如 iOS 10 中Foundation 的DateComponentsFormatter.UnitsStyle新增了briefcaseUIKit 的UIKeyboardType新增了asciiCapableNumberPad大型错误枚举也会随库支持的新操作而增长。库作者必须拥有在不破坏二进制兼容的前提下为枚举增加 case的能力。与此同时Swift 开发者非常依赖对枚举做穷尽switch它能防止漏处理分支也使得确定初始化definitive initialization可以在没有default的情况下被编译器强制执行。因此我们不能取消所有 case 已知的枚举而必须显式区分两类枚举frozen冻结枚举保证永远不会新增 case客户端可以穷尽匹配non-frozen非冻结枚举未来可能新增 case客户端必须提供兜底分支。提案作者对 macOS SDK 中 Foundation 公开头文件的调查佐证了这种划分的必要性约 60 个NS_ENUM中仅有 6 个被明确期望做穷尽匹配——ComparisonResult、NSKeyValueChange/NSKeyValueSetMutationKind、NSRectEdge、FileManager.URLRelationship、以及可能算上的Decimal.CalculationError。也就是说Objective-C 公开枚举的默认形态就应该是可能增长。术语说明语法上是 frozen / non-frozen 而非 unfrozen因为 unfrozen 暗示它曾经被冻结过。二、核心设计Swift 4.2 / 5.0 中的默认行为提案在 Swift 4.2 中落地了如下默认规则从 C 导入的枚举、标准库与 overlay 中定义的枚举被分为 frozen 与 non-frozen 两类客户端 switch 一个 non-frozen 枚举时必须包含某种兜底 casedefault、case _等Swift 5 模式下遗漏兜底 case 会产生警告根据提案被接受后的修订原本计划是错误后降级为警告Swift 4 模式下则完全没有诊断除标准库与 overlay 之外、用 Swift 写的所有枚举在 Swift 4.2 中隐式视为 frozen从 C 导入的枚举默认视为 non-frozen可通过新增的 C 侧注解改为 frozen。关键点在于只有switch的穷尽性检查受到 frozen/non-frozen 区分的影响。if case、枚举构造、访问成员等其他用法一概不变。对 frozen 枚举以及布尔值做非穷尽 switch 在所有语言模式下仍然是非法的。2.1 非穷尽 switch 的编译行为switch excuse { case .eatenByPet: // … case .thoughtItWasDueNextWeek: // … }在 Swift 5 中上面的代码会因缺少兜底 case 而产生警告如果运行时真的遇到未知 case程序会直接trap崩溃。更复杂的例子同样适用switch (excuse, notifiedTeacherBeforeDeadline) { case (.eatenByPet, true): // … case (.thoughtItWasDueNextWeek, true): // … case (_, false): // … }这个 switch 覆盖了所有已知模式但并未考虑第二个元组元素为true时出现新枚举 case的可能性因此在 Swift 5 中同样会产生警告。三、unknown属性在兜底与穷尽检查之间取得平衡用普通default兜底有一个明显的副作用编译器再也无法提醒开发者某个枚举有未被显式处理的元素。为此SE-0192 为 switch case 引入了新属性unknown。3.1 基本写法switch excuse { case .eatenByPet: // … case .thoughtItWasDueNextWeek: // … unknown default: // … }unknown default与普通default一样匹配任何值是兜底 case。区别在于如果枚举的所有已知元素尚未全部匹配编译器会产生警告而非错误。选择警告而非错误是为了让给枚举新增元素继续保持源代码兼容。这同时也是unknown default匹配任意值而非仅匹配编译期未知的值的原因。提案接受时做了一处关键修订最初的 unknown:新 case 写法被改为unknown属性且只能应用于default:与case _:。两种拼写unknown default:与unknown case _:均被接受。3.2 使用限制与告警规则unknown只能应用于default或由单一_模式构成的 case即使用于case _unknown也必须位于switch的最后一个 case如果被匹配的模式中所有枚举都被显式标注为 frozen或者模式中根本没有枚举编译器会警告unknown没有意义同样是警告而非错误以便把枚举标注为 frozen保持源代码兼容如果模式中包含隐式 frozen 的枚举例如用户自定义的 Swift 枚举unknown依然被允许以方便代码为未来新增的 case 做准备。3.3unknown不可测试但可与fallthrough组合unknown存在一个天然缺陷它无法被测试——你无法构造一个不匹配任何已知 case的枚举值即便构造出来也没有安全的使用方式。但将unknown与其他 case 组合fallthrough既可以复用另一分支的行为又能继续获得新 case 到来时编译器提醒的好处switch excuse { case .eatenByPet: showCutePicturesOfPet() case .thoughtItWasDueNextWeek: fallthrough unknown default: askForDueDateExtension() }四、C 枚举与enum_extensibilityC 导入的枚举存在一个麻烦很难判断它属于当前项目还是外部 SDK。NS_ENUM位于 Apple SDK 中时应当视为 non-frozen但位于你自己的框架中时可能应该是 frozen——即便那样还可能出现定义在.m文件里的私有 case// MyAppPaperSupport.h typedef NS_ENUM(NSInteger, PaperSize) { PaperSizeUSLetter 0, PaperSizeA4 1, PaperSizePhoto4x6 2 };// MyAppPaperSupport.m static const PaperSize PaperSizeStickyNote 255;这种模式在 Apple SDK 中确实存在虽然不常见。因此从 C 导入的枚举采取保守策略未标注的NS_ENUM一律按 non-frozen 导入并在所有上下文中如此对待。新增的 C 属性enum_extensibility可覆盖此行为typedef NS_ENUM(NSInteger, GregorianMonth) { GregorianMonthJanuary 1, GregorianMonthFebruary, GregorianMonthMarch, GregorianMonthApril, GregorianMonthMay, GregorianMonthJune, GregorianMonthJuly, GregorianMonthAugust, GregorianMonthSeptember, GregorianMonthOctober, GregorianMonthNovember, GregorianMonthDecember, } __attribute__((enum_extensibility(closed)));注意enum_extensibility(closed)或enum_extensibility(open)的存在会让 Swift 把该类型视为真枚举true enum此前唯一的途径是使用NS_ENUM/CF_ENUM宏。新增的flag_enumC 属性则用于标识NS_OPTIONS这类选项集合。除影响 switch 之外frozen C 枚举的init(rawValue:)还会强制要求传入的是编译期已知的 casenon-frozen 导入枚举则继续不对原始值做任何检查。五、标准库与 overlays 中的 frozen 清单多数标准库枚举不需要非冻结带来的灵活性因此被标记为 frozen❄️ClosedRange.Index❄️FloatingPointSign❄️FloatingPointClassification❄️Never❄️Optional❄️UnicodeDecodingResult❄️Unicode.ParseResult以下标准库公开枚举不标记为 frozenDecodingErrorEncodingErrorFloatingPointRoundingRuleMirror.AncestorRepresentationMirror.DisplayStylePlaygroundQuickLook本已弃用overlay 虽不严格属于 Swift 开源项目由 Apple 框架团队维护但提案给出的暂定计划是ARCamera.TrackingStateon / off / limited(Reason) 三态与DispatchTimeoutResultsuccess / timed out标记为 frozen其余公开枚举保持 non-frozen包括Calendar.Component、Calendar.Identifier、Calendar.MatchingPolicy、Data.Deallocator、DispatchTimeInterval、JSONDecoder.DateDecodingStrategy、JSONEncoder.KeyEncodingStrategy、MachErrorCode、POSIXErrorCode等 20 余个。对使用 Swift 的日常开发者而言这些清单意味着一个直观体验对Calendar.Component这类 SDK 枚举写 switch 时编译器会要求你写unknown default:。六、与其他语言设计的横向对比提案专门调研了其他语言对枚举可增长问题的处理方式可分为三类没有 non-frozen 枚举Haskell、OCaml新增 case 总是源代码破坏性变更它们也不太在意二进制兼容、Kotlinenum class 用得较少、C#官方文档承认语言对此帮助有限、Objective-C但 Apple 正在通过enum_extensibilityClang 属性探索。替代设计F# 的 union 要么全部暴露 case 要么完全不暴露相当于 Swift 中禁止 switch 该枚举D 语言区分switch与final switch只有后者要求穷尽——这是使用侧的决策而非定义侧Scala 更常用sealed traitsSwift 术语里近似所有遵循类型都已知的协议。与本次提案相似的设计Rust 有一个非常类似的 non-exhaustive 枚举提案但为不破坏既有 Rust 程序frozen 仍是默认。七、源代码兼容性规则与破坏契约提案确立了明确的源代码兼容性矩阵变更源代码兼容给 non-frozen 枚举新增 caseC 导入或标准库定义✅ 兼容给 frozen 枚举新增 case❌ 不兼容从公开枚举无论 frozen 与否移除 case❌ 仍不兼容non-frozen → frozen✅ 兼容frozen → non-frozen❌ 不兼容破坏契约的后果同样有明确定义若库作者给 frozen 枚举新增 case所有未处理该 case 的既有 switch即没有default或_模式的会得到与 Swift 4 中非穷尽 switch相同的错误若库作者把原本 frozen 的枚举改为 non-frozen任何缺少兜底 case 的 switch 会产生警告。八、对 ABI 稳定性与 Library Evolution 的影响8.1 布局与间接寻址non-frozen Swift 枚举的布局绝不能暴露给客户端——库可能在下一版本新增一个装不下的 case。这导致该类枚举出现在公开 API 中时需要额外的间接层而 frozen 枚举的布局仍对客户端开放供优化使用。此变更不影响objc枚举的布局无论从 C 导入还是在 Swift 中定义非objc枚举的 case 表示可能与其 raw value 不同这能提高所有 case 在编译期已知时switch的执行效率。8.2 二进制兼容规则给 non-frozen 枚举新增 case 现在是二进制兼容变更从公开枚举移除 case 仍然不是二进制兼容变更给枚举添加或移除objc都不是二进制兼容变更把 non-frozen 枚举改为 frozen 是希望未来在不破坏二进制兼容的前提下支持的目前尚无设计反向操作则被禁止。8.3 破坏 ABI 契约的严重性编译器依据 frozen 枚举的 case 集合决定其内存表示与调用约定因此给 frozen 枚举新增 case、或将其标记为 non-frozen会令未重新编译的客户端应用陷入未定义行为——其内存安全与类型安全损失可与误用 unsafe 类型相提并论最可能表现为崩溃但也可能使代码被意外执行或跳过。作为特例对objc枚举无论导入还是 Swift 定义switch 到意外值总是 trap 而非未定义行为即使该枚举是 frozen 的。8.4 与 SE-0260 的衔接SE-0260proposals/0260-library-evolution.md把这一概念扩展到了全部 struct/enum开启-enable-library-evolution后库中类型的默认行为变为 resilient非 frozen并通过frozen属性按类型逐一退出这种弹性。该提案明确指出枚举的默认行为将改为 non-frozen这是库使用者唯一可见的变化——他们需要使用 SE-0192 描述的unknown default:技巧见 proposals/0260-library-evolution.md。同时标记枚举为frozen将恢复库使用者不带unknown default:穷尽 switch 的能力因为它保证了不再新增 case见 proposals/0260-library-evolution.md。换句话说SE-0192 定义了非冻结时客户端怎么写SE-0260 则定义了冻结如何声明。九、未来方向提案中的前瞻标准库之外的非 frozen Swift 枚举早期版本曾为所有公开 Swift 枚举引入 frozen/non-frozen 区分核心团队认为这仅对存在二进制兼容诉求的库有价值需要更成熟的版本化概念应单独成案——这一方向后来由 SE-0487见下文部分落地。unknown模式理论上可以设计一种新模式在更大的模式如元组内部匹配未知 case例如case (#unknown, true):。但这会产生前一个已知 case 抢走匹配的意外结果且unknown只能放在最后一个 case 的限制无法推广到任意模式故未纳入本提案。与其他兜底 case 组合目前unknown只支持default:与case _:case let value:、case (_, let b):等兜底形态暂不支持但无技术障碍。非公开 casenon-frozen 枚举的实现机制同时允许公开枚举存在非公开 caseApple SDK 中已有实践建议未来frozen 枚举不得含非公开 case。兼容性检查通过比较各版本 swiftmodule 的 API 检查器、或把类型布局编码进符号名客户端链接该符号布局变化即启动失败防止库作者误给 frozen 枚举加 case。带 raw type 的高效表示曾考虑用 32 位整数表示无载荷枚举40 亿个 case 是合理上限但这会让增删 raw type 成为 ABI 破坏性变更并使下面两种写法不再等价故超出范围/* non-frozen */ public enum HTTPMethod: String { case get GET case put PUT case post POST case delete DELETE }/* non-frozen */ public enum HTTPMethod: RawRepresentable { case get case put case post case delete public init?(rawValue: String) { switch rawValue { case GET: return .get case PUT: return .put case POST: return .post case DELETE: return .delete default: return nil } } public var rawValue: String { switch self { case .get: return GET case .put: return PUT case .post: return POST case .delete: return DELETE } } }十、备选方案回顾为什么最终是unknown提案用了大量篇幅讨论备选方案理解它们有助于把握设计取舍术语之争closed/open 因与类的open语义冲突被否决exhaustive/non-exhaustive、final/non-final、sealed/non-sealed 等十余个候选complete/incomplete、covered、non-extensible、fixed、locked、total/partial……都被讨论过最终采纳 Becca Royal-Gordon 建议的frozen。unknown命名之争候选拼写包括future:、unexpected:、undeclared:、unknown case:、unknown default:、unused default:、runtimeReachableOnly default:、default unknown:、default(unknown):、fallback:、invisible:等。核心团队最终选定unknown default:/unknown case _:——因为它不被绑定到default上。switch!一个在遇到未知 case 时只能 trap、不支持其他动作的替代语法与明知未来会有新 case的非冻结枚举精神不符。测试无效 case用testable注解允许创建无效枚举值、配合#invalid表达式测试未知 case 的处理逻辑但因无法把无效值传回原库而搁置属附加特性可后续补。允许源码包中的枚举视为 non-frozen第一版提案曾覆盖所有公开枚举核心团队认为对不关心该能力的用户而言代价大于收益予以否决。去掉unknown初版提案只允许普通default但社区强烈不满穷尽性检查的丧失最终保留。unknown与其他兜底 case 混用如unknown case _:后接普通case _:同一段代码在重编译前后行为会变化故被禁止。引入新的声明种类如choices HomeworkExcuse { … }增加语言表面积且易让作者误用不如将 frozen/non-frozen 视为同一声明种类的两个变体。改用协议协议能模拟 non-frozen 枚举的全部能力但失去了unknown的穷尽性检查、也无法禁止他人添加case且需重写既有代码。把 non-frozen C 枚举导入为 RawRepresentable struct不能解决未来 Swift 库的问题还要求大量项目改写 switch。让 Apple 别再给 C 枚举加 case不可能这是 Apple 框架的既定模式。十一、实战小结现代 Swift 中你应该怎么写结合 SE-0192、SE-0260 与后续演进当前 Swift 的实践要点可归纳为对 SDK / 库导入的 non-frozen 枚举写 switch 时始终保留unknown default:或unknown case _:必须放在最后一个 case既满足穷尽性检查又让未来新增 case 只产生警告而非运行时崩溃或编译错误对自己库中的公开枚举若追求 ABI 稳定并可能新增 case默认保持 non-frozenresilient让客户端使用unknown default:若确认永不变更如Optional、Never这类可显式标注 frozenSE-0260 的frozen换取直接布局与优化给 frozen 枚举新增 case 是源代码与二进制双重破坏性变更等同于滥用 unsafe 级别的未定义行为风险务必通过 API 检查工具防止误操作unknown分支无法直接测试可通过fallthrough复用已知分支的行为后续 SE-0487proposals/0487-extensible-enums.md状态 Implemented进一步将可扩展枚举能力扩展到非 resilient 的普通 Swift 库标志着这条演进路线的延续。SE-0192 的核心结论可以概括为一句话给会变的枚举一个明确的身份并把可能变写进编译器的检查规则里——这正是 Swift 在保持穷尽匹配这一核心语言体验的同时得以支撑 ABI 稳定与库演进的基石。【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表