2

I have the following abstrac base class

abstract class Vec2t<T : Number>(open var x: T, open var y: T) {

    companion object {
        val SIZE = 2 * when (/** T instance type */) {
            is Byte, is Ubyte -> 1
            is Short, is Ushort -> 2
            is Int, is Uint, is Float -> 4
            is Long, is Ulong, is Double -> 8
            else -> throw ArithmeticException("Type undefined")
        }
    }
}

that is implemented, for example, by Vec2

data class Vec2(override var x: Float, override var y: Float) : Vec2t<Float>(x, y)

I am wondering if it is possible to define SIZE in Vec2t and call on one of its implementation, for example Vec2.SIZE

elect
  • 5,677
  • 9
  • 44
  • 97
  • A `size` property shouldn't be inside the companion object. You don't have access to the `length()`-function there. You can't measure the size of an object before it's created. – marstran Nov 08 '16 at 12:54
  • So where shall `size` go for you? I find that it might absolutely be static. Sorry about `length()`, its a wip. – elect Nov 08 '16 at 13:18
  • @elect, seems like it should not be static, because you want size to be different for different instances of Vec2t -- means it should behave differently depending on what it is called on, definitely not the behavior one would expect from something static. – hotkey Nov 08 '16 at 13:42
  • 2
    The JVM does not allow you to have static fields which have different values for different instantiations of a generic class. – yole Nov 08 '16 at 13:45
  • @yole's comment got me thinking more about this and I don't see a use case for a `Vec2t.SIZE`. Even if you could write `Vec2t.SIZE` (which you can't) different implementations of `Vec2t` could use different sizes (one implementation might use an `Int` to store both bytes instead of two actual `Byte` instances). We want the implementations to provide the size and those can be provided statically (types) and dynamically (instances): http://stackoverflow.com/a/40490174/3255152 – mfulton26 Nov 08 '16 at 15:08

3 Answers3

4

You can inspect the type of x and/or y at runtime and lazily initialize a size property:

abstract class Vec2t<T : Number>(open var x: T, open var y: T) {
    val size by lazy {
        2 * when (x) {
            is Byte -> 1
            is Short -> 2
            is Int, is Float -> 4
            is Long, is Double -> 8
            else -> throw ArithmeticException("Type undefined")
        }
    }
}

If x and/or y were final then you could also skip lazy initialization and initialize it directly:

abstract class Vec2t<T : Number>(var x: T, var y: T) {
    val size = 2 * when (x) {
        is Byte -> 1
        is Short -> 2
        is Int, is Float -> 4
        is Long, is Double -> 8
        else -> throw ArithmeticException("Type undefined")
    }
}
mfulton26
  • 24,821
  • 4
  • 55
  • 71
  • I accepted the other one, since it's not possible to achieve it statically. Anyway, thanks for your suggestion, +1 – elect Nov 08 '16 at 13:49
2

I like the solution @mfulton26 suggested, it fits this use case very well. The below answer is rather intended for the cases when you cannot rely on the types of the values (e.g. T is Any, and you want to know exactly what it is, but all the instances of T passed to your class happened to be Strings).


First, you cannot have a val in a companion object that has different values for different instances of your class, because, first of all, the companion object has nothing to do with the instances, it's a separate object and getting its property doesn't (and cannot) involve instances of its enclosing class. Seems like size should be a member property.

But even for member properties and functions, inspecting the type argument T cannot be done directly, because generics in Kotlin are similar to those in Java and have type erasure too, so that at runtime you cannot operate with actual type arguments.

To do that, you can store a KClass<T> (or Class<T>) object in your Vec2t<T> and implement your logic based on it:

abstract class Vec2t<T : Number>(open var x: T, 
                                 open var y: T, 
                                 private val type: KClass<T>) {
    val size: Int by lazy {
        2 * when (type) {
            Byte::class -> 1
            Short::class -> 2
            Int::class, Float::class -> 4
            Long::class, Double::class -> 8
            else -> throw ArithmeticException("Type undefined")
        }
    }
}

This would require the subclasses to add the argument to their superclass constructor call:

class Vec2(override var x: Float, override var y: Float) : Vec2t<Float>(x, y, Float::class)

If you choose this approach, Kotlin can also offer you reified generics for help, so that you can avoid explicitly specifying SomeType::class at use site. For example, if your Vec2t<T> was not abstract, you could construct it with this factory function:

inline fun <reified T: Number> vec2t(x: T, y: T) = Vec2t(x, y, T::class)

With inline functions, you can access actual type arguments only because the function is inlined at call sites, thus its type arguments are always known at compile-time. Unfortunately, constructors cannot have any type parameters.

With usage:

val i = vec2t(1, 1) // Vec2t<Int> inferred from arguments type Int
println(i.size) // 8

val d = vec2t(1.0, 1.0) // the same but it's Vec2t<Double> this time
println(d.size) // 16
Community
  • 1
  • 1
hotkey
  • 111,884
  • 27
  • 298
  • 285
  • I might be overlooking something but I believe adding `type` to `Vec2t` can be avoided in lieu of a package-level function: `inline fun sizeOfVec2t() = 2 * ...` – mfulton26 Nov 08 '16 at 14:10
  • @mfulton26, since `sizeOfVec2t()` is inline, its `reified T` is only determined by the static/inferred type at call site. For example, `(intVec as Vec2t).sizeOfVec2t()` will fail to discover that `intVec` has `Int` for `T`. – hotkey Nov 08 '16 at 14:14
  • Ah yes, yes, I thought I might be overlooking something. Thank you @hotkey! – mfulton26 Nov 08 '16 at 14:16
2

Although you cannot "have static fields which have different values for different instantiations of a generic class" (as @yole commented), you can define properties on each implementation and their companion objects. e.g.:

abstract class Vec2t<T : Number> {
    abstract var x: T
    abstract var y: T
    abstract val size: Int
}

class Vec2f(override var x: Float, override var y: Float) : Vec2t<Float>() {
    companion object {
        const val SIZE = 2 * 4
    }

    override val size: Int
        get() = SIZE
}

class Vec2d(override var x: Double, override var y: Double) : Vec2t<Double>() {
    companion object {
        const val SIZE = 2 * 8
    }

    override val size: Int
        get() = SIZE
}

This allows you to reference Vec2f.SIZE, Vec2d.SIZE, etc. when you want to know the size for a specific implementation and reference vec2t.size when you have an instance (possibly of unknown implementation type) and get its size.

Community
  • 1
  • 1
mfulton26
  • 24,821
  • 4
  • 55
  • 71