Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.
Generics
Generics let you write more general code that is not tied to specific types and is much more reusable, avoiding copy-pasting in many situations. If you have code that is applicable to more than one data type, you can make that type into a generic parameter and use it to write your logic.
Generic type casts
Given example definitions:
interface Producer<T> {
fun nextValue(): T
fun hasNext(): Boolean
}
class Stack<T>(initialValues: List<T>): Producer<T> {
private val values = initialValues.toMutableList()
fun push(value: T) {
values.add(value)
}
fun pop(): T? = values.removeLastOrNull()
override fun nextValue(): T = values.removeLast()
override fun hasNext(): Boolean = values.isNotEmpty()
}
Check whether a type is a specific type with generic parameters using is. Omit the type parameters:
val Producer<String>.isStack: Boolean
get() = this is Stack
Same for casting using as, omit the type parameters:
fun Producer<String>.takeOne(): String? =
if (isStack)
(this as Stack).pop()
else
try { nextValue() } catch (_: Exception) { null }
If you really like specifying the type arguments, you can use star-projection. The result is the same:
fun testForStarStack(producer: Producer<*>): Boolean {
return (producer is Stack) == (producer is Stack<*>)
}
fun <T: Number> testForTypedStack(producer: Producer<T>): Boolean {
return (producer is Stack) == (producer is Stack<*>)
}
Normally, you can’t type-check against a generic type. These are erased at run-time. But with inline functions, you can have special, reified generic type parameters that can be type-checked and cast to.
inline fun <reified T> Iterable<*>.firstOfType(): T? =
firstOrNull { it is T } as T?