模式匹配

模式匹配是一种允许深度匹配结构化值的特性:检查结构并将匹配的部分绑定到局部变量。

Ruby 中的模式匹配通过 case/in 表达式实现。

case <expression>
in <pattern1>
  ...
in <pattern2>
  ...
in <pattern3>
  ...
else
  ...
end

(请注意,inwhen 分支不能在同一个 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

有关语法、更多示例和解释,请参见下文。

模式

模式可以是

任何模式都可以嵌套在指定了 <子模式> 的数组/查找/哈希模式中。

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"

匹配非原始对象:deconstructdeconstruct_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

deconstructdeconstruct_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