Кейс-классы и сопоставление с шаблоном в Scala
Оператор switch во многих языках воплощает задумку классификации данных на несколько категорий. Но большинство реализаций не могут поддерживать классификацию объектов и поэтому редко используются в программах. В Scala есть очень мощная система сопоставления объекта с образцом. Однако экземпляр далеко не каждого класса можно сопоставлять с образцом без лишних телодвижений.
case class CaseClasName(parameterList) { /*опциональное тело класса*/ }
Все поля кейс-класса немутирующие и публичные. Это не является нарушением принципа инкапсуляции, поскольку он неприменим к публичным параметрам кейс-класса. Инкапсуляция полей имеет место, когда поля класса должны меняться некоторым сложным взаимосвязанным образом, и поэтому изменение полей должно производиться через методы класса. Здесь же нет никакого изменяемого состояния (мы крайне не рекомендуем его вводить в теле класса, чревато неожиданными последствиями), и инкапсулировать нечего. Инстанцировать экземпляр кейс-класса можно удобным синтаксисом:
CaseClasName(parameter1, parameter2, … , parameterN)
Примечание: здесь «под капотом» находится автоматически сгенерированный метод apply для этого класса, позволяющий пользоваться таким синтаксисом.
Пример:
case class User(name: Stirng, email: String) val user = User("FooBar", "[email protected]")
Второй тип сущностей, case object, похож на case class, но не имеет полей. Зачем же он тогда нужен? Для создания строго типизированных перечислений.
Заметим, что кейс-класс не может наследовать от другого кейс-класса, поэтому рекомендуем держать иерархию данных как можно более плоской и как можно менее древоподобной. Всегда можно задать кейс-классам общий надтип и, соответственно, нужный набор полей при помощи трейтов. Например, можно сделать так:
trait Account { def username: String def email: String } case class User(username: String, email: String) extends Account case class SuperUser(username: String, email: String, privileges: String) extends Account
Каким образом работает сопоставление с шаблоном (pattern matching)? При помощи следующего синтаксиса:
value match { case pattern1 if /*опциональное условие 1*/ => value1 case pattern2 if /*опциональное условие 1*/ => value2 ... case patternN if /*опциональное условие N*/=> valueN }
Шаблоны могут быть очень разнообразными в зависимости от используемого типа данных.
Кейс-класс в сопоставлении с шаблоном можно разложить на его параметры при помощи «вызова конструктора» этого кейс-класса. Например, приведенному выше классу User будет соответствовать шаблон User(name, email). Здесь переменные name и user извлекаются из объекта класса User и после этого становятся доступны блоку value, следующему за =>. Кроме того, вы можете ввести переменную для обозначения объекта, сопоставляемого с образцом, при помощи символа @. Например, такой шаблон введёт переменную user для блока value:
user @ User(name, email)
Ещё одной полезной особенностью сопоставления с образцом является возможность зафиксировать некоторые поля класса или же, наоборот, игнорировать некоторые из них.
Например, мы можем выбирать пользователя только с именем «dave» шаблоном User("dave", email). Если же нам безразличен email пользователя, мы можем не вводить переменную для этого поля шаблоном User(name, _).
Стоит отметить, что сопоставление с образцами происходит в порядке их расположения. Если шаблон User(name, email) находится выше шаблона User("dave", email), то сопоставления с последним никогда не произойдет, потому что первому из шаблонов соответствует любой экземпляр класса User.
Если же вам не нужны поля класса, а нужно просто соответствие по типу, используйте шаблон вида valName: TypeName.
Кроме того, существует шаблон «_», которому удовлетворяет любой объект.
Результат сопоставления с образцом — значение, возвращённое из value, приведенное к общему надтипу всех блоков value1, … valueN.
Если ни одному из шаблонов значение не соответствует, выбрасывается исключение PatternMatchingException.
Приведем пример, объединяющий написанное выше:
trait Account case class User(name: Stirng, email: String) extends Account case class SuperUser(name: Stirng, email: String, privileges: String) extends Account case object SomethingStrange extends Account def matcherFunc(acc: Account): Account = acc match { case usr @ User("dave", email) => println("This is dave, his email:" + email) usr case usr @ User(name, email) if email == "" => println("User " + name + " has no email" usr case usr @ User(name, email) => println("User " + name + " has email " + email) usr case su: SuperUser => println("It`s a superuser") su case smth => println("We don’t know what is it") } matcherFunc(User("dave", "[email protected]")) matcherFunc(User("dave", "")) matcherFunc(User("notadave", "")) matcherFunc(User("john", "[email protected]")) matcherFunc(SuperUser("neo", "[email protected]", "every possible")) matcherFunc(SomethingStrange)
На самом деле, сопоставление с образцом можно применять не только на кейс-классах. На всех типах-примитивах (числа, строки, символы, …), и типов, для которых определён метод-экстрактор. Например, вы можете сопоставлять строку с регулярным выражением. Просто создайте несколько регулярных выражений и применяйте сопоставление с шаблоном:
val regWord = """([\w]+)""".r val regTwoWords = """([\w]+) ([\w]+)""".r def regexMatcher(s: String) = s match { case regWord(word) => println("Single word: " + word) case regTwoWords(word1, word2) => println("First word: " + word1 + "second word: " + word2) } regexMatcher("hello") regexMatcher("hello word")
Такие действия можно осуществлять, определяя метод unapply и объект-экстрактор. Подробнее об этом можно прочитать в документации. Экстрактор имеет ещё одно полезное применение: с помощью него можно распаковывать кейс-классы и другие классы, имеющие экстрактор, используя точно такой же синтаксис, как вид шаблона в сопоставлении с образцом:
case class User(name: Stirng, email: String) extends Account val user = User("john", "[email protected]") val User(someName, someEmail) = user
Хотя сопоставление с шаблоном имеет больше возможностей, о которых вы можете почитать в документации, здесь мы ограничимся только этими.