PONY λ M2 Modula-2

Dart.CodeCompared.To/C#

An interactive executable cheatsheet comparing Dart and C#

Dart 3.7 C# 13
Basics & Running
Hello, World
Modern C# uses top-level statements: no class Program, no static void Main. The cells here show that body; the runner supplies the implicit usings and (when needed) the async entry point.
void main() { print('Hello, World!'); }
Console.WriteLine("Hello, World!");
Since C# 9, a file of top-level statements is the program — the compiler generates the Main for you — so this single line runs. Output goes through Console.WriteLine (from the System namespace, imported implicitly). Statements end in semicolons and the language is otherwise very close to Dart's C-family surface.
var, explicit types & interpolation
void main() { final answer = 42; double ratio = 3.5; var greeting = 'hello'; print('$answer $ratio $greeting'); }
var answer = 42; // inferred int double ratio = 3.5; // explicit type before the name (C-style) var greeting = "hello"; Console.WriteLine($"{answer} {ratio} {greeting}");
C# infers with var (Dart's var/final both map here — use const/readonly for immutability). Type annotations sit before the name, C-style (double ratio), not after a colon. String interpolation is $"{expr}" — a $ prefix and braces, close to Dart's ${…} but with no bare-$name form.
Numbers & typed literals
void main() { int whole = 7 ~/ 2; // integer division operator double exact = 7 / 2; // / always yields a double print('$whole $exact'); }
int whole = 7 / 2; // int / int truncates -> 3 double exact = 7.0 / 2; // force a double for 3.5 long big = 5_000_000_000L; decimal money = 19.99m; // decimal: base-10, for currency Console.WriteLine($"{whole} {exact} {big} {money}");
Like most C-family languages, C# has no ~// on two ints already truncates, and you force floating point with a double operand. There is no num supertype, but C# offers a richer numeric menu than Dart: sized integers (int/long/byte), double/float, and a base-10 decimal for money that avoids binary rounding error.
Nullable Reference Types
Nullable types: ?, ?. and ??
String? findName(int id) => id == 1 ? 'Ada' : null; void main() { final name = findName(2); print(name?.toUpperCase() ?? 'anonymous'); // ?. and ?? }
static string? FindName(int id) => id == 1 ? "Ada" : null; var name = FindName(2); Console.WriteLine(name?.ToUpper() ?? "anonymous"); // ?. and ?? as in Dart string? nickname = null; nickname ??= "assigned"; // ??= too Console.WriteLine(nickname);
The operators port over almost unchanged: ? marks a nullable type, and ?., ??, and ??= behave exactly as in Dart. Under nullable reference types (on by default in new projects) the compiler flow-analyzes nullability much as Dart does — so the surface feels like home, until you learn how much softer the guarantee is (next).
Nullability is unsound
This is the trap for a Dart programmer. C#'s null checks are warnings, not errors — they can be ignored, the whole feature can be switched off, and nothing is enforced at run time.
void main() { String? maybe = null; // print(maybe.length); // won't COMPILE: maybe may be null print(maybe?.length ?? 0); }
string? maybe = null; // Console.WriteLine(maybe.Length); // only a WARNING (CS8602), still compiles string forced = maybe!; // ! = "trust me, not null" — UNCHECKED // forced.Length here would throw NullReferenceException at run time Console.WriteLine(maybe?.Length ?? 0); // the safe way
Dart's null safety is sound: dereferencing a nullable is a compile error, full stop. C#'s is unsound — the analysis emits warnings you can suppress, older or opted-out code produces genuine nulls, and ! (the null-forgiving operator) silences the checker with no runtime guard, so a wrong ! yields a NullReferenceException. Treat the safety as advisory, not guaranteed.
Value Types: struct
struct copies; class references
C# has value types, which Dart lacks entirely. A struct is copied on assignment; a class is a reference like every Dart object.
class Point { int x, y; Point(this.x, this.y); } void main() { final first = Point(1, 2); final second = first; // same object — a reference second.x = 99; print(first.x); // 99 — aliasing }
var first = new Point(1, 2); var second = first; // an independent COPY (struct = value type) second.X = 99; Console.WriteLine(first.X); // 1 — the copy did not touch first struct Point(int x, int y) // primary constructor { public int X = x; public int Y = y; }
Declaring Point a struct makes it a value type, so second = first copies and the two are independent — the model Dart has no equivalent for. Had Point been a class, assignment would alias one object exactly as in Dart. The community default is class; reach for struct for small, immutable, value-like data, and prefer readonly struct to make the value semantics airtight.
Records & Value Equality
record: value equality (not a tuple!)
A false friend. Dart's record is an anonymous tuple; C#'s record is a named class with generated value equality — closer to Kotlin's data class or Dart's hand-written ==.
class Money { final int cents; const Money(this.cents); @override bool operator ==(Object other) => other is Money && other.cents == cents; @override int get hashCode => cents.hashCode; @override String toString() => 'Money($cents)'; } void main() { print(Money(500) == Money(500)); // true }
var a = new Money(500); var b = new Money(500); Console.WriteLine(a == b); // true — value equality is generated Console.WriteLine(a); // Money { Cents = 500 } — ToString generated record Money(int Cents); // one line replaces the whole Dart class
A C# record auto-generates value Equals/GetHashCode, a readable ToString, and deconstruction — collapsing the entire Dart boilerplate on the left into one line. Do not confuse it with Dart's record type (int, String), which is a positional tuple; C#'s tuple is (int, string) instead. A record is a reference type by default (record struct makes it a value type).
Non-destructive with
class Point { final int x, y; const Point(this.x, this.y); Point copyWith({int? x, int? y}) => Point(x ?? this.x, y ?? this.y); @override String toString() => 'Point($x, $y)'; } void main() { final origin = Point(0, 0); print(origin.copyWith(y: 9)); // Point(0, 9) }
var origin = new Point(0, 0); var moved = origin with { Y = 9 }; // copy, changing only Y Console.WriteLine(moved); // Point { X = 0, Y = 9 } Console.WriteLine(origin); // unchanged record Point(int X, int Y);
Records come with the with expression — origin with { Y = 9 } — which produces a copy with some properties changed, exactly the job of the hand-written copyWith every Dart value class needs (and the reason freezed exists on the Dart side). It is non-destructive: the original is untouched, so records plus with give you ergonomic immutable updates for free.
Classes & Properties
Getters/setters → properties
class Person { String name; int _age; Person(this.name, this._age); int get age => _age; set age(int value) => _age = value < 0 ? 0 : value; } void main() { final person = Person('Ada', 36); person.age = -5; print('${person.name} ${person.age}'); // Ada 0 }
var person = new Person("Ada", 36); person.Age = -5; Console.WriteLine($"{person.Name} {person.Age}"); // Ada 0 class Person(string name, int age) // primary constructor { public string Name { get; set; } = name; // auto-property private int _age = age; public int Age { get => _age; set => _age = value < 0 ? 0 : value; // 'value' is the incoming value } }
C# properties are the counterpart of Dart's getters/setters, but far more prominent in style. An auto-property { get; set; } declares backing storage in one line, while a full property uses get/set bodies with the implicit value parameter. init accessors (set only during construction) and primary constructors (C# 12) trim the ceremony further, so a class body stays terse.
Inheritance & override
abstract class Animal { String speak(); String describe() => 'a ${speak()}er'; } class Dog extends Animal { @override String speak() => 'Woof'; } void main() => print(Dog().describe());
Console.WriteLine(new Dog().Describe()); abstract class Animal { public abstract string Speak(); public string Describe() => $"a {Speak()}er"; } class Dog : Animal { public override string Speak() => "Woof"; // 'override' is REQUIRED }
Single inheritance uses : (class Dog : Animal), and abstract works as in Dart. The differences are keyword-level: a method must be marked virtual (or abstract) in the base to be overridable, and the subclass must write override explicitly — where Dart's @override is only an optional annotation and any method can be overridden. C# also separates class from interface, rather than every class defining an implicit interface.
Operator overloading
class Vector { final int x, y; const Vector(this.x, this.y); Vector operator +(Vector other) => Vector(x + other.x, y + other.y); @override String toString() => '($x, $y)'; } void main() { print(Vector(1, 2) + Vector(3, 4)); }
Console.WriteLine(new Vector(1, 2) + new Vector(3, 4)); // (4, 6) record Vector(int X, int Y) { public static Vector operator +(Vector a, Vector b) // STATIC operator => new(a.X + b.X, a.Y + b.Y); public override string ToString() => $"({X}, {Y})"; }
Both languages let a type overload +, but C# operators are static methods taking both operands (operator +(Vector a, Vector b)), where Dart's operator + is an instance method taking the right-hand side. Value equality via == comes free from the record here; on a plain class you would override Equals/GetHashCode (and, if you want ==, that operator too).
Interfaces (no Mixins)
Interfaces & default methods
mixin Greetable { String get who; String greet() => 'Hello, $who'; // mixin default method } class Person with Greetable { @override final String who; Person(this.who); } void main() => print(Person('Ada').greet());
IGreetable person = new Person("Ada"); // an INTERFACE reference Console.WriteLine(person.Greet()); // default methods need the interface type interface IGreetable { string Who { get; } string Greet() => $"Hello, {Who}"; // default interface method } class Person(string who) : IGreetable { public string Who { get; } = who; }
C# has no mixins. Shared behavior comes from an interface (conventionally prefixed I) that a class opts into with :, and since C# 8 an interface method may carry a default implementation — the nearest analog to a Dart mixin's concrete methods. One catch a Dart programmer hits: a default method is only callable through the interface type (IGreetable person = …), not the concrete class, because it is not inherited onto the class the way a Dart mixin method is. Composition covers the rest, and conformance is explicit here, unlike Dart's implicit interfaces.
Cascades → object initializers
class Server { String host = ''; int port = 0; bool tls = false; } void main() { final server = Server() ..host = 'db.example' ..port = 5432 ..tls = true; // cascade print('${server.host}:${server.port}'); }
var server = new Server // object initializer: set fields inline { Host = "db.example", Port = 5432, Tls = true, }; Console.WriteLine($"{server.Host}:{server.Port}"); class Server { public string Host = ""; public int Port; public bool Tls; }
C# has no cascade operator, but its object initializer syntax covers the most common use — setting several properties right after construction — inside { … } braces at the new site. It is less general than Dart's .. (which chains arbitrary method calls, not just assignments); for a fluent method chain you would design each method to return this. Collection initializers work the same way for lists and dictionaries.
Collections
List → List<T>
void main() { final numbers = <int>[1, 2, 3]; numbers.add(4); print(numbers.length); print(numbers.first); }
List<int> numbers = [1, 2, 3]; // collection expression (C# 12) numbers.Add(4); Console.WriteLine(numbers.Count); // .Count, not .length Console.WriteLine(numbers[0]); Console.WriteLine(string.Join(", ", numbers));
A Dart List is a C# List<T>, and since C# 12 you can build one with a collection expression [1, 2, 3] that looks just like Dart's literal. The renames to remember: it is .Count (not .length) and .Add (capitalized, like every public member). C# also has fixed-size arrays (int[]) as a distinct lower-level type, where Dart uses List for both.
Map → Dictionary
void main() { final ages = {'Ada': 36, 'Grace': 45}; print(ages['Ada']); print(ages.containsKey('Nobody')); ages.forEach((name, age) => print('$name: $age')); }
var ages = new Dictionary<string, int> { ["Ada"] = 36, ["Grace"] = 45 }; Console.WriteLine(ages["Ada"]); // 36 — throws if key absent! Console.WriteLine(ages.ContainsKey("Nobody")); if (ages.TryGetValue("Ada", out var age)) // the safe lookup Console.WriteLine(age); foreach (var (name, a) in ages) Console.WriteLine($"{name}: {a}");
A C# Dictionary<K, V> is Dart's Map with one sharp difference: indexing a missing key throws a KeyNotFoundException rather than returning null as Dart does. The idiomatic safe lookup is TryGetValue(key, out var value), which returns a bool and binds the value via an out parameter — a small pattern with no Dart equivalent worth getting comfortable with.
LINQ
Iterable methods → LINQ
void main() { final numbers = [1, 2, 3, 4, 5, 6]; final result = numbers .where((n) => n.isEven) .map((n) => n * n) .fold(0, (sum, n) => sum + n); print(result); // 56 }
int[] numbers = [1, 2, 3, 4, 5, 6]; var result = numbers .Where(n => n % 2 == 0) // where -> Where .Select(n => n * n) // map -> Select .Sum(); // fold -> Sum / Aggregate Console.WriteLine(result); // 56
LINQ is C#'s counterpart to Dart's Iterable methods, and the chain maps directly: where is Where, map is Select, fold is Aggregate (with dedicated Sum/Max/Count shortcuts). Like Dart's lazy iterables, LINQ is deferred — nothing runs until you enumerate or call a terminal like ToList()/Sum(). It also comes in a SQL-like query syntax (from n in numbers where … select …) with no Dart analog.
Grouping & aggregation
void main() { final words = ['ant', 'bee', 'cat', 'ape', 'bug']; final byFirst = <String, List<String>>{}; for (final word in words) { byFirst.putIfAbsent(word[0], () => []).add(word); } print(byFirst['a']); // [ant, ape] }
string[] words = ["ant", "bee", "cat", "ape", "bug"]; var byFirst = words .GroupBy(w => w[0]) // group by first letter .ToDictionary(g => g.Key, g => g.ToList()); Console.WriteLine(string.Join(", ", byFirst['a'])); // ant, ape
Where Dart's standard library makes you build a grouping by hand with putIfAbsent, LINQ has GroupBy as a first-class operator, alongside OrderBy, Distinct, Join, SelectMany, and more. This breadth is LINQ's real advantage over Dart's collection API — many transformations that are a manual loop in Dart are a single named operator in C#.
Pattern Matching
switch expressions & patterns
String classify(Object value) => switch (value) { int n when n < 0 => 'negative', int() => 'number', String() => 'text', _ => 'other', }; void main() { print(classify(-5)); print(classify('hi')); }
static string Classify(object value) => value switch { int n when n < 0 => "negative", // type + guard int => "number", string => "text", _ => "other", }; Console.WriteLine(Classify(-5)); Console.WriteLine(Classify("hi"));
C# and Dart 3 converged on nearly the same switch expression, down to type patterns (int), when guards, and the _ discard. The subject comes before switch in C# (value switch { … }) and arms use =>. C# adds property patterns ({ Length: > 3 }), relational patterns (> 0), and list patterns ([1, .., 9]) — the same family Dart patterns cover.
Property & positional patterns
class Point { final int x, y; const Point(this.x, this.y); } String describe(Point p) => switch (p) { Point(x: 0, y: 0) => 'origin', Point(x: 0) => 'on y-axis', _ => 'elsewhere', }; void main() => print(describe(Point(0, 0)));
Console.WriteLine(Describe(new Point(0, 0))); static string Describe(Point p) => p switch { { X: 0, Y: 0 } => "origin", // property pattern (0, _) => "on y-axis", // positional (needs Deconstruct) _ => "elsewhere", }; record Point(int X, int Y);
C# matches on an object's shape with property patterns{ X: 0, Y: 0 } — the direct counterpart of Dart's object pattern Point(x: 0, y: 0). Positional patterns like (0, _) work when the type has a Deconstruct method (records provide one for free). Combined with the relational and list patterns above, C# pattern matching reaches the same expressiveness as Dart's, from the same C-family starting point.
async/await & Task
Future → Task
Future<int> fetchCount() async { await Future.delayed(Duration(milliseconds: 10)); return 42; } void main() async { final count = await fetchCount(); print(count); }
static async Task<int> FetchCount() { await Task.Delay(10); return 42; } var count = await FetchCount(); // top-level await is allowed Console.WriteLine(count);
This is a near-perfect match: a Dart Future<int> is a C# Task<int>, and async/await read identically. A delay is Task.Delay, and top-level statements permit await directly. One semantic difference under the hood: a C# async method does not start until awaited-ish scheduling kicks in via the returned Task, and C# runs on a real thread pool, so continuations may resume on a different thread — where Dart's single isolate always resumes on the same one.
Future.wait → Task.WhenAll
Future<int> load(int id) async => id * 10; void main() async { final results = await Future.wait([load(1), load(2), load(3)]); print(results); // [10, 20, 30] }
static async Task<int> Load(int id) => id * 10; var tasks = new[] { 1, 2, 3 }.Select(Load); int[] results = await Task.WhenAll(tasks); // run concurrently, await together Console.WriteLine(string.Join(", ", results)); // 10, 20, 30
Dart's Future.wait is C#'s Task.WhenAll — run several tasks concurrently and await them as one, preserving order. C# offers the neighbors too: Task.WhenAny (Dart's Future.any), CancellationToken for cooperative cancellation, and IAsyncEnumerable<T> with await foreach for streaming — the equivalent of Dart's Stream.
Delegates, Events & Extensions
Function types → Func/Action
void main() { int Function(int) square = (n) => n * n; print(square(5)); void Function(String) log = (message) => print('[log] $message'); log('hi'); }
Func<int, int> square = n => n * n; // returns a value Console.WriteLine(square(5)); Action<string> log = message => Console.WriteLine($"[log] {message}"); // void log("hi");
C# expresses function values through built-in delegate types: Func<…, TResult> for functions that return a value and Action<…> for those that return void — where Dart writes the type inline as int Function(int). Lambdas (n => n * n) fill them, and you can also declare a named delegate type. It is more machinery than Dart's uniform function types, but the same first-class-function idea.
Events (no Dart analog)
void main() { // Dart has no built-in event/observer keyword; you use a Stream, a // callback list, or a package like ChangeNotifier for this pattern. final listeners = <void Function(String)>[]; listeners.add((msg) => print('heard: $msg')); for (final listener in listeners) listener('tick'); }
var clock = new Clock(); clock.Tick += message => Console.WriteLine($"heard: {message}"); // subscribe clock.Fire(); class Clock { public event Action<string>? Tick; // an event public void Fire() => Tick?.Invoke("tick"); // raise it }
C# has a first-class event — a member built on delegates that callers subscribe to with += and unsubscribe with -=, and that only the declaring type can raise. Dart has no such keyword; the same publish/subscribe pattern is built from a Stream, a list of callbacks, or Flutter's ChangeNotifier/Listenable. Events are woven deep into .NET UI and framework APIs, so they are worth recognizing.
Extension methods (both have them)
extension NumberWords on int { String get spelledOut => this == 1 ? 'one' : 'many'; } void main() { print(3.spelledOut); // 'many' }
Console.WriteLine(3.SpelledOut()); // "many" static class NumberExtensions { public static string SpelledOut(this int value) // 'this' marks the receiver => value == 1 ? "one" : "many"; }
Both languages can graft methods onto a type they do not own. C#'s extension methods are static methods in a static class whose first parameter is marked this, then called with member syntax (3.SpelledOut()). It is a bit more boilerplate than Dart's extension … on int { } block, and — importantly — LINQ itself is built entirely from extension methods on IEnumerable<T>.
Generics
Generics & constraints
T largest<T extends Comparable>(List<T> items) { var winner = items.first; for (final item in items) { if (item.compareTo(winner) > 0) winner = item; } return winner; } void main() { print(largest<int>([3, 9, 2])); }
static T Largest<T>(IList<T> items) where T : IComparable<T> { var winner = items[0]; foreach (var item in items) if (item.CompareTo(winner) > 0) winner = item; return winner; } Console.WriteLine(Largest([3, 9, 2]));
Generics are close, with two differences of note. C# spells the bound with a trailing where T : IComparable<T> clause (you can stack several: where T : class, new()), rather than Dart's inline extends. And C# generics are reified like Dart's — the type argument survives to run time, so typeof(T) and is T work — with the bonus of declaration-site variance (out T/in T) that Dart does not express.
Exceptions & using
try/catch/finally
class InsufficientFunds implements Exception { final int shortfall; InsufficientFunds(this.shortfall); @override String toString() => 'Short by $shortfall'; } void main() { try { throw InsufficientFunds(150); } on InsufficientFunds catch (error) { print(error); } finally { print('done'); } }
try { throw new InsufficientFunds(150); } catch (InsufficientFunds error) // catch by type { Console.WriteLine(error.Message); } finally { Console.WriteLine("done"); } class InsufficientFunds(int shortfall) : Exception($"Short by {shortfall}") { public int Shortfall { get; } = shortfall; }
Exception handling is almost identical: try/catch/finally and throw line up, and catch (SpecificException e) is Dart's on Type catch. A custom exception inherits from Exception (passing the message to base via the primary constructor here), where Dart implements the Exception interface. C# also adds an exception filter — catch (Exception e) when (e.Message.Length > 0) — which Dart lacks.
Deterministic cleanup: using
void main() { // Dart pairs acquisition and release by hand with try/finally. final buffer = StringBuffer(); try { buffer.write('data'); print(buffer.toString()); } finally { buffer.clear(); // manual cleanup } }
using (var writer = new StringWriter()) // disposed at end of block { writer.Write("data"); Console.WriteLine(writer.ToString()); } // writer.Dispose() runs here automatically // or the declaration form, disposed at end of scope: using var other = new StringWriter(); other.Write("more"); Console.WriteLine(other.ToString());
C# has using, which guarantees an IDisposable's Dispose runs when the block (or enclosing scope) ends — deterministic cleanup for files, sockets, and database connections without a manual finally. It is the pattern Dart handles case-by-case with try/finally (there is no language-level using). Under the hood it is exactly a compiler-generated try/finally calling Dispose.
Namespaces & NuGet
Libraries → namespaces
// geometry.dart // double areaOfCircle(double r) => 3.14159 * r * r; // // main.dart import 'geometry.dart'; void main() { print(areaOfCircle(2)); }
// Types are grouped into NAMESPACES, imported with 'using': // namespace Geometry { public static class Shapes { // public static double AreaOfCircle(double r) => 3.14159 * r * r; } } // // then: using static Geometry.Shapes; Console.WriteLine(Circle.Area(2)); static class Circle { public static double Area(double r) => 3.14159 * r * r; }
C# organizes code by namespace (a logical grouping, independent of files) and imports with using Namespace; — bringing in a whole namespace of types rather than Dart's per-file import of a path. Visibility is keyword-based (public/internal/private) instead of Dart's leading-underscore convention, and using static imports a type's static members directly. The Dart side references a second file, so its column is display-only.
pub → NuGet
// pubspec.yaml // dependencies: // http: ^1.2.0 // // Then: dart pub get // Then: import 'package:http/http.dart' as http; void main() { print('Dependencies are declared in pubspec.yaml and fetched by pub.'); }
// MyApp.csproj // <ItemGroup> // <PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> // </ItemGroup> // // Then: dotnet restore (or: dotnet add package Newtonsoft.Json) Console.WriteLine("Dependencies live in the .csproj and are fetched by NuGet.");
Dart's pubspec.yaml plus dart pub get corresponds to a .csproj project file with <PackageReference> entries, restored by the dotnet CLI from NuGet (dotnet add package, dotnet restore). Packages come from nuget.org, and the SDK-style .csproj is XML rather than YAML but plays the same role as the manifest.