模式比對

模式比對是一種允許深入比對結構化值的功能:檢查結構並將比對的部分繫結到局部變數。

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

變數繫結

除了深入結構檢查之外,樣式比對的一項非常重要的功能是將比對的部分繫結到區域變數。繫結的基本形式是在比對的(子)樣式之後指定 => 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"

比對非原始物件:deconstructdeconstruct_keys

如上所述,除了文字陣列和雜湊之外,陣列、find 和雜湊模式會嘗試比對任何實作 deconstruct(針對陣列/find 模式)或 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

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