Идиоматический способ проверить, равны ли некоторые (не все) свойства двух объектов в Котлине

Предположим, у меня есть класс с двумя экземплярами:

class Car(val id: Int, val color: Color, val pistons: Int, val spoiler: Boolean)

val audi = Car(1234, Color.BLUE, 8, false)
val bmw = Car(4321, Color.WHITE, 6, false)

Теперь я хотел бы проверить равенство для некоторых свойств (не для всех -> в этом случае я бы использовал класс данных)

fun looksSimilar(a: Car, b: Car) = a.color == b.color && a.spoiler == b.spoiler

Сейчас я ищу метод, который делает сравнение:

  • для более общих объектов T и их свойств
  • более идиоматично: никто не хочет читать тонны проверок на равенство
  • так же быстро

У меня появилось следующее предложение:

fun <T> Pair<T, T>.equalIn(vararg arguments: (T) -> Any?) =
    arguments.toList().all { it(first) == it(second) }

что позволяет мне написать вышеуказанную проверку как

val looksSimilar = (audi to bmw).equalIn({it.color}, {it.spoiler})

Кто-нибудь знает лучшее (например, более чистое/быстрое) решение?


Мой вариант использования следующий:

Я пишу приложение для Android с несколькими RecyclerView's (= причудливый вид для отображения списков)

У каждого RecyclerView есть ListAdapter (отвечает за базовый список)

Для каждого ListAdapter требуется DiffUtil.ItemCallback (для сравнения старых и новых элементов и внесения соответствующих изменений в представление)

val callback = object : DiffUtil.ItemCallback<Car>() {

    override fun areItemsTheSame(oldItem: Car, newItem: Car): Boolean
    // usually checks for id, e.g. oldItem.id == newItem.id

    override fun areContentsTheSame(oldItem: Car, newItem: Car): Boolean
    // checks if two items look the same.
    // Used for fancy enter/exit animations afaik.
    // e.g. (oldItem to newItem).equalIn({it.color}, {it.spoiler})
}

Tbh, я бы предпочел простые глупые проверки на равенство чему-то более причудливому, потому что все знают, как они выглядят и работают. Мне потребовалось почти 2 минуты, чтобы полностью понять, что делает ваша функция и как она работает. Если бы вместо этого были только простые проверки на равенство, я бы понял их за одну секунду при чтении кода.

vatbub 11.12.2020 16:13

На сайте использования было бы чище написать .equalIn(Car::color, Car::spoiler). В остальном ваш способ кажется мне приемлемым. Может быть, немного неудобно требовать промежуточную пару. Помимо этого, это создает функциональные объекты и массивы для проверки, так что есть немного оттока GC, которого у вас не было бы при непосредственном использовании == сравнений.

Tenfour04 11.12.2020 16:16

Вам не нужно ничего из этого, если вместо использования только class вы используете data class, именно поэтому data class существует, это позволяет структурное сравнение, просто говоря a == b, ваш Pistons также должен быть классом данных. И обычно в обратном вызове элемента сравнения areItemsTheSame это сравнение идентификаторов, а затем используемого вами контента oldItem == newItem

cutiko 11.12.2020 16:20

@vatbub я согласен, что на данный момент это не идеально для чтения; но это часть моего вопроса

m.reiter 11.12.2020 16:45

@ Tenfour04 Я не знал, что смогу это сделать, спасибо +1

m.reiter 11.12.2020 16:46

@cutiko, если честно, я не слишком знаком с ItemCallback. В документации areContentsTheSame говорится: «Например, если вы используете DiffUtil с RecyclerView.Adapter, вы должны вернуть, совпадают ли визуальные представления элементов». Для меня это не обязательно включает количество поршней в моем примере

m.reiter 11.12.2020 16:49

Лично вместо функции расширения я бы, вероятно, сделал какой-то класс сравнения (например, Comparator, который обрабатывает только equals), чтобы вы могли создать его экземпляр, настроить, как вам нравится, и просто использовать его для всех ваших проверок. Вы можете передать свой список свойств во время строительства, чтобы определить все проверки, которые он должен выполнять, а затем вы создаете его один раз - и у вас могут быть разные варианты, если они вам нужны.

cactustictacs 11.12.2020 18:23
3
7
507
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Просматривая историю StackOverflow, я снова нашел этот вопрос, и он щекотал мне мозг, поэтому я немного подумал об этом. Я по-прежнему придерживаюсь своего мнения, что я предпочитаю явные проверки if (проще понять IMO, самая быстрая производительность), но если бы мне пришлось делать это с помощью функции расширения, я бы предпочел использовать список вместо пары (допуская более двух inputs), и я бы использовал справочный синтаксис для лямбда-выражений на сайте вызова, сделав его немного более кратким:

fun <T> List<T>.equalIn(vararg arguments: (T) -> Any?): Boolean {
    if (isEmpty()) return false
    val argumentsList = arguments.toList()
    return all { item -> argumentsList.all { it(item) == it(first()) } }
}

fun main() {
    val audi = Car(1234, Color.BLUE, 8, false)
    val bmw = Car(4321, Color.BLUE, 6, false)
    println(listOf(audi, bmw).equalIn(Car::color, Car::spoiler))
}

Приятно видеть, что вопрос годовой давности все еще щекочет ваш мозг :D

m.reiter 08.11.2022 08:48

Я бы использовал if (isEmpty()) return true, хотя.

user42723 08.11.2022 13:36

Другие вопросы по теме