Swift 字面量

94 阅读9分钟

原文:Swift Literals

1911 年,语言学家 Franz Boas 观察到,讲爱斯基摩 - 阿留申语的人用不同的词来区分正在落下的雪花和地上的雪。相比之下,说英语的人通常把两者都称为 "snow",但对雨滴和水坑也有类似的区分。

随着时间的推移,这个简单的经验观察已经扭曲成一个可怕的陈词滥调:"爱斯基摩人 [原文如此] 对雪有 50 个不同的词"-- 这很不幸,因为博厄斯最初的观察是经验性的,而由此产生的语言相对性的薄弱主张是没有争议的:语言将语义概念分为不同的词,其方式可能(而且经常)是彼此不同的。这究竟是历史的偶然,还是反映了某种文化的更深层次的真相,还不清楚,有待进一步辩论。

正是在这种框架下,我们邀请你考虑 Swift 中不同种类的字词是如何塑造我们推理代码的方式的。

标准字面量

字面意义是源代码中一个值的表示,比如一个数字或一个字符串。

Swift 提供了以下种类的字面意义:

名称默认推断类型示例
IntegerInt123, 0b1010, 0o644, 0xFF、
Floating-PointDouble3.14, 6.02e23, 0xAp-2
StringString"Hello", """. . .""”
扩展字符群Character"A", "é", "🇺🇸”
Unicode.ScalarUnicode.Scalar"A", "´", "u{1F1FA}”
BooleanBooltrue, false
NilOptionalnil
ArrayArray[1, 2, 3]
DictionaryDictionary["a": 1, "b": 2]

关于 Swift 中的字面量,最重要的一点是,它们指定了一个值,但不是一个明确的类型。

当编译器遇到一个字面量时,它试图自动推断出类型。它通过寻找每个可能被这种字面符号初始化的类型,并根据任何其他约束条件缩小范围。

如果不能推断出类型,Swift 会初始化该类字面的默认类型 -- 整数字面为 Int,字符串字面为 String,以此类推。

57 // Integer literal
"Hello" // String literal

对于 "nil" 字面量,其类型永远不会被自动推断,因此必须声明:

nil // ! cannot infer type
nil as String? // Optional<String>.none

对于数组和字典的字面量,集合的相关类型是根据其内容推断出来的。然而,推断大型或嵌套集合的类型是一个复杂的操作,可能会大大增加编译代码的时间。你可以通过在你的声明中显式添加类型来保持事情的快速性

// Explicit type in the declaration
// prevents expensive type inference during compilation
let dictionary: [String: [Int]] = [
    "a": [1, 2],
    "b": [3, 4],
    "c": [5, 6],
    
]

Playground 字面量

除了上面列出的标准字面量,还有一些额外的字词类型用于 Playgrounds 中的代码:

名称默认推断类型示例
ColorNSColor/UIColor#colorLiteral(red: 1, green: 0, blue: 1, alpha: 1)
ImageNSImage/UIImage#imageLiteral(resourceName: "icon")
FileURL#fileLiteral(resourceName: "articles.json")

在 Xcode 或 iPad 上的 Swift Playgrounds 中,这些带井字形前缀的字面量表达式会自动被一个交互式控件取代,该控件提供了所引用颜色、图像或文件的可视化表示。

// Code
#colorLiteral(red: 0.7477839589, green: 0.5598286986, blue: 0.4095913172, alpha: 1)

// Rendering
🏽

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c517aa711df546f290fc08b03c5b6a7b~tplv-k3u1fbpfcp-zoom-1.image

这个控件也使新值的选择变得很容易:不是输入 RGBA 值或文件路径,而是呈现给你一个颜色选择器或文件选择器。

大多数编程语言都有布尔值、数字和字符串的字面量,许多语言也有数组、字典和正则表达式的字面量。

字面量在开发者的编程心理模型中是如此根深蒂固,以至于我们大多数人都不会主动考虑编译器实际上在做什么。

有了这些基本构件的速记方法,代码就更容易阅读和编写了。

字面量如何工作

字面符号就像单词一样:它们的含义可以根据周围的环境而改变。

["h", "e", "l", "l", "o"] // Array<String>
["h" as Character, "e", "l", "l", "o"] // Array<Character>
["h", "e", "l", "l", "o"] as Set<Character>

在上面的例子中,我们看到一个包含字符串字面量的数组默认被初始化为一个字符串数组。然而,如果我们明确地将第一个数组元素转换为字符,那么该字面就被初始化为一个字符数组。另外,我们也可以把整个表达式铸成 Set<Character> 来初始化一个字符集。

这是怎么做到的?

在 Swift 中,编译器通过查看所有实现相应字面量表达协议的可见类型,来决定如何初始化字面量。

字面量协议
IntegerExpressibleByIntegerLiteral
Floating-PointExpressibleByFloatLiteral
StringExpressibleByStringLiteral
Extended Grapheme ClusterExpressibleByExtendedGrapheme
ClusterLiteral
Unicode ScalarExpressibleByUnicodeScalar
Literal
BooleanExpressibleByBooleanLiteral
NilExpressibleByNilLiteral
ArrayExpressibleByArrayLiteral
DictionaryExpressibleByDictionaryLiteral

为了符合一个协议,一个类型必须实现它所要求的初始化方法。例如,ExpressibleByIntegerLiteral 协议要求实现 init(integerLiteral:) 方法。

这种方法真正伟大的地方在于,它可以让你为你自己的自定义类型添加字面量初始化方法。

支持自定义类型的子面初始化

在适当的时候支持字面量式的初始化方法可以大大改善自定义类型的人机工程学,使它们感觉像是内置的。

例如,如果你想支持模糊逻辑,除了标准的布尔运算之外,你可以实现一个像下面这样的模糊类型:

struct Fuzzy: Equatable {
    var value: Double

    init(_ value: Double) {
        precondition(value >= 0.0 && value <= 1.0)
        self.value = value
    }
}

一个模糊值表示一个真值,其范围在 0 到 1(包括)的数字范围内的完全真实和完全虚假之间。也就是说,1 的值意味着完全真实,0.8 意味着大部分真实,0.1 意味着大部分虚假。

为了更方便地与标准布尔逻辑一起工作,我们可以扩展 Fuzzy 以采用 ExpressibleByBooleanLiteral 协议。

extension Fuzzy: ExpressibleByBooleanLiteral {
    init(booleanLiteral value: Bool) {
        self.init(value ? 1.0 : 0.0)
    }
}

💡 实际上,在很多情况下,一个类型使用布尔字面量进行初始化是不合适的。对字符串、整数和浮点字面的支持要普遍得多。

这样做并不改变真或假的默认含义。我们不必担心现有的代码会因为我们在代码库中引入了半真半假的概念而被破坏("视图确实出现了动画...... 也许?")。truefalse 初始化模糊值的唯一情况是当编译器可以推断出类型是模糊的:

true is Bool // true
true is Fuzzy // false

(true as Fuzzy) is Fuzzy // true
(false as Fuzzy).value // 0.0

因为 Fuzzy 是用一个 Double 值来初始化的,所以允许用浮点字元来初始化数值也是合理的。很难想象有什么情况下一个类型会支持浮点字元而不支持整数字元,所以我们也应该这样做(不过,反过来也不对;有很多类型可以使用整数而不使用浮点数)。

extension Fuzzy: ExpressibleByIntegerLiteral {
    init(integerLiteral value: Int) {
        self.init(Double(value))
    }
}

extension Fuzzy: ExpressibleByFloatLiteral {
    init(floatLiteral value: Double) {
        self.init(value)
    }
}

有了这些协议,Fuzzy 类型现在看起来就像 Swift 标准库中的一个真正的成员:

let completelyTrue: Fuzzy = true
let mostlyTrue: Fuzzy = 0.8
let mostlyFalse: Fuzzy = 0.1

(现在唯一要做的就是实现标准的逻辑运算符!)

如果你想优化便利性和开发人员的生产力,你应该考虑实现适合你的自定义类型的任何字面协议。

未来的发展

子面量是语言未来的一个活跃的讨论话题。展望 Swift 5,目前有一些提议可能会对我们编写代码的方式产生巨大的影响。

Raw String Literals

在写这篇文章的时候,Swift Evolution 提案 0200 正在积极审查中。如果它被接受,未来版本的 Swift 将支持 "原始" 字符串,或忽略转义序列的字符串字面量。

来自该提案:

我们的设计增加了可定制的字符串定界符。你可以用一个或多个 #(磅,数字符号,U+0023)字符来填充字符串字面量 [......] 字符串开头的磅符号数量(在这些例子中,0、1 和 4)必须与字符串结尾的#符号数量一致。

"This is a Swift string literal"

#"This is also a Swift string literal"#

####"So is this"####

这一提议是 Swift 4(SE-0165)中新增的多行字符串字面量的自然延伸,它将使 JSON 和 XML 等数据格式的工作更加容易。

如果不出意外的话,采用这一建议可以消除在 Windows 上使用 Swift 的最大障碍:处理 C:\Windows\All Users\Application Data 这样的文件路径。

通过强制执行的字面初始化

另一个最近的提案,SE-0213: 通过强制执行的字面初始化已经在 Swift 5 中实现。

从该提案来看:

T (literal) 应该尽可能使用适当的字面量协议来构造 T。

目前,符合字面协议的类型使用常规初始化器规则进行类型检查,这意味着对于 UInt32 (42) 这样的表达式,类型检查器会查找一组可用的初始化器选择,并逐一尝试,以推导出最佳解决方案。

在 Swift 4.2 中,用最大值初始化 UInt64 会导致编译时溢出,因为编译器会首先尝试用字面值初始化 Int

UInt64(0xffff_ffff_ffff_ffff) // overflows in Swift 4.2

从 Swift 5 开始,这种表达方式不仅可以成功编译,而且编译速度也会更快一些。

语言使用者可用的词语不仅影响他们所说的内容,也影响他们的思维方式。同样地,编程语言的各个部分对开发者的工作方式也有相当大的影响。

Swift 分割数值语义空间的方式使其不同于那些不区分整数和浮点或对字符串、字符和 Unicode 标量有单独概念的语言。因此,当我们写 Swift 代码时,我们经常在一个较低的层次上考虑数字和字符串,而不是用 JavaScript 等语言进行砍伐,这并不是巧合。

按照同样的思路,Swift 目前在字符串字面意义和正则表达式之间缺乏区分,这也导致了与其他脚本语言相比,regex 的使用相对缺乏。

这并不是说有或没有某些词就无法表达某些想法 -- 只是有点模糊。我们可以理解 "untranslatable" 的词,如葡萄牙语中的 "Saudade",韩语中的 "Han",或德语中的 "Weltschmerz"。

我们都是人。我们都能理解痛苦。

通过允许任何类型支持字面量初始化,Swift 邀请我们成为更多对话的一部分。利用这一点,让你自己的代码感觉像是标准库的自然延伸。