模式匹配¶ ↑
模式匹配是一种允许对结构化值进行深度匹配的功能:检查结构并将匹配的部分绑定到局部变量。
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'"
<表达式> in <模式>
与 case <表达式>; in <模式>; 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
);(值模式) -
数组模式:
[<子模式>,<子模式>,<子模式>,...]
;(数组模式) -
查找模式:
[*变量, <子模式>, <子模式>, <子模式>, ..., *变量]
;(查找模式) -
哈希模式:
{key: <子模式>,key: <子模式>,...}
;(哈希模式) -
使用
|
的模式组合;(可选模式) -
变量捕获:
<模式> => 变量
或变量
;(As 模式,变量模式)
任何模式都可以嵌套在指定 <子模式>
的数组/查找/哈希模式中。
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"
模式的“rest”部分也可以绑定到变量
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"
这些核心和库类实现了析构
守卫子句¶ ↑
当 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