Polymorphism on extension functions in Kotlin

2019-07-02 15:33发布

问题:

I have a several classes I don't control, upon which I've already created several identically-named extension methods across several common "attributes". The identically-named extension functions always return the same value type, though calculated in different ways for each type of receiver. Here is a simplified example based on built-in types for just one attribute:

// **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}")
}

Imagine several other "common" properties extended in the same manner as .len in some classes I don't control. If I don't want to repeat myself in every class, how do I construct a properly typed class that can operate on .len (and other such properties) generically for these three classes?

I've researched the following but not found workable solutions yet:

  • generics, in the example above, but can't get the syntax right.
  • sealed classes, but I don't have control of these classes.
  • union types, which I've found aren't supported in Kotlin.
  • wrapper classes, but couldn't get the syntax right.
  • passing lambdas a la this blog explanation, but didn't get it right, and it seemed boptimalsu to pass multiple lambdas around for every method.

There must be a better way, right?

回答1:

Here's a example with sealed classes and a single extension property to convert anything to something which can give you len or double. Not sure if it has better readability thogh.

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