Статьи

Улучшенное сопоставление с образцом в Котлине

У Котлина нет точного соответствия шаблону, и это нормально. Чтобы создать подходящие классы в Scala, для его работы требуется очень много накладных расходов, и я очень уважаю цель Котлина — нигде не добавлять много накладных расходов. Но это не значит, что мы не можем попытаться найти собственный способ приблизиться к сопоставлению с образцом.

Использование when

Котлин, when блок невероятно удобен; У этого есть несколько способов, которыми это может работать. Первый способ — это простая проверка на равенство:

1
2
3
4
5
when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> print("x is neither 1 nor 2")
}

И случаи могут быть объединены с помощью запятой:

1
2
3
4
when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

Это также можно сделать и in проверках:

1
2
3
4
when(x) {
    in 1..10 -> print("in range")
    is String -> print("I guess it's not even a number")
}

И с последним, вы можете видеть, что вы можете объединить любой из предыдущих в один блок when . Вам также не нужно else если вы используете when как выражение вместо выражения. Вам также не нужно else если версия выражения имеет все перечисленные возможности (насколько может сказать компилятор).

Вы также можете использовать без значения top, чтобы он просто работал как набор блоков if-else if :

1
2
3
4
5
when {
    a == b -> doSomething()
    b == c -> doSomethingElse()
    else -> doThatOtherThing()
}

Имея все эти возможности, знаете ли вы, какую версию мы будем использовать для построения нашей системы сопоставления с образцом? Удивительно, но это самый простой с проверками на равенство.

Теперь я понимаю, что вы можете делать sealed классы как своего рода объединенный тип, и is когда нужно сопоставлять их, но у этого есть ограниченный набор вариантов использования. Я полагаю, что с помощью следующей системы вы сможете охватить все варианты использования.

Так как мы это делаем?

Во-первых, мы понимаем, что проверки на равенство используют equals() и equals() — это то, что мы можем переопределить. Итак, мы делаем какой-то тип Pattern для использования в блоке when , и equals() проверяет, является ли объект is Pattern и переходит к использованию Pattern для вычисления «равенства».

Вот краткий обзор того, как это выглядит слабо:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
interface Pattern<in Subject> {
    fun match(subject: Subject): Boolean
}
 
class MySubject {
    
    fun equals(other: Any): Boolean {
        if(other is Pattern<*>)
            return other.match(this)
        else
    }
}
 
class SomePattern {
    override fun match(subject: Any): Boolean {
        
    }
}

И это будет использоваться следующим образом:

1
2
3
4
5
6
7
val x = MySubject()
when(x) {
    SomePattern() -> doSomething()
    SomeOtherPattern() -> doSomethingElse()
}
So, you probably get the idea now.

Tweaks

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

Shortcutting

Во-первых, вы можете попытаться сделать шаблоны немного более доступными, используя ярлыки на предметном классе. Если шаблон параметризован — например, List может иметь параметризованный шаблон, который проверяет определенную длину, IsLength который должен принимать параметр для длины — вы можете поместить функцию-ярлык в companion object вместо прямого вызова конструктор класса. Если он не параметризован, вы можете кэшировать экземпляр шаблона как значение в companion object предметного класса.

Лямбда-паттерн

Интерфейс Pattern имеет только один метод. Вы знаете что это значит? Это функциональный интерфейс (в терминах Java 8). Это означает, что в Kotlin Pattern даже не нужно существовать. Вместо того, чтобы equals() проверял, является ли объект Pattern , пусть он проверяет, является ли это Function1<SubjectType, Boolean> . Очевидно, что вы все еще можете использовать некоторые встроенные шаблоны, но теперь вы можете даже вставить некоторые лямбды на лету в блок when :

1
2
3
when(x) {
    {it: Subject -> it.isTheCoolest} -> doSomething()
}

К сожалению, это не так уж и полезно, поскольку вывод типа не сможет определить тип входного параметра. Тебе надо. В этот момент вы также можете использовать непараметризованный блок:

1
2
3
when {
    x.isTheCoolest -> doSomething()
}

Это не значит, что использование лямбд для шаблона — это плохо. Вы по-прежнему можете использовать ссылки на методы, что делает возможным быстрые и простые шаблоны на лету (даже для свойств):

1
2
3
when(x) {
    Subject::isTheCoolest -> doSomething()
}

Это, безусловно, лучше, чем полностью квалифицированная лямбда. Более сложные лямбды могут быть определены как функции или значения:

01
02
03
04
05
06
07
08
09
10
fun moreComplexCheck(subject: Subject): Boolean {
    
}
 
val moreComplexCheck2 = {subject: Subject -> …}
 
when(x) {
    ::moreComplexCheck -> doSomething()
    moreComplexCheck2 -> doSomethingElse()
}

Outro

Итак, вот оно! Лучшее сопоставление с образцом в Котлине! Что вы думаете? Я понимаю, что это неправильное использование equals() , но я думаю, что в некоторых случаях оно того стоит.