前几天看到篇C#6.0的draft spec,发现C#6.0里将惊现模式匹配。如是便在做了翻宣讲说“模式匹配”是一个很好用的特性,但是不知道跟C#的结合会怎么样。而同时却又发现周边了解“模式匹配”的人好少,于是诞生了写一篇文章稍微介绍下这一功能特性的想法。

如果说模式匹配这一概念有人不知道,但是正则表达式应该绝大多数的程序员都知道的了。而简言之,模式匹配就是更强大的正则表达式,或者说正则表达式是模式匹配这一概念在字符串处理上的一个应用。有了正则表达式为例,我们可以为模式匹配定义一个更为通用的定义:

模式匹配是判断输入的数据(信息)是否与特定的结构相匹配,并按模式从中取得数据(信息)。

如下以正则表达式为例,从输入数据value里按pattern提取数据digits的模式匹配过程。

1
2
3
var value="abc123";
var pattern=/\d/g;//定义模式
var digits=value.match(pattern);//["1","2","3"]

那更广泛意义上的模式匹配是什么呢?其实很简单,就是输入/提取数据(信息)的范围不再局限为字符串了,甚至可以是编程语言支持的所有数据结构。第一次接触模式匹配这一概念,是前几年学习erlang的时候,下面就主要以erlang为例,来对模式匹配稍作介绍。

先上点erlang里的”甜点”为例来说明:

(erlang基础知识:大写字母开头的声明为变量,比如下面例子里的P,H,T;小写字母的声明是原子,比如abc)

模式{abcP}与输入数据项{abc,123}匹配,并在匹配的同时,发生绑定P->123

模式[H|T]与输入数据项[1,2,3,4]匹配,并在匹配的同时,发生绑定H->1,T->[2,3,4]

第一个例子匹配的过程如下:

1.先进行类型匹配:模式和数据项都是一个元组(“{}”),匹配通过。

2.模式元组的第一个元素为常量,进行匹配:模式和数据项都是“abc”,匹配通过。

3.模式元组的第二个元素是一个变量P,同时后面不再有任何元素,结束匹配,并将P绑定到数据项第一个元素后的所有元素。

第二个例子匹配过程如下:

1.先进行类型匹配,模式和数据项都是一个列表(“[]”),匹配通过。

2.模式”[H|T]”表示的是一个“首尾”匹配,用“|”分开两个变量(H,T),H表示的首元素匹配,T表示尾部(除首元素外的所有元素)匹配。匹配是通过的,并绑定H和T的值为首元素和尾元素。

再来一个高级点的例子:使用模式匹配来处理消息的接收:

(erlang基础知识:erlang的调用是基于CSP的,各个actor之间通过消息通讯带完成调用;receive原语是erlang提供的消息接收机制,可监听和接收来自其它actor发送来的消息;”!”标识符用于向对应的actor发送消息)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
loop(Dict) -\>
receive %消息接收的原语,获取调用方的消息
{store, Key, Value} -> %匹配数据存储的消息,"store"
loop(dict:store(Key, Value, Dict)); %真实的存储
{From, {get, Key}} -> %匹配数据获取的消息, From匹配消息来源,
From ! dict:fetch(Key, Dict), %向查询请求消息来源发送查询结果消息
loop(Dict) %尾递归:持续监听新的消息
end.

Pid = spawn(loop) %启动一个进程(actor),Pid为进程号

Pid!{store ,key1,"123"} %向进程发送一个键值对存储消息

Pid!{get,key1} %发送一个查询消息,获取"key1"对应的值

上面展示例子模式匹配在erlang中进行消息匹配的使用,需要注意的一点是,这里的匹配是从上到下来进行的,即会先进行“store”的匹配,匹配失败的话才进行“get”匹配。消息匹配是erlang里的一项杀手锏特性,它能大大减少在消息处理的代码量,代码只要专注于实际算法的实现即可,将相应消息的解析和内容提取,交给模式匹配来完成。

从本质上来讲,模式匹配体现的是一个解构的过程和手段,解构是日常编程过程中一个非常常见的场景,比如编译器将程序代码解析为一个抽象语法树(AST)就是一个解构的过程,再比如,将XML/JSON进行反序列化为程序对象也是一例。对于许多语言,并未提供强大的模式匹配特性,但是为了提供可扩展的解构功能,一般都会采取一些设计模式来解决。比如面向对象编程里,将解析操作抽离出独立的模块,采取基于基类的虚方法/抽象方法扩展的方式,然后结合想对应的规则配置,来完成有区别性的模式解构。但是,这样的方式,稍显笨重,代码量也更多,与模式匹配相比,不在一个复杂度等级之上。

去年我用golang写了一个web框架,其中的路由解析过程就采取了类似的方式(代码位置:https://github.com/JustinHuang917/gof)。通过如以下路由规则配置,匹配不同的URL模式,从URL中提取相应的参数:

1
2
3
4
5
6
7
8
9
10
"RouteRules":[
{
"/Order/{id:[0-9]+}":
{"controller":"Order","action":"Order","id":"0"}
},
{
"/{controller}/{action}":
{"controller":"Home","action":"Index"}
}
]

这个看似简单功能,我花了200多行代码才得以完成,使用的还是堪称语法最为精简的Golang,而如果使用Java/C#来完成,代码量肯定还要增加不少。而如果语言本身具有强大的模式匹配的话,实现代码将会变得非常精简。而这一次C#6.0声称引入模式匹配,希望是面向对象语言领域的一次有益尝试。

同时,我也希望我喜欢的Golang在模式匹配上迈开尝试的脚步,在现有的goroutine+channel的组合上应用模式匹配,将是一件非常有效率的实践。现在Golang提供”select..case…“机制只能识别不同channel返回的消息,而不能识别实际消息的内容,这一点来说,与erlang相比,降低了CSP的编程效率。

总结:

模式匹配是来自函数式编程领域的高效的编程功能特性,它能成倍增加“解构”代码的编写效率,明显降低代码复杂度。同时,尽管大多数的编程语言,尚未提供这一功能,但是实际代码编写过程中,我们可以借鉴其基于模式声明的思想,抽离出相应的模式匹配模块,在保证可扩展性的同时,控制数据(信息)匹配带来的代码复杂度。