Generics
Generic Kotlin classes and functions become generic C# classes and methods, keeping the same type parameter, constraints, and variance.
Type mappings
Kotlin | C# |
|---|---|
|
|
|
|
|
|
| a generic method |
| an ordinary method |
| an ordinary generic method |
| the aliased type itself, no separate alias type |
Nullable properties
A nullable property on a generic class (val x: T?) is T? in C#. At a reference-type instantiation a null read stays null; at a value-type instantiation it collapses to default(T), same as any other unconstrained C# generic.
Constraints
A bound (<T : Pet>) becomes a C# where T : ... clause. Any type assignable to the bound works as the type argument:
A generic bound from another package
When the bound is declared in a different Kotlin package than the generic class itself, the generated where clause spells it fully qualified (where T : global::TestLibrary.Cat.IPet) instead of a bare name. This only matters if you inspect the constraint through reflection; calling the class works the same either way.
Variance
out T/in T on an interface carries straight through to the C# interface:
Variance declared on a class's own type parameter, as opposed to an interface's, is dropped: C# does not support variance on classes. The class still generates and works, just without out/in on its type parameter.
Methods on a generic class
A public method declared on a generic class binds as an instance method on the C# generic carrier, T positions included, the same ADR-062 plan an ordinary class's methods bind on:
A position that never mentions T (Label above) binds exactly as it would on a non-generic class. A position that does (Describe, Pick) crosses as the same boxed handle a T-typed property already uses: a value type pays one box mint and dispose per call, an exported class instantiation borrows the argument's own live handle and mints nothing on the way in.
T is admitted only at a top-level position: a parameter, a return, a constructor parameter, or a property getter, optionally nullable. It is refused, named, everywhere else: nested in a collection or lambda (List<T>, (T) -> Unit, Flow<T>), a var property's setter (var item: T renders a get-only T Item), a suspend fun, a Flow/StateFlow member, a stored-callback member, and the method's own type parameter (fun <R> map(f: (T) -> R): R). A type parameter declared on an interface rather than a class is unaffected by this and keeps its own, unrelated named refusal.
Subclassing a generic base
A class extending an exported generic base spells the closed type argument, and inherits members, including methods, from that closed base:
Because Parcel and Crate are declared open, their generated Dispose() is virtual so a subclass can override it. A subclass declaring its own overload of a name it also inherits from the generic base keeps exactly its own declared member; the base's substituted overload is not re-declared alongside it, and C# overload resolution then picks between the two exactly as Kotlin does. Generic subclasses (class Sub<T> : Base<T>(...)) are not supported yet; declare the member directly on the closed subclass instead.
Generic functions
A constrained generic function carries the same where clause as a constrained class. reified and non-reified inline fun (like a plain square(x: Int)) both generate as an ordinary method or generic method; inlining and reification only matter inside Kotlin and don't change the C# side.
This row only binds for a top-level function with a T-typed direct parameter (fun <T> f(value: T): T). A generic function declared on a class, object, or interface, or a top-level one with no T-typed parameter (e.g. fun <T> f(): List<T>), is not generated.
Type aliases
A typealias erases to its underlying type; there is no separate alias type in the generated C#.
Erasure applies to an extension's receiver too, including a nested-type alias (see Extensions: Nested receivers).
Limitations
A generic class declared in a dependency module and reachable through the export closure has never been exercised across a module boundary; if you hit this, declare the generic class in the publishing module itself instead.