本文由CocoaChina译者ztdj121(微博)翻译
作者: Ole Begemann
原文: More Pattern Matching Examples
译文地址
这个系列的其他文章:
1、自定义模式匹配
2、范围和间隔(Ranges and Intervals)
3、多模式匹配的例子(这篇)
Download this article as a playground for Xcode 7 .
在模式匹配这个小系列的最后部分,我将给你们展示更多的例子,来说明用这个我们可以做些什么。
在 第一部分里 ,我们用一个变量重载了模式匹配的运载符 ~= , 这个变量用T->Bool类型函数作为它的第一个参数:
func ~=(pattern: T -> Bool, value: T) -> Bool { return pattern(value) }
这个实现非常通用。我们可以用它来使任何类型T的值和任何接受T并返回Bool的方法相匹配(就像第一部分里的isEven例子一样)。
当我们尝试用 String.hasPrefix 方法的模式匹配形式时,我们会遇到一个问题,就是参数的顺序。记住实例方法是 把实例作为第一个参数的柯里化方法(curried function) 。所以String.prefix方法的类型是String -> String -> Bool,String的第一个参数是方法接收器,第二个参数是我们想匹配的前缀:
// This: "Hello World".hasPrefix("H") // true // is equivalent to this: String.hasPrefix("Hello World")("H") // true
参数顺序正是后面我们使用有模式匹配操作符的部分应用版本的方法中所需要的。我们需要一个版本,让接收方法的变量在最后(除非你想使一个前缀匹配多个字符串,这种情况下,顺序是正确地,但是方法名会混淆)。第一部分里,我们写了一个翻转参数的小的辅助方法:
func hasPrefix(prefix: String)(_ value: String) -> Bool { return value.hasPrefix(prefix) } // Now we can call it with the arguments flipped: hasPrefix("H")("Hello World") // true
这样,我们可以部分应用前缀并返回一个方法,我们可以在~=中把这个方法当作pattern参数。
有作用了,但是我们把想使用的每个方法都这样做得话,很快就会变得冗长乏味。所以让我们来写一个通用的方法,翻转,把柯里化方法(curried function)的第一个参数移到后面,最后返回值的前面。
/// Moves the first argument to the back func flip(method: A -> B -> C) -> (B -> A -> C) { return { (b: B) in { (a: A) in method(a)(b) } } }
我喜欢这个方法的类型签名,因为这个类型非常清晰地显示它所做的:只有一个类型方法(A -> B -> C) -> (B -> A -> C)可能实现的函数类型。尽管两个嵌套的闭合表达式的函数体可能解析起来有点困难。写这个函数的另一个方法是利用Swift里柯里化函数(curried function)的特殊语法功能。这个变量使函数体非常容易写,但在我看来类型签名有点难以理解:
func flip(method: A -> B -> C)(_ b: B)(_ a: A) -> C { return method(a)(b) }
现在我们可以像这样使字符串和不同的前缀相匹配:
let str = "ABCDEF" switch str { case flip(String.hasPrefix)("A"): print("A") case flip(String.hasPrefix)("B"): print("B") default: "default" }
由于翻转(flip)是通用的,它适用于所有类型的所有方法--即使是那些有不同数量的参数。最后一点可能会令人惊讶,但是一个(非柯里化)需要多个参数的方法实际只需要一个参数:一个有相应数量的元素的元组有一样的类型作为参数。这并非偶然,元组的语法(a,b)和方法参数的语法是一样的,f(a,b)--他们是一样的东西。这就意味着任何匹配泛型类型A->B->C是我们翻转方法预料的,不管参数元组(泛型类型B代表)包含多少元素。
例子
翻转(flip)甚至适用于没有参数的方法(除了隐式接收器参数),因为空元组()也是一个有效的类型。不过,我们不能直接使用翻转(flip)属性,因为Swift目前不允许指定一个类型的属性作为一个方法。我们需要写一个独立的封装器方法,就像例子中collection的 isEmpty 属性:
extension CollectionType { func isEmptyFunc() -> Bool { return isEmpty } }
这有一个我们对数字数组进行模式匹配的完整的示例。除了 contains 方法,这个方法包含在标准库里,我们使用一个带两个参数的自定义的contains的重载方法,检查队列中是否包含两者。
extension SequenceType where Generator.Element : Equatable { func contains(a: Self.Generator.Element, and b: Self.Generator.Element) -> Bool { return contains(a) && contains(b) } } let numbers = [1,2,3,4,5,6,7,8,9] switch numbers { case flip(Array.isEmptyFunc)(): print("is empty") case flip(Array.contains)(10): print("contains 10") case flip(Array.contains)(2, 4): print("contains 2 and 4") case flip(Array.contains)(5): print("contains 5") default: print("default") }
又如匹配两个CGRect怎么样?
import CoreGraphics let rect1 = CGRect(x: 20, y: 20, width: 50, height: 50) let rect2 = CGRect(x: 40, y: 40, width: 100, height: 100) switch rect1 { case flip(CGRect.contains)(rect2): "contains" case flip(CGRect.intersects)(rect2): "intersects" default: "default" }
最后一个例子,比较两个集合。这里我们使用另一个辅助函数,而非否定一个模式。
func not(f: T -> Bool) -> T -> Bool { return { !f($0) } } let set1: Set = [1,2,3,4,5,6,7,8,9] let set2: Set = [3,4,5] switch set1 { case flip(Set.contains)(10): "contains 10" case not(flip(Set.isSupersetOf)(set2)): "is not a superset of /(set2)" case flip(Set.isSupersetOf)(set2): "is superset of /(set2)" case flip(Set.isDisjointWith)(set2): "is disjoint with /(set2)" default: "default" }
再次声明我不是要你把代码写成这样。尽管我认为我们开发的模式极其简洁,语法很短。上面的许多例子在我看来很丑,比另一种(一堆if语句)更难读懂。
这个系列的目标是鼓励你们(和我)开始用方法来考虑编程问题。通过把方法当成可以传递的值,然后在另外的方法中返回,并通过把多个简单的方法组合成一个更复杂的方法,我们可以用一些简单的块构建非常通用的系统。