【译】Express By String插值

54 阅读8分钟

原文链接 ExpressibleByStringInterpolation

Swift的设计——首先也是最重要的——是一种**安全的**语言。检查数字和集合是否存在溢出,变量总是在首次使用前初始化,选项确保正确处理非值,并相应地命名任何潜在的不安全操作。

这些语言特性对消除一些最常见的编程错误大有帮助,但如果我们放松警惕,那将是失职。

今天,我想谈谈Swift 5中最令人兴奋的新特性之一:通过Express By String Interpolation协议对字符串文字中的值进行插值的彻底改革。很多人对你可以用它做的酷事情感到兴奋。(这是理所当然的!我们将在一会儿讨论所有这些)但我认为,从更广泛的角度看待这一功能以理解其影响的全部范围是很重要的。

格式化字符串很糟糕*。*

在不正确的NULL处理、缓冲区溢出和未初始化的变量之后,**printf/scanf**风格的格式字符串可以说是C风格编程语言中最有问题的保留。

在过去的20年中,安全专业人员记录了**数百个与格式字符串漏洞相关的漏洞。它是如此普遍,以至于它被赋予了自己的共同弱点**计数(CWE)类别。

它们不仅不安全,而且很难使用。是,很难用。

请考虑Date Forect上的date Format属性,它采用**strftime**格式字符串。如果我们想创建一个包含年份的日期的字符串表示形式,我们会使用“Y”,就像“Y”代表年份……对吗?

import Foundationlet formatter = DateFormatter()formatter.dateFormat = "M/d/Y"formatter.string(from: Date()) // "2/4/2019"

看起来确实是这样,至少在一年中的前360天是这样。但是当我们跳到一年的最后一天会发生什么呢?

let dateComponents = DateComponents(year: 2019,                                    month: 12,                                    day: 31)let date = Calendar.current.date(from: dateComponents)!formatter.string(from: date) // "12/31/2020" (😱)

嗯哼什么?原来“Y”是国际标准化组织星期编号**年的格式,2019年12月31日返回2020年,因为第二天是新年第一周的星期三。

我们实际上想要的是“y”。

formatter.dateFormat = "M/d/y"formatter.string(from: date) // 12/31/2019 (😄)

格式化字符串是最难使用的一种,因为它们很容易被错误使用。日期格式字符串是最糟糕的,因为在为时已晚之前,你可能不清楚自己做错了。它们是代码库中定时炸弹。

到目前为止的问题是,APIs不得不在危险但表现力强的领域特定语言(DSL)(如格式字符串)和正确但不太灵活的方法调用之间做出选择。

在Swift 5中新增的Express By String Interpolation协议允许这些类型的API既正确又具有表现力。通过这样做,它推翻了几十年来有问题的行为。

所以废话不多说,让我们看看什么是Express By String Interpolation以及它是如何工作的:

Express By String插值

符合Express By String Interpolation协议的类型可以自定义如何在字符串文字中插入值(即\(…)转义的值)。

您可以通过扩展默认的String插值类型(Default String Interpolation)或创建符合Express By String Interpolation的新类型来利用此新协议。

扩展默认字符串插值

默认情况下,在Swift 5之前,字符串文字中的所有内插值都直接发送到String初始化器。现在,使用Express By String Interpolation,您可以像调用方法一样指定其他参数(事实上,这就是您在幕后所做的)。

作为一个例子,让我们重温一下之前“Y”和“y”的混淆,看看如何通过快速按字符串插值来避免这种混淆。

通过扩展String的默认插值类型(恰如其分地命名为Default String Interpolation),我们可以定义一个名为appending Interpolation的新方法。第一个未命名参数的类型决定了哪些插值方法可用于要插值的值。在我们的示例中,我们将定义一个追加插值方法,该方法采用日期参数和日历类型的附加组件参数。我们将用来指定哪个组件的组件

import Foundationextension DefaultStringInterpolation {    mutating func appendInterpolation(_ value: Date,                                      component: Calendar.Component)    {        let dateComponents =            Calendar.current.dateComponents([component],                                            from: value)        self.appendInterpolation(            dateComponents.value(for: component)!        )    }}

现在我们可以插值每个单独组件的日期:

"\(date, component: .month)/\(date, component: .day)/\(date, component: .year)"// "12/31/2019" (😊)

是的,很冗长。但是你永远不会把. year For WeekOf year,相当于“Y”的日历组件,误认为你真正想要的是什么:. year。

但说真的,我们不应该像这样手工格式化日期。我们应该将这一责任委托给Date For ane:

您可以像任何其他Swift方法一样重载插值,并且具有多个同名但不同类型签名的插值。例如,我们可以为采用相应类型的格式化程序的日期和数字定义内插器。

import Foundationextension DefaultStringInterpolation {    mutating func appendInterpolation(_ value: Date,                                      formatter: DateFormatter)    {        self.appendInterpolation(            formatter.string(from: value)        )    }    mutating func appendInterpolation<T>(_ value: T,                                         formatter: NumberFormatter)        where T : Numeric    {        self.appendInterpolation(            formatter.string(from: value as! NSNumber)!        )    }}

这允许一个一致的接口到等效的功能,如格式化内插的日期和数字。

let dateFormatter = DateFormatter()dateFormatter.dateStyle = .fulldateFormatter.timeStyle = .none"Today is \(Date(), formatter: dateFormatter)"// "Today is Monday, February 4, 2019"let numberformatter = NumberFormatter()numberformatter.numberStyle = .spellOut"one plus one is \(1 + 1, formatter: numberformatter)"// "one plus one is two"

实现自定义字符串插值类型

除了扩展Default String Interpolation之外,还可以在符合Express By String Interpolation的自定义类型上定义自定义字符串插值行为。如果以下任何一条是正确的,你可能会这样做:

  • 你想区分文字段和插值段

  • 您希望限制可以插值的类型

  • 您希望支持不同于默认提供的插值行为

  • 您希望避免给内置字符串插值类型增加过多的API表面积

对于一个简单的例子,考虑一个转义XML中值的自定义类型,类似于**我们上周**描述的记录器之一。我们的目标是提供一个很好的模板API,它允许我们编写XML/超文本标记语,并以自动转义<和>等字符的方式插值值。

我们将从围绕单个字符串值的包装器开始。

struct XMLEscapedString: LosslessStringConvertible {  var value: String  init?(_ value: String) {    self.value = value  }  var description: String {    return self.value  }}

我们在扩展中向Express By String Interpolation添加一致性,就像任何其他协议一样。它继承自Express By String Literal,它需要一个init(string Literal:)初始化项。Express By String Interpolation本身需要一个init(string Interpolation:)初始化器,该初始化器接受所需的关联String Interpolation类型的实例。

此关联的String Interpolation类型负责从字符串文字中收集所有文字段和内插值。所有文字段都传递给append Literal(_:)方法。对于内插值,编译器将查找与指定参数匹配的append Interpolation方法。在这种情况下,文字值和内插值都被收集到可变字符串中。

import Foundationextension XMLEscapedString: ExpressibleByStringInterpolation {  init(stringLiteral value: String) {    self.init(value)!  }  init(stringInterpolation: StringInterpolation) {    self.init(stringInterpolation.value)!  }  struct StringInterpolation: StringInterpolationProtocol {    var value: String = ""    init(literalCapacity: Int, interpolationCount: Int) {        self.value.reserveCapacity(literalCapacity)    }    mutating func appendLiteral(_ literal: String) {        self.value.append(literal)    }    mutating func appendInterpolation<T>(_ value: T)        where T: CustomStringConvertible    {        let escaped = CFXMLCreateStringByEscapingEntities(            nil, value.description as NSString, nil        )! as NSString        self.value.append(escaped as String)    }  }}

有了所有这些,我们现在可以用一个自动转义插值值的字符串文字初始化XML Escape ed String。(我们没有XSS漏洞,谢谢!)

let name = "<bobby>"let markup: XMLEscapedString = """<p>Hello, \(name)!</p>"""print(markup)// <p>Hello, <bobby>!</p>

这个功能最好的部分之一是它的实现有多透明。对于感觉非常神奇的行为,你永远不必怀疑它是如何工作的。

将上面的字符串文字与下面的等效API调用进行比较:

var interpolation =    XMLEscapedString.StringInterpolation(literalCapacity: 15,                                         interpolationCount: 1)interpolation.appendLiteral("<p>Hello, ")interpolation.appendInterpolation(name)interpolation.appendLiteral("!</p>")let markup = XMLEscapedString(stringInterpolation: interpolation)// <p>Hello, <bobby>!</p>

读起来就像诗歌,不是吗?

看到Express By String Interpolation是如何工作的,很难不四处看看,找到无数可以使用它的机会:

  • 格式化字符串内插为日期和数字格式字符串提供了一个更安全、更容易理解的替代方案。

  • 转义无论是转义URL、XML文档、shell命令参数中的实体,还是SQL查询中的值,可扩展字符串插值都使正确的行为无缝且自动。

  • 装饰使用字符串插值创建类型安全的DSL,用于为应用程序和终端输出创建属性字符串,使用ANSI控制序列进行颜色和效果,或填充未装饰的文本以匹配所需的对齐。

  • 本地化字符串内插允许我们构建工具,利用编译器找到本地化字符串的所有实例,而不是依赖于扫描源代码以在“NS Localize String”上寻找匹配项的脚本。

如果考虑到所有这些因素,并考虑到未来可能对**编译时常量表达式**的支持,您会发现Swift 5可能刚刚偶然发现了处理格式的新最佳方法。