最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

Why does Kotlin variance restriction with "out T" prevent function parameters that return "T&quot

programmeradmin3浏览0评论

The following class uses an out T generic parameter:

class Foo<out T> {
  fun bar(f: () -> T): T {
    return f()
  }
}

This does not compile: "Type parameter T is declared as 'out' but occurs in 'in' position in type () -> T"

I don't understand why f: () -> T is not legal since it appears in an out position as a result. Can someone please explain an give an example where allowing this would break variance semantics?

The following class uses an out T generic parameter:

class Foo<out T> {
  fun bar(f: () -> T): T {
    return f()
  }
}

This does not compile: "Type parameter T is declared as 'out' but occurs in 'in' position in type () -> T"

I don't understand why f: () -> T is not legal since it appears in an out position as a result. Can someone please explain an give an example where allowing this would break variance semantics?

Share Improve this question asked Mar 20 at 12:47 user3612643user3612643 5,9327 gold badges37 silver badges64 bronze badges
Add a comment  | 

3 Answers 3

Reset to default 3

In terms of "input position vs output position", fun bar(f: () -> T) is really no different from fun bar(f: T). In both cases, bar is taking a T as input. It's just that in the former case, the input is wrapped in a function, and bar can call it as many times as it wants.

In other words, if fun bar(f: () -> T) is allowed, then effectively fun bar(f: T) is also allowed, because the implementation of bar can just call f(), and do whatever a fun bar(f: T) would have done with f.


Kotlin's documentation on this is rather bad, so I will adapt the rules from the C# spec to explain the general rules. We say that an input-unsafe type cannot be used as a function parameter type, and an output-unsafe type cannot be used as a return type.

A type T is output-unsafe if one of the following holds:

  • T is a contravariant (in) type parameter.
  • T is a generic type S<A1, A2, ... An> where for at least one of the type parameters Ai, one of the following holds:
    • Ai is output-unsafe and the declaration site variance of the corresponding type parameter is covariant or invariant
    • Ai is input-unsafe and the declaration site variance of the corresponding type parameter is contravariant or invariant

A type T is input-unsafe if one of the following holds:

  • T is a covariant (out) type parameter.
  • T is a generic type S<A1, A2, ... An> where for at least one of the type parameters Ai, one of the following holds:
    • Ai is input-unsafe and the declaration site variance of the corresponding type parameter is covariant or invariant
    • Ai is output-unsafe and the declaration site variance of the corresponding type parameter is contravariant or invariant

Function types have contravariant parameters and covariant return types. We can easily show that () -> T (aka Function0<T>) in your code is input-unsafe. First, T is input-unsafe because it is covariant. Then we can conclude that () -> T is input-unsafe because T is an input-unsafe type and the type parameter of Function0 is covariant.

T is still in an 'in' position in bar() when the argument type is the producer () -> T rather than T, as both types are a source of Ts. This is illegal as it could lead to objects being incorrectly typed.

Why it doesn't work

Say it was allowed.

Then we might write Foo.bar() such that it stored the results of all the producer functions it received, but it returns the result of the first one it received.

class Foo<out T> {
    val tStore: MutableList<T> = mutableListOf()

    fun bar(f: () -> T): T {
        tStore.add(f())
        return tStore.first()
    }
}

Then imagine we have the interfaces:

interface Animal

interface Cat : Animal {
    fun purr()
}

interface Dog : Animal {
    fun bark()
}

Let's create a Foo<Dog>.

val fooDog: Foo<Dog> = Foo<Dog>()

The out projection means we can do1:

val fooAnimal: Foo<Animal> = fooDog // since Foo<Dog> is a subtype of Foo<Animal>
fooAnimal.bar { SiameseCat() }

But then can go back to fooDog and do:

val someDog: Dog = fooDog.foo { HoundDog() }
someDog.bark() // Runtime error, since `someDog` is actually of type `SiameseCat`

Note: example is a bit misleading

As noted by @Sweeper in the comments, this example doesn't quite work because it relies on declaring a MutableList<T> inside Foo, which is also illegal as a MutableList requires its type to be invariant.

To try and summarise the issue, in order to create an error from the mistyping in the argument for bar(), we need Foo to produce the mistyped T received in that function. But it seems there is no type-safe place to store that T in Kotlin2, as all vars or objects we might use themselves would not allow an object 'invariant' in T to be used inside Foo.

All in all, arguably a credit to the type-safety of Kotlin! But hopefully the answer illustrates the concept of how producing and consuming objects must in general be distinct to ensure type safety.


1 By definition, where Y : X then Foo<out Y> : Foo<out X>, since out designates covariance in Kotlin

2 I suspect a Java array would work as it is not as strictly typed as a Kotlin array

It does only appear in an out position of the lambda f, but it appears in the in position of the function bar since it is part of its parameter.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论