PONY λ M2 Modula-2

C#.CodeCompared.To/Kotlin

An interactive executable cheatsheet comparing C# and Kotlin

C# 14 (.NET 10) Kotlin 2.4
Hello World & Basics
Hello, World
Console.WriteLine("Hello, World!");
fun main() { println("Hello, World!") }
C# top-level statements dropped the Main ceremony; Kotlin kept a fun main() but it is one line, with no class wrapper. println replaces Console.WriteLine, and semicolons are gone.
Top-level functions
Console.WriteLine(Square(7)); static int Square(int number) => number * number;
fun square(number: Int) = number * number fun main() { println(square(7)) }
Kotlin functions live at the top level of a file — no class, no static. The expression body = number * number is C#’s =>, with the return type inferred. Note the convention flip: functions and properties are camelCase, not PascalCase.
val — the read-only local C# never had
var counter = 0; // mutable — the only kind of local counter += 1; const int limit = 10; // const covers compile-time constants only Console.WriteLine($"{counter} {limit}");
fun main() { val fixed = 10 // read-only local var counter = 0 // mutable local counter += 1 println("$fixed $counter") }
val is a genuine read-only local — assignable once, from any runtime expression — which C# has no equivalent for (const handles only compile-time constants). Kotlin style is val everywhere, var only where mutation is the point.
Null Safety — Enforced This Time
Errors, not warnings
#nullable enable string? nickname = null; // nickname.Length compiles anyway — CS8602 is just a WARNING, // and the ? annotation is erased at runtime. Console.WriteLine(nickname ?? "none");
fun main() { val nickname: String? = null // println(nickname.length) // compile ERROR, not a warning println(nickname ?: "none") }
The headline difference: C#’s nullable reference types are advisory — warnings a build can ignore, annotations the runtime never sees. Kotlin’s String? is a different type from String, and dereferencing it without a check refuses to compile. The Elvis operator ?: is C#’s ??.
Safe calls
#nullable enable string[]? words = null; Console.WriteLine(words?.Length ?? 0);
fun main() { val words: List<String>? = null println(words?.size ?: 0) }
?. transfers directly. The fallback operator changes shape: C#’s ?? becomes Kotlin’s ?: — same precedence position, same short-circuit behavior.
! → !! (and this one actually checks)
#nullable enable string? configured = "value"; string forced = configured!; // suppresses the warning; does NOTHING at runtime Console.WriteLine(forced);
fun main() { val configured: String? = "value" val forced: String = configured!! // throws NullPointerException if null println(forced) }
C#’s null-forgiving ! is erased — a wrong one lets null glide on to fail somewhere else. Kotlin’s !! performs a real check and throws at that line, so the failure is immediate and localized. Both are code smells; Kotlin’s at least fails honestly.
as → as?
object value = "text"; string? text = value as string; // null on failure int? number = value as int?; Console.WriteLine(text ?? "not a string"); Console.WriteLine(number?.ToString() ?? "not an int");
fun main() { val value: Any = "text" val text: String? = value as? String // null on failure val number: Int? = value as? Int println(text ?: "not a string") println(number?.toString() ?: "not an int") }
Kotlin’s as? is C#’s as — null on failure. The bare as in Kotlin is the throwing cast, C#’s (string)value. And Any is object: the root of every Kotlin type.
?.let — run only when present
#nullable enable string? entered = " hello "; if (entered is not null) { Console.WriteLine(entered.Trim()); }
fun main() { val entered: String? = " hello " entered?.let { text -> println(text.trim()) } }
?.let { } runs the block only when the value is non-null, with the unwrapped value as the parameter — the expression-shaped version of the is not null guard. Plain if (entered != null) also works and smart-casts (see the Sealed Types section).
Strings
Interpolation without the $ prefix
var name = "Ada"; var count = 3; Console.WriteLine($"{name} has {count} items ({count * 2} shoes)");
fun main() { val name = "Ada" val count = 3 println("$name has $count items (${count * 2} shoes)") }
Every Kotlin string literal interpolates — there is no opt-in $"…" prefix. A bare identifier needs only $name; any real expression takes braces, ${expr}.
Raw strings
var json = """ { "name": "Ada" } """; Console.WriteLine(json);
fun main() { val json = """ { "name": "Ada" } """.trimIndent() println(json) }
Both languages triple-quote raw strings. C# 11’s version strips indentation implicitly, keyed off the closing quotes; Kotlin keeps every space unless you call trimIndent() (or trimMargin()). Kotlin raw strings still interpolate $.
Common string operations
var phrase = "stitch in time"; Console.WriteLine(phrase.ToUpper()); Console.WriteLine(phrase.Contains("time")); Console.WriteLine(string.Join("-", phrase.Split(' ')));
fun main() { val phrase = "stitch in time" println(phrase.uppercase()) println("time" in phrase) println(phrase.split(" ").joinToString("-")) }
ToUpper is uppercase(), membership reads naturally as "time" in phrase, and joining is an extension on the collection itself (joinToString) rather than a static string.Join.
Everything Is an Expression
if is an expression — no ternary
var temperature = 30; var description = temperature > 25 ? "hot" : "mild"; Console.WriteLine(description);
fun main() { val temperature = 30 val description = if (temperature > 25) "hot" else "mild" println(description) }
Kotlin has no ternary operator because it does not need one: if/else is already an expression. With block bodies, the last expression of each branch is the value.
switch expression → when
var status = 404; var message = status switch { 200 => "ok", 301 or 302 => "redirect", >= 400 and < 500 => "client error", _ => "other", }; Console.WriteLine(message);
fun main() { val status = 404 val message = when (status) { 200 -> "ok" 301, 302 -> "redirect" in 400..499 -> "client error" else -> "other" } println(message) }
when is the closest cousin of C# 8’s switch expression: comma-separated alternatives replace or patterns, ranges are the literal in 400..499, and else plays _. There is no fallthrough anywhere in the language.
when over types
object[] items = { 42, "text", 3.14 }; foreach (var item in items) { var described = item switch { int number => $"int {number}", string text => $"string of length {text.Length}", _ => "something else", }; Console.WriteLine(described); }
fun main() { val items: List<Any> = listOf(42, "text", 3.14) for (item in items) { val described = when (item) { is Int -> "Int $item" is String -> "String of length ${item.length}" else -> "something else" } println(described) } }
A C# type pattern must bind a fresh name (int number); Kotlin’s is Int -> smart-casts item itself inside the branch — item.length compiles because the compiler already knows it is a String there.
try is an expression too
var input = "not a number"; int parsed; try { parsed = int.Parse(input); } catch (FormatException) { parsed = 0; } Console.WriteLine(parsed);
fun main() { val input = "not a number" val parsed = try { input.toInt() } catch (exception: NumberFormatException) { 0 } println(parsed) }
Because try yields a value, the result lands in a val with no mutable placeholder declared outside the block. (For this particular job, input.toIntOrNull() ?: 0 is more idiomatic still — the stdlib prefers null returns over exceptions for parsing.)
Collections & LINQ → Operators
Read-only is the default
var numbers = new List<int> { 1, 2, 3 }; // always mutable numbers.Add(4); IReadOnlyList<int> view = numbers; // read-only VIEW, opt-in Console.WriteLine(string.Join(", ", view));
fun main() { val numbers = listOf(1, 2, 3) // read-only by default val editable = mutableListOf(1, 2, 3) // mutability is the opt-in editable.add(4) println(numbers) println(editable) }
The polarity flips: listOf returns a List interface with no mutating members at all — not a wrapper view over something mutable, a different type. mutableListOf is the explicit choice, the mirror image of C# reaching for IReadOnlyList.
Where/Select → filter/map
var numbers = new[] { 1, 2, 3, 4, 5, 6 }; var result = numbers .Where(number => number % 2 == 0) .Select(number => number * 10) .Sum(); Console.WriteLine(result);
fun main() { val numbers = listOf(1, 2, 3, 4, 5, 6) val result = numbers .filter { number -> number % 2 == 0 } .map { number -> number * 10 } .sum() println(result) }
The whole LINQ vocabulary exists under functional names: Wherefilter, Selectmap, Firstfirst, GroupBygroupBy. The lambda moves outside the parentheses (trailing-lambda syntax), and empty parentheses disappear entirely.
Eager by default; sequences are the LINQ
var firstThree = Enumerable.Range(1, 1000000) .Where(number => number % 7 == 0) .Select(number => number * 2) .Take(3); // deferred — nothing runs until enumeration Console.WriteLine(string.Join(", ", firstThree));
fun main() { val firstThree = (1..1_000_000).asSequence() .filter { number -> number % 7 == 0 } .map { number -> number * 2 } .take(3) .toList() // the terminal operation executes the pipeline println(firstThree) }
The performance instinct to recalibrate: LINQ is lazy by default, but Kotlin collection operators are eager — each step materializes a new list. asSequence() opts back into LINQ-style deferred, element-at-a-time evaluation, worth it for long chains or early-exit operations like take.
Dictionary → Map
var stock = new Dictionary<string, int> { ["apples"] = 5, ["pears"] = 2 }; stock["plums"] = 7; foreach (var (fruit, quantity) in stock) { Console.WriteLine($"{fruit}: {quantity}"); }
fun main() { val stock = mutableMapOf("apples" to 5, "pears" to 2) stock["plums"] = 7 for ((fruit, quantity) in stock) { println("$fruit: $quantity") } }
The infix to builds a Pair, indexers work both ways, and destructuring in the loop matches C#’s tuple deconstruction. One guarantee C# never gives: mutableMapOf is backed by LinkedHashMap, so iteration order is insertion order, always.
Real range syntax
foreach (var count in Enumerable.Range(1, 5)) { Console.Write($"{count} "); } Console.WriteLine(); for (var count = 10; count >= 0; count -= 2) { Console.Write($"{count} "); } Console.WriteLine();
fun main() { for (count in 1..5) { print("$count ") } println() for (count in 10 downTo 0 step 2) { print("$count ") } println() }
Ranges are language syntax: 1..5 is inclusive of both ends (note the off-by-one trap against Enumerable.Range(start, count)), ..< excludes the end, and downTo/step replace the C-style descending loop.
Functions & Lambdas
Named & default arguments
Console.WriteLine(Resize(width: 800, height: 600)); static string Resize(int width, int height, bool preserveAspect = true) => $"{width}x{height} preserve={preserveAspect}";
fun resize(width: Int, height: Int, preserveAspect: Boolean = true) = "${width}x$height preserve=$preserveAspect" fun main() { println(resize(width = 800, height = 600)) }
A near-perfect rhyme — both languages have named and default arguments. The only visible difference is the separator: C# names with width:, Kotlin with width =. Because defaults exist, Kotlin has almost no method overloading in practice.
Extension methods, minus the ceremony
Console.WriteLine("racecar".IsPalindrome()); static class StringExtensions { public static bool IsPalindrome(this string text) => text.SequenceEqual(text.Reverse()); }
fun String.isPalindrome(): Boolean = this == this.reversed() fun main() { println("racecar".isPalindrome()) }
No static class, no this parameter — fun ReceiverType.name() declares an extension anywhere a function can live. Kotlin also has extension properties (val String.wordCount get() = …), which C# extension methods cannot express.
Lambdas & it
var numbers = new[] { 1, 2, 3 }; var doubled = numbers.Select(number => number * 2); Console.WriteLine(string.Join(", ", doubled));
fun main() { val doubled = listOf(1, 2, 3).map { it * 2 } println(doubled) }
Braces are the lambda literal, and a single parameter needs no declaration at all — it is the implicit name. For anything beyond a short expression, naming the parameter (number ->) is better style.
Func/Action → (Int) -> Int
Func<int, int> increment = number => number + 1; Action<string> report = message => Console.WriteLine(message); Console.WriteLine(increment(41)); report("done");
fun main() { val increment: (Int) -> Int = { number -> number + 1 } val report: (String) -> Unit = { message -> println(message) } println(increment(41)) report("done") }
Function types are structural syntax, not a delegate family: (Int) -> Int replaces Func<int, int>. Because Unit is a real type (void-as-a-value), the separate Action family is unnecessary — a returning-nothing function is just (String) -> Unit.
Classes & Properties
Primary constructors that declare properties
var rectangle = new Rectangle(3, 4); Console.WriteLine(rectangle.Area); class Rectangle(double width, double height) { public double Width { get; } = width; public double Height { get; } = height; public double Area => Width * Height; }
class Rectangle(val width: Double, val height: Double) { val area: Double get() = width * height } fun main() { val rectangle = Rectangle(3.0, 4.0) println(rectangle.area) }
C# 12 primary constructor parameters are still just parameters — the properties are written separately. Kotlin’s val/var in the constructor header declares real properties in one stroke. Two more surprises: no new keyword, and no implicit IntDouble conversion — Rectangle(3, 4) would not compile.
Properties with custom accessors
var thermostat = new Thermostat { Celsius = 25 }; Console.WriteLine(thermostat.Fahrenheit); class Thermostat { public double Celsius { get; set; } public double Fahrenheit { get => Celsius * 9 / 5 + 32; set => Celsius = (value - 32) * 5 / 9; } }
class Thermostat { var celsius: Double = 0.0 var fahrenheit: Double get() = celsius * 9 / 5 + 32 set(value) { celsius = (value - 32) * 5 / 9 } } fun main() { val thermostat = Thermostat() thermostat.celsius = 25.0 println(thermostat.fahrenheit) }
Properties transfer almost verbatim — accessors nest under the declaration, and a custom accessor can name its backing field via the field identifier. What is missing is C#’s object-initializer syntax { Celsius = 25 }; Kotlin’s answer is apply { } (see Scope Functions).
final by default — classes too
var animals = new List<Animal> { new Animal(), new Dog() }; foreach (var animal in animals) { Console.WriteLine(animal.Speak()); } class Animal { public virtual string Speak() => "..."; } class Dog : Animal { public override string Speak() => "Woof"; }
open class Animal { open fun speak(): String = "..." } class Dog : Animal() { override fun speak(): String = "Woof" } fun main() { val animals: List<Animal> = listOf(Animal(), Dog()) for (animal in animals) { println(animal.speak()) } }
C# methods are sealed-unless-virtual; Kotlin extends that stance to the class itself — nothing can be subclassed or overridden without an explicit open. The parentheses in : Animal() are the superclass constructor call, made visible in the supertype list.
record → data class
var original = new Person("Ada", 36); var older = original with { Age = 37 }; Console.WriteLine(original); Console.WriteLine(older); Console.WriteLine(original == older); record Person(string Name, int Age);
data class Person(val name: String, val age: Int) fun main() { val original = Person("Ada", 36) val older = original.copy(age = 37) println(original) println(older) println(original == older) }
data class is the record: generated value equality, toString, and destructuring. C#’s with { Age = 37 } expression becomes the generated copy(age = 37) method with named arguments — same non-destructive mutation, method call instead of syntax.
== calls equals — always
var first = new List<int> { 1, 2, 3 }; var second = new List<int> { 1, 2, 3 }; Console.WriteLine(first == second); // False — reference comparison Console.WriteLine(first.SequenceEqual(second)); // True
fun main() { val first = listOf(1, 2, 3) val second = listOf(1, 2, 3) println(first == second) // true — == always calls equals() println(first === second) // false — reference identity is === }
A deep instinct inverts: Kotlin’s == is always structural (it compiles to equals()), on every type, with no operator overloading required. Reference identity gets its own operator, ===. In C#, == on classes defaults to reference comparison unless someone overloaded it.
No static: object & companion
static members → companion object
Console.WriteLine(Temperature.FromFahrenheit(212).Celsius); class Temperature { public double Celsius { get; } private Temperature(double celsius) => Celsius = celsius; public static Temperature FromFahrenheit(double fahrenheit) => new((fahrenheit - 32) * 5 / 9); }
class Temperature private constructor(val celsius: Double) { companion object { fun fromFahrenheit(fahrenheit: Double) = Temperature((fahrenheit - 32) * 5 / 9) } } fun main() { println(Temperature.fromFahrenheit(212.0).celsius) }
Kotlin has no static keyword. The companion object is a singleton attached to the class that holds the would-be statics — call sites still read Temperature.fromFahrenheit(…). Note where the private constructor lands: inline in the class header.
The singleton is a keyword
Counter.Instance.Increment(); Counter.Instance.Increment(); Console.WriteLine(Counter.Instance.Count); class Counter { public static Counter Instance { get; } = new(); public int Count { get; private set; } public void Increment() => Count += 1; }
object Counter { var count = 0 private set fun increment() { count += 1 } } fun main() { Counter.increment() Counter.increment() println(Counter.count) }
An object declaration is a language-level singleton — the hand-written Instance pattern compressed into one keyword, initialized lazily and thread-safely. private set under the property mirrors { get; private set; }.
Sealed Types & Smart Casts
Sealed hierarchies: exhaustive without _
Shape shape = new Circle(2); var area = shape switch { Circle circle => Math.PI * circle.Radius * circle.Radius, Rectangle rectangle => rectangle.Width * rectangle.Height, _ => throw new InvalidOperationException("unknown shape"), }; Console.WriteLine($"{area:F2}"); abstract record Shape; record Circle(double Radius) : Shape; record Rectangle(double Width, double Height) : Shape;
sealed interface Shape data class Circle(val radius: Double) : Shape data class Rectangle(val width: Double, val height: Double) : Shape fun main() { val shape: Shape = Circle(2.0) val area = when (shape) { is Circle -> Math.PI * shape.radius * shape.radius is Rectangle -> shape.width * shape.height } println("%.2f".format(area)) }
Kotlin’s sealed means the compiler knows every subtype, so the when needs no else — and adding a third shape breaks this expression at compile time until it is handled. A C# switch over a hierarchy cannot prove exhaustiveness, hence the defensive _ arm. Note the smart cast: shape.radius, no binding variable.
Pattern variables → smart casts
PrintLength("hello"); PrintLength(42); static void PrintLength(object value) { if (value is string text) { Console.WriteLine(text.Length); // needs the new name 'text' } else { Console.WriteLine("not a string"); } }
fun printLength(value: Any) { if (value is String) { println(value.length) // value itself is a String here — no new name } else { println("not a string") } } fun main() { printLength("hello") printLength(42) }
C# type patterns introduce a second name for the narrowed value; Kotlin flows the narrowed type through the same variable. The compiler tracks it through &&, when branches, and null checks alike — with the caveat that only stable values (vals, not open properties) smart-cast.
Deconstruct → componentN
var (name, age) = new Person("Ada", 36); Console.WriteLine($"{name}, {age}"); record Person(string Name, int Age);
data class Person(val name: String, val age: Int) fun main() { val (name, age) = Person("Ada", 36) println("$name, $age") }
The syntax is identical. The mechanism differs in name only: records generate Deconstruct, data classes generate component1()/component2() — which is also what powers destructuring in for ((key, value) in map) loops.
Scope Functions
Object initializers → apply
var settings = new Settings { Theme = "dark", FontSize = 14 }; Console.WriteLine($"{settings.Theme} {settings.FontSize}"); class Settings { public string Theme { get; set; } = "light"; public int FontSize { get; set; } = 12; }
class Settings { var theme: String = "light" var fontSize: Int = 12 } fun main() { val settings = Settings().apply { theme = "dark" fontSize = 14 } println("${settings.theme} ${settings.fontSize}") }
apply { } is Kotlin’s object initializer: the block runs with the new instance as this (so bare property names assign), and the expression returns the configured object. Unlike C#’s initializer syntax, it works on any expression, not just constructions.
The scope-function family
var input = " 42 "; var trimmed = input.Trim(); // step by step, var parsed = int.Parse(trimmed); // temporary names var doubledValue = parsed * 2; Console.WriteLine(doubledValue);
fun main() { val doubledValue = " 42 " .let { text -> text.trim() } .let { trimmed -> trimmed.toInt() * 2 } println(doubledValue) }
The scope functions are a signature Kotlin idiom with no C# counterpart: let (transform it), run (transform this), apply (configure, return the object), also (side effect, return the object), with. They turn temporary-variable sequences into pipelines — used sparingly, they read beautifully; nested, they become soup.
Delegation
Lazy<T> → by lazy
var report = new Lazy<string>(() => { Console.WriteLine("computing..."); return "the report"; }); Console.WriteLine("before access"); Console.WriteLine(report.Value);
val report: String by lazy { println("computing...") "the report" } fun main() { println("before access") println(report) }
by lazy makes the laziness invisible at every use site — the property’s type is plain String, no .Value unwrapping. It is one instance of property delegation, a general mechanism (Delegates.observable, map-backed properties, custom delegates) with no C# analog.
Interface delegation with by
var logger = new TimestampLogger(new ConsoleLogger()); logger.Log("started"); interface ILogger { void Log(string message); } class ConsoleLogger : ILogger { public void Log(string message) => Console.WriteLine($"log: {message}"); } class TimestampLogger(ILogger inner) : ILogger { public void Log(string message) => inner.Log($"[stamped] {message}"); }
interface Logger { fun log(message: String) } class ConsoleLogger : Logger { override fun log(message: String) = println("log: $message") } class ForwardingLogger(delegate: Logger) : Logger by delegate fun main() { val logger = ForwardingLogger(ConsoleLogger()) logger.log("started") }
Logger by delegate generates every forwarding member automatically — composition over inheritance without hand-writing the pass-through methods C# requires. Override only the members you want to change; the rest keep forwarding.
Generics
Reified natively → reified by inlining
// .NET generics are reified: typeof(T) always works, at runtime, // in any generic method. Console.WriteLine(DescribeType<string>()); Console.WriteLine(DescribeType<int>()); static string DescribeType<T>() => typeof(T).Name;
// JVM generics are ERASED — T is gone at runtime. inline + reified // recovers it by pasting the function body at each call site. inline fun <reified T> describeType(): String = T::class.simpleName ?: "unknown" fun main() { println(describeType<String>()) println(describeType<Int>()) }
A rare inversion where C# has the stronger runtime: .NET reifies generics natively, while the JVM erases them. Kotlin’s inline fun <reified T> recovers T only because inlining copies the body — with a concrete type — into every call site. Outside an inline function, T::class does not compile.
out/in — the same keywords
IEnumerable<string> words = new List<string> { "covariant" }; IEnumerable<object> values = words; // IEnumerable<out T> — covariance Console.WriteLine(values.Count());
fun main() { val words: List<String> = listOf("covariant") val values: List<Any> = words // List is declared List<out E> println(values.size) }
Kotlin borrowed C#’s exact keywords: out for covariance, in for contravariance, declared at the type definition. Kotlin extends the idea past interfaces and delegates to classes, and adds use-site projections (Array<out Number>) for types that declare no variance.
Error Handling
try/catch/finally
try { throw new InvalidOperationException("engine offline"); } catch (InvalidOperationException exception) { Console.WriteLine($"caught: {exception.Message}"); } finally { Console.WriteLine("cleanup"); }
fun main() { try { throw IllegalStateException("engine offline") } catch (exception: IllegalStateException) { println("caught: ${exception.message}") } finally { println("cleanup") } }
The model matches C# exactly: all exceptions are unchecked (Kotlin deliberately dropped Java’s checked exceptions), finally works the same, and the catch clause reads (name: Type). Message becomes the property message, and the JVM names differ (IllegalStateExceptionInvalidOperationException).
TODO() and the Nothing type
Console.WriteLine(Describe(-5)); Console.WriteLine(Describe(0)); static string Describe(int value) => value switch { < 0 => "negative", 0 => "zero", _ => throw new NotImplementedException("positive numbers still unhandled"), };
fun describe(value: Int): String = when { value < 0 -> "negative" value == 0 -> "zero" else -> TODO("positive numbers still unhandled") } fun main() { println(describe(-5)) println(describe(0)) }
TODO() throws NotImplementedError, but its return type is Nothing — the type of "never returns" — so it type-checks in any position, exactly like a throw expression in a C# switch arm. Also shown: a subject-less when is Kotlin’s if/else-if chain.
Coroutines vs async/await
async Task<T> → suspend fun
var greeting = await FetchGreetingAsync(); Console.WriteLine(greeting); static async Task<string> FetchGreetingAsync() { await Task.Delay(50); return "hello from a task"; }
import kotlinx.coroutines.* suspend fun fetchGreeting(): String { delay(50) return "hello from a coroutine" } fun main() = runBlocking { println(fetchGreeting()) }
suspend replaces async, and — the striking part — calls need no await: a suspend call looks like a plain call, with suspension implicit. The return type stays honest too: String, not Task<string>. runBlocking bridges from the non-suspending world at the entry point.
Task.WhenAll → async/await in a scope
var results = await Task.WhenAll(SquareAsync(3), SquareAsync(4)); Console.WriteLine(string.Join(", ", results)); static async Task<int> SquareAsync(int value) { await Task.Delay(10); return value * value; }
import kotlinx.coroutines.* suspend fun square(value: Int): Int { delay(10) return value * value } fun main() = runBlocking { val first = async { square(3) } val second = async { square(4) } println(listOf(first.await(), second.await())) }
async { } starts a Deferred (≈ Task<T>) — but inside a scope that owns it: if one child fails, the scope cancels its siblings, and runBlocking cannot exit leaving orphans behind. That structure is what C# approximates with Task.WhenAll discipline. await() is a method here, not a keyword.
Task.Run → launch
var worker = Task.Run(() => Console.WriteLine("working in the pool")); await worker; Console.WriteLine("done");
import kotlinx.coroutines.* fun main() = runBlocking { val job = launch { println("working in a coroutine") } job.join() println("done") }
launch is the fire-and-forget side (it returns a Job, no value) to async’s Deferred. Unlike Task.Run, launching does not imply a thread pool — coroutines are cheap enough to start thousands, and they stay on the current dispatcher unless one is specified.