
Swift 字符串插值协议重构SE-0228ExpressibleByStringInterpolation重设计详解【免费下载链接】swift-evolutionThis maintains proposals for changes and user-visible enhancements to the Swift Programming Language.项目地址: https://gitcode.com/gh_mirrors/sw/swift-evolution本文基于 Swift Evolution 仓库中的 proposals/0228-fix-expressiblebystringinterpolation.md 提案文档展开。SE-0228 是 Swift 5.0 中落地的一项核心语言改动它彻底重写了自 Swift 3 起就被废弃的ExpressibleByStringInterpolation协议用插值缓冲类型 appendLiteral/appendInterpolation语句化构建取代了旧的逐段构造Self再拼接模型让自定义字符串插值首次支持参数、标签、类型限制与抛错语义。读完本文你将掌握 SE-0228 的设计动机、新协议的完整形态、DefaultStringInterpolation的源码级实现以及如何在自己的类型上实现高性能、类型安全的自定义字符串插值。SE-0228 由 Becca Royal-Gordon 与 Michael Ilseman 提出评审经理为 Doug Gregor状态为Implemented (Swift 5.0)。它不仅是字符串插值能力的一次升级其伴随类型 非正式方法集合的构建器模式builder pattern还直接启发了后续 SE-0289 Result Builders详见 proposals/0289-result-builders.md以及 SE-0253 Callable Values 中对appendInterpolation非正式约定的讨论详见 proposals/0253-callable.md。一、背景字符串插值与旧协议的困境1.1 字符串插值是什么插值字符串字面量interpolated string literal包含一个或多个内嵌表达式表达式以\(开头、以)结尾。运行时这些表达式会被求值并与字符串字面量拼接最终产生一个值。与在字符串字面量、拼接运算符和任意表达式之间来回切换的写法相比插值通常可读性更强let name Swift print(Hello, \(name)!) // Hello, Swift!与 Swift 中大多数字面量特性一样插值字符串字面量通过一个协议实现即ExpressibleByStringInterpolation。但该协议自 Swift 3 起就已处于废弃状态原因在于它在性能、灵活性和语义清晰度上都存在结构性问题。1.2 三类期望的插值用例提案将希望遵循ExpressibleByStringInterpolation的类型划分为三个类别简单文本数据Simple textual data表示简单、无约束文本的类型如Swift.String本身来自其他语言的字符串类型如JavaScriptCore.JSValue或字符串的替代表示如假设的ASCIIString类型也可能想参与插值。结构化文本数据Structured textual data表示文本但带有额外语义的类型。例如Foundation.AttributedString可能允许你插值字典来设置或清除属性提案中引用的LocalizableString类型则通过插值构造格式字符串 key运行时在Foundation.Bundle的本地化表中查找并格式化。机器可读代码片段Machine-readable code fragments以机器可理解格式表示数据的类型如SQLKit.SQLStatement或SanitizedHTML。这类类型通常要求内嵌数据被转义或带外传递一个好的设计应允许自动完成转义而无需程序员显式操作也可能只支持特定类型或默认转义但提供插入未转义数据的方式。旧设计只能良好服务第一类简单文本用例对结构化文本和机器可读代码片段的支持力不从心。二、旧设计剖析编译器的降级处理与四大问题2.1 旧语义表达式在旧设计中编译器把字符串字面量解析为一系列segment段要么是包含字符与转义的literal segment字面段要么是包含待插值表达式的interpolated segment插值段。如果段不止一个编译器会先把每个段包进对init(stringInterpolationSegment:)的调用再把所有段一起包进对init(stringInterpolation:)的调用// 语义表达式对应源码: hello \(name)! String(stringInterpolation: String(stringInterpolationSegment: hello ), String(stringInterpolationSegment: name), String(stringInterpolationSegment: !))类型检查器会考虑init(stringInterpolationSegment:)的所有重载而不仅是实现协议要求的那一个。Swift.String正是利用这一点为遵循CustomStringConvertible和TextOutputStreamable的类型添加了快速路径。2.2 问题一低效的朴素内存管理每一次init(stringInterpolationSegment:)调用都会创建一个Self的临时实例随后这些实例被拼接起来。取决于遵循类型或段的大小每一个插值段都可能触发一次堆分配和 ARC 开销。更糟的是编译器明明知道字面段与插值段的数量和大小但这些信息完全没有传达给遵循类型。如果遵循类型能拿到尺寸信息就可以预估最终值的大小并预分配容量如果段在拼接前不先转换成Self数据就可以直接写入预分配容量而无需任何临时实例。2.3 问题二不灵活——无额外参数、段类型无约束、段语义丢失没有额外参数旧方法不允许遵循类型指定额外的参数或选项来控制插值表达式的求值。许多遵循类型希望提供不同的插值行为如SanitizedHTML关闭转义或希望接受选项如控制LocalizableString使用的格式串String本身未来也希望支持格式参数。段类型无约束init(stringInterpolationSegment:)接受无约束的泛型值参数可以是任意类型。但有些遵循类型希望限制可插值类型例如SQLKit.SQLStatement只能把整数、字符串等特定类型绑定到 SQL 语句参数上。此外无约束泛型参数带来第二个问题当字面量被传给init(stringInterpolationSegment:)时它默认形成String默认字面量类型这偏离了标准库让遵循类型自己提供字面量类型的惯例。段语义丢失遵循类型无法轻易判断传入的段来自字面量还是表达式除非采用把编译器内部细节写死的 hack详见下文。2.4 问题三被迫依赖编译器内部细节写死假设Baking in assumptionsinit(stringInterpolationSegment:)实现无法判断参数是字面段还是插值段。但init(stringInterpolation:)可以钻编译器怪癖的空子解析器总是先生成字面段并且总是让字面段与插值段交替出现必要时生成空字面段因此段的位置可以告诉你它是字面段还是插值段。这显然是用户不应依赖的晦涩实现细节。而且为了让init(stringInterpolation:)能按两种类型之一处理段遵循类型往往需要增加额外属性、甚至为了支持插值而专门改动类型设计。类型检查器 hack如果语义分析直接生成上述语义表达式再走常规类型检查很多字符串插值会因为过于复杂而无法通过类型检查。于是编译器改为先对每个段单独做类型检查再为段创建init(stringInterpolationSegment:)调用并只对这个调用做一次类型检查来解析其重载。字符串插值是这种类型检查器入口的唯一剩余客户提案希望彻底移除它。三、新设计总览协议形态与编译器生成代码3.1 重新设计后的协议提案完全重做了ExpressibleByStringInterpolation注释从略public protocol ExpressibleByStringInterpolation : ExpressibleByStringLiteral { associatedtype StringInterpolation : StringInterpolationProtocol String.StringInterpolation where StringInterpolation.StringLiteralType StringLiteralType init(stringInterpolation: StringInterpolation) } public protocol StringInterpolationProtocol { associatedtype StringLiteralType : _ExpressibleByBuiltinStringLiteral init(literalCapacity: Int, interpolationCount: Int) mutating func appendLiteral(_ literal: StringLiteralType) // 非正式要求: mutating func appendInterpolation(...) }3.2 编译器生成代码的形态一个插值字符串将被转换成如下代码共三步初始化一个关联的StringInterpolation类型实例传入总字面段大小和插值段数量作为参数逐段调用其appendLiteral(_:)方法追加字面值调用appendInterpolation追加插值。插值被当作调用括号处理——即\(x, with: y)变成对appendInterpolation(x, with: y)的调用把该实例传给init(stringInterpolation:)以产生最终值。下面是与编译器生成代码大致相当的语义表达式// 语义表达式对应源码: hello \(name)! String(stringInterpolation: { var temp String.StringInterpolation(literalCapacity: 7, interpolationCount: 1) temp.appendLiteral(hello ) temp.appendInterpolation(name) temp.appendLiteral(!) return temp }())注意literalCapacity: 7正是hello 6 个字符与!1 个字符两个字面段的总长度interpolationCount: 1表示有一个插值段——尺寸信息第一次被明确传给了遵循类型这正是旧设计缺失的关键能力。四、StringInterpolation伴随类型的设计价值新协议把插值缓冲从Self中剥离出来作为一个关联类型StringInterpolation。它相当于一个缓冲区或草稿纸插值字符串字面量的值在其中累积。相比旧设计中直接用Self充当缓冲这一改变带来三方面收益命名空间隔离新类型可以为各种appendLiteral/appendInterpolation方法充当命名空间遵循类型新增插值方法时不会污染代码补全、文档等。额外状态存储独立类型可以保存形成结果所需的临时状态。例如Foundation.AttributedString可能需要用属性记录当前 attributes由解析数据结构支撑的类型如LambdaCalculusExp、Regexp可以存放未解析的字符串或解析器状态。当类型不需要任何额外状态时伴随类型不带来任何开销。实现共享多个不同类型可以共用同一套实现。例如String与Substring共用同一个遵循StringInterpolationProtocol的类型。4.1 标准库提供的默认实现标准库提供DefaultStringInterpolation类型StringProtocol进而String和Substring都使用它进行插值。值得一提Substring之前是不允许插值的SE-0228 顺带修复了这一点。标准库同时提供两组默认实现对使用DefaultStringInterpolation的类型提供默认的init(stringInterpolation:)在插值完成后取出值并转发给init(stringLiteral:)。因此当前已遵循ExpressibleByStringLiteral且以String为字面量类型的类型只需把遵循改为ExpressibleByStringInterpolation即可获得插值支持。对其他类型提供默认的init(stringLiteral:)构造一个Self.StringInterpolation实例调用其appendLiteral(_:)方法再转发给init(stringInterpolation:)。一个不可用或已废弃的init(stringLiteral:)用来确保它永远不会与为DefaultStringInterpolation使用类型提供的init(stringInterpolation:)组合使用——那会导致无限递归。五、appendInterpolation方法自由签名的非正式要求StringInterpolation类型必须遵循StringInterpolationProtocol该协议要求init(literalCapacity:interpolationCount:)与appendLiteral(_:)两个方法。非字面段在编译期被限制为遵循类型提供的appendInterpolation重载集合。这意味着遵循类型可以通过只实现自己想支持的类型对应的方法来限制可插值值的范围且appendInterpolation可以被重载以支持多个互不相关的类型。appendInterpolation方法的参数签名几乎完全自由可以接受多个参数带或不带默认值可以要求任何参数带标签包括第一个参数可以有可变参数variadic方法本身可以抛错throwing若抛错字符串字面量必须被try、try?或try!覆盖。这套自由签名的约定极大提升了灵活性但也引入了编译器与遵循类型声明的 ad-hoc 方法之间的隐式关系并且限制了在泛型StringInterpolationProtocol上下文中的可插值类型范围更强的约束可以解除该限制。值得注意的是尽管协议中没有列出正式要求编译器被修改为在以下情况发出错误——StringInterpolationProtocol遵循类型至少需要一个appendInterpolation重载要求是可见性不低于该类型本身、不返回值或返回可丢弃值、且非 static。这是非正式要求 编译器强制检查的典型组合也是后续 SE-0253Callable Values讨论中提到的非正式要求先例proposals/0253-callable.md 第 586-588 行明确指出StringInterpolationProtocol对appendInterpolation方法的约定与此类似。六、插值解析的语法变化与源码兼容性6.1 插值按参数列表解析插值将被解析为参数列表允许标签与多参数但不允许尾随闭包。这一变化略有源码破坏性Swift 4.2 中\(x, y)表示插值一个元组新语法下需写成\((x, y))。虽然可以通过 n 元appendInterpolation重载处理无标签元组但带标签的元组仍然会破坏。因此在 Swift 4.2 模式下模拟旧行为带警告迁移到 Swift 5 时容易修正。6.2 兼容性策略由于ExpressibleByStringInterpolation自 Swift 3 起就已废弃提案无需维持与既有遵循的源码兼容也不在 Swift 4 模式下保留既有遵循。旧的init(stringInterpolation:)与init(stringInterpolationSegment:)初值器不保留它们一直被视为不应直接调用的接口。但源码兼容测试套件中存在意外使用init(stringInterpolationSegment:)的代码——即在期望CustomStringConvertible或TextOutputStreamable类型的上下文中写出String.init。对此提案设计了一组init(describing:)重载来匹配这些意外、隐式使用而不保留对init(stringInterpolationSegment:)的显式使用。同时提案提供一组与当前init(stringInterpolationSegment:)重载完全匹配的String.StringInterpolation.appendInterpolation重载保证正常插值行为与以前完全一致。奇怪的插值如\(x, y)或\(foo: x)在 Swift 5 模式下是错误在 Swift 4.2 模式下保留旧行为并给出警告。这意味着 Swift 4.2 代码只能使用单一无标签参数的appendInterpolation重载除非其他参数都有默认值。迁移方式为插入一对额外括号或移除参数标签。七、源码级实现DefaultStringInterpolation全景提案文档完整给出了DefaultStringInterpolation的标准库实现这是理解新插值机制性能特征的最佳入口源码全文见 proposals/0228-fix-expressiblebystringinterpolation.md 第 245-411 行。以下为关键片段_fixed_layout public struct DefaultStringInterpolation: StringInterpolationProtocol { /// 该实例累积的字符串内容。 usableFromInline internal var _storage: String /// 创建按给定属性预分配容量的字符串插值。 inlinable public init(literalCapacity: Int, interpolationCount: Int) { let capacityPerInterpolation 2 let initialCapacity literalCapacity interpolationCount * capacityPerInterpolation _storage.reserveCapacity(initialCapacity) } inlinable public mutating func appendLiteral(_ literal: String) { _storage literal } inlinable public mutating func appendInterpolationT: TextOutputStreamable CustomStringConvertible(_ value: T) { value.write(to: _storage) } inlinable public mutating func appendInterpolationT: TextOutputStreamable(_ value: T) { value.write(to: _storage) } inlinable public mutating func appendInterpolationT: CustomStringConvertible(_ value: T) { _storage value.description } inlinable public mutating func appendInterpolationT(_ value: T) { _print_unlocked(value, _storage) } }7.1 预分配策略init(literalCapacity:interpolationCount:)中capacityPerInterpolation 2是一个经验值每个插值段预估追加约 2 个字符如\(n)这类最短情况因此初始容量 literalCapacity interpolationCount * 2再调用_storage.reserveCapacity(_:)一次性预分配。这正是旧设计做不到的编译器把段尺寸信息告知遵循类型的直接落地。7.2 四级appendInterpolation重载的优先级DefaultStringInterpolation对泛型约束做了四级重载编译器按约束强弱选择最匹配者重载约束追加方式适用场景T: TextOutputStreamable CustomStringConvertiblevalue.write(to: _storage)同时支持流式输出与描述的类型最高优先级T: TextOutputStreamablevalue.write(to: _storage)自定义输出目标避免中间字符串T: CustomStringConvertible_storage value.description有自定义描述的类型T无约束_print_unlocked(value, _storage)兜底走标准打印路径7.3 取回最终值标准库内部通过make()取出最终值对想使用DefaultStringInterpolation但又需要在init(stringInterpolation:)中做额外处理的类型标准库提供了公开等价物CustomStringConvertible遵循——DefaultStringInterpolation的description直接返回_storage。7.4 扩展默认插值行为提案文档的 doc comment 给出了一个重要的扩展模式不遵循任何新协议直接扩展DefaultStringInterpolation即可为String及许多常见类型扩展插值行为。例如给插值增加转义能力extension DefaultStringInterpolation { fileprivate mutating func appendInterpolation( escaped value: String, asASCII forceASCII: Bool false) { for char in value.unicodeScalars { appendInterpolation(char.escaped(asASCII: forceASCII)) } } } print(Escaped string: \(escaped: string))约束包括扩展应只添加mutating成员不应复制self或在逃逸闭包中捕获它。八、编译器的实现方式与性能数据8.1 语句化生成无需特殊类型检查新设计把每个appendLiteral(_:)与appendInterpolation调用放在独立语句中因此不再需要特殊的类型检查器处理。每个插值自然地被单独类型检查appendInterpolation的重载解析与值本身的类型检查同步完成——这有助于类型检查器的持续重构。由于部分初始化变量捕获问题这些语句不包裹在闭包中而是使用一种新的 AST 节点。8.2 基准测试提案给出了 Swift 标准库基准套件的实测数据。部分字符串插值基准有 20-30% 的回退但大多数基准有提升有些提升非常显著基准-O 速度提升-Osize 速度提升StringInterpolationManySmallSegments2.15x1.80xStringInterpolationSmall2.01x2.03xArrayAppendStrings1.16x1.14xFloatingPointPrinting_Double_interpolated1.15x1.16xFloatingPointPrinting_Float80_interpolated1.09x1.08xStringInterpolation0.82x0.79xFloatingPointPrinting_Float_interpolated0.82x0.73x对StringInterpolation基准的回退提案的解释是该基准特定的字面段与插值段尺寸组合恰好导致新设计下缓冲区多增长一次不代表设计的整体性能。三个浮点打印基准最初都回退后来通过给Float、Double、Float80遵循TextOutputStreamable并在TextOutputStream中增加私有 ASCII-only 快速路径将Double与Float80转为小幅提升但Float改善有限。编译产物代码体积平均小幅改善StringInterpolation.o在 -O / -Osize 下分别缩小 1.18x / 1.16xSwift 库体积同样改善如libswiftSwiftPrivateLibcExtras.dylib1.20x、libswiftFoundation.dylib1.15x、libswiftXCTest.dylib1.10x 等。另外默认init(stringLiteral:)仅供完全自定义插值的类型使用当前约为手写init(stringLiteral:)的 0.5x 速度但原型验证表明内联String.reserveCapacity(_:)与String.append(_:)的若干快速路径可把代价降到 0.93x对性能敏感的类型也始终可以手动实现init(stringLiteral:)。九、潜在应用场景提案展望非提案内容提案明确声明以下只是未来可能性展示不代表提案承诺的功能任何未来提案都可能不同。但它们展示了新设计的能力边界格式化字符串The price is $\(cost, format: %.2f)、The checksum is 0x\(checksum, radix: 16)等模拟String.init(_:radix:uppercase:)的用法日志脱敏log(Processing \(public: tagName) tag containing \(private: contents))——限制可记录数据类型并附加元数据属性字符串\([.link: supportURL])Click here\([.link: nil]) to visit our support site本地化let message: LocalizableString The document “\(name)” could not be saved.再经String(localized: message)查找 Bundle 本地化表。这些示例中体现的语句化构建 伴随类型思想正是后续 SE-0289 Result Builders 直接借鉴的构建器模式proposals/0289-result-builders.md 第 81 行明确说明A similar builder pattern was used successfully for string interpolation in SE-0228。十、ABI 与 API 韧性影响ABI 稳定性ExpressibleByStringInterpolation需在 Swift 5 起保持 ABI 稳定提案强调应在 Swift 5 之前采纳本方案或某种替代方案并解除ExpressibleByStringInterpolation的废弃状态。API 韧性该 API 非常基础未来很难再做兼容性修改——这也是设计必须一次做对的原因。十一、备选方案与取舍提案认真考虑了两类替代设计最终均被否决11.1 基于可变参数的方案继续像旧设计那样把段传给可变参数。例如用init(stringLiteral:)包装字面段、保留init(stringInterpolationSegment:)String(stringInterpolation: String(stringLiteral: hello ), String(stringInterpolationSegment: name), String(stringLiteral: !))或用枚举区分字面段与插值段String(stringInterpolation: .literal(hello ), .interpolation(String.StringInterpolationType(name)), .literal(!))否决理由要求遵循类型暴露同构的返回值会带来可表达性和/或效率上的缺陷。提案采用的语句化方案把这一细节保留为遵循类型的内部实现。11.2 正式的appendInterpolation(_:)要求考虑过设立一个带无约束泛型参数的形式化appendInterpolation(_:)要求来模拟旧行为甚至提供默认实现产出字符串且仍尊重重载。否决理由必须放弃遵循类型限制允许的插值类型或插值段形态的能力——而这恰是新设计最重要的价值之一。结语从协议重构到语言模式SE-0228 的意义远超修好一个废弃协议它以伴随类型作缓冲 语句化构建 自由签名的非正式方法 编译器强制检查的组合确立了 Swift 中一类新的语言模式——构建器模式。这一模式在 SE-0289 中开花结果成为 SwiftUI 等声明式 API 的基础其非正式要求的约定方式也在 SE-0253、SE-0358ExpressibleByStringInterpolation的主关联类型演进见 proposals/0358-primary-associated-types-in-stdlib.md 第 128-131 行中持续演进。对普通开发者而言SE-0228 意味着String插值更快、Substring首次支持插值、以及一个真正类型安全且可定制的自定义插值接口——只需实现StringInterpolationProtocol或简单扩展DefaultStringInterpolation即可让任何类型拥有符合自己语义的\(...)语法。【免费下载链接】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),仅供参考