In Kotlin, how can I derive fields from a base definition (abstract, interface, inheritance, something else) without explicitly overriding them?
The closest I can get is:
abstract class Person {
open val name: String = "Stranger"
}
data class Doctor(
override val name: String,
val yearsOfExperience: Int
): Person()
val doc = Doctor(yearsOfExperience = 20, name = "Eric")
But ideally, since I have a use case of an unchangeable model with hundreds of fields, I would like to have:
abstract class Person {
open val name: String = "Stranger"
}
data class Doctor(
val yearsOfExperience: Int
): Person()
val doc = Doctor(yearsOfExperience = 20, name = "Eric")
You can't. If you want Doctor to be able to change Person.name to anything other than what's defined in Person, you are by definition overriding the behavior in Person - Kotlin is just forcing you to make that contract explicit.
If it didn't do that, it would be possible to do something like this:
data class Doctor(
val yearsOfExperience: Int
) : Person()
Then later decide to add a name field:
data class Doctor(
val yearsOfExperience: Int,
val name: String = "Doctor"
) : Person
Now Doctor.name has a default value of "Doctor" which is different to the expected behavior defined in Person. Previous code that did Doctor(yearsOfExperience = 20) will now behave differently - it will get the name "Doctor" instead of "Stranger". Kotlin is making sure that you realise that, and explicitly ask for it by adding the override modifier.
So you can omit the fields you want to inherit, but not the ones you want to override.
Related
I have a custom type declared as:
data class User (
val name: String,
val rank: Int
)
and wish to declare a Map whose keys are the name and values are the ranks, referring to the types as declared for User.
I have tried kotlin reflection,
val foo1 = Map <User::name, User::rank>() //compiler error 'not enough information to infer type...'
Is there a normative way to do this?
So far, I "just have to know" the types for key and value, e.g:
val foo = Map <String, Int>()
But this is brittle, and would break if I change User in the future, e.g .rank from Int to Float. Of course, I wouldn't remain ignorant of my oversight for long, I expect the compiler would start complaining, but is there something elegant I can do now?
You could use a value class to define the user's name and rank.
#JvmInline
value class UserName(val name: String)
#JvmInline
value class UserRank(val rank: Int)
data class User (
val name: UserName,
val rank: UserRank,
)
val userMap = Map<UserName, UserRank>() // type-safe map
Or create a value class that implements Map<String, Int>, and use delegation.
#JvmInline
value class UserMap(
private val map: Map<String, Int>
) : Map<String, Int> by map {
operator fun contains(user: User): Boolean =
map[user.name] == user.rank
}
Or both together!
I have this super class:
abstract class Node(rawId: String) {
open val id: String
init {
id = Base64.toBase64(this.javaClass.simpleName + "_" + rawId)
}
}
And this subclass that extends Node:
data class Vendor (
override var id: String,
val name: String,
val description: String,
val products: List<Product>?
): Node(id)
When I initialize the Vendor class like this:
new Vendor(vendor.getId(), vendor.getGroup().getName(), description, products);
I can see the init block in Node get fired as expected. However, when I get the id from the Vendor object, it is the rawId and not the encoded Id.
So I am a bit confused about the initialization order/logic in Kotlin classes. I want the encoding code to be common across all subclasses. Is there a better way to do it?
The problem is because you are overriding the id field in the subclass and hence it would always remain the rawId value.
Since the base class has already an id field which has to be an encoded value, you don't need to override it in the subclass. You need to provide the rawId to the Node class in your Vendor class and let the base class take care of the id value to be instantiated with. You can have your abstract class as
abstract class Node(rawId: String) {
val id: String = Base64.toBase64(this.javaClass.simpleName + "_" + rawId)
}
and then define your subclass as
data class Vendor (
val rawId: String,
val name: String,
val description: String,
val products: List<Product>?
): Node(rawId)
Then with
Vendor newVendor = new Vendor(vendor.getId(), vendor.getGroup().getName(), description, products);
newVendor.getId() // would be the encoded id as you expect
since Vendor is a subclass of Node, the id field is also available to the Vendor object with the encoded value.
This feature is inspired by TypeScript which allows us to create arrays based on the property of another class, whatever that property's type is.
For example assume you have this class in Kotlin:
class Person(
val name: String,
val age: Int
)
And later, somewhere else in the code I want to have a list of names, so I would do something like this:
val namesList = List<Person::name>()
And Kotlin will know that this will be equivalent to List<String>() at compile time.
This avoids me to manually propagate the type of a field I already declared in one place. Plus, if one day the name type changes from String to something else, all the collections would get updated automatically.
Can this be done in Kotlin?
No, Kotlin is very explicit about types. It is a strongly-typed language.
Maybe the closest you could do is define a type alias next to your class and use that:
typealias PersonName = String
data class Person(val name: PersonName, val age: Int)
and then:
val namesList = mutableListOf<PersonName>()
However, in most cases you don't have to explicitly write the types anyway because they can be inferred.
// Is a List<String> and would automatically update if name type changed
val nameList = personList.map(Person::name)
// Or to get an empty mutable list:
val nameList = emptyList<Person>().map(Person::name).toMutableList()
The standard thing to do is to use map to extract the type you need:
val people = listOf(
Person("a", 1),
Person("b", 2),
Person("c", 3),
)
val names = people.map { it.name } // statically inferred to List<String>
If you changed the type of name to something else, you wouldn't need to change the val names = people.map { it.name } line - the new type will be inferred automatically.
I need to check if any variables inside of my data class are null. To do this I need retrieve them first but I can't access them directly (e.g. myDataClass.name) because I need it to be generic. Is there a way to access these variables without directly naming them. For example, like accessing a member of an array (myArray[0]).
The mechanism you're looking for is called "reflection" and it allows to introspect objects at runtime. You'll find a lot of information on the internet, but just to give you a link you may want to check this answer.
In your case you could do something like this:
data class MyDataClass(
val first: String?,
val second: String?,
val third: Int?
)
fun main() {
val a = MyDataClass("firstValue", "secondValue", 1)
val b = MyDataClass("firstValue", null, null)
printProperties(a)
printProperties(b)
}
fun printProperties(target: MyDataClass) {
val properties = target::class.memberProperties
for (property in properties) {
val value = property.getter.call(target)
val propertyName = property.name
println("$propertyName=$value")
}
}
Note that for this code to work you must add kotlin-reflect package as a dependency.
i'm new in kotlin and i want to know if we can transform a content value at initialisation : with this example :
#Document
data class Category(
#Id val id: Id? = null,
val label: String
)
Category is a document (entity for mongodb) and when i'm instanciating this object, i want to transform label property in uppercase. How can i do that to stay idiomatic with the language ? The point is to keep the immutable properties of the val keyword.
val categ = Category(label = "Test")
println(categ.label) // --> TEST
Thanks.
You can encapsulate the "upperCasing" into a factory:
data class Category constructor(val label: String) {
init {
if (label != label.toUpperCase()) {
throw IllegalStateException("Label must be uppercase")
}
}
companion object {
fun createInstance(str: String) = Category(str.toUpperCase())
}
}
The init block ensures, that clients don't create unwanted instances with non-upper labels (which should be documented).
Create an instance like this:
val instance = Category.createInstance("xy")
You might want to make explicit that you do transformations if the parameter is not upper case already by naming the factory accordingly, e.g. withTransformedLabel or simply add some documentation ;-)