В первой части мы узнали, как определяются экстракторы и как они могут быть использованы при сопоставлении с образцом. Мы рассмотрели лишь те экстракторы, что позволяют извлекать фиксированный набор значений. Но мы можем извлекать и произвольное число значений.
К примеру, мы можем определить образец, который представляет только списки из двух элементов, или образец, который представляет только списки из трёх элементов:
val xs = 3 :: 6 :: 12 :: Nil
xs match {
case List(a, b) => a * b
case List(a, b, c) => a + b + c
case _ => 0
}
Также мы можем представить списки, размер которых нам заранее неизвестен, с помощью оператора _*
:
val xs = 3 :: 6 :: 12 :: 24 :: Nil
xs match {
case List(a, b, _*) => a * b
case _ => 0
}
Здесь сопоставление с образцом проходит успешно. В первой case
-альтернативе происходит связывание
переменных a
и b
, остаток списка отбрасывается. Нам не важно, сколько элементов осталось в списке.
Конечно, экстракторы для таких образцов не могут быть определены с помощью методов из предыдущей главы. Нам нужно определить экстрактор, который принимает объекты определённого типа и возвращает коллекцию значений, длина которой неизвестна на этапе компиляции.
Как раз для этого и предназначен метод unapplySeq
. Посмотрим на возможные сигнатуры типов:
def unapplySeq(object: S): Option[Seq[T]]
Он принимает на вход значение типа S
и возвращает либо None
, если значение совсем не подходит,
или коллекцию значений некоторого типа T
, обёрнутую в Some
.
Давайте поупражняемся с новым типом экстракторов, пусть и на несколько надуманном примере. Предположим, что в нашем приложении нам необходимо извлечь имя пользователя. В английском языке имя может состоять из нескольких имён, например: "Daniel", "Catherina Johanna", или "Matthew John Michael". Нам бы хотелось уметь извлекать все части составного имени.
Вот очень простой экстрактор, определённый с помощью метода unapplySeq
, который
как раз этим и занимается:
object GivenNames {
def unapplySeq(name: String): Option[Seq[String]] = {
val names = name.trim.split(" ")
if (names.forall(_.isEmpty)) None else Some(names)
}
}
Если строка содержит одно или несколько имён, экстрактор извлечёт всю последовательность
имён. Если строка не содержит ни одного имени, экстрактор вернёт None
и строка не пройдёт
соответствие с образцом.
Давайте протестируем наш новый экстрактор:
def greetWithFirstName(name: String) = name match {
case GivenNames(firstName, _*) => "Good morning, " + firstName + "!"
case _ => "Welcome! Please make sure to fill in your name!"
}
Этот изящный метод возвращает приветствие для полного имени, извлекая лишь первую часть.
Так greetWithFirstName("Daniel")
вернёт "Good morning, Daniel!"
, в то время как,
вызвав greetWithFirstName("Catherina Johanna")
, мы получим "Good morning, Catherina!"
.
Иногда нам хочется извлечь определённый набор значений, число которых известно на этапе компиляции, а также дополнительный набор значений, число которых заранее неизвестно, их может и не быть вовсе.
Предположим, что нам нужно извлечь полное имя. Оно содержит имя человека (возможно составное), а
также и фамилию, к примеру: "John Doe"
или "Catherina Johanna Peterson"
. Мы хотим связать
фамилию с первой переменной образца, основное имя — со второй переменной и с третьей
коллекцию из оставшихся имён.
Для этого воспользуемся другим вариантом метода unapplySeq
:
def unapplySeq(object: S): Option[(T1, .., Tn-1, Seq[T])]
Как видно из сигнатуры, метод unapplySeq
также может возвращать кортеж значений, в котором
последний элемент должен быть коллекцией типа Seq
. Этот вариант очень похож на то, что
мы видели в предыдущей главе для метода unapply
.
Посмотрим на определение экстрактора:
object Names {
def unapplySeq(name: String): Option[(String, String, Seq[String])] = {
val names = name.trim.split(" ")
if (names.size < 2) None
else Some((names.last, names.head, names.drop(1).dropRight(1)))
}
}
Присмотритесь повнимательней к типу возвращаемого значения. Обратите внимание на то, как мы построили кортеж
в конструкторе Some
. Мы передали в него кортеж Tuple3
, построенный с помощью специального синтаксиса для кортежей.
Мы просто заключили три элемента — фамилию, имя и список дополнительных имён в круглые скобки и разделили их запятыми
(компилятор Scala передаст значения в конструктор Tuple3 за нас).
Значение пройдёт сопоставление с образцом для данного экстрактора только в том случае, если строка содержит по крайней мере имя и фамилию. Набор дополнительных имён образуется отбрасыванием первого и последнего элемента из списка имён.
Воспользуемся нашим экстрактором для приветствия пользователя:
def greet(fullName: String) = fullName match {
case Names(lastName, firstName, _*) => "Good morning, " + firstName + " " + lastName + "!"
case _ => "Welcome! Please make sure to fill in your name!"
}
Поэкспериментируйте с этим примером в интерпретаторе или в интерактивной странице IDE (worksheet).
В этой статье мы узнали, как определяются экстракторы, которые могут извлекать коллекции значений. Экстракторы — очень мощная возможность языка. С их помощью мы можем существенно расширить возможности стандартных образцов.
К концу этой серии статей мы ещё вернёмся к экстракторам. В следующей части мы узнаем о всевозможных вариантах применения сопоставления с образцом. Пока мы увидели лишь малую часть.
Обновление, 24.01.2013: Я поправил пример для экстрактора GivenNames
, спасибо Christophe Bliard за то, что
указал на ошибку.