I have following class:
class Person(val name: String) {
private var surname: String = "Unknown"
constructor(name: String, surname: String) : this(name) {
this.surname = surname
}
}
But when I want to have the name parameter immutable in second constructor:
constructor(val name: String, surname: String) : this(name) {
this.surname = surname
}
I have the following compile-time error:
Kotlin: 'val' on secondary constructor parameter is not allowed
Can someone explain why is Kotlin compiler not allowing to do this?
Parameters in Kotlin are always immutable. Marking a constructor parameter as a val turns it into a property of a class, and this can only be done in the primary constructor, because the set of properties of a class cannot vary depending on the constructor used to create an instance of the class.
In addition to the great answer of yole, the documentation is pretty clear as well:
Note that parameters of the primary constructor can be used in the initializer blocks. They can also be used in property initializers declared in the class body.
[...] In fact, for declaring properties and initializing them from the primary constructor, Kotlin has a concise syntax:
class Person(val firstName: String, val lastName: String, var age: Int) {
// ...
}
Much the same way as regular properties, the properties declared in the primary constructor can be mutable (var) or read-only (val).
This all does not apply to secondary constructors.
You can define the variable as val or var in the class you inherit from
open class Human(val name: String) constructor(name: String) {
open fun showInfo()
{
println("Show Info")
}
}
class Person:Human {
constructor(name: String) : super(name)
private var surname: String = "Unknown"
override fun showInfo() {
println("$name And surname is $surname")
}
}
The currently accepted answer is correct in explaining why your initial attempt did not work. As such, given your particular scenario, I would inverse the solution and make your secondary constructor the primary, and make that second parameter have a default value.
class Person(val name: String, private var surname: String = "Unknown")
Also, if the class's purpose is to simply hold data, I would make it a data class to improve its handling.
Related
I'm writing a client for a third-party REST API that returns JSON with a variety of alternative values instead of proper null or omitting the property entirely if null. Depending on the entity or even property in question, null could be represented by either null, "", "0" or 0.
It's easy enough to make a custom serializer, e.g. something like this works fine:
#Serializable
data class Task(
val id: String,
#Serializable(with = EmptyStringAsNullSerializer::class)
val parentID: String?
)
object EmptyStringAsNullSerializer : KSerializer<String?> {
private val delegate = String.serializer().nullable
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("EmptyStringAsNull", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: String?) {
when (value) {
null -> encoder.encodeString("")
else -> encoder.encodeString(value)
}
}
override fun deserialize(decoder: Decoder): String {
return delegate.deserialize(decoder) ?: ""
}
}
fun main() {
val json = """
{
"id": "37883993",
"parentID": ""
}
""".trimIndent()
val task = Json.decodeFromString(json)
println(task)
}
But annotating many properties like this is a bit ugly/noisy. And I'd also like to use inline/value classes for strong typing, like this:
#Serializable
data class Task(
val id: ID,
val parentID: ID?
/* .... */
) {
#JvmInline
#Serializable
value class ID(val value: String)
}
This means that in addition to annotating these properties I also need a custom serializer for each of them. I tried some generic/parameters-based solution that can work for all cases like this:
open class BoxedNullAsAlternativeValue<T, V>(
private val delegate: KSerializer<T>,
private val boxedNullValue: T,
private val unboxer: (T) -> V
) : KSerializer<T> {
private val unboxedNullValue by lazy { unboxer.invoke(boxedNullValue) }
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor(this::class.simpleName!!, PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: T) {
when (value) {
null -> delegate.serialize(encoder, boxedNullValue)
else -> delegate.serialize(encoder, value)
}
}
override fun deserialize(decoder: Decoder): T {
#Suppress("UNCHECKED_CAST")
return when (val boxedValue = delegate.deserialize(decoder)) {
boxedNullValue -> null as T
else -> boxedValue
}
}
}
But that doesn't work because #Serializable(with = ...) expects a static class reference as argument, so it can't have parameters or generics. Which means I'd still need a concrete object for each inline/value type:
#Serializable
data class Task(
val id: ID, // <-- missing serializer because custom serializer is of type ID? for parentID
val parentID: ID?
) {
#JvmInline
#Serializable(with = IDSerializer::class)
value class ID(val value: String)
}
internal object IDSerializer : BoxedNullAsAlternativeValue<Task.ID?, String>(
delegate = Task.ID.serializer().nullable, // <--- circular reference
boxedNullValue = Task.ID(""),
unboxer = { it.value }
)
That doesn't work because there is no longer a generic delegate like StringSerializer and using Task.ID.serializer() would mean the delegate would be the custom serializer itself, so a circular reference. It also fails to compile because one usage of the ID value class is nullable and the other not, so I would need nullable + non-nullable variants of the custom serializer and I would need to annotate each property individually again, which is noisy.
I tried writing a JsonTransformingSerializer but those need to be passed at the use site where encoding/decoding happens, which means I'd need to write one for the entire Task class, e.g. Json.decodeFromString(TaskJsonTransformingSerializer, json) and then also for all other entities of the api.
I found this feature request for handling empty strings as null, but it doesn't appear to be implemented and I need it for other values like 0 and "0" too.
Question
Using kotlinx.serialization and if necessary ktor 2, how to deserialize values like "", "0" and 0 as null for inline/values classes, considering that:
Properties of the same (value) type can be nullable and non-nullable in the same class, but I'd like to avoid having to annotate each property individually
I'd like a solution that is as generic as possible, i.e. not needing a concrete serializer for each value class
It needs to work both ways, i.e. deserializing and serializing
I read in the documentation that serializing is done in 2 distinct phases: breaking down a complex object to it's constituent primitives (serializing) --> writing the primitives as JSON or any other format (encoding). Or in reverse: decoding -> deserializing;
Ideally I'd let the compiler generate serializers for each value class, but annotate each of them with a reference to one of three value transformers (one each for "", "0" and 0) that sit in between the two phases, inspects the primitive value and replaces it when necessary.
I've been at this for quite some time, so any suggestions would be much appreciated.
I am new to Kotlin and working through the tutorials that are available.
However now I seem to have a problem with a secondary constructor:
Parameters declared in the primary constructor can be accessed in a function,
but when I try to do this with a parameter from the secondary constructor I get an error: Unresolved reference:nbr
The code:
class Test(_name: String) {
val name: String = _name
constructor(_name: String, _nbr: Int) : this(_name) {
val nbr: Int = _nbr
}
fun printNameAndNumber() {
println("Name: $name")
println("Number: $nbr")
}
}
It is clear to me that I am doing something basically wrong but who can tell me what?
nbr should be a variable, because in this specific case it is optional:
class Test(_name: String) {
val name: String = _name
var nbr: Int? = null
constructor(_name: String, _nbr: Int) : this(_name) {
this.nbr = _nbr
}
fun printNameAndNumber() {
println("Name: $name")
println("Number: $nbr")
}
}
Parameters of the primary constructor are not available in member functions. Fields are. Fortunately Kotlin has a short syntax to make primary constructor parameters member properties right away.
What do you expect nbr to be when constructed using the primary constructor? I suggest you to swap your constructors, so it's clear what are properties and what are just parameters:
class Test(val name: String, val nbr: Int) {
constructor(name: String) : this(name, 0)
fun printNameAndNumber() {
println("Name: $name")
println("Number: $nbr")
}
}
fun main(args : Array<String>) {
Test("Péter").printNameAndNumber()
}
name is accessible because it is a member.
nbr is not accessible because it is a local (immutable) variable inside the secondary constructor.
If you declare nbr as member: putting val nbr: Int for example below the val name line, it will be accessible, however it will not compile if nbr is defined as immutable (val).
A simpler structure would be:
class Test(_name: String, _nbr: Int = 0) {
val name: String = _name
val nbr: Int = _nbr
fun printNameAndNumber() {
println("Name: $name")
println("Number: $nbr")
}
}
or even simpler
class Test(val name: String, val nbr: Int = 0) {
fun printNameAndNumber() {
println("Name: $name")
println("Number: $nbr")
}
}
If you want your nbr member as nullable, you couldgo with the suggestion of #gil.fernandes.
I know that data class are like simple models in kotlin with getters and setter by default and are as simple this:
data class User(val name: String, val age: Int)
Is it possible to declare a second constructor for that data class?
A Kotlin data class must have a primary constructor that defines at least one member. Other than that, you can add secondary constructors as explained in Classes and Inheritance - Secondary Constructors.
For your class, and example secondary constructor:
data class User(val name: String, val age: Int) {
constructor(name: String): this(name, -1) { ... }
}
Notice that the secondary constructor must delegate to the primary constructor in its definition.
Although many things common to secondary constructors can be solved by having default values for the parameters. In the case above, you could simplify to:
data class User(val name: String, val age: Int = -1)
If calling these from Java, you should read the Java interop - Java calling Kotlin documentation on how to generate overloads, and maybe sometimes the NoArg Compiler Plugin for other special cases.
Updated answer for data classes:
Yes you can, but you will need to delegate everything to the primary constructor
data class User(val name: String, val age: Int)
{
constructor(name: String): this(name, -1) {
}
constructor(age: Int): this("Anon", age) {
}
}
// Anon name: Anon
println("Anon name: " + User(30).name)
// No age: -1
println("No age: " + User("Name").age)
// Name: Name age: 20
val u = User("Name", 20)
println("Name: " + u.name + " age: " + u.age)
You can also set default values in your primary constructor as Alexey did.
I wanted to have a class similar to below (with a constructor that parses an input)
data class Data(val a: String, val b: String) {
constructor(spaceSeparated: String) { // error because we don't call this()
val split = spaceSeparated.split(" ")
this(split.first(), split.last()) // error again because it's not valid there
}
}
The solution is to do this:
data class Data(val a: String, val b: String) {
companion object {
operator fun invoke(spaceSeparated: String): Data {
val split = spaceSeparated.split(" ")
return Data(split.first(), split.last())
}
}
}
And it can be called just as if it were a constructor
Default values in the primary constructor eliminates many needs for secondary constructors, but if the needed instance depends on logic based on data that must be analyzed the better answer may be to use a companion object.
data class KeyTag(val a: String, val b: Int, val c: Double) {
companion object Factory {
val empty = KeyTag("", 0, 0.0)
fun create(bigString: String): KeyTag {
// Logic to extract appropriate values for arguments a, b, c
return KeyTag(a, b, c)
}
fun bake(i: Int): KeyTag = KeyTag("$i", i, i.toDouble())
}
}
Usage is then:
val ks = KeyTag.create("abc:1:10.0")
val ke = KeyTag.empty
val kb = KeyTag.bake(2)
Yes, but each variable should be initialized, so you may set default arguments in your data class constructor, like this:
data class Person(val age: Int, val name: String = "Person without name")
Now you can create instance of this data class in two ways
Person(30)
Person(20, "Bob")
you can set the data class like this
data class User(val name: String? = null, val id: String? = null, val email: String? = null)
and you can instance the object with multiple constructors like this
val userId = User(id = "123456")
val userMail = User(email= "email#email.com")
val userName = User("Name")
Data class will ensure consistency and meaningful behavior also we need to have val for immutability.
data class SampleData(val name: String, val age: Int, val email: String ?= null) {
constructor(name: String, age: Int) : this(name, age, null) {
}
}
secondary constructor must delegate to the primary constructor in its definition, so to maintain the immutability, having "null" will work.
Instructs the Kotlin compiler to generate overloads for this function that substitute default parameter values.
If a method has N parameters and M of which have default values, M overloads are generated: the first one takes N-1 parameters (all but the last one that takes a default value), the second takes N-2 parameters, and so on.
data class User #JvmOverloads constructor(
var email: String="",
var password: String="")
Yes, we can use like below code, and in primary constructor for data class should have min one parameter.
data class SampleData(val name: String, val age: Int) {
constructor(name: String, age: Int, email: String) : this(name, age) {
}
}
Yes you can have multiple contractors on data classes. But there is something which makes the primary constructor special. The compiler will auto generate methods like equals, hashCode, copy, toStrings based on the primary constructor for the data class.
Below is an example of two instances of a data class which looks different (first.gender = male, second.gender = female) but equals method would evaluate to true because gender is not defined in the primary constructor and therefore not considered in the auto generated methods. Likewise, gender would not be included in the string representation.
data class A(val name: String, val age: Int) {
var gender: String = "Female"
constructor(name: String, age: Int, gender: String) : this(name, age) {
this.gender = gender
}
}
fun main(args: Array<String>) {
val first = A("foo", 10)
val second = A("foo", 10, "Male")
println(first == second) //prints true
println(second) //prints A(name=foo,age=10)
}
I am sending a json reponse from server in the following format:
{id: Int, name: String, childJSON: String}
and willing to map it to
export class Student{
constructor(public id: string,
public name: string,
public childJSON: ChildObject) {
}
export class ChildObject {
constructor(public class: number,
public age: number){}
on doing response.json() as Student; I am getting {id:1, name: "sumit", childJSON: "{class: 5, age: 10}" i.e. childJSON has string type instead of ChildObject type. Basically the string is not mapped to my child object. Is this the correct way to achieve it or i need to send child object from the server instead of just JSON String
You need to "re-hydrate" the objects manually in the constructor and you can't use the "parameter property" shortcut to do that (the technique where you used public in the constructor to automatically convert constructor params into class properties).
Here's how I would do it:
export class Student{
constructor(options: {
id: string;
name: string;
childJSON: any;
}) {
// Now you have to instantiate the class properties one by one.
this.id = options.id;
this.name = options.name;
this.childJSON = new ChildObject(options.childJSON);
}
}
And then to instantiate:
const student = new Student(studentJson);
Or, if you're using an Observable to fetch the data:
this.http.get(...).map(...).subscribe(studentJson =>
this.student = new Student(studentJson)
}
This solution is more flexible, as you can pass the original JSON object directly for instanciation. In your example, you had to write something like:
// NOT GOOD: You must pass each property individually, in the right order...
const student = new Student(studentJson.id, studentJson.name,...);
I am using Cerialize as Json serializer/deserializer.
My Api connector works this way:
ApiService.get<T>(endpoint:string):Promise<T>
calls
ApiConnectorService.get<ApiResponse<T>>(tClazz:{new():T;}, endpoint:string);
to deserialize, Cerialize uses class parameter:
function Deserialize(json: any, type?: Function | ISerializable): any;
So, when I call ApiService.get<T>(endpoint), I call ApiConnectorService.get<ApiResponse<T>>(ApiResponse, endpoint) in it.
Problem
I can't provide ApiResponse<T> as tClazz parameter, compiler says
TS1109: Expression expected
Is there a way to provide Generic class with its Generic type as parameter? This way, when I call get<User>() I get a User in an ApiResponse<User> type, at the moment I only get an Object in ApiResponse, which is not whet we need.
Here is the ApiResponse class:
export class ApiResponse<T>{
#deserialize
data:T;
#deserializeAs(ErrorData)
error:ErrorData;
#deserialize
meta:Object;
}
EDIT: Same error if I want to give an array as class parameter:
ApiService.get<Foo[]>(Foo[], '/bar');
TS1109: Expression expected
You cannot, if you look at the transpiled generic class here: Typescript Playground it looses the generics, so in runtime you have no types so you can't get the type of the generic, meaning, in runtime you cannot know what type is your T. You have to pass the class itself as a parameter. You can use the generics for compilation help, but thats the best you can do.
Taking into account that nested generics are not supported, and generics are just compile time construct, I have made a small sample that I hope might be helpful to you:
export class User
{
private name: string;
constructor(serializedValue: string)
{
this.name = serializedValue;
}
}
export class ApiResponse<T>
{
constructor(val: T)
{
this.data = val;
}
data:T;
error:string;
meta:Object;
}
class ApiConnectorService<T>
{
public get<U extends ApiResponse<T>>(tClazz: {new(s: string):T;}, uClazz: {new(t: T):U;}, endpoint:string): U
{
let val = 'user';//get serializedvalue from endpoint
return new uClazz(new tClazz(val));
}
}
class ApiService
{
public get<T>(tClazz: {new(s: string):T;}, endpoint:string): T
{
let s = new ApiConnectorService<T>();
return s.get(tClazz, ApiResponse, '123').data;
}
}
let a = new ApiService();
console.log(a.get<User>(User, '12'));