2

Kotlin allows you to extend concrete instances of a generically typed class. For example, suppose I have the following class Foo, with extension functions Foo<Int>.bar() and Foo<String>.bar():

class Foo<T>(t: T) {
  val a: String = "bar"
  val b: Int = 111
}

fun Foo<Int>.bar() = a
fun Foo<String>.bar() = b

fun main(args: Array<String>) {
  val stringBar: String = Foo(1).bar()
  val intBar: Int = Foo("").bar()
}

Is it possible to achieve this behavior without extension functions, and if so, how do I convert the extension functions into members? Is it possible without renaming or changing the type signatures?

Sergio
  • 27,326
  • 8
  • 128
  • 149
breandan
  • 1,965
  • 26
  • 45

3 Answers3

1

You can create inline member function with reified generic parameter:

class Foo<T>(t: T) {
    val a: String = "bar"
    val b: Int = 111

    inline fun <reified T, E> member(): E? {
        var r: E? = null
        when(T::class) {
            String::class -> r = b as E
            Int::class -> r = a as E
        }
        return r
    }
}

fun main(args: Array<String>) {
   val str = Foo(1).member<Int, String>()
   val i = Foo("").member<String, Int>()
}

But extension functions are better in your case: they are type-safe, more concise and readable.

Sergio
  • 27,326
  • 8
  • 128
  • 149
  • Your code also allows calling e.g. `Foo(1.0).member()` which isn't desirable. – Alexey Romanov Jan 12 '19 at 18:43
  • First generic parameter is meant to be a class type that is checked in `when` expression, the second - class type of returning result. If we specify `File` as the second generic type, it should be handled in the function, otherwise Exception will be thrown. – Sergio Jan 12 '19 at 20:02
  • "Meant to", but there is no way to enforce or even check this. E.g. you have val x = Foo(1) to start with but then modify to 1.0, you need to remember to modify all method calls too. In the original code you don't, the compiler will tell you. And the second parameter here manages to be less useful than returning Any (IMO). – Alexey Romanov Jan 13 '19 at 05:00
1

No, it isn't possible (the currently accepted answer doesn't behave at all similarly to your code).

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487
  • So how does Kotlin implement extensions to be able to dispatch based on a generic type (which is normally erased), and how does Kotlin make extensions on generic data types available to Java consumers, i.e. what makes extensions special here? – breandan Jan 12 '19 at 19:40
  • Like normal overloading, the method is selected at compile-time, before anything is erased. For Java consumers, extensions are just methods taking the receiver as an argument. Because of JVM restrictions, they'll have different names in this case. – Alexey Romanov Jan 13 '19 at 04:46
  • Sorry for the misleading last sentence of the previous comment. Because return types are different after erasure, this overloading is allowed by JVM. I don't remember if Java itself allows it. – Alexey Romanov Jan 13 '19 at 15:08
  • Yes, the JVM allows two functions which differ only by return type in the same scope, but Java does not. See this thread: https://stackoverflow.com/questions/42916801/how-does-erasure-work-in-kotlin – breandan Jan 13 '19 at 15:19
  • What I meant is a combination of argument types differing before erasure and return types afterwards. Normal reasons for not allowing overload solely on return types don't apply. But yes, Java doesn't allow it (https://ideone.com/pgyRCx) while Kotlin does, whether for extension methods or not. – Alexey Romanov Jan 20 '19 at 17:36
1

Using reflection you don't have to do the mapping between type and property by yourself.

The member function propertyValue() will return the value of the first property which has the type of the generic parameter or null.

class Foo {
    val a: String = "bar"
    val b: Int = 111

    inline fun <reified T> propertyValue() = Foo::class.memberProperties.firstOrNull {
        it.returnType == T::class.createType()
    }?.get(this)
}

Foo().propertyValue<Int>()) // 111
Foo().propertyValue<String>()) // bar
Foo().propertyValue<Double>()) // null

You could of course extend this function so it would give you the values of all properties of the desired type T (if you have multiple Int properties for example).

Willi Mentzel
  • 27,862
  • 20
  • 113
  • 121