Как сортировать / сравнивать несколько значений в Котлине?

85

Скажем, у меня есть class Foo(val a: String, val b: Int, val c: Date)и я хочу отсортировать список на Fooоснове всех трех свойств. Как мне это сделать?

Кирилл Рахман
источник

Ответы:

149

Stdlib Kotlin предлагает для этого ряд полезных вспомогательных методов.

Во-первых, вы можете определить компаратор с помощью compareBy()метода и передать его sortedWith()методу расширения, чтобы получить отсортированную копию списка:

val list: List<Foo> = ...
val sortedList = list.sortedWith(compareBy({ it.a }, { it.b }, { it.c }))

Во-вторых, вы можете позволить Fooреализовать Comparable<Foo>с помощью compareValuesBy()вспомогательного метода:

class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {
    override fun compareTo(other: Foo)
            = compareValuesBy(this, other, { it.a }, { it.b }, { it.c })
}

Затем вы можете вызвать sorted()метод расширения без параметров, чтобы получить отсортированную копию списка:

val sortedList = list.sorted()

Направление сортировки

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

list.sortedWith(compareBy<Foo> { it.a }.thenByDescending { it.b }.thenBy { it.c })

Соображения производительности

varargВерсия compareValuesByне встраиваются в байткоде означающего анонимные классов будет сгенерирована для лямбды. Однако, если сами лямбды не фиксируют состояние, будут использоваться экземпляры синглтонов, а не каждый раз создавать экземпляры лямбда-выражений.

Как отметил Пол Войташек в комментариях, сравнение с несколькими селекторами будет каждый раз создавать экземпляр массива для вызова vararg. Вы не можете оптимизировать это, извлекая массив, поскольку он будет копироваться при каждом вызове. С другой стороны, вы можете извлечь логику в экземпляр статического компаратора и повторно использовать его:

class Foo(val a: String, val b: Int, val c: Date) : Comparable<Foo> {

    override fun compareTo(other: Foo) = comparator.compare(this, other)

    companion object {
        // using the method reference syntax as an alternative to lambdas
        val comparator = compareBy(Foo::a, Foo::b, Foo::c)
    }
}
Кирилл Рахман
источник
4
Обратите внимание, что если вы используете несколько лямбда-функций (есть перегрузка только с одной встроенной), они не встроены . Это означает, что каждый вызов comapreTo создает новые объекты. Чтобы предотвратить это, вы можете переместить селекторы в сопутствующий объект, чтобы селекторы выделялись только один раз. Я создал фрагмент здесь: gist.github.com/PaulWoitaschek/7f3c4d5310a66ed4984785ee2d6f70ed
Пол Войташек
1
@KirillRakhman Он создает синглтоны для функций, но по-прежнему выделяет массивы:ANEWARRAY kotlin/jvm/functions/Function1
Пол Войташек
1
Начиная с Kotlin 1.1.3 compareByс несколькими лямбдами, новый массив не будет выделяться при каждом compareToвызове.
Илья
1
@Ilya, не могли бы вы указать мне на соответствующий журнал изменений или другую информацию для такого рода оптимизации?
Кирилл Рахман
1
@KirillRakhman github.com/JetBrains/kotlin/commit/…
Илья
0

Если вы хотите отсортировать по убыванию, вы можете использовать принятый ответ:

list.sortedWith(compareByDescending<Foo> { it.a }.thenByDescending { it.b }.thenByDescending { it.c })

Или создайте функцию расширения, например compareBy:

/**
 * Similar to
 * public fun <T> compareBy(vararg selectors: (T) -> Comparable<*>?): Comparator<T>
 *
 * but in descending order.
 */
public fun <T> compareByDescending(vararg selectors: (T) -> Comparable<*>?): Comparator<T> {
    require(selectors.size > 0)
    return Comparator { b, a -> compareValuesByImpl(a, b, selectors) }
}

private fun <T> compareValuesByImpl(a: T, b: T, selectors: Array<out (T) -> Comparable<*>?>): Int {
    for (fn in selectors) {
        val v1 = fn(a)
        val v2 = fn(b)
        val diff = compareValues(v1, v2)
        if (diff != 0) return diff
    }
    return 0
}

и использовать: list.sortedWith(compareByDescending ({ it.a }, { it.b }, { it.c })).

CoolMind
источник
0

Если вам нужна сортировка по нескольким полям, а некоторые поля по убыванию, а другие по возрастанию, вы можете использовать:

YOUR_MUTABLE_LIST.sortedWith(compareBy<YOUR_OBJECT> { it.PARAM_1}.thenByDescending { it.PARAM_2}.thenBy { it.PARAM_3})
Яго Рей Виньяс
источник
1
Это описано в моем ответе.
Кирилл Рахман