多类型和容器
Swift 中常用的原生容器类型有三种,它们分别是 Array
、Dictionary
和 Set
。它们都是泛型的,也就是说我们在一个集合中只能放同一个类型的元素。
let numbers = [1,2,3,4,5] // numbers 的类型是 [Int]
let strings = ["hello", "world"] // numbers 的类型是 [String]
如果我们要把不相关的类型放到同一个容器类型中的话,需要做一些转换的工作:
import UIKit
let mixed: [Any] = [1, "two", 3] // Any 类型可以隐式转换
let objectArray = [1 as NSObject, "two" as NSObject, 3 as NSObject] // 转换为 [NSObject]
这样的转换会造成部分信息的损失,我们从容器中取值时只能得到信息完全丢失后的结果,在使用时还需要进行一次类型转换。这其实是在无其他可选方案后的最差选择: 因为使用这样的转换的话,编译器就不能再给我们提供警告信息了。我们可以随意地将任意对象添加进容器,也可以将容器中取出的值转换为任意类型,这是一件十分危险的事情。
我们注意到,Any
其实不是具体的某个类型。因此就是说其实在容器类型泛型的帮助下,我们不仅可以在容器中添加同一具体类型的对象,也可以添加实现了同一协议的类型的对象。绝大多数情况下,我们想要放入一个容器中的元素或多或少会有某些共同点,这就使得用协议来规定容器类型会很有用。
import Foundation
let mixed1:[CustomStringConvertible] = [1,"two", 3]
for obj in mixed1 {
print(obj.description)
}
这种方法虽然也损失了一部分类型信息,但是相对于 Any
或者 AnyObject
还是改善很多,在对于对象中存在某种共同特性的情况下无疑是最方便的。另一种做法是使用 enum
可以带有值的特点,将类型信息封装到特定的 enum
中。下面的代码封装了Int
或者 String
类型:
//使用枚举封装数据
enum IntOrString {
case intValue(Int)
case stringValue(String)
}
// mixed: [IntOrString]
let mixed = [IntOrString.intValue(1),
IntOrString.stringValue("two"),
IntOrString.intValue(3)]
for value in mixed {
switch value {
case let .intValue(i):
print(i*2)
case let .stringValue(s):
print(s.capitalized)//首字母大写
}
}
通过这种方法,我们完整地在编译时保留了不同类型的信息。为了方便,我们甚至可以进一步为 IntOrString
使用字面量转换的方法编写简单的获取方式。
字面量表达
所谓字面量,就是指像特定的数字,字符串或者是布尔值这样,能够直截了当地指出自己的类型并为变量进行赋值的值。比如在下面:
let aNumber = 3
let aString = "Hello"
let aBool = true
中的 3
,Hello
以及 true
就称为字面量。在Swift中,Array
和 Dictionary
在使用简单的描述赋值的时候,使用的也是字面量,比如:
let anArray = [1, 2 ,3]
let aDictionary = ["key1": "value1", "key2": "value2"]
Swift为我们提供了一组非常有意思的协议,使用字面量来表达特定的类型。对于那些实现了字面量表达协议的类型,在提供字面量赋值的时候,就可以简单地按照协议方法中定义的规则“无缝对应”地通过赋值的方式将值表达为对应类型。这些协议包括了各个原生的字面量,在实际开发中我们经常可能用到的有:
ExpressibleByNilLiteral
ExpressibleByIntegerLiteral
ExpressibleByFloatLiteral
ExpressibleByBooleanLiteral
ExpressibleByStringLiteral
ExpressibleByArrayLiteral
ExpressibleByDictionaryLiteral
所有的字面量都定义了一个 associatetype
和对应的 init
方法。拿 ExpressibleByBooleanLiteral
举个例子:
public protocol ExpressibleByBooleanLiteral {
associatedtype BooleanLiteralType
//Creates an instance initialized to the given Boolean value.
public init(booleanLiteral value: Self.BooleanLiteralType)
}
于是在我们需要自己实现一个字面量表达的时候,可以简单地只实现定义 init
的方法就行了。举个不太有实际意义的例子,比如我们想实现一个自己的 Bool
类型,可以这么做:
enum MyBool: Int {
case myTrue, myFalse
}
extension MyBool : ExpressibleByBooleanLiteral {
init(booleanLiteral value: Bool) {
self = value ? .myTrue : .myFalse
}
}
这样我们就能很容易地直接使用 Bool
的 true
和 false
来对类型进行赋值了:
let myTrue: MyBool = true
let myFalse: MyBool = false
print(myTrue.rawValue) //0
print(myFalse.rawValue) //1
BooleanLiteralType
大概是最简单的形式,如果我们深入一点,就会发现像是 ExpressibleByStringLiteral
这样的协议要复杂一些。这个协议不仅类似于上面布尔的情况,定义了 StringLiteralType
及接受其的初始化方法,这个协议本身还要求实现下面两个协议:
ExpressibleByExtendedGraphemeClusterLiteral
ExpressibleByUnicodeScalarLiteral
这两个协议我们在日常项目中基本上不会使用,它们对应字符簇和字符的字面量表达。虽然复杂一些,但是形式上还是一致的,只不过在实现时我们需要将这三个init
方法都进行实现。
还是以例子来说明,比如我们有个类 Person
,里面有这个人的名字:
class Person {
let name: String
init (name value: String) {
self.name = value
}
}
如果想要通过 String
赋值来生成 Person
对象的话,可以改写这个类:
//通过名字(字符串)创建Person对象
class Person: ExpressibleByStringLiteral {
let name: String
init (name value: String) {
self.name = value
}
required init(stringLiteral value: String) {
self.name = value
}
required init(extendedGraphemeClusterLiteral value: String) {
self.name = value
}
required init(unicodeScalarLiteral value: String) {
self.name = value
}
}
在所有的协议定义的 init
前面我们都加上了 required
关键字,这是由初始化方法的完备性需求所决定的,这个类的子类都需要保证能够做类似的字面量表达,以确保类型安全。
在上面的例子里有很多重复的对 self.name
赋值的代码,这是我们所不乐见的。一个改善的方式是在这些初始化方法中去调用原来的 init(name value: String)
,这种情况下我们需要在这些初始化方法前加上 convenience
:
//通过名字(字符串)创建Person对象
class Person: ExpressibleByStringLiteral {
let name: String
init (name value: String) {
self.name = value
}
required convenience init(stringLiteral value: String) {
self.init(name: value)
}
required convenience init(extendedGraphemeClusterLiteral value: String) {
self.init(name: value)
}
required convenience init(unicodeScalarLiteral value: String) {
self.init(name: value)
}
}
let xiaoming:Person = "xiaoming"
print(xiaoming.name) //xiaoming
上面的 Person
的例子中,我们没有像 MyBool
中做的那样,使用一个 extension
的方式来扩展类使其可以用字面量赋值,这是因为在 extension
中,我们是不能定义 required
的初始化方法的。 也就是说,我们无法为现有的非 final
的 class
添加字面量表达(不过也许这在今后的Swift版 本中能有所改善)。