Ky -
Ky -

Reputation: 32073

Setter for field is removed by type projection

I have the following SSCCE:

class Foo(val bars: Map<Int, Bar<*>>) {

    fun <Baz> qux(baz: Baz) {
        val bar2 = bars[2]!!
        bar2.bazes += baz
    }

    interface Bar<Baz> {
        var bazes: MutableList<Baz>
    }
}

This seems fine to me, but the compiler complains that:

Error:(5, 9) Kotlin: Setter for 'bazes' is removed by type projection

I have no idea what this even means, much less how to correct it. What's going on here and how do I get around it?

Upvotes: 4

Views: 3208

Answers (2)

s1m0nw1
s1m0nw1

Reputation: 81859

The issue: Your bars property is using "star projection", which, put simply, just means: "I don't know the generic type of Bar". This in turn leads to a problem: You'll only be able to get something from the MutableList, adding is prohibited (Thus the error message: No Setter for bazes!).

A possible solution:

 class Foo<in Baz>(private val bars: Map<Int, Bar<in Baz>>) {

    fun qux(baz: Baz) {
        val bar2 = bars[2]!!
        bar2.bazes.add(baz)
    }

    interface Bar<Baz> {
        var bazes: MutableList<Baz>
    }
}

What I changed here, is that I made Foo typed with in Baz, which means Foo is made contravariant in its type parameter, thus Bazs can only be consumed. The same type is then used for the input parameter bars. As a result, you're now able to add values into bars.bazes, because the associated MutableList is said to be a "Consumer" of Bars.

btw: Using Baz as a name for the generic type isn't recommended - You should rather use T which is more obvious to be a generic type.

I know, it's a complex topic and I really advise to consult the good documentation as a further step :)

The plusAssign issue

The usage of += (plusAssign operator) on the other hand is a bit weird: The compiler complains not to be able to choose the correct operator:

Assignment operators ambiguity:
public operator fun Collection.plus(element: Any?): List >defined in kotlin.collections

@InlineOnly public operator inline fun MutableCollection.plusAssign(element: Baz): Unit defined in kotlin.collections

I tried to explicitly cast bazes to MutableList which in fact solves the issue. The compiler then complains about an unecessary cast though. I don't know how to handle this properly to be honest except using add instead ;-)

Upvotes: 4

ephemient
ephemient

Reputation: 204678

There's a couple little issues. Temporarily using bars[2]!! as Bar<Baz>,

w: (4, 20): Unchecked cast: Foo.Bar<Any?> to Foo.Bar<Baz>
e: (5, 20): Assignment operators ambiguity: 
public operator fun <T> Collection<Baz>.plus(element: Baz): List<Baz> defined in kotlin.collections
@InlineOnly public operator inline fun <T> MutableCollection<in Baz>.plusAssign(element: Baz): Unit defined in kotlin.collections

Kotlin doesn't know whether to handle it as bar2.bazes = bar2.bazes.plus(baz) or bar2.bazes.plusAssign(baz). If you change it to var2.bazes.add(baz) or val bazes the ambiguity goes away.

Fixing that and removing the unsafe cast brings up

e: (5, 20): Out-projected type 'MutableList<out Any?>' prohibits the use of 'public abstract fun add(element: E): Boolean defined in kotlin.collections.MutableList'

the question of what can safely be done with the type projection. It gets treated like out Any?, so you can read from the list, but in Nothing, which means you can't add anything to the list.

It's unclear from this example why you're using *. If it doesn't cause any other issues, perhaps you could lift out the <Baz> parameter, e.g.

class Foo<Baz>(val bars: Map<Int, Bar<Baz>>)

Upvotes: 1

Related Questions