回到Ruby系列文章
Ruby 2.7的模式匹配和in操作符 吼吼,Ruby 2.7支持模式匹配了。
Ruby 2.7模式匹配简介 Ruby 2.7的模式匹配通过操作符in
实现,匹配成功后可以直接绑定赋值,匹配失败时将报错(抛异常NoMatchingPatternError)。此外,除了以前就支持的case when的===
匹配,现在还支持case in匹配。
所以,模式匹配有两种用法:
1 2 3 4 5 6 7 8 9 10 11 12 <value> in <pattern> case <value>in <pattern1> ... in <pattern2> ... else ... end
注意,case…in和case…when不能混用,但case…in也具备case…when的===
匹配功能。
目前模式匹配还是实验特性,如果代码中使用了模式匹配,将会给出警告,如果想要禁止警告信息,可:
1 2 Warning [:experimental ] = false {a: 1 , b: 2 } in {a: }
或者以ruby -W:no-experimental
方式运行ruby脚本。
in单独使用 单独使用in操作符做模式匹配时,可做变量的解构赋值,而且模式不匹配时会报错。
值与值匹配 1 2 3 4 5 6 7 8 9 10 0 in 0 [2 , 3 ] in [2 , 3 ] {one: 1 , two: 2 } in {one: 1 , two: 2 } 0 in 1 [2 , 3 ] in [2 , 33 ] {one: 1 , two: 2 } in {one: 1 , two: 22 } {one: 1 , two: 2 } in {one: 1 , twooo: 2 }
值与类型匹配 1 2 3 4 5 6 7 8 9 10 0 in Integer [2 , 3 ] in Array [2 , 3 ] in [2 , Integer ] {one: 1 , two: 2 } in {one: 1 , two: Integer } 0 in String [2 , 3 ] in [2 , String ] {one: 1 , two: 2 } in {one: 1 , two: String }
in使用===做匹配 其实值与值、值与类型的匹配都是in使用===
符号进行匹配的特列。
例如:
1 2 3 0 in 0 0 in -1 ..1 0 in Integer
标量格式的匹配模式 例如:
1 2 3 4 5 6 0 in aa [2 , 3 ] in b b
数组格式的匹配模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 langs = %w[perl shell ruby] langs in [x,y,z] x y z [1 , 2 , 3 , 4 ] in [first, second, *other] first second other [0 ,[1 ,2 ,3 ]] in [a, [b, *c]] a b c [0 ,[1 ,2 ,3 ]] in [a, [1 , b, 3 ]] a b
如果模式匹配失败,则报错:
1 2 3 4 langs = %w[perl shell ruby] langs in [a,b] [2 ,3 ] in [b]
数组格式的模式匹配是全匹配的,要求值中的每个元素都有对应变量去接收。
hash格式的模式匹配 除了标量和数组的模式匹配,还支持hash的模式匹配,此时根据key进行匹配:
可见,hash格式的模式匹配是半匹配的,允许对部分key进行匹配。
可在pattern中使用**KEY
匹配剩余的key:
1 2 3 hs = {one: 1 , two: 2 , three: 3 } hs in {one: , **rest} rest
在做hash格式的模式匹配时,被匹配的value中可能会有剩余的key,如果要限制不允许有多余的key,可在pattern中使用特殊的值**nil
,相当于强制hash格式的模式匹配根据pattern进行全匹配。
1 2 3 4 5 hs = {one: 1 , two: 2 , three: 3 } hs in {one: , three: , two: } hs in {one: , three: } hs in {one: , three: , **nil } hs in {one: , three: , two: , **nil }
所以,**nil
的含义是:除了pattern中指定的key外,被匹配的value中没有剩余的元素了。
匹配时检查数据类型 在pattern中可以限制匹配时某变量的数据类型:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 0 in Float => a 0 in Integer => alangs = %w[perl shell ruby] langs in [String => a, String => b, c] langs in [Integer => a,*other] hs = {one: 1 , two: 2 , three: 3 } hs in {one: Integer => a, two: Integer => b, **rest} a b hs in {one: Integer , three: Integer }
丢弃不需要的值 在模式匹配中进行变量的绑定赋值时,可使用_
丢弃不感兴趣的元素,它是一个元素占位符,但不对对应的元素做匹配操作。
1 2 3 4 [0 , [1 , 2 ]] in [0 , [1 , _] => a] a [2 ,3 ,4 ] in [a,b,_]
此外,对于数组格式和hash格式的模式匹配,可分别使用*
或**
来表示丢弃剩余元素:
1 2 [1 ,2 ,3 ] in [Integer , *] {a: 1 , b: 2 , c: 3 } in {a: , **}
case…in模式匹配 case…in使用in进行模式匹配:
1 2 3 4 5 6 7 8 case <value>in <pattern1> ... in <pattern2> ... else ... end
匹配成功后选择对应的分支执行。如果所有的分支都未匹配成功,此时,如果有else分支,则选择else分支,否则报错。所以,如非必要,强烈建议写else语句,而不是去捕获case…in无法匹配时抛出的异常。
因为case…in使用in做模式匹配,所以前面介绍的【单独使用in】的模式匹配方式也都适用于case…in的分支中。除此之外,case…in还支持一些额外的语法。
case…in的分支中,如果是数组格式、hash格式的匹配方式,可省略pattern中最外层 的中括号或大括号。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 case [1 , 2 ]in Integer , Integer "matched" else "not matched" end case {a: 1 , b: 2 , c: 3 }in a: Integer "matched" else "not matched" end case {name: 'John' , friends: [{name: 'Jane' }, {name: 'Rajesh' }]}in name: , friends: [{name: first_friend}, *] "matched: #{first_friend} " else "not matched" end
pattern中可以使用二选一符号|
来表示逻辑或的关系:只要匹配其中一个表达式,就算成功。
1 2 3 4 5 6 7 8 case 0 in 0 | 1 | 2 "0 or 1 or 2" in Hash | Array "Hash or Array" else "not matched" end
但如果存在变量绑定赋值,则不能使用|
进行多选一,这会报语法错误:
1 2 3 4 5 6 7 case {a: 1 , b: 2 }in {a: } | Array "matched: #{a}" else "not matched" end # SyntaxError (illegal variable in alternative pattern (a))
case…in有时候会带来很大便利,特别是在处理hash结构的数据时(或类hash的结构,比如struct、json、object等)优势更大,熟悉函数式编程的都知道模式匹配的威力(尽管模式匹配和函数式编程没有必然的关联关系)。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 require 'json' json = '{ "name": "Alice", "age": 30, "children": [ { "name": "Bob", "age": 2 } ] }' case JSON .parse(json, symbolize_names: true )in {children: [{name: "Bob" , age: Integer => age}]} puts age end person = JSON .parse(json, symbolize_names: true ) children = person[:children ] if children.length == 1 and children[0 ][:name ] == "Bob" puts children[0 ][:age ] if children[0 ][:age ].is_a? Integer end
case…in在模式匹配时还支持if/unless条件判断,即使某分支的模式能够匹配成功,但如果条件判断失败,仍然不会选择该分支。条件判断中可使用模式匹配时绑定的本地变量。
例如,上面的case中加一个条件:父亲年龄大于等于30岁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 case JSON .parse(json, symbolize_names: true )in {age: p_age, children: [{name: "Bob" , age: }]} if p_age >= 30 puts age end person = JSON .parse(json, symbolize_names: true ) if person[:age ] >= 30 children = person[:children ] if children.length == 1 and children[0 ][:name ] == "Bob" puts children[0 ][:age ] if children[0 ][:age ].is_a? Integer end end
由于pattern中会进行变量的绑定赋值,如果变量是已存在的变量,那么不会将其值替换在pattern中,而是进行变量赋值:
1 2 3 4 5 6 7 a=0 case 1 in a puts "matched: #{a} " else puts "not matched" end
如果想要在pattern中使用已存在变量的值,使用^VAR
表示法:
1 2 3 4 5 a = 0 case 1 in ^a puts "aaa" end
^VAR
的VAR也可以来自于当前pattern中已绑定的变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 jane = {school: 'high' , schools: [{id: 1 , level: 'middle' }, {id: 2 , level: 'high' }]} john = {school: 'high' , schools: [{id: 1 , level: 'middle' }]} case janein school: , schools: [*, {id: , level: ^school}] "matched. school: #{id} " else "not matched" end case john in school: , schools: [*, {id: , level: ^school}] "matched. school: #{id} " else "not matched" end
自定义对象的模式匹配 如果对象具有deconstruct()方法返回数组,那么该对象可以进行数组格式的模式匹配;
如果对象具有deconstruct_keys(keys)方法返回hash,那么该对象可以进行Hash格式的模式匹配,其中参数keys是要匹配的key的数组。
例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 class Point def initialize (x, y ) @x , @y = x, y end def deconstruct [@x , @y ] end def deconstruct_keys (keys ) {x: @x , y: @y } end end case Point .new(1 , -2 )in px, Integer "matched: #{px} " else "not matched" end "matched: 1" case Point .new(1 , -2 )in x: 0 .. => px "matched: #{px} " else "not matched" end
目前,Ruby已支持Struct对象进行数组格式和hash格式的模式匹配:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Color = Struct .new(:r , :g , :b )p Color [0 , 10 , 20 ].deconstruct p Color [0 , 10 , 20 ].deconstruct_keys([:r , :g ]) Color [0 ,10 ,20 ] in [r,g,b]Color [0 ,10 ,20 ] in {r: ,g: ,**}case colorin Color [0 , 0 , 0 ] puts "Black" in Color [255 , 0 , 0 ] puts "Red" in Color [r, g ,b] puts "#{r} , #{g} , #{b} " end
实验阶段需要注意的事项 目前模式匹配还是实验特性,虽然将来不会改变模式匹配的大方向,但有些行为细节可能在将来会发生变化。
下面这个特性是需要特别注意的,将来可能会发生改变:即使分支匹配失败,但仍然会绑定变量。
例如:
1 2 3 4 5 6 7 8 case [1 , 2 ]in aa, String "matched 0" in [xx, yy] "matched 1" else "not matched" end
很明显,上面会匹配第二个分支成功,所以xx=1,yy=2
,但第一个分支已经匹配过,且其中的一部分匹配成功了,只是整个分支没有匹配成功,这里仍然会设置aa=1
。
但如果第一个分支改成in String => aa, String
,那么每一个元素都匹配失败,此时aa将会赋值为nil。
1 2 3 4 5 6 7 8 case [1 , 2 ]in String => aa, String "matched 0" in [xx, yy] "matched 1" else "not matched" end
因为匹配失败的分支可能会对变量进行赋值,所以写在下面的分支中可以使用写在上面的分支中绑定的变量:
1 2 3 4 5 6 7 8 case [1 , 2 ]in aa, String "matched" in [xx,yy] if aa == 1 "matched 1" else "not matched" end
但这并非好事,因为使用它上面的分支中的变量时,并不确定那个分支能成功绑定所使用的变量:
1 2 3 4 5 6 7 8 case [1 , 2 ]in {aa: } "matched 0" in [xx,yy] if aa == 1 "matched 1" else "not matched" end
另外,如果case…in之前已经存在变量a,那么case…in中分支绑定的变量a可能会覆盖已存在的变量a,即使该分支匹配失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 a = 5 case [1 , 2 ]in String => a, String "matched" else "not matched" end a case [1 , 2 ]in a, String "matched" else "not matched" end a