模式匹配¶ ↑
模式匹配是一种允许对结构化值进行深度匹配的功能:检查结构并将匹配的部分绑定到局部变量。
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