模式匹配¶ ↑
模式匹配是一种允许对结构化值进行深度匹配的功能:检查结构并将匹配的部分绑定到局部变量。
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
中);(值模式) -
数组模式:
[<subpattern>, <subpattern>, <subpattern>, ...]
;(数组模式) -
查找模式:
[*variable, <subpattern>, <subpattern>, <subpattern>, ..., *variable]
;(查找模式) -
哈希模式:
{key: <subpattern>, key: <subpattern>, ...}
;(哈希模式) -
使用
|
组合模式;(备选模式) -
变量捕获:
<pattern> => variable
或variable
;(作为模式,变量模式)
任何模式都可以嵌套在指定了 <subpattern>
的数组/查找/哈希模式中。
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"
数组模式和哈希模式都支持“rest”规范
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
变量绑定¶ ↑
除了深入的结构检查,模式匹配的一个非常重要的特性是将匹配的部分绑定到局部变量。绑定的基本形式只是在匹配的(子)模式后指定=> variable_name
(人们可能会发现这类似于在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"
Binding
也适用于嵌套模式
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}"
Binding
到变量目前不适用于使用|
连接的备选模式
case {a: 1, b: 2} in {a: } | Array "matched: #{a}" else "not matched" end # SyntaxError (illegal variable in alternative pattern (a))
以_
开头的变量是此规则的唯一例外
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"
这些核心和库类实现了解构
防护子句¶ ↑
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"
附录 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