Reputation: 7190
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
Upvotes: 3
Views: 2575
Reputation: 31274
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.
Upvotes: 3
Reputation: 148179
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 String
s).
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
Upvotes: 2
Reputation: 31274
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")
}
}
Upvotes: 4