skip to content

When should you reach for a use-site projection (Array<out T>) versus declaration-site variance, and why can't Array itself be declared covariant?

level: seniorimportance: should knowfreq 30%

answer

  1. declaration-site: one-directional + you own the class
  2. use-site projection: invariant or third-party types
  3. Array has get(out) AND set(in) → must be invariant
  4. projection = local; declaration variance = global
  5. covariant class needs T only in out positions

basics

~20 s

Declare variance on the class when the type is always a producer or always a consumer of T. Use a use-site projection when the class is invariant because it both reads and writes T, like Array, and you only need one direction at a specific call site.

solid answer

~50 s

Declaration-site variance (class Box<out T>) is the right tool when the class itself can be uniformly covariant or contravariant: T only ever appears in out positions (covariant) or only in positions (contravariant) across all members. Array cannot do this: it has both get(): T (out) and set(value: T) (in), so T appears in both positions and the class must stay invariant for safety. When you nonetheless need directional flexibility at a particular use site, you project there: Array<out T> for read-only access, Array<in T> for write-only access. So the rule of thumb: prefer declaration-site variance when you control the class and it is naturally one-directional; fall back to use-site projection for invariant-by-necessity types (mutable containers, Array) or third-party types you cannot annotate. Projection is local and per-use; declaration-site variance is global and applies to every usage automatically.

go deeper

for a junior

Knows projection exists for arrays but may not articulate the decision rule.

for a middle

States that one-directional classes use declaration-site variance and Array is projected because it is mutable.

for a senior

Explains the in/out-position constraint that forces Array to be invariant and gives the decision rule cleanly.

for a principal

Weighs DRY global declaration variance vs local flexible projection, including third-party types and API-design implications.

## Two ways to introduce variance Kotlin offers variance at two levels: 1. **Declaration-site variance** — annotate the class's type parameter once: `class Source<out T>` or `interface Sink<in T>`. Every usage is automatically variant; callers write no extra keywords. 2. **Use-site projection (type projection)** — annotate a single usage: `fun f(a: Array<out T>)`. Variance applies only at that spot. ## When declaration-site variance is possible A class can be declared `out T` (covariant) only if `T` appears **exclusively in out positions** (return types) across all its public members — i.e., it is a pure **producer**. It can be `in T` (contravariant) only if `T` appears **exclusively in in positions** (parameters) — a pure **consumer**. The Kotlin compiler enforces this; it rejects `class Box<out T> { fun set(t: T) }` with a variance error. ## Why Array must stay invariant `Array<T>` exposes both: ```kotlin operator fun get(index: Int): T // T in OUT position operator fun set(index: Int, value: T) // T in IN position ``` Because `T` appears in **both** positions, the class is **invariant by necessity** — it cannot be declared `out` or `in` without becoming unsound. If `Array<Int>` were silently usable as `Array<Any>`, a `set(i, "x")` would corrupt it. The same is true of `MutableList<T>` and most mutable containers. ## The decision rule - The class is **naturally one-directional** and **you own it** → use **declaration-site variance**. Example: a read-only `interface Producer<out T> { fun next(): T }`. - The class is **invariant by necessity** (mutable, reads + writes) **or third-party / not annotatable** → use a **use-site projection** at each call site that needs one direction. - You only need the directional view in **one function**, not everywhere → projection keeps the restriction local. ```kotlin // Declaration-site: Producer is always covariant interface Producer<out T> { fun produce(): T } val p: Producer<Any> = object : Producer<String> { override fun produce() = "hi" } // OK // Use-site: Array stays invariant; project only where needed fun copyInto(dest: Array<in Any>, src: Array<out Any>) { for (i in src.indices) dest[i] = src[i] } ``` ## Trade-offs - Declaration-site is **DRY**: declared once, free everywhere; but only legal for one-directional types. - Use-site is **flexible and local**: works on any type including invariant ones and types you cannot modify; but you repeat the annotation at each site and lose the other direction's members there. ## Mental model Reach for declaration-site variance first when you control the type and it is purely producer or consumer. Reach for a projection when the type genuinely needs both `get` and `set` (so it must be invariant) but a given call site only needs one half.

  • Can you apply a use-site projection to a type that already has declaration-site variance?
    It is redundant/disallowed to project in a direction the class already declares; e.g. projecting List<out E> when List is already declared out E adds nothing and the compiler warns. Projection matters for invariant parameters.
  • Name another standard type besides Array that is invariant for the same reason.
    MutableList<T> (and MutableMap, MutableSet): they expose add/set consuming T and get producing T, so T is in both positions and the type stays invariant.

saying these in an interview costs you the question

  • Claiming Array could be declared out T if Kotlin wanted
  • Saying projection and declaration-site variance are interchangeable everywhere
  • Not knowing that covariance requires T only in out positions
  • Forgetting projection works on third-party/unannotatable types
  • Confusing local (use-site) with global (declaration-site) scope of the rule

context