简体   繁体   中英

Why isn't Kotlin complaining about ambiguity? And why is it calling the secondary constructor?

I was testing something in Kotlin and now I won't be able to sleep properly if I don't find an answer.

Look at this class:

data class Person(
    val name: String,
    val age: Int? = 0
) {
    constructor(
        secondName: String,
        secondAge: Int = 5
    ) : this(
        name = "$secondName 2",
        age = secondAge
    )
}

fun main() {
    val firstCase  = Person("Lorem")     // Complaints about ambiguity.
    val secondCase = Person("Lorem", 42) // Calls secondary constructor.
}

Complaining about ambiguity in the firstCase is understandable, but why it is not happening the same in the secondCase? And why did it decide to call the secondary constructor and not the main constructor?

Now if I add another parameter on the main constructor with a default value :

data class Person(
    val name: String,
    val age: Int? = 0,
    val isAdult: Boolean = false
) {
    constructor(
        secondName: String,
        secondAge: Int = 5
    ) : this(
        name = "$secondName 2",
        age = secondAge
    )
}

fun main() {
    val thirdCase = Person("Lorem") // Calls secondary constructor.
}

I was expecting to have ambiguity for the thirdCase, just like in the firstCase. But no. It calls the secondary constructor? Why?!

Kotlin gives more priority to a function that is more specific. Consider this example:

foo(dog) // invokes foo(Dog)

fun foo(animal: Animal) {}
fun foo(dog: Dog) {}

In this case foo(dog) is also ambiguous, but in fact, it uses foo(Dog) function. To use the other one, we have to explicitly upcast:

foo(dog as Animal)

Because Int is a subtype of Int? , your secondary constructor is more specific than the primary constructor and this is why it is preferred.

The rules of method resolution in Kotlin (and in Java) can be a bit arcane. Thankfully, they do the obvious thing in nearly all situations — which is clearly the point. — but there are a few surprising corner cases.

The general principle is that it picks the most specific method that could match, and only gives an error if there isn't a single winner.

So in your second case, the arguments are String and Int . The candidates are the primary constructor (taking String and Int? ), and the secondary constructor (taking String and Int ). The latter is a more specific match, because Int is a subtype of Int? , and so it picks that one.

But in your first case, the only argument provided is a String , which matches both constructors equally, so there's no clear winner, and it flags up the ambiguity.

Your third case is even less obvious. However, the section of the Kotlin spec that discusses how to choose the most specific candidate from all the overloads says:

For each candidate we count the number of default parameters not specified in the call (ie, the number of parameters for which we use the default value). The candidate with the least number of non-specified default parameters is a more specific candidate

I think that's what's happening in your third case: it picks the secondary constructor (which would leave only one parameter with its default value) over the primary (which would leave two).

In your first example, there is no ambiguity when you use two arguments, because it selects the constructor with the most specific type matching your parameter. There is no ambiguity to choose between the one with Int and the one with Int? because Int is more specific than Int? .

When you provide a single argument, it is ambiguous since you are not providing any argument that can help distinguish whether you want Int or Int? .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM