模式匹配¶ ↑
模式匹配是一种允许对结构化值进行深度匹配的功能:检查结构并将匹配的部分绑定到局部变量。
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);(值模式) -
数组模式:
[<子模式>, <子模式>, <子模式>, ...];(数组模式) -
查找模式:
[*变量, <子模式>, <子模式>, <子模式>, ..., *变量];(查找模式) -
哈希模式:
{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"
数组和哈希模式都支持“剩余”规范
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:
查找模式类似于数组模式,但可用于检查给定对象是否具有与模式匹配的任何元素
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"
这些核心类和库类实现了析构
-
Time#deconstruct_keys、Date#deconstruct_keys、DateTime#deconstruct_keys。
守卫子句¶ ↑
在 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