Hello World & Basics
Hello, World
Console.WriteLine("Hello, World!"); print("Hello, World!") Both languages run top-level statements with no
Main ceremony. print replaces Console.WriteLine, and semicolons are optional (and omitted by convention).let — 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}"); let fixed = 10 // read-only local, any runtime expression
var counter = 0 // mutable local
counter += 1
print(fixed, counter) let is a genuine read-only binding — assignable once, from any runtime value — which C# locals cannot express (const handles only compile-time constants). Swift style is let everywhere; the compiler suggests demoting any var that is never mutated.Inference & numeric strictness
var price = 4.5; // double
var quantity = 3; // int
var total = price * quantity; // int implicitly widens to double
Console.WriteLine(total); let price = 4.5 // Double
let quantity = 3 // Int
let total = price * Double(quantity) // NO implicit conversion — ever
print(total) Inference looks the same, but Swift performs no implicit numeric conversions at all — not even
Int to Double. Every mixed-type expression needs an explicit constructor call like Double(quantity), a strictness beyond anything in C#.Optionals — Enforced This Time
string? that actually stops you
#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"); var nickname: String? = nil
// print(nickname.count) // compile ERROR — must unwrap first
print(nickname ?? "none") The shared headline with Kotlin, and the biggest safety upgrade from C#:
String? is a distinct type (Optional<String>, a real enum), and using it without unwrapping refuses to compile. C#’s nullable annotations warn and vanish at runtime; Swift’s optionals are values the runtime actually represents.if let — unwrapping as a binding
#nullable enable
string? stored = "token";
if (stored is not null)
{
Console.WriteLine($"found {stored}");
}
else
{
Console.WriteLine("empty");
} let stored: String? = "token"
if let stored {
print("found \(stored)") // stored is a plain String in here
} else {
print("empty")
} if let unwraps and binds in one step — inside the braces, stored is a non-optional String. The shorthand if let stored (no right-hand side) shadows the optional with its unwrapped self, the closest analog to C#’s flow-narrowing after is not null.guard let — early exit, then certainty
Console.WriteLine(Describe("hello"));
Console.WriteLine(Describe(null));
static string Describe(string? input)
{
if (input is null)
{
return "nothing";
}
return $"got {input}";
} func describe(_ input: String?) -> String {
guard let input else {
return "nothing"
}
return "got \(input)" // input is non-optional for the REST of the function
}
print(describe("hello"))
print(describe(nil)) guard is the early-return check made a language feature: its else block must exit the scope, and in exchange the unwrapped binding survives for the rest of the function — the inverse scoping of if let, and the backbone of idiomatic Swift function bodies.Optional chaining & ??
#nullable enable
var customer = new Customer(null);
Console.WriteLine(customer.Address?.City ?? "unknown");
record Customer(Address? Address);
record Address(string City); struct Address { let city: String }
struct Customer { let address: Address? }
let customer = Customer(address: nil)
print(customer.address?.city ?? "unknown") The operators you already use daily transfer verbatim:
?. short-circuits on absence, ?? supplies the fallback. The difference is that the compiler forces the chain to end in a fallback or an unwrap — the optional never leaks silently.The ! that fails honestly
#nullable enable
string? configured = "value";
string forced = configured!; // suppresses the warning; does NOTHING at runtime
Console.WriteLine(forced); let configured: String? = "value"
let forced: String = configured! // traps immediately if nil
print(forced) C#’s null-forgiving
! is erased; a wrong one lets null flow on to crash somewhere else. Swift’s force-unwrap performs a real check and stops the program at that line with a clear message. Both are last resorts — Swift’s at least fails at the scene of the crime.Value Types Are the Culture
struct is the default choice
var original = new Point { X = 1, Y = 1 };
var copy = original; // struct — copied (C# structs are the niche case)
copy.X = 99;
Console.WriteLine($"{original.X} {copy.X}");
struct Point { public int X; public int Y; } struct Point {
var x: Int
var y: Int
}
var original = Point(x: 1, y: 1)
var copy = original // structs copy — and structs are the DEFAULT here
copy.x = 99
print(original.x, copy.x) // 1 99 C# has structs but reserves them for small, niche types; in Swift the culture inverts —
struct is the default modeling tool and class the special case for identity and sharing. Note the free memberwise initializer Point(x:y:): no constructor boilerplate.String, Array, Dictionary — all value types
var first = new List<int> { 1, 2, 3 };
var second = first; // reference — the same list
second.Add(4);
Console.WriteLine($"{first.Count} {second.Count}"); // 4 4 var first = [1, 2, 3]
var second = first // value — copied (copy-on-write under the hood)
second.append(4)
print(first.count, second.count) // 3 4 The standard collections —
String, Array, Dictionary, Set — are all structs. Assignment copies (lazily, via copy-on-write), so the aliasing bugs C# collections invite simply cannot happen. Sharing must be chosen explicitly with a class.mutating methods
var counter = new Counter();
counter.Increment(); // C# struct methods may mutate freely (a footgun)
counter.Increment();
Console.WriteLine(counter.Count);
struct Counter
{
public int Count { get; private set; }
public void Increment() => Count += 1;
} struct Counter {
private(set) var count = 0
mutating func increment() {
count += 1
}
}
var counter = Counter()
counter.increment()
counter.increment()
print(counter.count) A struct method that changes state must be declared
mutating — and calling it on a let struct is a compile error. C# lets struct methods mutate silently (the classic source of "modified a copy" bugs); Swift makes value mutation visible in the signature and impossible on immutable bindings.let is deep for value types
var numbers = new List<int> { 1, 2, 3 }; // reference in a readonly field
numbers.Add(4); // ...would still be mutable
Console.WriteLine(numbers.Count); let numbers = [1, 2, 3]
// numbers.append(4) // compile error: let + value type = deeply immutable
print(numbers.count)
var editable = numbers // a mutable COPY
editable.append(4)
print(editable.count) Because arrays are value types,
let freezes the whole value — there is no C#-style "readonly reference to a mutable list" gap. Immutability is decided at the binding, not the type: the same Array type is frozen under let and mutable under var.Strings
String interpolation
var name = "Ada";
var count = 3;
Console.WriteLine($"{name} has {count} items ({count * 2} shoes)"); let name = "Ada"
let count = 3
print("\(name) has \(count) items (\(count * 2) shoes)") Every Swift string literal interpolates — no
$ prefix to remember. The delimiter is \(expression), and any expression fits inside. Formatting alignment/precision uses String(format:) or formatted values rather than C#’s {value:F2} inline syntax.Raw & multiline strings
var json = """
{ "name": "Ada" }
""";
Console.WriteLine(json); let json = """
{ "name": "Ada" }
"""
print(json) Triple-quoted strings work almost identically — including the indentation stripping keyed off the closing delimiter, which C# 11 borrowed from Swift’s design. Swift’s version still interpolates
\(…); for regex-heavy content, #"raw"# delimiters disable escaping.Length counts characters, not code units
var family = "👨👩👧👦";
Console.WriteLine(family.Length); // 11 — UTF-16 code units
var enumerator = System.Globalization.StringInfo.GetTextElementEnumerator(family);
var graphemes = 0;
while (enumerator.MoveNext()) graphemes += 1;
Console.WriteLine(graphemes); // 1 — needs StringInfo let family = "👨👩👧👦"
print(family.count) // 1 — user-perceived characters, by default
print(family.unicodeScalars.count) // 7 — the scalars underneath C#’s
Length counts UTF-16 code units — the family emoji is 11 of them. Swift’s count counts grapheme clusters (what a person sees) by default, with unicodeScalars/utf8/utf16 views when you need the underlying encodings. The price: Swift strings are not integer-indexable (text[0] does not compile).Collections
Arrays
var fruits = new List<string> { "apple", "banana" };
fruits.Add("cherry");
fruits.RemoveAt(0);
Console.WriteLine(string.Join(", ", fruits));
Console.WriteLine(fruits.Count); var fruits = ["apple", "banana"]
fruits.append("cherry")
fruits.remove(at: 0)
print(fruits)
print(fruits.count) The literal syntax
[…] replaces the new List<string> { } ceremony, and printing an array shows its contents (no string.Join needed). Note the argument label in remove(at: 0) — labels are part of the method name.Dictionaries return optionals
var stock = new Dictionary<string, int> { ["apples"] = 5 };
stock["pears"] = 2;
// stock["plums"] would THROW KeyNotFoundException:
Console.WriteLine(stock.TryGetValue("plums", out var quantity) ? quantity : 0); var stock = ["apples": 5]
stock["pears"] = 2
// Lookup returns Int? — absence is an optional, never an exception:
print(stock["plums"] ?? 0) A dictionary lookup returns an optional, so the missing-key case is handled with
?? or if let instead of C#’s exception-throwing indexer and the TryGetValue/out dance. The optional system absorbs a whole API pattern.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); let numbers = [1, 2, 3, 4, 5, 6]
let result = numbers
.filter { $0.isMultiple(of: 2) }
.map { $0 * 10 }
.reduce(0, +)
print(result) The LINQ verbs exist under FP names —
Where → filter, Select → map, Aggregate → reduce. Trailing closures drop the parentheses, $0 is the implicit parameter, and operators like + pass directly as functions (reduce(0, +)).Eager by default; .lazy is 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)); let firstThree = (1...1_000_000).lazy
.filter { $0.isMultiple(of: 7) }
.map { $0 * 2 }
.prefix(3)
print(Array(firstThree)) Swift’s collection operators are eager — each step builds an array — where LINQ defers by default. The
.lazy view opts back into LINQ-style element-at-a-time evaluation; prefix is Take. The instinct to keep: reach for .lazy on long chains with early exits.Control Flow
Conditions & the surviving ternary
var temperature = 30;
var description = temperature > 25 ? "hot" : "mild";
if (temperature > 25)
{
Console.WriteLine(description);
} let temperature = 30
let description = temperature > 25 ? "hot" : "mild"
if temperature > 25 {
print(description)
} Two small changes: no parentheses around the condition, and braces are required even for one-liners (the classic dangling-else and goto-fail bugs are unrepresentable). The ternary operator survives unchanged — unlike Kotlin, Swift kept it.
The C-style for loop is gone
for (var count = 1; count <= 5; count++)
{
Console.Write($"{count} ");
}
Console.WriteLine();
foreach (var fruit in new[] { "apple", "banana" })
{
Console.WriteLine(fruit);
} for count in 1...5 {
print(count, terminator: " ")
}
print()
for fruit in ["apple", "banana"] {
print(fruit)
} Swift removed the C-style
for (;;) loop entirely — iteration is always for‑in over a sequence, with ranges (1...5 inclusive, 1..<5 half-open) covering the counting cases. stride(from:to:by:) handles arbitrary steps.guard beyond optionals
Console.WriteLine(Validate(""));
Console.WriteLine(Validate("Ada"));
static string Validate(string name)
{
if (string.IsNullOrEmpty(name))
{
return "invalid";
}
return $"welcome, {name}";
} func validate(_ name: String) -> String {
guard !name.isEmpty else {
return "invalid"
}
return "welcome, \(name)"
}
print(validate(""))
print(validate("Ada")) guard works with any condition, not just unwrapping: state the requirement positively, and the compiler enforces that the else branch exits. Function bodies become a flat list of preconditions followed by the happy path — no arrow-shaped nesting.switch & Pattern Matching
switch: exhaustive, no breaks
var status = 404;
var message = status switch
{
200 => "ok",
301 or 302 => "redirect",
>= 400 and < 500 => "client error",
_ => "other",
};
Console.WriteLine(message); let status = 404
switch status {
case 200:
print("ok")
case 301, 302:
print("redirect")
case 400..<500:
print("client error")
default:
print("other")
} Swift’s
switch is the statement form of C#’s switch expression: no fallthrough (no break needed), comma-separated alternatives, range patterns, and compiler-enforced exhaustiveness. Where C# still carries the legacy fallthrough statement form, Swift has only the safe one.Value binding & where
var point = (X: 3, Y: -3);
var described = point switch
{
(0, 0) => "origin",
var (x, y) when x == -y => $"on the anti-diagonal at {x}",
var (x, _) => $"somewhere with x = {x}",
};
Console.WriteLine(described); let point = (x: 3, y: -3)
switch point {
case (0, 0):
print("origin")
case let (x, y) where x == -y:
print("on the anti-diagonal at \(x)")
case let (x, _):
print("somewhere with x = \(x)")
} The pattern vocabularies have converged remarkably: tuple patterns, binding (
case let vs var (x, y)), guards (where vs when), and wildcards. Swift checks tuple-pattern exhaustiveness here without a default because the bound case matches anything.is-pattern → if case
object value = 42;
if (value is int number && number > 40)
{
Console.WriteLine($"big int: {number}");
} enum Measurement {
case temperature(Double)
case humidity(Int)
}
let reading = Measurement.temperature(39.5)
if case .temperature(let degrees) = reading, degrees > 38 {
print("fever: \(degrees)")
} if case applies one pattern outside a switch — the analog of C#’s is-pattern in an if. The comma chains further conditions the way C# uses && after a pattern. Most commonly used to pluck one enum case’s payload without a full switch.Enums Are Sum Types
Enums carry payloads
// A C# enum is a named integer — payload-carrying cases need a
// record hierarchy instead.
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; enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
let shape = Shape.circle(radius: 2)
switch shape {
case .circle(let radius):
let area = (Double.pi * radius * radius * 100).rounded() / 100
print("circle area \(area)")
case .rectangle(let width, let height):
print("rectangle area \(width * height)")
} The single biggest upgrade over C# enums: Swift enum cases carry typed payloads, making the enum a true sum type — what C# fakes with an abstract record hierarchy. The
switch is exhaustive with no defensive _ arm, and adding a case breaks every non-exhaustive switch at compile time.Raw values & CaseIterable
foreach (var level in Enum.GetValues<LogLevel>())
{
Console.WriteLine($"{level} = {(int)level}");
}
enum LogLevel { Debug = 1, Info = 2, Warning = 3 } enum LogLevel: Int, CaseIterable {
case debug = 1, info, warning
}
for level in LogLevel.allCases {
print(level, level.rawValue)
} C#’s named-integer style still exists as raw values, opted into per enum.
CaseIterable synthesizes allCases (C#’s Enum.GetValues), and LogLevel(rawValue: 2) converts back — returning an optional, naturally, since the integer might match nothing.Functions & Closures
Argument labels are required
// C# named arguments are OPTIONAL sugar at the call site:
Console.WriteLine(Resize(800, 600));
Console.WriteLine(Resize(width: 800, height: 600)); // same call
static string Resize(int width, int height) => $"{width}x{height}"; func resize(width: Int, height: Int) -> String {
"\(width)x\(height)"
}
// Labels are part of the method's NAME — omitting them will not compile:
print(resize(width: 800, height: 600))
// print(resize(800, 600)) // compile error The polarity flips: C# names arguments when the caller feels like it; Swift requires labels unless the declaration opts out with
_ (func resize(_ width: Int, …)). Labels are part of the function’s identity — resize(width:height:) — which is why Swift APIs read like sentences.Defaults & variadics
Console.WriteLine(Total(1, 2, 3));
static int Total(params int[] values) => values.Sum(); func total(_ values: Int..., bonus: Int = 0) -> Int {
values.reduce(0, +) + bonus
}
print(total(1, 2, 3))
print(total(1, 2, 3, bonus: 10)) params becomes the Int... variadic, arriving as an array in the body. Because labels disambiguate, a variadic does not have to be last — a labeled parameter can follow it, which C#’s params cannot allow.Lambdas → closures & trailing syntax
var numbers = new[] { 3, 1, 2 };
var ordered = numbers.OrderBy(number => number).ToArray();
Console.WriteLine(string.Join(", ", ordered));
Func<int, int> doubler = number => number * 2;
Console.WriteLine(doubler(21)); let numbers = [3, 1, 2]
let ordered = numbers.sorted { $0 < $1 } // trailing closure
print(ordered)
let doubler = { (number: Int) -> Int in number * 2 }
print(doubler(21)) The closure literal is braces with an
in separating signature from body; $0/$1 are the implicit parameters. When the last argument is a closure, it moves outside the parentheses (trailing-closure syntax) — the shape SwiftUI’s entire DSL is built on.Tuples & multiple returns
var (minimum, maximum) = Bounds(new[] { 3, 1, 4 });
Console.WriteLine($"{minimum}..{maximum}");
static (int Minimum, int Maximum) Bounds(int[] values) =>
(values.Min(), values.Max()); func bounds(_ values: [Int]) -> (minimum: Int, maximum: Int) {
(values.min()!, values.max()!)
}
let (minimum, maximum) = bounds([3, 1, 4])
print("\(minimum)..\(maximum)") Named tuples rhyme almost perfectly with C#’s
ValueTuple — labels in the return type, destructuring at the call site. The ! after min()/max() force-unwraps the optional those methods return for a possibly-empty array; production code would guard instead.Classes, Protocols & Extensions
Classes — when identity matters
var account = new BankAccount("Ada");
account.Deposit(100);
Console.WriteLine($"{account.Owner}: {account.Balance}");
class BankAccount(string owner)
{
public string Owner { get; } = owner;
public int Balance { get; private set; }
public void Deposit(int amount) => Balance += amount;
} class BankAccount {
let owner: String
private(set) var balance = 0
init(owner: String) {
self.owner = owner
}
func deposit(_ amount: Int) {
balance += amount
}
}
let account = BankAccount(owner: "Ada")
account.deposit(100)
print("\(account.owner): \(account.balance)") Classes are reference types in both languages — but in Swift you reach for one only when identity or sharing is the point.
init replaces the constructor, there is no new, and note that a let holding a class instance still permits mutating its state (the reference is constant, not the object — exactly like C# readonly).Interfaces → protocols
ISpeaker[] speakers = { new Dog(), new Robot() };
foreach (var speaker in speakers)
{
Console.WriteLine(speaker.Speak());
}
interface ISpeaker { string Speak(); }
class Dog : ISpeaker { public string Speak() => "Woof"; }
class Robot : ISpeaker { public string Speak() => "Beep"; } protocol Speaker {
func speak() -> String
}
struct Dog: Speaker {
func speak() -> String { "Woof" }
}
struct Robot: Speaker {
func speak() -> String { "Beep" }
}
let speakers: [any Speaker] = [Dog(), Robot()]
for speaker in speakers {
print(speaker.speak())
} Protocols are interfaces without the
I prefix — and structs conform as readily as classes, so polymorphism does not require reference types. The any Speaker spelling marks an existential (heterogeneous) collection explicitly, a distinction C# leaves implicit.Protocol extensions — default implementations
Console.WriteLine(new Dog().Describe());
interface ISpeaker
{
string Speak();
// C# 8 default interface methods exist, but are callable only
// through the interface reference and rarely used.
string Describe() => $"I say {Speak()}";
}
class Dog : ISpeaker
{
public string Speak() => "Woof";
public string Describe() => $"I say {Speak()}";
} protocol Speaker {
func speak() -> String
}
extension Speaker {
// Every conforming type inherits this for free:
func describe() -> String { "I say \(speak())" }
}
struct Dog: Speaker {
func speak() -> String { "Woof" }
}
print(Dog().describe()) Protocol extensions are Swift’s signature move — shared behavior attached to the protocol itself, inherited by every conforming type and callable directly on the concrete value (no interface-reference restriction like C#’s default interface methods). Large parts of the standard library are protocol extensions on
Sequence and Collection.Retroactive conformance
// C# cannot make SOMEONE ELSE'S type implement YOUR interface —
// extension methods add behavior, never conformance. The workaround
// is a wrapper class.
Console.WriteLine(new PrettyInt(42).Pretty());
interface IPretty { string Pretty(); }
record PrettyInt(int Value) : IPretty
{
public string Pretty() => $"<{Value}>";
} protocol Pretty {
func pretty() -> String
}
// Conform a type you don't own — even Int — to a protocol you do:
extension Int: Pretty {
func pretty() -> String { "<\(self)>" }
}
print(42.pretty()) An extension can add a protocol conformance to any existing type, including the standard library’s — no wrapper, no ownership required. This is the capability C# extension methods approach but never reach: they add methods, but the type never becomes an implementer of the interface.
Properties
Computed 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;
} struct Rectangle {
var width: Double
var height: Double
var area: Double { width * height }
}
let rectangle = Rectangle(width: 3, height: 4)
print(rectangle.area) A read-only computed property is just a braced body after the type — the analog of C#’s expression-bodied
=> member. Add explicit get/set blocks for read-write computed properties; stored and computed properties are otherwise declared identically.willSet / didSet observers
var thermostat = new Thermostat();
thermostat.Celsius = 25;
thermostat.Celsius = 30;
class Thermostat
{
private double celsius;
public double Celsius
{
get => celsius;
set
{
Console.WriteLine($"changing {celsius} -> {value}");
celsius = value; // observer logic hand-rolled into the setter
}
}
} struct Thermostat {
var celsius: Double = 0 {
willSet {
print("changing \(celsius) -> \(newValue)")
}
didSet {
print("changed (was \(oldValue))")
}
}
}
var thermostat = Thermostat()
thermostat.celsius = 25
thermostat.celsius = 30 Property observers attach to a stored property — no backing-field ceremony, no custom setter.
willSet sees the incoming newValue, didSet the departed oldValue. C# expresses the same thing only by expanding the auto-property into a manual one.Error Handling
try is visible at every call site
// Nothing at the call site says Parse can throw:
try
{
var value = ParseQuantity("many");
Console.WriteLine(value);
}
catch (FormatException exception)
{
Console.WriteLine($"caught: {exception.Message}");
}
static int ParseQuantity(string text) =>
int.TryParse(text, out var value)
? value
: throw new FormatException($"not a number: {text}"); enum ParseError: Error {
case notANumber(String)
}
func parseQuantity(_ text: String) throws -> Int {
guard let value = Int(text) else {
throw ParseError.notANumber(text)
}
return value
}
do {
let value = try parseQuantity("many") // try is REQUIRED here
print(value)
} catch ParseError.notANumber(let text) {
print("caught: not a number: \(text)")
} Two honesty upgrades over C#: a function that can throw says so in its signature (
throws), and every call to it must be marked try — the invisible exception paths that plague C# codebases are syntactically impossible. Catch clauses pattern-match on error cases, payloads included.try? and try!
// C#'s Try-pattern is a separate method returning bool + out:
Console.WriteLine(int.TryParse("42", out var parsed) ? parsed : 0);
Console.WriteLine(int.TryParse("many", out var failed) ? failed : 0); enum ParseError: Error {
case notANumber(String)
}
func parseQuantity(_ text: String) throws -> Int {
guard let value = Int(text) else {
throw ParseError.notANumber(text)
}
return value
}
print((try? parseQuantity("42")) ?? 0) // error -> nil, then ??
print((try? parseQuantity("many")) ?? 0) try? converts any thrown error into nil, folding the whole TryParse pattern into the optional system — one throwing function serves both styles, instead of C#’s parallel Parse/TryParse method pairs. try! asserts no error will occur and traps if one does.using/finally → defer
try
{
Console.WriteLine("working");
throw new InvalidOperationException("failed mid-way");
}
catch (InvalidOperationException)
{
Console.WriteLine("caught");
}
finally
{
Console.WriteLine("cleanup"); // one finally per try block
} func work() {
defer { print("cleanup") } // runs on EVERY exit from this scope
print("working")
}
work() defer schedules cleanup for scope exit — return, throw, or fall-through — right next to the acquisition it balances, with multiple defers unwinding in reverse order. It covers what C# splits between finally and using, without tying cleanup to a try block or an IDisposable.ARC, not GC
Deterministic deinit
// A C# finalizer runs whenever the GC gets around to it —
// deterministic cleanup needs IDisposable + using.
using (var session = new Session())
{
Console.WriteLine("working");
}
Console.WriteLine("after the using block");
class Session : IDisposable
{
public Session() => Console.WriteLine("opened");
public void Dispose() => Console.WriteLine("closed");
} class Session {
init() { print("opened") }
deinit { print("closed") }
}
func work() {
let session = Session()
print("working")
} // last reference gone -> deinit runs HERE, deterministically
work()
print("after work()") Swift uses automatic reference counting, not a tracing GC: the moment the last reference disappears,
deinit runs — deterministically, like C++ RAII. The whole IDisposable/using apparatus exists because C# finalizers cannot promise that; in Swift the destructor alone suffices.Retain cycles are your problem now
// Under a tracing GC, two objects referencing each other still
// get collected — cycles are a non-issue in C#.
var parent = new Node("parent");
var child = new Node("child");
parent.Next = child;
child.Next = parent; // harmless in C#
Console.WriteLine("cycle built; the GC will reclaim both");
class Node(string name)
{
public string Name { get; } = name;
public Node? Next { get; set; }
} class Node {
let name: String
var next: Node?
weak var previous: Node? // weak breaks the cycle
init(name: String) { self.name = name }
deinit { print("\(name) deallocated") }
}
func buildChain() {
let first = Node(name: "first")
let second = Node(name: "second")
first.next = second
second.previous = first // weak — no cycle, both deinit
}
buildChain()
print("after buildChain()") The price of ARC’s determinism: reference cycles leak, because each object keeps the other’s count above zero. Back-references are declared
weak (an optional that becomes nil when the target dies) or unowned. The same discipline applies to closures capturing self — the [weak self] capture list you will meet everywhere in UI code.Concurrency
async/await — the direct rhyme
var greeting = await FetchGreetingAsync();
Console.WriteLine(greeting);
static async Task<string> FetchGreetingAsync()
{
await Task.Delay(50);
return "hello from a task";
} func fetchGreeting() async throws -> String {
try await Task.sleep(for: .milliseconds(50))
return "hello from an async function"
}
let greeting = try await fetchGreeting()
print(greeting) The closest concurrency rhyme on this page:
async/await transfer almost keyword-for-keyword, and Swift’s return type stays unwrapped (String, not Task<string>). Note async throws composing with try await — effects stack explicitly in the signature and at the call site.Task.WhenAll → async let
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;
} func square(_ value: Int) async -> Int {
value * value
}
async let first = square(3)
async let second = square(4)
let results = await [first, second]
print(results) async let starts child tasks that run concurrently and must be awaited before the scope ends — structured concurrency the compiler enforces, where Task.WhenAll is a convention the programmer maintains. For a dynamic number of children, withTaskGroup is the scaling version.lock → actor
var account = new BankAccount();
account.Deposit(10);
account.Deposit(5);
Console.WriteLine(account.Balance);
class BankAccount
{
private readonly object gate = new();
public int Balance { get; private set; }
public void Deposit(int amount)
{
lock (gate) { Balance += amount; } // discipline, not enforcement
}
} actor BankAccount {
private var balance = 0
func deposit(_ amount: Int) -> Int {
balance += amount // no lock — the actor serializes access
return balance
}
}
let account = BankAccount()
print(await account.deposit(10))
print(await account.deposit(5)) An
actor is a reference type whose state is isolated: all access is serialized by the runtime, and cross-actor calls must be awaited. Where C#’s lock is a discipline the compiler never checks, Swift 6’s strict concurrency makes unsynchronized shared mutation a compile error — the data race, not the deadlock, becomes impossible.