Полиморфизм на функции расширения в Котлине

У меня есть несколько классов, которые я не контролирую, на которых я уже создал несколько методов расширения с одинаковыми именами для нескольких общих "атрибутов". Функции расширения с одинаковыми именами всегда возвращают один и тот же тип значения, хотя рассчитываются по-разному для каждого типа получателя. Вот упрощенный пример, основанный на встроенных типах только для одного атрибута:

// **DOES NOT COMPILE**

// three sample classes I don't control extended for .len
inline val String.len get() = length
inline val <T> List<T>.len get() = size
inline val <T> Sequence<T>.len get() = count()

// another class which needs to act on things with .len
class Calc<T>(val obj:T) {       // HERE IS THE PROBLEM...
  val dbl get() = obj?.len * 2   // dummy property that doubles len
  // ... and other methods that use .len and other parallel extensions 
}

fun main(a:Array<String>) {
  val s = "abc"
  val l = listOf(5,6,7)
  val q = (10..20 step 2).asSequence()
  val cs = Calc(s)
  val cl = Calc(l)
  val cq = Calc(q)
  println("Lens:  ${cs.dbl}, ${cl.dbl}, ${cq.dbl}")
}

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

Я исследовал следующее, но пока не нашел работоспособного решения:

  • generics, в приведенном выше примере, но не может получить правильный синтаксис.
  • закрытые классы, но я не могу контролировать эти классы.
  • типы соединений, которые я нашел, не поддерживаются в Kotlin.
  • классы-обертки, но не смогли получить правильный синтаксис.
  • Передача лямбда-а-ля в этом объяснении в блоге, но не поняла его правильно, и казалось бы оптимальным разослать несколько лямбд-знаков для каждого метода.

Должен быть лучший способ, верно?

1 ответ

Решение

Вот пример с запечатанными классами и одним свойством расширения, чтобы преобразовать что-либо во что-то, что может дать вам len или же double, Не уверен, что он лучше читается.

val Any?.calc get() = when(this) {
    is String -> Calc.CalcString(this)
    is List<*> -> Calc.CalcList(this)
    is Sequence<*> -> Calc.CalcSequense(this)
    else -> Calc.None
}

/* or alternatively without default fallback */

val String.calc get() = Calc.CalcString(this)
val List<*>.calc get() = Calc.CalcList(this)
val Sequence<*>.calc get() = Calc.CalcSequense(this)

/* sealed extension classes */

sealed class Calc {

    abstract val len: Int?

    val dbl: Int? by lazy(LazyThreadSafetyMode.NONE) { len?.let { it * 2 } }

    class CalcString(val s: String): Calc() {
        override val len: Int? get() = s.length
    }

    class CalcList<out T>(val l: List<T>): Calc() {
        override val len: Int? get() = l.size
    }

    class CalcSequense<out T>(val s: Sequence<T>): Calc() {
        override val len: Int? get() = s.count()
    }

    object None: Calc() {
        override val len: Int? get() = null
    }

}

fun main(args: Array<String>) {
    val s = "abc".calc
    val l = listOf(5,6,7).calc
    val q = (10..20 step 2).asSequence().calc

    println("Lens:  ${s.dbl}, ${l.dbl}, ${q.dbl}")
}

Расширения разрешаются статически.

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

Я лично считаю, что это дизайнерское решение ограничивает удобство использования расширений, поскольку мы не можем вызывать функцию по полиморфизму подтипа. В настоящее время нет другого способа, кроме использования специального полиморфизма путем перегрузки функций расширения и принятия предполагаемого получателя в качестве параметра, как показано в этом ответе. Затем JVM выбирает соответствующую перегруженную функцию для вызова во время выполнения на основе типа аргумента получателя.

Есть другой способ решить эту проблему, используя классы типов; вместо определенияlen для каждого типа вы определяете интерфейс Lengthy у этого есть метод length(), определите преобразования для каждого конкретного типа в Lengthyи вызвать someLengthy.length(). Подробнее см. В этой статье. Однако в статье используется Scala, который поддерживает неявные преобразования, а Kotlin - нет, поэтому код не будет таким лаконичным.

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