PONY λ M2 Modula-2

Swift.CodeCompared.To/Dart

An interactive executable cheatsheet comparing Swift and Dart

Swift 6.3 Dart 3.7
Hello World & Basics
Hello, World
print("Hello, World!")
void main() { print("Hello, World!"); }
Dart has no top-level statement mode: execution begins at a required void main(). Swift lets you write statements at file scope; Dart insists on the entry-point function, and every statement ends in a semicolon.
Comments
// a line comment /* a block comment */ /// A doc comment for the next declaration. let answer = 42 print(answer)
void main() { // a line comment /* a block comment */ /// A doc comment for the next declaration. final answer = 42; print(answer); }
Comment syntax is identical, including the triple-slash /// documentation comment. Dart’s doc comments feed dart doc the way Swift’s feed DocC.
Printing values
let name = "Ada" let count = 3 print("\(name) has \(count) items")
void main() { final name = "Ada"; final count = 3; print("$name has $count items"); }
Both use print, but the interpolation sigil differs: Swift’s \(expr) becomes Dart’s $name for a bare identifier and ${expr} for anything more complex.
Variables & Constants
let/var → final/var
let fixed = 10 // immutable var counter = 0 // mutable counter += 1 print("\(fixed) \(counter)")
void main() { final fixed = 10; // immutable binding var counter = 0; // mutable counter += 1; print("$fixed $counter"); }
Swift’s let maps to Dart’s final, and var is mutable in both. The keyword flips: in Swift the short word (let) is the immutable one, in Dart the short word (var) is the mutable one.
Compile-time const
// Swift's 'let' is a runtime constant; there is no // separate compile-time const keyword for values. let maxUsers = 100 print(maxUsers)
void main() { const maxUsers = 100; // canonicalized at compile time const list = [1, 2, 3]; // deeply immutable, shared instance print(maxUsers); print(list); }
Dart draws a distinction Swift does not: final is set once at run time, while const must be known at compile time and is canonicalized — two identical const values are the same object. This underpins Dart’s const constructors.
Type inference & annotations
let inferred = 3.14 // Double let annotated: Int = 42 let items: [String] = ["a", "b"] print("\(inferred) \(annotated) \(items)")
void main() { final inferred = 3.14; // double final int annotated = 42; final List<String> items = ["a", "b"]; print("$inferred $annotated $items"); }
Both infer from the initializer. The annotation position differs: Swift writes name: Type, Dart writes Type name — the type precedes the name, C-style.
The dynamic escape hatch
// Swift's 'Any' still requires a cast before use; // there is no unchecked dynamic dispatch. let anything: Any = "hello" if let text = anything as? String { print(text.uppercased()) }
void main() { dynamic anything = "hello"; // No cast needed — the call is checked at run time. print(anything.toUpperCase()); anything = 42; print(anything + 1); // also fine at run time }
Dart’s dynamic turns off static checking for that variable: any method call compiles and is resolved at run time, throwing NoSuchMethodError if absent. Swift’s Any has no such hole — you must cast with as? before sending a message.
Optionals & Null Safety
Optional → nullable type
var middleName: String? = nil middleName = "Quinn" print(middleName ?? "(none)")
void main() { String? middleName = null; middleName = "Quinn"; print(middleName ?? "(none)"); }
Dart has sound null safety like Swift: String? is the nullable type and ?? is the same coalescing operator. The difference is spelling — Swift’s Optional<T> is a real enum you can pattern-match, while Dart’s T? is a union of T and Null.
if let → type promotion
let raw: String? = "42" if let raw, let number = Int(raw) { print("parsed \(number)") } else { print("no value") }
void main() { String? raw = "42"; if (raw != null) { // raw is promoted to non-nullable String here final number = int.tryParse(raw); print(number != null ? "parsed $number" : "bad"); } else { print("no value"); } }
Dart has no if let; instead a != null check promotes the variable to its non-nullable type for the rest of the branch, so you keep using the same name. Promotion works only on local variables, not on mutable fields.
Optional chaining & force
let words: [String]? = ["hello", "world"] print(words?.count ?? 0) // 2 let forced = words! // trap if nil print(forced[0])
void main() { List<String>? words = ["hello", "world"]; print(words?.length ?? 0); // 2 final forced = words!; // throws if null print(forced[0]); }
The operators line up almost exactly: ?. for safe access and postfix ! to assert non-null. Swift calls ! "force unwrap"; Dart calls it the "null assertion operator", and both crash if the value is actually absent.
late variables
// Swift's nearest match is the implicitly-unwrapped // optional: declared without a value, asserted non-nil // on every use. var greeting: String! = nil greeting = "computed once" print(greeting!)
late String greeting; void main() { // Assigned before first read; no null needed. greeting = "computed once"; print(greeting); }
Dart’s late promises the compiler a non-nullable variable will be assigned before it is read, deferring the check to run time — the nearest match to Swift’s implicitly-unwrapped optional (String!), but without leaving the variable nullable. Paired with an initializer it also gives lazy, compute-on-first-read initialization, the role Swift’s lazy var fills inside a type.
Strings
Interpolation
let width = 3 let height = 4 print("area is \(width * height)")
void main() { final width = 3; final height = 4; print("area is ${width * height}"); }
A bare variable interpolates as $name, but any expression needs the braces: ${width * height}. Swift wraps every interpolation in \( ) regardless of complexity.
Common operations
let phrase = "hello world" print(phrase.count) print(phrase.uppercased()) print(phrase.contains("world")) print(phrase.split(separator: " "))
void main() { final phrase = "hello world"; print(phrase.length); print(phrase.toUpperCase()); print(phrase.contains("world")); print(phrase.split(" ")); }
The method names differ (countlength, uppercased()toUpperCase()), and Dart’s split takes a plain string or pattern rather than a Character separator. Note length counts UTF-16 code units, unlike Swift’s grapheme-cluster count.
Multiline & raw strings
let poem = """ line one line two """ let path = #"C:\temp\new"# print(poem) print(path)
void main() { final poem = ''' line one line two'''; final path = r"C:\temp\new"; print(poem); print(path); }
Dart’s multiline literal uses triple quotes (single or double), and its raw string prefix is a lowercase r rather than Swift’s #"…"# delimiters. In a raw string, backslashes are literal in both languages.
Numbers
Int/Double → int/double
let whole: Int = 7 let fraction: Double = 3.5 print(whole + Int(fraction)) // explicit conversion
void main() { int whole = 7; double fraction = 3.5; print(whole + fraction.toInt()); // explicit conversion }
Both keep int and double distinct and refuse to mix them implicitly. Dart adds a shared supertype num that either can flow into, whereas Swift has no common numeric supertype — you convert explicitly with Int( ) / toInt().
Integer division
let total = 7 let half = total / 2 // 3 — Int / Int stays Int let exact = 7.0 / 2.0 // 3.5 print("\(half) \(exact)")
void main() { final total = 7; final half = total ~/ 2; // 3 — truncating division final ratio = total / 2; // 3.5 — / always yields double print("$half $ratio"); }
This is a genuine trap: in Dart / always produces a double, even for two ints, so 7 / 2 is 3.5. Truncating integer division has its own operator, ~/. In Swift, Int / Int stays an Int.
Number methods
print(abs(-4)) print((10).isMultiple(of: 2)) print(Int(3.7.rounded())) print(max(3, 8))
import 'dart:math' as math; void main() { print((-4).abs()); print(10.isEven); print(3.7.round()); print(math.max(3, 8)); }
Dart numbers carry many helpers as methods and getters: abs() and round() are methods, and isEven/isOdd are properties rather than the method call Swift uses. But max is a free function in dart:math, not a method — unlike Swift’s global max(_:_:). Note (-4).abs() needs the parentheses so the minus binds to the literal.
Collections
Array → List
var numbers = [1, 2, 3] numbers.append(4) print(numbers) print(numbers.first ?? 0) print(numbers.map { $0 * 2 })
void main() { final numbers = [1, 2, 3]; numbers.add(4); print(numbers); print(numbers.first); print(numbers.map((n) => n * 2).toList()); }
Dart’s List replaces Swift’s Array, but two things bite: List is a reference type (a final list is still add-able), and map returns a lazy Iterable that you must finish with .toList(). Swift’s map returns an Array directly.
Dictionary → Map
var ages = ["Ada": 36, "Alan": 41] ages["Grace"] = 45 print(ages["Ada"] ?? 0) print(ages.count)
void main() { final ages = {"Ada": 36, "Alan": 41}; ages["Grace"] = 45; print(ages["Ada"]); // null if absent print(ages.length); }
The literal syntax is nearly identical. The catch: indexing a Dart Map returns a nullable value (V?), so ages["Ada"] has type int? — the compiler forces you to handle the missing-key case, much as Swift’s dictionary subscript returns an Optional.
Set
let unique: Set = [1, 2, 2, 3] print(unique.contains(2)) print(unique.count)
void main() { final unique = {1, 2, 2, 3}; print(unique.contains(2)); print(unique.length); }
A brace literal with no colons is a Set in Dart, just as {} with colons is a Map. Beware the empty case: {} is an empty Map, so an empty set needs an explicit <int>{} type argument.
Spread & collection-if/for
let base = [1, 2, 3] let includeZero = true var built = [Int]() if includeZero { built.append(0) } built.append(contentsOf: base) built += base.map { $0 * 10 } print(built)
void main() { final base = [1, 2, 3]; const includeZero = true; final built = [ if (includeZero) 0, ...base, for (final n in base) n * 10, ]; print(built); }
Dart builds collections declaratively with the spread operator ..., the if-element, and the for-element right inside the literal. There is no Swift equivalent — you would append imperatively or chain map/filter. This shines in Flutter widget lists.
Value vs Reference Semantics
No structs — everything is a reference
struct Point { var x: Int; var y: Int } var first = Point(x: 1, y: 2) var second = first // COPIES second.x = 99 print("\(first.x) \(second.x)") // 1 99
class Point { int x, y; Point(this.x, this.y); } void main() { final first = Point(1, 2); final second = first; // SAME object, no copy second.x = 99; print("${first.x} ${second.x}"); // 99 99 }
This is the single most important difference. Dart has no value types: a class instance is always a reference, so second = first aliases the same object and the mutation is visible through both. The Swift struct copies. Reach for records or immutable classes when you want value semantics.
Records → value semantics
// A Swift tuple is a value type with a fixed shape. let point = (x: 1, y: 2) print(point.x) let (a, b) = (10, 20) print("\(a) \(b)")
void main() { // A record: value type, structural equality, fixed shape. final point = (x: 1, y: 2); print(point.x); final (a, b) = (10, 20); // positional fields $1, $2 print("$a $b"); }
Dart records (3.0) are the value types the language otherwise lacks: they compare by structure, copy by value, and destructure like a Swift tuple. Named fields are accessed by name (point.x); positional fields by $1, $2. They are the idiomatic way to return multiple values.
Immutable "copyWith"
struct Config { var host: String var port: Int } let base = Config(host: "localhost", port: 80) var updated = base // copy updated.port = 443 print("\(base.port) \(updated.port)")
class Config { final String host; final int port; const Config({required this.host, required this.port}); Config copyWith({String? host, int? port}) => Config(host: host ?? this.host, port: port ?? this.port); } void main() { const base = Config(host: "localhost", port: 80); final updated = base.copyWith(port: 443); print("${base.port} ${updated.port}"); }
Because a class cannot copy itself, the community convention is a hand-written (or generated) copyWith that returns a new instance with some fields replaced — exactly the mutate-a-copy that Swift gets for free from var updated = base. Flutter code is full of these.
Control Flow
if, for, while
for index in 0..<3 { print(index) } var count = 0 while count < 2 { count += 1 } print("done \(count)")
void main() { for (var index = 0; index < 3; index++) { print(index); } var count = 0; while (count < 2) { count += 1; } print("done $count"); }
Dart uses the C-style three-clause for and requires parentheses around every condition. There is no Swift range operator; iterate with for (final x in iterable) or a counting loop. Note the braces are mandatory even for a single statement in idiomatic Dart.
for-in over collections
let colors = ["red", "green", "blue"] for (index, color) in colors.enumerated() { print("\(index): \(color)") }
void main() { final colors = ["red", "green", "blue"]; for (final (index, color) in colors.indexed) { print("$index: $color"); } }
Dart’s indexed getter pairs each element with its position as a record, which the for loop destructures — the direct counterpart to Swift’s enumerated(). Before records existed, Dart code used asMap().entries for this.
Ternary & cascades
let score = 72 let grade = score >= 60 ? "pass" : "fail" print(grade) // Swift has no cascade; repeat the receiver. var parts = [String]() parts.append("a") parts.append("b") print(parts)
void main() { final score = 72; final grade = score >= 60 ? "pass" : "fail"; print(grade); // Cascade: call many methods on one receiver. final parts = <String>[] ..add("a") ..add("b"); print(parts); }
The ternary is identical, but the cascade (..) is pure Dart: it calls a sequence of methods on the same object and evaluates to that object, so you configure a value without naming it repeatedly. Swift has nothing like it.
Functions & Closures
Function definition
func add(_ x: Int, _ y: Int) -> Int { return x + y } func square(_ x: Int) -> Int { x * x } // implicit return print(add(3, 4)) print(square(5))
int add(int x, int y) { return x + y; } int square(int x) => x * x; // arrow body void main() { print(add(3, 4)); print(square(5)); }
Dart writes the return type first and the parameter types before their names. The => arrow body is Dart’s shorthand for a function whose whole body is one expression — the analog of Swift’s implicit single-expression return.
Argument labels → named parameters
func makeUser(name: String, admin: Bool = false) -> String { return "\(name) admin=\(admin)" } print(makeUser(name: "Ada")) print(makeUser(name: "Alan", admin: true))
String makeUser({required String name, bool admin = false}) { return "$name admin=$admin"; } void main() { print(makeUser(name: "Ada")); print(makeUser(name: "Alan", admin: true)); }
Dart named parameters go inside { } and are optional by default — the opposite of Swift, where every argument label is required unless you write _. Mark a named parameter required to force it, and give a default to make it truly optional.
Closures & trailing syntax
let numbers = [1, 2, 3, 4] let evens = numbers.filter { $0 % 2 == 0 } let doubled = numbers.map { value in value * 2 } print(evens) print(doubled)
void main() { final numbers = [1, 2, 3, 4]; final evens = numbers.where((n) => n % 2 == 0).toList(); final doubled = numbers.map((value) => value * 2).toList(); print(evens); print(doubled); }
Dart closures always list their parameters explicitly — (value) => … — with no $0 shorthand and no trailing-closure sugar, so the function goes inside the parentheses. Also note filter is named where, and it returns a lazy Iterable.
Functions as values
func greet(_ name: String) -> String { "Hi, \(name)" } let fn: (String) -> String = greet print(fn("Ada")) ["a", "b"].forEach { print(fn($0)) }
String greet(String name) => "Hi, $name"; void main() { final String Function(String) fn = greet; print(fn("Ada")); ["a", "b"].forEach((name) => print(fn(name))); }
A function type is written ReturnType Function(ArgTypes) in Dart, versus Swift’s (ArgTypes) -> ReturnType. Otherwise functions are first-class in both — assignable, passable, and returnable.
Classes & Constructors
Constructors
class Animal { let name: String var legs: Int init(name: String, legs: Int = 4) { self.name = name self.legs = legs } } let dog = Animal(name: "Rex") print("\(dog.name) \(dog.legs)")
class Animal { final String name; int legs; Animal(this.name, {this.legs = 4}); // this.name is sugar } void main() { final dog = Animal("Rex"); print("${dog.name} ${dog.legs}"); }
Dart’s this.name parameter assigns the field directly, collapsing Swift’s init boilerplate. The constructor is named after the class, and there is no self/init keyword pair — just ClassName(...).
Named constructors
struct Point { var x: Double; var y: Double init(x: Double, y: Double) { self.x = x; self.y = y } // Swift uses static factory methods for alternatives. static func origin() -> Point { Point(x: 0, y: 0) } } let p = Point.origin() print("\(p.x) \(p.y)")
class Point { final double x, y; Point(this.x, this.y); Point.origin() : x = 0, y = 0; // named constructor } void main() { final p = Point.origin(); print("${p.x} ${p.y}"); }
Dart supports multiple named constructors (Point.origin()) as a first-class feature, using an initializer list after the colon to set final fields. Swift achieves the same with additional inits or static factory methods.
Computed properties
struct Circle { var radius: Double var area: Double { radius * radius * 3.14159 } } let circle = Circle(radius: 2) print(circle.area)
class Circle { double radius; Circle(this.radius); double get area => radius * radius * 3.14159; } void main() { final circle = Circle(2); print(circle.area); }
Dart spells a computed property as a get (and optionally set) member, where Swift uses a braces block with an implicit getter. Both are accessed like a stored field, with no call parentheses.
Inheritance & super
class Vehicle { func describe() -> String { "a vehicle" } } class Car: Vehicle { override func describe() -> String { "a car, which is " + super.describe() } } print(Car().describe())
class Vehicle { String describe() => "a vehicle"; } class Car extends Vehicle { @override String describe() => "a car, which is ${super.describe()}"; } void main() { print(Car().describe()); }
Dart uses extends (not :) for the superclass and an @override annotation that is advisory rather than required — unlike Swift’s mandatory override keyword. super works the same in both.
Enums & Sealed Classes
Simple enums
enum Direction: String { case north = "N", south = "S", east = "E", west = "W" } let heading = Direction.north print(heading.rawValue)
enum Direction { north("N"), south("S"), east("E"), west("W"); final String code; const Direction(this.code); } void main() { final heading = Direction.north; print(heading.code); }
Dart’s enhanced enums (2.17+) carry fields and a const constructor, which is how you attach a payload like Swift’s raw value — but there is no built-in rawValue, so you declare the field (code) yourself. Each case calls the constructor.
Enums with methods
enum Planet { case earth, mars var gravity: Double { switch self { case .earth: return 9.8 case .mars: return 3.7 } } } print(Planet.mars.gravity)
enum Planet { earth, mars; double get gravity => switch (this) { Planet.earth => 9.8, Planet.mars => 3.7, }; } void main() { print(Planet.mars.gravity); }
Both languages let an enum declare methods and computed properties. Dart separates the case list from members with a semicolon, and a switch over an enum is exhaustive in both — the compiler flags a missing case.
Associated values → sealed classes
enum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double) } func area(_ shape: Shape) -> Double { switch shape { case .circle(let radius): return radius * radius * 3.14159 case .rectangle(let width, let height): return width * height } } print(area(.circle(radius: 2)))
sealed class Shape {} class Circle extends Shape { final double radius; Circle(this.radius); } class Rectangle extends Shape { final double width, height; Rectangle(this.width, this.height); } double area(Shape shape) => switch (shape) { Circle(:final radius) => radius * radius * 3.14159, Rectangle(:final width, :final height) => width * height, }; void main() { print(area(Circle(2))); }
This is the enum false friend. A Swift enum with associated values is not a Dart enum — Dart enum cases cannot carry per-case data. Model it as a sealed class hierarchy; the switch over the sealed type is still exhaustive, and object patterns (Circle(:final radius)) bind the fields.
Pattern Matching
switch expressions
func describe(_ n: Int) -> String { switch n { case 0: return "zero" case 1...9: return "small" default: return "big" } } print(describe(5))
String describe(int n) => switch (n) { 0 => "zero", >= 1 && <= 9 => "small", _ => "big", }; void main() { print(describe(5)); }
Dart 3.0’s switch expression returns a value with => arms and uses _ as the wildcard. Relational and logical patterns (>= 1 && <= 9) replace Swift’s range case 1...9. Both are exhaustive and both are expressions.
Destructuring in switch
let point = (2, 0) switch point { case (0, 0): print("origin") case (let x, 0): print("on x-axis at \(x)") case (0, let y): print("on y-axis at \(y)") case (let x, let y): print("at \(x), \(y)") }
void main() { final point = (2, 0); switch (point) { case (0, 0): print("origin"); case (final x, 0): print("on x-axis at $x"); case (0, final y): print("on y-axis at $y"); case (final x, final y): print("at $x, $y"); } }
Record patterns destructure positionally just like Swift tuple patterns, binding with final x where Swift writes let x. A constant in a position (0) constrains the match. This is the feature that makes Dart 3 feel closest to Swift.
if-case binding
let response: (Int, String) = (200, "OK") if case (200, let message) = response { print("success: \(message)") }
void main() { final (int, String) response = (200, "OK"); if (response case (200, final message)) { print("success: $message"); } }
Dart’s if (value case Pattern) is the direct analog of Swift’s if case Pattern = value — a single-branch match that binds variables into the if body. The keyword order flips, but the behavior is the same.
Protocols → Interfaces & Mixins
Protocol → implicit interface
protocol Greeter { func greet() -> String } struct English: Greeter { func greet() -> String { "Hello" } } func announce(_ g: Greeter) { print(g.greet()) } announce(English())
// Every class defines an implicit interface. abstract interface class Greeter { String greet(); } class English implements Greeter { @override String greet() => "Hello"; } void announce(Greeter g) => print(g.greet()); void main() { announce(English()); }
Dart has no protocol keyword; instead every class defines an implicit interface, and you use implements to conform to one. An abstract interface class declares a contract with no implementation — the closest match to a Swift protocol.
Protocol extensions → mixins
protocol Logger {} extension Logger { func log(_ message: String) { print("[log] \(message)") } } struct Service: Logger {} Service().log("started")
mixin Logger { void log(String message) => print("[log] $message"); } class Service with Logger {} void main() { Service().log("started"); }
A Dart mixin supplies shared implementation to any class that uses with — the role Swift fills with a protocol extension that provides default methods. Unlike a protocol extension, a mixin can also declare state and can be constrained with on to require a superclass.
Extensions
extension Int { var squared: Int { self * self } } print(5.squared)
extension IntExtras on int { int get squared => this * this; } void main() { print(5.squared); }
Both languages add members to existing types without subclassing. Dart’s extension is named (IntExtras) and declares its receiver with on int, referring to the value as this rather than Swift’s self. The name lets you hide or resolve conflicting extensions.
Generics
Generic functions
func firstOrNil<Element>(_ items: [Element]) -> Element? { return items.first } print(firstOrNil([10, 20, 30]) ?? -1)
T? firstOrNull<T>(List<T> items) { return items.isEmpty ? null : items.first; } void main() { print(firstOrNull([10, 20, 30]) ?? -1); }
Generic syntax matches closely: a type parameter in angle brackets, used as a normal type. The declaration order differs because Dart puts the return type first (T? firstOrNull<T>(...)).
Bounded type parameters
func largest<T: Comparable>(_ items: [T]) -> T { return items.max()! } print(largest([3, 9, 2]))
T largest<T extends Comparable<T>>(List<T> items) { var result = items.first; for (final item in items.skip(1)) { if (item.compareTo(result) > 0) result = item; } return result; } void main() { print(largest([3, 9, 2])); }
Dart bounds a type parameter with extends where Swift uses :. A subtle difference from Swift generics: Dart generics are reified — the type argument survives to run time, so items is List<int> actually works, unlike on the JVM.
Generic classes
struct Box<Value> { let value: Value func map<U>(_ transform: (Value) -> U) -> Box<U> { Box<U>(value: transform(value)) } } let boxed = Box(value: 21).map { $0 * 2 } print(boxed.value)
class Box<T> { final T value; const Box(this.value); Box<U> map<U>(U Function(T) transform) => Box(transform(value)); } void main() { final boxed = Box(21).map((n) => n * 2); print(boxed.value); }
Generic classes work the same way in both languages. Note Dart infers the type argument of Box(21) from the constructor argument, just as Swift infers Box(value: 21) — no explicit <int> needed.
Error Handling
throws → throw/catch
enum ParseError: Error { case empty } func parse(_ text: String) throws -> Int { if text.isEmpty { throw ParseError.empty } return Int(text) ?? 0 } do { print(try parse("")) } catch { print("failed: \(error)") }
class ParseError implements Exception { final String message; ParseError(this.message); @override String toString() => "ParseError: $message"; } int parse(String text) { if (text.isEmpty) throw ParseError("empty"); return int.tryParse(text) ?? 0; } void main() { try { print(parse("")); } catch (error) { print("failed: $error"); } }
The big shift: Dart exceptions are unchecked. A method that throws carries no throws in its signature and callers are never forced to handle it — nothing like Swift’s try at every call site. You can throw any object, though implementing Exception is the convention.
Typed catch & finally
enum NetError: Error { case timeout } func fetch() throws { throw NetError.timeout } do { try fetch() } catch NetError.timeout { print("timed out") } catch { print("other: \(error)") }
class TimeoutError implements Exception {} void fetch() => throw TimeoutError(); void main() { try { fetch(); } on TimeoutError { print("timed out"); } catch (error) { print("other: $error"); } finally { print("cleaned up"); } }
Dart selects a handler by type with on TypeName, and a bare catch (error) takes anything — versus Swift’s catch Pattern. Dart’s finally is the counterpart to Swift’s defer, though defer is scoped to the enclosing function rather than a try block.
Concurrency
async/await
func doubled(_ x: Int) async -> Int { return x * 2 } let result = await doubled(21) print(result)
Future<int> doubled(int x) async { return x * 2; } void main() async { final result = await doubled(21); print(result); }
An async Dart function returns a Future<T> — the concrete type Swift keeps implicit behind async. You await a Future just as in Swift, and main itself can be async.
Awaiting multiple futures
func value(_ n: Int) async -> Int { n } async let a = value(1) async let b = value(2) let total = await a + b print(total)
Future<int> value(int n) async => n; void main() async { final results = await Future.wait([value(1), value(2)]); print(results.reduce((a, b) => a + b)); }
Where Swift kicks off concurrent work with async let, Dart collects a list of Futures and awaits them together with Future.wait, which resolves to a list of results. Both let independent async work overlap rather than run in series.
AsyncSequence → Stream
func countUp() -> AsyncStream<Int> { AsyncStream { continuation in for index in 1...3 { continuation.yield(index) } continuation.finish() } } for await value in countUp() { print(value) }
Stream<int> countUp() async* { for (var index = 1; index <= 3; index++) { yield index; } } void main() async { await for (final value in countUp()) { print(value); } }
Dart’s Stream is the analog of Swift’s AsyncSequence, and an async* generator with yield is far terser to write than building an AsyncStream by hand. You consume it with await for, mirroring Swift’s for await.
Actors → isolates
// A Swift actor protects mutable state with the type // system; other tasks await access to it. actor Counter { private var count = 0 func increment() { count += 1 } func value() -> Int { count } } let counter = Counter() await counter.increment() await counter.increment() print(await counter.value())
import 'dart:isolate'; int heavySum(int n) { var total = 0; for (var i = 1; i <= n; i++) total += i; return total; } void main() async { // An isolate has NO shared memory — data is copied across. final result = await Isolate.run(() => heavySum(1000)); print(result); }
Dart’s concurrency model is fundamentally different: there is no shared mutable memory. An Isolate has its own heap, and you communicate by copying messages, so data races are impossible by construction — a stronger guarantee than Swift’s actor, which shares memory but serializes access. Most Dart code stays on one isolate’s event loop and only spawns one for heavy CPU work.