How does declaration-site variance interact with private members and the `@UnsafeVariance` annotation? Give a real standard-library example where the position check is deliberately bypassed.
answer
- private members skip the variance check
- @UnsafeVariance suppresses the check at one occurrence
- List<out E>.contains(element: @UnsafeVariance E)
- only safe for read-only query params
- misuse → runtime ClassCastException
basics
~20 sPrivate members are skipped by the variance check because they aren't part of the public type. @UnsafeVariance lets you mark one spot to ignore the check on purpose. Kotlin's List<out E>.contains uses it because the method only reads.
solid answer
~40 sThe position check only applies to members that form the **external type contract**, so `private` members are exempt — you can freely use an `out T` as a `private var`. For public members that would otherwise violate variance but are provably safe, Kotlin offers `@UnsafeVariance`, which suppresses the check at that single type occurrence. The canonical stdlib case is `List<out E>`: methods like `contains(element: @UnsafeVariance E): Boolean` and `indexOf(element: @UnsafeVariance E)` take E in an `in` position, which contradicts covariance, yet they only *read* the argument and never store it. Marking E `@UnsafeVariance` keeps the interface covariant while still exposing those query methods. The danger is real: `@UnsafeVariance` removes the compiler's soundness guarantee, so misuse (actually storing the value) can produce a runtime `ClassCastException`. Reserve it for read-only query parameters in covariant types.
code
kotlin · 11 lines// stdlib pattern: covariant List keeps query methods via @UnsafeVariance
public interface List<out E> : Collection<E> {
override fun contains(element: @UnsafeVariance E): Boolean
fun indexOf(element: @UnsafeVariance E): Int
}
// private members are exempt from the check
class Producer<out T>(private var cache: T) {
fun get(): T = cache
private fun set(v: T) { cache = v } // private in-position: allowed
}go deeper
Aware that some special annotation lets List stay covariant despite contains.
Explains that private members are exempt and that @UnsafeVariance suppresses the check.
Identifies the read-only safety condition and the concrete List<out E> methods involved.
Judges when bypassing the check is acceptable in a library and documents the manually-verified invariant and its failure mode.
## Private members are exempt The variance position check exists to protect **substitutability** of the type as seen from outside. Private members aren't visible externally and can't affect how the type is used through its public surface, so the compiler does **not** check them: ```kotlin class Producer<out T>(private var cache: T) { // private var T is fine fun get(): T = cache // public out position private fun reset(v: T) { cache = v } // private in position — allowed } ``` If `reset` were `public`, the check would reject it because T would appear in a public `in` position. ## `@UnsafeVariance` `@UnsafeVariance` is an annotation applied to a **type usage** that tells the compiler: *"I know this occurrence violates the declared variance; trust me, it's safe."* It disables the position check for that one spot only. ```kotlin interface ReadOnlyBag<out T> { operator fun contains(element: @UnsafeVariance T): Boolean // reads only } ``` ## The real stdlib example: `List<out E>` `List` is covariant, yet several of its methods take E as a parameter: ```kotlin public interface List<out E> : Collection<E> { override fun contains(element: @UnsafeVariance E): Boolean fun indexOf(element: @UnsafeVariance E): Int fun lastIndexOf(element: @UnsafeVariance E): Int override fun containsAll(elements: Collection<@UnsafeVariance E>): Boolean } ``` These are **query** methods: they only compare the passed element against stored ones; they never insert it. Without `@UnsafeVariance`, the position check would forbid them and `List` couldn't be covariant. With it, `List<String>` stays a subtype of `List<Any>` and you can still call `list.contains(x)`. ## Why it is genuinely unsafe The annotation removes the guarantee. If you (wrongly) used `@UnsafeVariance` on a parameter you then **store**, a caller treating `List<Cat>` as `List<Any>` could attempt to insert a Dog, leading to a heap-pollution-style `ClassCastException` at runtime. The stdlib gets away with it because those methods provably don't store the argument. ## Guidance - Prefer redesign (split interfaces, invariance) over `@UnsafeVariance`. - Use it only for **read-only** query parameters in a covariant type, where you can prove the argument is never retained. - Document the invariant you're relying on.
- Why can the stdlib safely use `@UnsafeVariance` on `List.contains` but not on a hypothetical `List.add`?`contains` only reads the argument to compare it; nothing is stored, so covariance can't be violated at runtime. `add` would store the element, letting a `List<Cat>` viewed as `List<Any>` accept a Dog — an actual unsoundness.
- If a private member uses T in an in-position, what happens when you later make it public?The variance position check then applies; if the class is `out T`, the now-public in-position usage fails to compile, forcing you to redesign, drop variance, or add `@UnsafeVariance`.
saying these in an interview costs you the question
- Believing `@UnsafeVariance` is checked/validated by the compiler for safety
- Thinking private members are checked like public ones
- Recommending `@UnsafeVariance` as a routine fix
- Not knowing `List.contains` uses it
- Claiming `@UnsafeVariance` has no runtime consequences when misused