Jak sortować na podstawie / porównywać wiele wartości w Kotlinie?

85

Powiedzmy, że mam class Foo(val a: String, val b: Int, val c: Date)i chcę posortować listę Foos na podstawie wszystkich trzech właściwości. Jak bym to zrobił?

Kirill Rakhman
źródło

Odpowiedzi:

149

Stdlib Kotlina oferuje do tego szereg przydatnych metod pomocniczych.

Najpierw możesz zdefiniować komparator za pomocą compareBy()metody i przekazać go do sortedWith()metody rozszerzającej, aby otrzymać posortowaną kopię listy:

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

Po drugie, możesz pozwolić Fooimplementacji Comparable<Foo>za pomocą compareValuesBy()metody pomocniczej:

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

Następnie możesz wywołać sorted()metodę rozszerzenia bez parametrów, aby otrzymać posortowaną kopię listy:

val sortedList = list.sorted()

Kierunek sortowania

Jeśli potrzebujesz sortować rosnąco według niektórych wartości i malejąco według innych wartości, standardowa biblioteka oferuje również funkcje do tego:

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

Uwagi dotyczące wydajności

varargWersja compareValuesBynie jest wstawiane w kodu bajtowego oznaczającego anonimowe klas zostanie wygenerowany dla lambda. Jeśli jednak same lambdy nie przechwytują stanu, będą używane pojedyncze wystąpienia zamiast tworzenia wystąpienia lambd za każdym razem.

Jak zauważył Paul Woitaschek w komentarzach, porównanie z wieloma selektorami spowoduje za każdym razem utworzenie instancji tablicy dla wywołania vararg. Nie możesz tego zoptymalizować, wyodrębniając tablicę, ponieważ będzie ona kopiowana przy każdym wywołaniu. Z drugiej strony możesz wyodrębnić logikę do statycznej instancji komparatora i użyć jej ponownie:

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)
    }
}
Kirill Rakhman
źródło
4
Zwróć uwagę, że jeśli używasz wielu funkcji lambda (istnieje przeciążenie z dokładnie jedną, która jest wstawiana ), nie są one wstawiane . Co oznacza, że ​​każde wywołanie comapreTo tworzy nowe obiekty. Aby temu zapobiec, możesz przenieść selektory do obiektu towarzyszącego, aby były one przydzielane tylko raz. Utworzyłem wycięty
Paul Woitaschek
1
@KirillRakhman Tworzy singletony dla funkcji, ale nadal przydziela tablice:ANEWARRAY kotlin/jvm/functions/Function1
Paul Woitaschek
1
Począwszy od Kotlin 1.1.3 compareByz wieloma lambdami nie będzie alokować nowej tablicy przy każdym compareTowywołaniu.
Ilya
1
@Ilya, czy możesz wskazać mi odpowiednią listę zmian lub inne informacje dotyczące tego rodzaju optymalizacji?
Kirill Rakhman,
0

Jeśli chcesz posortować w kolejności malejącej, możesz użyć zaakceptowanej odpowiedzi:

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

Lub utwórz funkcję rozszerzenia, taką jak 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
}

i użyć: list.sortedWith(compareByDescending ({ it.a }, { it.b }, { it.c })).

CoolMind
źródło
0

Jeśli potrzebujesz sortować według wielu pól, a niektóre pola malejąco, a inne rosnąco, możesz użyć:

YOUR_MUTABLE_LIST.sortedWith(compareBy<YOUR_OBJECT> { it.PARAM_1}.thenByDescending { it.PARAM_2}.thenBy { it.PARAM_3})
Yago Rey Viñas
źródło
1
Jest to zawarte w mojej odpowiedzi.
Kirill Rakhman