kotlin-native-nuget Help

Interfaces, abstract classes, and sealed classes

Kotlin's three flavours of inheritance each get a distinct C# shape: interface becomes an I-prefixed C# interface with default methods delegating back to Kotlin, abstract class becomes a C# abstract class whose subclasses share an inherited _handle, and sealed class becomes an abstract class with its subtypes nested inside it.

Kotlin

C#

Notes

interface

interface (I-prefixed)

default methods delegate to Kotlin

abstract class

abstract class

_handle inherited by subclasses

sealed class

abstract class

subclasses nested, see ADR-009

Kotlin

From test-library/src/nativeMain/kotlin/.../cat/Pet.kt and Animal.kt:

interface Pet { val name: String fun speak(): String fun greet(): String = "Hi, I'm $name" } abstract class Animal(override val name: String) : Pet { override fun greet(): String = "Hi, I'm $name" fun introduce(): String = "My name is $name" }

Cat (see Classes and objects) extends Animal, which implements Pet.

A sealed hierarchy, from test-library/src/nativeMain/kotlin/.../cat/Observation.kt:

sealed class Observation { data object Superposition : Observation() data class Alive(val cat: Cat) : Observation() data class Dead(val cause: String) : Observation() } fun openBox(name: String): Observation { if (name == "Oreo") return Observation.Alive(Cat("Oreo")) return Observation.Dead("The cat was not $name") }

Generated C#

From Interop.cs. IPet gets a default-implemented Greet(), and Animal is abstract with an inherited _handle:

public interface IPet : IDisposable { string Name { get; } string Speak(); string Greet(); } public abstract class Animal : IPet { internal IntPtr _handle; internal Animal(IntPtr handle) { _handle = handle; } public string Name { get { /* ... */ } } public string Greet() { /* delegates to the Kotlin-side override */ } public string Introduce() { /* ... */ } public abstract string Speak(); public abstract void Dispose(); }

Cat : Animal inherits _handle and only overrides Speak() and Dispose(). It never redeclares the field.

Observation renders as an abstract class with each Kotlin subtype as a nested sealed class, plus a FromHandle dispatcher that reads a type tag off the native handle:

public abstract class Observation : IDisposable { internal IntPtr _handle; public sealed class Alive : Observation { internal Alive(IntPtr handle) : base(handle) { } public Cat Cat => new Cat(Native_Get_cat(_handle)); public override bool Equals(object? obj) { /* ... */ } public override int GetHashCode() => Native_HashCode(_handle); public override string ToString() => Marshal.PtrToStringUTF8(Native_ToString(_handle))!; public override void Dispose() { /* ... */ } } public sealed class Dead : Observation { public string Cause => Marshal.PtrToStringUTF8(Native_Get_cause(_handle))!; // Equals / GetHashCode / ToString / Dispose ... } public sealed class Superposition : Observation { public override string ToString() => "Superposition"; public override void Dispose() { /* ... */ } } [DllImport("sample", CallingConvention = CallingConvention.Cdecl, EntryPoint = "observation_get_type")] private static extern int Native_GetType(IntPtr handle); internal static Observation FromHandle(IntPtr handle) { return Native_GetType(handle) switch { 0 => new Alive(handle), 1 => new Dead(handle), 2 => new Superposition(handle), _ => throw new InvalidOperationException("Unknown sealed class type") }; } public abstract void Dispose(); }

Alive and Dead are Kotlin data class subtypes, so they also get Equals/GetHashCode/ToString (see Data classes). Superposition is a data object, so it has a fixed ToString() and no Equals/GetHashCode override (reference equality is enough for a singleton).

Using it from C#

Polymorphism through IPet, from IntegrationTests/InterfaceTests.cs:

[Fact] public void IPet_Greet_UsesDefaultImplementation() { using IPet pet = new Cat("Oreo", 9); Assert.Equal("Hi, I'm Oreo", pet.Greet()); } [Fact] public void IPet_CanBeUsedPolymorphically() { using IPet pet = new Cat("Mylo", 9); string greeting = Greet(pet); Assert.Equal("Hi, I'm Mylo", greeting); } private static string Greet(IPet pet) => pet.Greet();

Abstract-class inheritance, from IntegrationTests/AbstractClassTests.cs:

[Fact] public void Animal_CannotBeInstantiated() { // Animal is abstract — this verifies the C# class is also abstract Assert.True(typeof(Animal).IsAbstract); }

Pattern matching over a sealed hierarchy, from IntegrationTests/SealedClassTests.cs:

[Fact] public void Observation_WorksWithPatternMatching() { using Observation result = ObservationKt.openBox("Oreo"); string message = result switch { Observation.Superposition => "Unknown - cat is in superposition", Observation.Alive a => $"Alive: {a.Cat!.Name}", Observation.Dead d => $"Dead: {d.Cause}", _ => throw new InvalidOperationException(), }; Assert.Equal("Alive: Oreo", message); }
Last modified: 21 July 2026