I’m a bit confused that this compiles: ```val x: I...
# announcements
r
I’m a bit confused that this compiles:
Copy code
val x: Int? = null
val xs: Set<Int> = setOf(1, 2, 3)
xs.contains(x) // compiles, but why?
because the type of
contains
is
public operator fun <@kotlin.internal.OnlyInputTypes T> Iterable<T>.contains(element: T): Boolean
. Does
@kotlin.internal.OnlyInputTypes
mean that `T`is defined by the type of
element
not of the receiver? I guess that would type check, as
Int
is a subtype of
Int?
- right?
s
I think what happens is that
T
is inferred to be the widest of the two types I think the purpose of
T
here is to prevent you checking if
Set<Int>
contains something that can never be true ie. a
String
r
But in principle
Set<Int>.contains(null: Int?)
can never be true.
s
But you could have any
Int
in
x
r
Good point, yes.
s
Remove the type from
x
and you get the same error as with a
String
r
Actually the type params don’t seem to do much - this all compiles & runs, presumably because
T
can resolve to `Any?`:
Copy code
fun main() {
  fun <T> Set<T>.contains2(n: T): Boolean = this.any { it == n }

  val int: Int = 1
  val nullableInt: Int? = null
  val string: String = ""
  val nullableString: String? = null

  val ints: Set<Int> = setOf(1, 2, 3)
  val nullableInts: Set<Int?> = setOf(1, 2, null, 3)
  val strings: Set<String> = setOf("1", "2", "3")
  val nullableStrings: Set<String?> = setOf("1", "2", null, "3")

  ints.contains2(int)
  ints.contains2(nullableInt)
  ints.contains2(string)
  ints.contains2(nullableString)
  
  nullableInts.contains2(int)
  nullableInts.contains2(nullableInt)
  nullableInts.contains2(string)
  nullableInts.contains2(nullableString)
  
  strings.contains2(int)
  strings.contains2(nullableInt)
  strings.contains2(string)
  strings.contains2(nullableString)
  
  nullableStrings.contains2(int)
  nullableStrings.contains2(nullableInt)
  nullableStrings.contains2(string)
  nullableStrings.contains2(nullableString)
}
n
I can see then reasoning for why this is happening but in this specific case it's pretty yikes
Downsides of covariance
s
Its a common gotcha in cases like your
contains2
. Its been years since I had the problem myself but dabbed a bit with the internal
Exact
annotation that should prevent this case ( related discussion here https://discuss.kotlinlang.org/t/how-prevent-type-inference/8140/4)
Especially since it would not be a problem had the method been specified on the
Set
instead of as an extension method (as far as I can recall)
i
n
that's even more surprising
i thought it was an extension function too
i
But we also provide an extension that accepts a supertype of an element type
n
This
Any
implicitly at the base of every hierarchy is really a blow to type safety
a lot of code compiles that you would want to give you an error 99% of the time, and actually find useful 1% of the time
(probably less)
Things like
listOf(1) + listOf("hello")
compiling is a bit of a tragedy
i
What's unsafe here and how is it different from any other common supertype?
n
I mean the whole point of a type system is that it catches errors at compile time, that would otherwise be deferred to runtime. in this case, someone could have made an error (e.g. they did
a + b
instead of
a + c
), and you don't get a compiler error for it
s
@ilya.gorbunov I was trying to say that the gotcha is that many(or atleast I) think that the type signatures of
contains
and
contains2
in this example are equivalent. But as can be seen in the
test()
function they are not. I assume that with the
Exact
annotation it would be possible to get the same compiler error for the
contains2
usage. Feel free to correct me if it is possible to achieve this without internal type annotations.
Copy code
interface MySet<out T> {
    fun contains(t: @UnsafeVariance T): Boolean
}

fun <T> MySet<T>.contains2(t: T): Boolean {
    TODO()
}

fun test(){
    val stringSet: MySet<String> by lazy { TODO() }
    
    stringSet.contains2(1) // Not compile error
    
    stringSet.contains(1) // compile error
}