模式匹配
模式匹配是一种允许深度匹配结构化值的特性:检查结构并将匹配的部分绑定到局部变量。
Ruby 中的模式匹配通过 case/in 表达式实现。
case <expression> in <pattern1> ... in <pattern2> ... in <pattern3> ... else ... end
(请注意,in 和 when 分支不能在同一个 case 表达式中混合使用。)
或者使用 => 运算符和 in 运算符,它们可以用作独立表达式。
<expression> => <pattern> <expression> in <pattern>
case/in 表达式是详尽的:如果表达式的值不匹配 case 表达式的任何分支(且不存在 else 分支),则会引发 NoMatchingPatternError。
因此,case 表达式可用于条件匹配和解包。
config = {db: {user: 'admin', password: 'abc123'}} case config in db: {user:} # matches subhash and puts matched value in variable user puts "Connect with user '#{user}'" in connection: {username: } puts "Connect with user '#{username}'" else puts "Unrecognized structure of config" end # Prints: "Connect with user 'admin'"
而 => 运算符在预先知道期望的数据结构时最有用,只需解包其中的一部分。
config = {db: {user: 'admin', password: 'abc123'}} config => {db: {user:}} # will raise if the config's structure is unexpected puts "Connect with user '#{user}'" # Prints: "Connect with user 'admin'"
<expression> in <pattern> 等同于 case <expression>; in <pattern>; true; else false; end。当你只想知道模式是否匹配时,可以使用它。
users = [{name: "Alice", age: 12}, {name: "Bob", age: 23}] users.any? {|user| user in {name: /B/, age: 20..} } #=> true
有关语法、更多示例和解释,请参见下文。
模式
模式可以是
-
任何 Ruby 对象(通过
===运算符匹配,就像在when中一样);(值模式) -
数组模式:
[<子模式>, <子模式>, <子模式>, ...];(数组模式) -
查找模式:
[*变量, <子模式>, <子模式>, <子模式>, ..., *变量];(查找模式) -
哈希模式:
{键: <子模式>, 键: <子模式>, ...};(哈希模式) -
使用
|组合的模式;(备选模式) -
变量捕获:
<模式> => 变量或变量;(作为模式,变量模式)
任何模式都可以嵌套在指定了 <子模式> 的数组/查找/哈希模式中。
Array 模式和查找模式匹配数组,或响应 deconstruct 的对象(后者请参见下文)。Hash 模式匹配哈希,或响应 deconstruct_keys 的对象(后者请参见下文)。请注意,哈希模式只支持符号键。
数组和哈希模式行为之间的一个重要区别是,数组只匹配整个数组。
case [1, 2, 3] in [Integer, Integer] "matched" else "not matched" end #=> "not matched"
而哈希即使存在除指定部分之外的其他键也会匹配。
case {a: 1, b: 2, c: 3} in {a: Integer} "matched" else "not matched" end #=> "matched"
{} 是此规则的唯一例外。它只在给出空哈希时匹配。
case {a: 1, b: 2, c: 3} in {} "matched" else "not matched" end #=> "not matched" case {} in {} "matched" else "not matched" end #=> "matched"
还有一种方法可以通过 **nil 指定匹配的哈希中除了模式显式指定的键之外不应有其他键。
case {a: 1, b: 2} in {a: Integer, **nil} # this will not match the pattern having keys other than a: "matched a part" in {a: Integer, b: Integer, **nil} "matched a whole" else "not matched" end #=> "matched a whole"
数组和哈希模式都支持“剩余”元素的指定。
case [1, 2, 3] in [Integer, *] "matched" else "not matched" end #=> "matched" case {a: 1, b: 2, c: 3} in {a: Integer, **} "matched" else "not matched" end #=> "matched"
这两种模式都可以省略括号。
case [1, 2] in Integer, Integer "matched" else "not matched" end #=> "matched" case {a: 1, b: 2, c: 3} in a: Integer "matched" else "not matched" end #=> "matched" [1, 2] => a, b [1, 2] in a, b {a: 1, b: 2, c: 3} => a: {a: 1, b: 2, c: 3} in a:
Find 模式类似于数组模式,但可用于检查给定对象是否包含任何与模式匹配的元素。
case ["a", 1, "b", "c", 2] in [*, String, String, *] "matched" else "not matched" end
变量绑定
除了深度结构检查之外,模式匹配的一个非常重要的特性是将匹配的部分绑定到局部变量。绑定的基本形式是在匹配的(子)模式后指定 => 变量名(这可能让你联想到在 rescue ExceptionClass => var 子句中将异常存储到局部变量中)。
case [1, 2] in Integer => a, Integer "matched: #{a}" else "not matched" end #=> "matched: 1" case {a: 1, b: 2, c: 3} in a: Integer => m "matched: #{m}" else "not matched" end #=> "matched: 1"
如果不需要额外的检查,只想将数据的某个部分绑定到变量,可以使用更简单的形式。
case [1, 2] in a, Integer "matched: #{a}" else "not matched" end #=> "matched: 1" case {a: 1, b: 2, c: 3} in a: m "matched: #{m}" else "not matched" end #=> "matched: 1"
对于哈希模式,甚至存在更简单的形式:仅键指定(不带任何子模式)会将局部变量绑定到键的名称。
case {a: 1, b: 2, c: 3} in a: "matched: #{a}" else "not matched" end #=> "matched: 1"
绑定也适用于嵌套模式。
case {name: 'John', friends: [{name: 'Jane'}, {name: 'Rajesh'}]} in name:, friends: [{name: first_friend}, *] "matched: #{first_friend}" else "not matched" end #=> "matched: Jane"
模式的“剩余”部分也可以绑定到变量。
case [1, 2, 3] in a, *rest "matched: #{a}, #{rest}" else "not matched" end #=> "matched: 1, [2, 3]" case {a: 1, b: 2, c: 3} in a:, **rest "matched: #{a}, #{rest}" else "not matched" end #=> "matched: 1, {b: 2, c: 3}"
目前,绑定到变量不适用于用 | 连接的备选模式。
case {a: 1, b: 2}
in {a: } | Array
# ^ SyntaxError (variable capture in alternative pattern)
"matched: #{a}"
else
"not matched"
end
以 _ 开头的变量是此规则的唯一例外。
case {a: 1, b: 2} in {a: _, b: _foo} | Array "matched: #{_}, #{_foo}" else "not matched" end # => "matched: 1, 2"
尽管如此,不建议重用绑定值,因为此模式的目标是表示丢弃的值。
变量固定
由于变量绑定特性,现有的局部变量不能直接用作子模式。
expectation = 18 case [1, 2] in expectation, *rest "matched. expectation was: #{expectation}" else "not matched. expectation was: #{expectation}" end # expected: "not matched. expectation was: 18" # real: "matched. expectation was: 1" -- local variable just rewritten
在这种情况下,可以使用固定运算符 ^ 来告诉 Ruby“仅将此值用作模式的一部分”。
expectation = 18 case [1, 2] in ^expectation, *rest "matched. expectation was: #{expectation}" else "not matched. expectation was: #{expectation}" end #=> "not matched. expectation was: 18"
变量固定的一个重要用法是指定同一值应在模式中出现多次。
jane = {school: 'high', schools: [{id: 1, level: 'middle'}, {id: 2, level: 'high'}]} john = {school: 'high', schools: [{id: 1, level: 'middle'}]} case jane in school:, schools: [*, {id:, level: ^school}] # select the last school, level should match "matched. school: #{id}" else "not matched" end #=> "matched. school: 2" case john # the specified school level is "high", but last school does not match in school:, schools: [*, {id:, level: ^school}] "matched. school: #{id}" else "not matched" end #=> "not matched"
除了固定局部变量之外,还可以固定实例变量、全局变量和类变量。
$gvar = 1 class A @ivar = 2 @@cvar = 3 case [1, 2, 3] in ^$gvar, ^@ivar, ^@@cvar "matched" else "not matched" end #=> "matched" end
还可以使用括号固定任意表达式的结果。
a = 1 b = 2 case 3 in ^(a + b) "matched" else "not matched" end #=> "matched"
匹配非原始对象:deconstruct 和 deconstruct_keys
如上所述,数组、查找和哈希模式除了匹配字面量数组和哈希之外,还会尝试匹配实现 deconstruct(用于数组/查找模式)或 deconstruct_keys(用于哈希模式)的任何对象。
class Point def initialize(x, y) @x, @y = x, y end def deconstruct puts "deconstruct called" [@x, @y] end def deconstruct_keys(keys) puts "deconstruct_keys called with #{keys.inspect}" {x: @x, y: @y} end end case Point.new(1, -2) in px, Integer # sub-patterns and variable binding works "matched: #{px}" else "not matched" end # prints "deconstruct called" "matched: 1" case Point.new(1, -2) in x: 0.. => px "matched: #{px}" else "not matched" end # prints: deconstruct_keys called with [:x] #=> "matched: 1"
会将 keys 传递给 deconstruct_keys,为被匹配类提供优化空间:如果计算完整的哈希表示很昂贵,可以只计算必要的子哈希。当使用 **rest 模式时,会将 nil 作为 keys 值传递。
case Point.new(1, -2) in x: 0.. => px, **rest "matched: #{px}" else "not matched" end # prints: deconstruct_keys called with nil #=> "matched: 1"
此外,匹配自定义类时,可以指定预期的类作为模式的一部分,并使用 === 进行检查。
class SuperPoint < Point end case Point.new(1, -2) in SuperPoint(x: 0.. => px) "matched: #{px}" else "not matched" end #=> "not matched" case SuperPoint.new(1, -2) in SuperPoint[x: 0.. => px] # [] or () parentheses are allowed "matched: #{px}" else "not matched" end #=> "matched: 1"
这些核心和库类实现了解构。
保护子句
在 case/in 表达式中,可以使用 if 在模式匹配时附加一个额外的条件(保护子句)。此条件可以使用绑定的变量。
case [1, 2] in a, b if b == a*2 "matched" else "not matched" end #=> "matched" case [1, 1] in a, b if b == a*2 "matched" else "not matched" end #=> "not matched"
unless 也有效。
case [1, 1] in a, b unless b == a*2 "matched" else "not matched" end #=> "matched"
请注意,=> 和 in 运算符不能有保护子句。下面的示例会被解析为带修饰符 if 的独立表达式。
[1, 2] in a, b if b == a*2
附录 A. 模式语法
近似语法为:
pattern: value_pattern
| variable_pattern
| alternative_pattern
| as_pattern
| array_pattern
| find_pattern
| hash_pattern
value_pattern: literal
| Constant
| ^local_variable
| ^instance_variable
| ^class_variable
| ^global_variable
| ^(expression)
variable_pattern: variable
alternative_pattern: pattern | pattern | ...
as_pattern: pattern => variable
array_pattern: [pattern, ..., *variable]
| Constant(pattern, ..., *variable)
| Constant[pattern, ..., *variable]
find_pattern: [*variable, pattern, ..., *variable]
| Constant(*variable, pattern, ..., *variable)
| Constant[*variable, pattern, ..., *variable]
hash_pattern: {key: pattern, key:, ..., **variable}
| Constant(key: pattern, key:, ..., **variable)
| Constant[key: pattern, key:, ..., **variable]
附录 B. 一些未定义行为的示例
为了在未来为优化留出空间,规范包含了一些未定义行为。
在未匹配的模式中使用变量
case [0, 1] in [a, 2] "not matched" in b "matched" in c "not matched" end a #=> undefined c #=> undefined
deconstruct、deconstruct_keys 方法的调用次数
$i = 0 ary = [0] def ary.deconstruct $i += 1 self end case ary in [0, 1] "not matched" in [0] "matched" end $i #=> undefined