Spec-Zone.ru › Ruby 3.4

Сопоставление с образцом

Сопоставление с образцом — это возможность выполнять глубокое сопоставление структурированных значений: проверять структуру и связывать сопоставленные части с локальными переменными.

В 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); (образец-значение)

  • образец-массив: [<subpattern>, <subpattern>, <subpattern>, ...]; (образец-массив)

  • образец поиска: [*variable, <subpattern>, <subpattern>, <subpattern>, ..., *variable]; (образец поиска)

  • образец-хеш: {key: <subpattern>, key: <subpattern>, ...}; (образец-хеш)

  • комбинация образцов с помощью |; (альтернативный образец)

  • захват значения в переменную: <pattern> => variable или variable; (образец-связка, образец-переменная)

Любой образец можно вложить в образцы-массивы, образцы поиска и образцы-хеши в тех местах, где указано <subpattern>.

Образцы 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"

Часть образца, обозначающая «остаток», также может быть связана с переменной:

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 в качестве значения keys передаётся nil:

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"

Декомпозицию реализуют следующие основные классы и классы из библиотек:

  • MatchData#deconstruct и MatchData#deconstruct_keys;

  • Time#deconstruct_keys, Date#deconstruct_keys, DateTime#deconstruct_keys.

Предложения-ограничители

if можно использовать, чтобы добавить дополнительное условие (предложение-ограничитель) при сопоставлении образца в выражениях case/in. В этом условии можно использовать связанные переменные:

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

Приложение А. Синтаксис образцов

Приблизительный синтаксис:

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]

Приложение Б. Примеры неопределённого поведения

Чтобы оставить возможность для будущих оптимизаций, спецификация включает случаи с неопределённым поведением.

Использование переменной в несопоставившемся образце:

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

Ruby Core © 1993–2025 Yukihiro Matsumoto
Licensed under the Ruby License.
Ruby Standard Library © contributors
Licensed under their own licenses.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API