Swift中的模式匹配
2015-11-23 09:50:13 | 来源:玩转帮会 | 投稿:佚名 | 编辑:小柯

原标题:Swift中的模式匹配

本文由CocoaChina–@ALEX吴浩文翻译

原文:Pattern Matching in Swift

更新:

2015.9.19包含关于该问题现有Swift语法的说明。

2015.9.25添加关于标准库中现有的~>操作符的说明

其他文章系列

(1) Custom Pattern Matching(本篇)

(2) Ranges and Intervals

(3) More Pattern Matching Examples

点我预告Xcode 7的playground样例

Swift有一个很好的特性,那就是模式匹配的扩展。模式是用于匹配的规则值,如switch语句的case,do语句的catch子句,以及if、while、guard、for-in语句的条件。

例如,假设你想判断一个整数是大于、小于还是等于零,你可以用if-else if-else语句,尽管这并不美观:

letx=10
ifx>0{
print("大于零")
}elseifx<0{
print("小于零")
}else{
print("等于零")
}

用switch语句会好很多,我理想的代码是这样:

//伪代码
switchx{
case>0:
print("大于零")
case<0:
print("小于零")
case0:
print("等于零")
}

但模式匹配默认并不支持不等式。让我们看看能不能改变这个现状。为了使过程更加清晰,我先忽略>0的情况,用greaterThan(0)来代替它,过后我再来定义这个操作符。

扩展模式匹配

Swift的模式匹配是基于~=操作符的,如果表达式的~=值返回true则匹配成功。标准库自带四个~=操作符的重载:一个用于Equatable,一个用于Optional,一个用于Range,一个用于Interval。这些都不符合我们的需求,尽管Range和Interval很接近了,关于它们你可以看这篇文章。

所以我们要实现我们自己的~=。这个方法的原型是:

func~=(pattern:???,value:???)->Bool

我们知道这个方法必须返回一个Bool,那正是我们需要的,我们需要知道这个值是否匹配模式。接下来要问我们自己的是:参数的类型是什么?

对于值,我们可以使用Int,这正是我们在之前的例子中需要的。但让我们把它一般化,让它能够接受任何类型。在我们的情况里,模式形如greaterThan()或lessThan()。更一般化,模式应该是一个方法,一个能够将值作为参数并返回true或false的方法。值的类型为T,所以模式的类型应为T -> Bool:

func~=(pattern:T->Bool,value:T)->Bool{
returnpattern(value)
}

现在我们需定义方法greaterThan和lessThan来创建模式。注意不要把模式greaterThan(0)中的0和我们想匹配的值混淆了。greaterThan的参数是模式的一部分,这个部分将在第二步中用到。举个例子,greaterThan(0) ~= x和greaterThan(0)(x)是一样的。

我们知道方法greaterThan(0)必须返回一个方法,这个方法要能接受一个值并返回Bool。所以greaterThan必须是一个方法,接受另一个值并返回之前方法。我们把参数限制成Comparable,为了能在实现中用Swift的>和<操作符:

funcgreaterThan(a:T)->(T->Bool){
return{(b:T)->Boolinb>a}
}

这个方法接受一个参数,调用接受不止一个参数的方法并返回,像这样的方法这被称为Curried functions。(Swift的部分实例方法就是一种Curried functions)Swift提供了一种特别的语法用于Curried functions,正如它们的名字一样形象。使用这种语法,我们的方法变成了这样:

funcgreaterThan(a:T)(_b:T)->Bool{
returnb>a
}
funclessThan(a:T)(_b:T)->Bool{
returnb<a
}

这样我们有了第一个版本的switch语句:

switchx{
casegreaterThan(0):
print("大于零")
caselessThan(0):
print("小于零")
case0:
print("等于零")
default:
fatalError("不会发生")
}

很不错,但看看default,这个解决方案不能给编译器任何提示进行完整性检查,所以我们不得不提供一个default。如果你确定模式覆盖了每一个可能的值,在default下调用fatalError()是一个不错的主意,这表明这段代码绝对不会执行到。

自定义操作符

回想一开始的想法,以及那段伪代码。理想情况下,我们想用>0和<0取代greaterThan(0)和lessThan(0)。

自定义操作符存在争议,因为其他读者经常不熟悉这些,它们降低了可读性。回到我们的例子中,类似greaterThan(0)则是完全可读,所以完全可以认为不需要自定义操作符。但同时,每个人都知道>0意味着什么。所以让我们来尝试一下,但正如我们将看到的,它不会很漂亮。

我们自定义的操作符是一元的——它们只有一个操作数。同时,它们是前置操作符(而不是后置,那种操作符在操作数后的)。在一元操作符和操作数之间不能有空格,因为Swift用空格来区分一元和二元操作符。此外,<不允许用作前置操作符,我们只好用别的东西代替。(>允许前置,但不是允许后置)。

我建议我们使用~>和~<。虽然~>只是非常像箭头并不理想,但波浪号暗示了模式匹配操作符~=。其他我可以想出的操作符(如>>和<<)则容易造成混淆。

9月25日更新:我从Nate Cook那了解到操作符~>在标准库中已经存在。虽然它的实现都没有公有,但Nate发现它是用来增加集合的索引。鉴于此,为一个完全不同的目的而使用相同的操作符可能不是一个好主意。你可以选个别的。

真正的实现并不重要。我们要做的就只是声明操作符和实现方法,这些只是我们已有的方法greaterThan和lessThan的委托:

prefixoperator~>{}
prefixoperator~<{}
prefixfunc~>(a:T)(_b:T)->Bool{
returngreaterThan(a)(b)
}
prefixfunc~Bool{
returnlessThan(a)(b)
}

这样,我们的switch语句变成:

switchx{
case~>0:
print("大于零")
case~<0:
print("小于零")
case0:
print("等于零")
default:
fatalError("不会发生")
}

再次提醒,操作符和操作数之间没有空格。

这样已是我们的极限,很接近原始计划,但显然并不完美。

9月19日更新:Joseph Lord提醒我,Swift有一个类似的语法:

switchx{
case_wherex>0:
print("大于零")
case_wherex<0:
print("小于零")
case0:
print("等于零")
default:
fatalError("不会发生")
}

这个语法,虽然它可能不像我们定制的解决方案那么简洁,但绝对足够好,因为你不应该为这么一个简单的目的此创建一个自定义语法。然而,我们的解决方案是一般化的,能在不同的地方应用。继续往下看。

其他应用

顺便说一句,这里给出的解决方案是非常一般化的。我们重载的模式匹配操作符~=适用任何T类型和任何接受T类型返回Bool的方法。换句话说,我们的实现使得pattern ~= value和pattern(value)一样好用。更进一步,switch value { case pattern: … }和 if pattern(value) { … }一样好用。

检查数字奇偶性

举几个例子。首先,一个简单的例子说明了其可应用性,虽然其实际意义不大。假设你有一个方法isEven用来检查数数字是不是偶数:

funcisEven(a:T)->Bool{
returna%2==0
}

现在:

switchisEven(x){
casetrue:print("偶数")
casefalse:print("奇数")
}

可以变成:

switchx{
caseisEven:print("偶数")
default:print("奇数")
}

注意default,下面的代码无效:

switchx{
caseisEven:print("偶数")
caseisOdd:print("奇数")
}
//error:Switchmustbeexhaustive,consideraddingadefaultclause

匹配字符串

举一个更实际的例子,假设你想要匹配一个字符串的前缀或后缀。我们先写两个方法hasPrefix和hasSuffix,它们接受两个字符串,并检查第一个参数是否是第二个参数的前缀/后缀。这些只是现有标准库中String.hasPrefix和String.hasSuffix方法的变形,只是使参数有一个方便的顺序(前缀/后缀第一,完整的字符串第二)。如果你经常使用Partial Applied Function(偏应用方法,缺少部分参数的方法)并将它们传递给其他方法,你会发现你常常需要重复出现参数来符合被调用方法的参数。烦人,但这不难。

funchasPrefix(prefix:String)(value:String)->Bool{
returnvalue.hasPrefix(prefix)
}
funchasSuffix(suffix:String)(value:String)->Bool{
returnvalue.hasSuffix(suffix)
}

现在我们可以这样,在我看来这很容易阅读了:

letstr="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
switchstr{
casehasPrefix("B"),hasPrefix("C"):
print("以B或C开头")
casehasPrefix("D"):
print("以D开头")
casehasSuffix("Z"):
print("以Z结尾")
default:
print("其他情况")
}

结论

为了解决我们最初的问题,我们提出了一个一般化的解决方案,它可以解决很多不同的问题。我发现这种情况经常发生,当你将方法看作值来传递,它可以用在你通常想不到的地方。这是函数式编程改进可组合性这一说法背后的核心概念之一。

扩展Swift的模式匹配系统,使其有了新的功能,无论是对于内置类型还是自定义类型,都是极其强大的。一如既往,注意不要把它扩展太多。即使一个自定义的语法看上去比保守的解决方案更为干净,但对于那些不熟悉它的人它使代码更加难读了。

tags:

上一篇  下一篇

相关:

5天内搞定产品设计是怎样一种体验

产品设计过程是有一个比较固定的周期的。但是,如果你能将整个流程的运作速度提升起来,用更频繁的反馈获得

为什么Java中1000==1000为false而100==100为true?

本文由玩赚乐(www.banghui.org)– 小峰原创翻译,转载请看清文末的转载要求,欢迎加入技术翻译小组!这是一

RESTAPI最佳入门指南

如果你看到这里,你以前可能听说过API 和REST,然后你就会想:“这些都是什么东西?”。也许你已经了解过一些

Java8简明教程

欢迎阅读我编写的 Java 8 介绍。本教程将带领你一步步认识这门语言的所有新特性。通过简单明了的代码示例,

Webkit远程调试协议初探

任何做过 Web 开发的同学,都避免不了在浏览器内进行调试。而大部分同学的首选工具,就是 Chrome DevTools。

Android应用启动优化:一种DelayLoad的实现和原理

0. 应用启动优化概述在 Android 开发中,应用启动速度是一个非常重要的点,应用启动优化也是一个非常重要的过

Wireshark基本介绍和学习TCP三次握手

这篇文章介绍另一个好用的抓包工具 Wireshark,用来获取网络数据封包,包括 HTTP、TCP、UDP 等网络协议包。

一站式学习Wireshark(三):应用WiresharkIO图形工具分析数据流

基本IO Graphs:IO graphs是一个非常好用的工具。基本的Wireshark IO graph会显示抓包文件中的整体流量情况,

有了这个叫Kontor的网站,看图装修办公室soeasy

关于 Kontor 网站有一个有趣的比喻,拿一部分的 Houzz(一个被称为室内外设计的维基百科的网站)和全部的

一个保有黄埔军校记忆的「深井」古村

图文原创:林漓 责任编辑:黎梓杰同学今天,我来带大家专门探索一下深井这个古村落。其坐标位于广州市黄埔区

站长推荐: