Кейс-классы и сопоставление с шаблоном в Scala | OTUS

Кейс-классы и сопоставление с шаблоном в Scala

Оператор switch во многих языках воплощает задумку классификации данных на несколько категорий. Но большинство реализаций не могут поддерживать классификацию объектов и поэтому редко используются в программах. В Scala есть очень мощная система сопоставления объекта с образцом. Однако экземпляр далеко не каждого класса можно сопоставлять с образцом без лишних телодвижений. Для упрощения этого процесса были введены два специальных вида сущностей: кейс-классы (case classes) и кейс-обжэкты (case objects). Кейс-классы подобны именованным кортежам, используемым для хранения данных. Часто они могут не иметь своих методов, выступая пассивными контейнерами, ведь 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

Хотя сопоставление с шаблоном имеет больше возможностей, о которых вы можете почитать в документации, здесь мы ограничимся только этими.

Не пропустите новые полезные статьи!

Спасибо за подписку!

Мы отправили вам письмо для подтверждения вашего email.
С уважением, OTUS!

Автор
0 комментариев
Для комментирования необходимо авторизоваться
Популярное
Сегодня тут пусто