Як сортувати на основі / порівнювати кілька значень у Kotlin?


84

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

Відповіді:


146

Stdlib Котліна пропонує ряд корисних допоміжних методів для цього.

По-перше, ви можете визначити порівняльник за допомогою 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 створює нові об'єкти. Щоб запобігти тому, що ви можете перемістити селектори до супутнього об’єкта, щоб селектори виділялися лише один раз. Я створив тут відрізаний
Пол Войташек

1
@KirillRakhman Він створює синглтони для функцій, але все одно виділяє масиви:ANEWARRAY kotlin/jvm/functions/Function1
Пол Войташек,

1
Починаючи з Kotlin 1.1.3 compareByз декількома лямбдами, не буде виділятися новий масив під час кожного compareToдзвінка.
Ілля

1
@ Ілля, не могли б ви вказати мені відповідний журнал змін чи іншу інформацію для такого роду оптимізації?
Kirill Rakhman


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 })).


0

Якщо вам потрібно сортувати за кількома полями, а деякі поля за спаданням, а інші за зростанням, ви можете використовувати:

YOUR_MUTABLE_LIST.sortedWith(compareBy<YOUR_OBJECT> { it.PARAM_1}.thenByDescending { it.PARAM_2}.thenBy { it.PARAM_3})

1
Це висвітлено в моїй відповіді.
Кирило Рахман
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.