Swift 5—表达通过字符串插值

Swift的设计 - 首先是 - 是一种安全的语言。检查数字和集合是否有溢出,变量总是在第一次使用之前初始化,选项确保正确处理非值,并且相应地命名任何可能不安全的操作。

这些语言功能在很大程度上消除了一些最常见的编程错误,但我们不得不让我们guard失望。

今天,我想谈谈Swift 5中最激动人心的新功能之一:对字符串文字中的值如何通过协议进行插值的大修。很多人都对你可以用它做的很酷的事感到兴奋。(理所当然!我们将在短时间内完成所有这些工作)但我认为重要的是要更广泛地了解这一功能,以了解其影响的全部范围。ExpressibleByStringInterpolation

小编这里有大量的书籍和面试资料哦(点击下载


格式字符串很糟糕

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

在过去的20年中,安全专业人员已经记录了 数百个与格式字符串漏洞相关的漏洞。它是如此普遍,它被赋予了自己的 Common Weakness Enumeration常见的弱点列举类别。

他们不仅不安全,而且难以使用。是的,很难用。

考虑属性on ,它采用 格式字符串。如果我们想要创建一个包含其年份的日期的字符串表示,我们将使用,就像年份一样...... 对吗 dateFormat DateFormatter strftime "Y" "Y"

import Foundation

let 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"是ISO周编号年的格式 ,它将在2019年12月31日返回2020,因为第二天是新年第一周的星期三。

我们真正想要的是"y"

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

格式化字符串是最难以使用的类型,因为它们很容易被错误地使用。日期格式字符串是最糟糕的,因为可能不清楚你做错了,直到为时已晚。它们是你的代码库中的字面时间炸弹。

现在花点时间(如果您还没有),"Y"在实际意图使用时,审核您的代码库以使用日期格式字符串"y"


到目前为止,问题一直是API必须在危险但富有表现力的特定于域的语言特定领域的语言之间进行选择,例如格式字符串,以及正确但灵活性较低的方法调用。

Swift 5中的新功能,该协议允许这些类型的API既正确又富有表现力。在这样做的过程中,它推翻了几十年来有问题的行为。ExpressibleByStringInterpolation

所以不用多说,让我们来看看它是什么以及它是如何工作的:ExpressibleByStringInterpolation


ExpressibleByStringInterpolation

符合协议的类型可以自定义字符串文字中的内插值(即,转义的值)。
ExpressibleByStringInterpolation \(...)

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

有关更多信息,请参阅Swift Evolution提议 SE-0228:“Fix ExpressibleByStringInterpolation”

扩展默认字符串插值

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

作为一个例子,让我们回顾以前的mixup "Y""y" ,看看这种混乱可能与避免。ExpressibleByStringInterpolation

通过扩展String默认插值类型(aptly-named ),我们可以定义一个名为的新方法。第一个未命名参数的类型确定哪些插值方法可用于要插值的值。在我们的例子中,我们将定义一个方法,该方法接受一个参数和一个 我们将用来指定哪个类型的附加参数DefaultStringInterpolation appendingInterpolation appendInterpolation Date component Calendar.Component

import Foundation

#if swift(<5)
#error("Download Xcode 10.2 Beta 2 to see this in action")
#endif

extension 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" (😊)

这很冗长,是的。但是你永远不会误认为日历组件等同于你真正想要的东西:。.yearForWeekOfYear "Y" .year

但实际上,我们不应该像这样手工格式化日期。我们应该将责任委托给:DateFormatter

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

import Foundation

extension 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 = .full
dateFormatter.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"

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

除了扩展,您还可以在符合的自定义类型上定义自定义字符串插值行为。如果满足以下任何条件,您可以这样做:
DefaultStringInterpolation ExpressibleByStringInterpolation

  • 您希望区分文字和插值段
  • 您想限制可以插入的类型
  • 您希望支持与默认情况下提供的不同的插值行为
  • 您希望避免使用过多的API表面区域来增加内置字符串插值类型的负担

对于一个简单的例子,考虑一个转义XML中的值的自定义类型,类似于我们上周描述的一个记录器。我们的目标:提供了一个很好的模板的API,允许我们编写XML / HTML和在自动转义字符一样的方式插入值<>

我们将简单地用一个包含单个String值的包装器开始。

struct XMLEscapedString: LosslessStringConvertible {
  var value: String

  init?(_ value: String) {
    self.value = value
  }

  var description: String {
    return self.value
  }
}

我们在扩展中添加一致性,就像任何其他协议一样。它继承自,需要初始化程序。 本身需要一个初始化程序,它接受所需的关联类型的实例。ExpressibleByStringInterpolation ExpressibleByStringLiteral init(stringLiteral:) ExpressibleByStringInterpolation init(stringInterpolation:) StringInterpolation

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

这需要一个初始化器; 作为可选的优化,容量和插值计数可用于,例如,分配足够的空间来保存结果字符串。
StringInterpolationProtocol init(literalCapacity:interpolationCount:)

import Foundation

extension 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)
    }
  }
}

完成所有这些后,我们现在可以使用自动转义插值的字符串文字进行初始化。(没有XSS漏洞利用给我们,谢谢!XMLEscapedString

let name = "<bobby>"
let markup: XMLEscapedString = """
<p>Hello, \(name)!</p>
"""
print(markup)
// <p>Hello, &lt;bobby&gt;!</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, &lt;bobby&gt;!</p>

阅读就像诗歌一样,不是吗?

有关更高级的示例,请查看Swift Strings Flight School指南示例代码中包含 的 Unicode样式操场


看看它是如何运作的,很难不去环顾四周,找到无数机会可以使用它:ExpressibleByStringInterpolation

  • 格式化 字符串插值为日期和数字格式字符串提供了更安全,更易于理解的替代方法。
  • 转义 无论是URL中的转义实体,XML文档,shell命令参数还是SQL查询中的值,可扩展字符串插值都可以无缝且自动地进行正确的行为。
  • 装饰 使用字符串插值创建类型安全的DSL,用于为应用程序和终端输出创建属性字符串,使用ANSI控制序列来显示颜色和效果,或填充未加修饰的文本以匹配所需的对齐方式。
  • 本地化 而不是依赖于扫描源代码以查找“NSLocalizedString”匹配的脚本,字符串插值允许我们构建利用编译器查找本地化字符串的所有实例的工具。

如果您考虑所有这些因素并考虑将来可能支持 编译时常量表达式,那么您发现Swift 5可能只是偶然发现了处理格式化的新方法。


点击此处进交流群 有技术的来闲聊 没技术的来学习

原文转载地址:https://nshipster.com/expressiblebystringinterpolation/

推荐阅读更多精彩内容