Swift 元组高级用法和最佳实践
猛戳查看最终版@SwiftGG 作为 Swift 中比较少见的语法特性,元组只是占据了结构体和数组之间很小的一个位置。此外,它在 Objective-C(或者很多其他语言)中没有相应的结构。最后,标准库以及 Apple 示例代码中对元组的使用也非常少。可能它在 Swift 中给人的印象就是用来做模式匹配,但我并不这么认为。 和元组相关的大部分教程都只关注三种使用场景(模式匹配、返回值和解构),且浅尝辄止。本文会详细介绍元组,并讲解元组使用的最佳实践,告诉你何时该用元组,何时不该用元组。同时我也会列出那些你不能用元组做的事情,免得你老是去 StackOverflow 提问。好了,进入正题。 绝对基础因为这部分内容你可能已经知道得七七八八了,所以我就简单介绍下。 元组允许你把不同类型的数据结合到一起。它是可变的,尽管看起来像序列,但是它不是,因为不能直接遍历所有内容。我们首先通过一个简单的入门示例来学习如何创建和使用元组。 创建和访问元组// 创建一个简单的元组
let tp1 = (2,3)
let tp2 = (2,3,4)
//创建一个命名元组
let tp3 = (x: 5,y: 3)
// 不同的类型
let tp4 = (name: "Carl",age: 78,pets: ["Bonny","Houdon","Miki"])
// 访问元组元素
let tp5 = (13,21)
tp5.0 // 13
tp5.1 // 21
let tp6 = (x: 21,y: 33)
tp6.x // 21
tp6.y // 33
使用元组做模式匹配就像之前所说,这大概是元组最常见的使用场景。Swift 的 // 特意造出来的例子
// 这些是多个方法的返回值
let age = 23
let job: String? = "Operator"
let payload: AnyObject = NSDictionary()
在上面的代码中,我们想要找一个 30 岁以下的工作者和一个字典 switch (age,job,payload) {
case (let age,_?,_ as NSDictionary) where age < 30:
print(age)
default: ()
}
把 把元组做为返回类型这可能是元组第二多的应用场景。因为元组可以即时构建,它成了在方法中返回多个值的一种简单有效的方式。 func abc() -> (Int,Int,String) {
return (3,5,"Carl")
}
元组解构Swift 从不同的编程语言汲取了很多灵感,这也是 Python 做了很多年的事情。之前的例子大多只展示了如何把东西塞到元组中,解构则是一种迅速把东西从元组中取出的方式,结合上面的 let (a,b,c) = abc() print(a)
另外一个例子是把多个方法调用写在一行代码中: let (a,c) = (a(),b(),c())
或者,简单的交换两个值: var a = 5
var b = 4
(b,a) = (a,b)
进阶元组做为匿名结构体元组和结构体一样允许你把不同的类型结合到一个类型中: struct User {
let name: String
let age: Int
}
// vs.
let user = (name: "Carl",age: 40)
正如你所见,这两个类型很像,只是结构体通过结构体描述声明,声明之后就可以用这个结构体来定义实例,而元组仅仅是一个实例。如果需要在一个方法或者函数中定义临时结构体,就可以利用这种相似性。就像 Swift 文档中所说:
下面来看一个例子:需要收集多个方法的返回值,去重并插入到数据集中: func zipForUser(userid: String) -> String { return "12124" }
func streetForUser(userid: String) -> String { return "Charles Street" }
// 从数据集中找出所有不重复的街道
var streets: [String: (zip: String,street: String,count: Int)] = [:]
for userid in users {
let zip = zipForUser(userid)
let street = streetForUser(userid)
let key = "(zip)-(street)"
if let (_,_,count) = streets[key] {
streets[key] = (zip,street,count + 1)
} else {
streets[key] = (zip,1)
}
}
drawStreetsOnMap(streets.values)
这里,我们在短暂的临时场景中使用结构简单的元组。当然也可以定义结构体,但是这并不是必须的。 再看另外一个例子:在处理算法数据的类中,你需要把某个方法返回的临时结果传入到另外一个方法中。定义一个只有两三个方法会用的结构体显然是不必要的。 // 编造算法
func calculateInterim(values: [Int]) -> (r: Int,alpha: CGFloat,chi: (CGFloat,CGFLoat)) {
...
}
func expandInterim(interim: (r: Int,CGFLoat))) -> CGFloat {
...
}
显然,这行代码非常优雅。单独为一个实例定义结构体有时候过于复杂,而定义同一个元组 4 次却不使用结构体也同样不可取。所以选择哪种方式取决于各种各样的因素。 私有状态除了之前的例子,元组还有一种非常实用的场景:在临时范围以外使用。Rich Hickey 说过:“如果树林中有一棵树倒了,会发出声音么?“因为作用域是私有的,元组只在当前的实现方法中有效。使用元组可以很好的存储内部状态。 来看一个简单的例子:保存一个静态的 let tableViewValues = [(title: "Age",value: "user.age",editable: true),(title: "Name",value: "user.name.combinedName",(title: "Username",value: "user.name.username",editable: false),(title: "ProfilePicture",value: "user.pictures.thumbnail",editable: false)]
另一种选择就是定义结构体,但是如果数据的实现细节是纯私有的,用元组就够了。 更酷的一个例子是:你定义了一个对象,并且想给这个对象添加多个变化监听器,每个监听器都包含它的名字以及发生变化时被调用的闭包: func addListener(name: String,action: (change: AnyObject?) -> ()) func removeListener(name: String)
你会如何在对象中保存这些监听器呢?显而易见的解决方案是定义一个结构体,但是这些监听器只能在三种情况下用,也就是说它们使用范围极其有限,而结构体只能定义为 var listeners: [(String,(AnyObject?) -> ())] func addListener(name: String,action: (change: AnyObject?) -> ()) { self.listeners.append((name,action)) } func removeListener(name: String) { if let idx = listeners.indexOf({ e in return e.0 == name }) { listeners.removeAtIndex(idx) } } func execute(change: Int) { for (_,listener) in listeners { listener(change) } }
就像你在 把元组作为固定大小的序列元组的另外一个应用领域是:固定一个类型所包含元素的个数。假设需要用一个对象来计算一年中所有月份的各种统计值,你需要分开给每个月份存储一个确定的 var monthValues: [Int]
然而,这样的话我们就不能确定这个属性刚好包含 12 个元素。使用这个对象的用户可能不小心插入了 13 个值,或者 11 个。我们没法告诉类型检查器这个对象是固定 12 个元素的数组(有意思的是,这是 C 都支持的事情)。但是如果使用元组,可以很简单地实现这种特殊的约束: var monthValues: (Int,Int,Int)
还有一种选择就是在对象的功能中加入约束逻辑(即通过新的 元组当做复杂的可变参数类型可变参数(比如可变函数参数)是在函数参数的个数不定的情况下非常有用的一种技术。 // 传统例子
func sumOf(numbers: Int...) -> Int {
// 使用 + 操作符把所有数字加起来
return numbers.reduce(0,combine: +)
}
sumOf(1,2,7,9) // 24
如果你的需求不单单是 func batchUpdate(updates: (String,Int)...) -> Bool {
self.db.begin()
for (key,value) in updates {
self.db.set(key,value)
}
self.db.end()
}
// 我们假想数据库是很复杂的
batchUpdate(("tk1",5),("tk7",9),("tk21",44),("tk88",12))
高级用法元组迭代在之前的内容中,我试图避免把元组叫做序列或者集合,因为它确实不是。因为元组中每个元素都可以是不同的类型,所以无法使用类型安全的方式对元组的内容进行遍历或者映射。或者说至少没有优雅的方式。 Swift 提供了有限的反射能力,这就允许我们检查元组的内容然后对它进行遍历。不好的地方就是类型检查器不知道如何确定遍历元素的类型,所以所有内容的类型都是 let t = (a: 5,b: "String",c: NSDate())
let mirror = Mirror(reflecting: t)
for (label,value) in mirror.children {
switch value {
case is Int:
print("int")
case is String:
print("string")
case is NSDate:
print("nsdate")
default: ()
}
}
这当然没有数组迭代那么简单,但是如果确实需要,可以使用这段代码。 元组和泛型Swift 中并没有 所以,与其定义一个支持泛型的元组,还不如根据自己需求定义一个包含具体数据类型的元组。 func wantsTuple<T1,T2>(tuple: (T1,T2)) -> T1 {
return tuple.0
}
wantsTuple(("a","b")) // "a"
wantsTuple((1,2)) // 1
你也可以通过 class BaseClass<A,B> {
typealias Element = (A,B)
func addElement(elm: Element) {
print(elm)
}
}
class IntegerClass<B> : BaseClass<Int,B> {
}
let example = IntegerClass<String>()
example.addElement((5,""))
// Prints (5,"")
定义具体的元组类型在之前好几个例子中,我们多次重复一些已经确定的类型,比如 typealias Example = (Int,String)
func add(elm: Example) {
}
但是,如果需要如此频繁的使用一个确定的元组结构,以至于你想给它增加一个 用元组做函数参数就像 Paul Robinson 的文章 中说到的一样, // 从 Paul Robinson 的博客拷贝来的,你也应该去读读这篇文章:
// http://www.paulrobinson.net/function-parameters-are-tuples-in-swift/
func foo(a: Int,_ b: Int,_ name: String) -> Int
return a
}
let arguments = (4,"hello")
foo(arguments) // 返回 4
这看起来很酷是不是?但是等等…这里的函数签名有点特殊。当我们像元组一样增加或者移除标签的时候会发生什么呢?哦了,我们现在开始实验: // 让我们试一下带标签的:
func foo2(a a: Int,name: String) -> Int {
return a
}
let arguments = (4,"hello")
foo2(arguments) // 不能用
let arguments2 = (a: 4,b: 3,name: "hello")
foo2(arguments2) // 可以用 (4)
所以如果函数签名带标签的话就可以支持带标签的元组。 但我们是否需要明确的把元组写入到变量中呢? foo2((a: 4,name: "hello")) // 出错
好吧,比较倒霉,上面的代码是不行的,但是如果是通过调用函数返回的元组呢? func foo(a: Int,_ name: String) -> Int
return a
}
func get_tuple() -> (Int,String) {
return (4,4,"hello")
}
foo(get_tuple()) // 可以用! 返回 4!
太棒了!这种方式可以! 这种方式包含了很多有趣的含义和可能性。如果对类型进行很好的规划,你甚至可以不需要对数据进行解构,然后直接把它们当作参数在函数间传递。 更妙的是,对于函数式编程,你可以直接返回一个含多个参数的元组到一个函数中,而不需要对它进行解构。 元组做不到啊~最后,我们把一些元组不能实现事情以列表的方式呈现给大家。 用元组做字典的
|