Сопоставление с образцом
Сопоставление с образцом — это возможность выполнять глубокое сопоставление структурированных значений: проверять структуру и связывать сопоставленные части с локальными переменными.
В 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"
Декомпозицию реализуют следующие основные классы и классы из библиотек:
Предложения-ограничители
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.