Skip to main content

[From C# to Swift] 05. Control Flow

Learning Swift from a C# Perspective

Swift : Control Flow

For-In Loops
#

1. Core Concepts
#

  • Concept Explanation: for-in is the primary iteration syntax in Swift. It is used to iterate over sequences, such as Arrays, Dictionaries, Ranges, or Strings. Its syntax is more concise and safer than C-style for loops.
  • Key Syntax: for-in, ... (Closed Range), ..< (Half-Open Range), stride, _ (Ignore value).
  • Official Note:

The contents of a Dictionary are inherently unordered, and iterating over them does not guarantee the order in which they will be retrieved. It is unrelated to the insertion order.

2. Example Analysis
#

Documentation Source Code:

let names = ["Anna", "Alex", "Brian", "Jack"]
for name in names {
    print("Hello, \(name)!")
}

let numberOfLegs = ["spider": 8, "ant": 6, "cat": 4]
for (animalName, legCount) in numberOfLegs {
    print("\(animalName)s have \(legCount) legs")
}

for index in 1...5 {
    print("\(index) times 5 is \(index * 5)")
}

Logic Explanation:

  1. The first segment demonstrates iterating over an array; name is a constant within each iteration.
  2. The second segment demonstrates iterating over a dictionary. Swift treats each element of a dictionary as a (key, value) Tuple, allowing for direct decomposition in the loop declaration.
  3. The third segment demonstrates numeric range iteration, using the ... operator to represent a range that includes both the start and end values.

3. C# Developer Perspective
#

Concept Correspondence: Equivalent to the foreach loop in C#.

C# Comparison Code:

var names = new[] { "Anna", "Alex", "Brian", "Jack" };
foreach (var name in names) {
    Console.WriteLine($"Hello, {name}!");
}

var numberOfLegs = new Dictionary<string, int> { {"spider", 8}, {"ant", 6}, {"cat", 4} };
foreach (var kvp in numberOfLegs) { // C# accesses via KeyValuePair
    Console.WriteLine($"{kvp.Key}s have {kvp.Value} legs");
}
// Or using C# 7.0+ Deconstruction
foreach (var (animal, legs) in numberOfLegs) {
    Console.WriteLine($"{animal}s have {legs} legs");
}

foreach (var index in Enumerable.Range(1, 5)) {
    Console.WriteLine($"{index} times 5 is {index * 5}");
}

Key Difference Analysis:

  • Syntax: Swift’s range operators 1...5 (closed range) and 0..<5 (excluding the end) are very intuitive. C# typically requires Enumerable.Range or a traditional for loop.
  • Behavior: The iteration variable in Swift’s for-in (like index) is a constant (let) by default and cannot be modified inside the loop. If the variable is not needed, Swift requires using _ to explicitly ignore the value. This aims to improve readability and avoid misuse; whether this results in actual performance optimization is decided by the compiler.

While Loops
#

1. Core Concepts
#

  • Concept Explanation: Used when the number of iterations is unknown and execution needs to repeat until a specific condition is met. Swift provides two forms: while (checks condition first) and repeat-while (checks condition last).
  • Key Syntax: while, repeat-while
  • Official Note:

The repeat-while loop in Swift is analogous to the do-while loop in other languages.

2. Example Analysis
#

Documentation Source Code:

// While
while square < finalSquare {
    // Game logic...
    square += diceRoll
}

// Repeat-While
repeat {
    // Game logic...
    square += diceRoll
} while square < finalSquare

Logic Explanation: This code demonstrates the logic for a “Snakes and Ladders” game. while evaluates the condition before executing the block; repeat-while guarantees the code inside the block executes at least once before checking the condition.

3. C# Developer Perspective
#

Concept Correspondence: Completely corresponds to while and do-while in C#.

C# Comparison Code:

// C#
while (square < finalSquare) {
    // Logic
}

do {
    // Logic
} while (square < finalSquare);

Key Difference Analysis:

  • Syntax: Swift uses the repeat keyword to replace C#’s do.
  • Behavior: The behavioral logic is identical. The main difference is that Swift’s conditional statements do not require mandatory parentheses () like C#, e.g., while square < finalSquare is sufficient.

Conditional Statements
#

1. Core Concepts
#

  • Concept Explanation: Swift’s if is not just a statement; it can also be used as an expression to return a value. switch is much more powerful than the traditional C# switch, supporting ranges, Tuples, and pattern matching.
  • Key Syntax: if, else if, else

2. Example Analysis
#

Documentation Source Code:

// Standard usage
if temperatureInFahrenheit <= 32 {
    print("It's very cold.")
}

// If Expression (Assignment usage)
let weatherAdvice = if temperatureInCelsius <= 0 {
    "It's very cold. Consider wearing a scarf."
} else if temperatureInCelsius >= 30 {
    "It's really warm. Don't forget to wear sunscreen."
} else {
    "It's not that cold. Wear a T-shirt."
}

Logic Explanation: This demonstrates using if as an expression, directly assigning the result of the evaluation to the variable weatherAdvice. This makes the code more concise, removing the need to declare the variable before assignment.

3. C# Developer Perspective
#

Concept Correspondence:

  • Standard if corresponds to C# if.
  • if expression corresponds to C#’s ternary operator ? : or the newer Switch Expression.

C# Comparison Code:

// C# uses the ternary operator to achieve a similar effect to if expression
var weatherAdvice = temperatureInCelsius <= 0 ? "It's very cold..." 
    : temperatureInCelsius >= 30 ? "It's really warm..." 
    : "It's not that cold...";

Key Difference Analysis:

  • Syntax: Swift’s if conditions do not require parentheses (), but the braces {} for the execution block are mandatory (even for a single line of code). C# allows omitting braces for single lines; Swift does not.
  • Behavior: Swift’s if expression offers better readability compared to nested ternary operators in C#.

Switch Statement
#

1. Core Concepts
#

  • Concept Explanation: Swift’s switch is extremely powerful. It must be Exhaustive, meaning all possible cases must be handled (usually via default). Most importantly: No Implicit Fallthrough.
  • Key Syntax: switch, case, default, ... (Range matching), (a, b) (Tuple matching), let (Value binding), where (Additional condition)
  • Official Note:

Although break is not required in Swift, you can use a break statement to match and ignore a specific case or to exit execution early. If you need C-style fallthrough behavior, you must explicitly use the fallthrough keyword.

2. Example Analysis
#

Documentation Source Code:

let somePoint = (1, 1)
switch somePoint {
case (0, 0):
    print("origin")
case (_, 0): // Matches any x value, y is 0
    print("on the x-axis")
case (0, _): // Matches x is 0, any y value
    print("on the y-axis")
case (-2...2, -2...2): // Range matching
    print("inside the box")
default:
    print("outside")
}

// Value Binding and Where Clause
let yetAnotherPoint = (1, -1)
switch yetAnotherPoint {
case let (x, y) where x == y:
    print("on the line x == y")
case let (x, y) where x == -y:
    print("on the line x == -y")
case let (x, y):
    print("arbitrary point (\(x), \(y))")
}

Logic Explanation:

  1. The first example demonstrates Tuple matching and the wildcard _. Swift can compare multiple values simultaneously.
  2. The second example demonstrates “Value Binding”, temporarily storing matched values as constants x, y, and applying additional logic via the where clause.

3. C# Developer Perspective
#

Concept Correspondence: Similar to C# switch, especially Pattern Matching introduced in C# 8.0+.

C# Comparison Code:

// C# 8.0+ Pattern Matching
var somePoint = (1, 1);
switch (somePoint) {
    case (0, 0):
        Console.WriteLine("origin");
        break;
    case (var x, 0): // Similar to Swift's (_, 0) but syntax differs slightly
        Console.WriteLine($"on x-axis at {x}");
        break;
    case (0, _):
        Console.WriteLine("on y-axis");
        break;
    case (var x, var y) when x == y: // Corresponds to Swift's case let ... where
        Console.WriteLine("x == y");
        break;
    default:
        Console.WriteLine("outside");
        break;
}

Key Difference Analysis:

  • Syntax: Swift does not require writing break, which is the biggest habit change. In C#, even an empty case cannot fall through to the next non-empty case (unless using goto case); Swift strictly prohibits implicit fallthrough.
  • Behavior: Swift’s switch must handle all possibilities. This is particularly useful for Enums; when a new member is added to an Enum, the compiler forces you to update the switch code, reducing bugs.
  • Compound Cases: Swift supports case "a", "b":; C# traditionally stacks case labels, though modern pattern matching also supports or logic.

Control Transfer Statements
#

1. Core Concepts
#

  • Concept Explanation: Statements that change the order of code execution. Includes continue (skip current iteration), break (exit loop or switch), fallthrough (switch fallthrough).
  • Key Syntax: continue, break, fallthrough, return, throw

2. Example Analysis
#

Documentation Source Code:

let integerToDescribe = 5
var description = "The number \(integerToDescribe) is"
switch integerToDescribe {
case 2, 3, 5, 7, 11, 13, 17, 19:
    description += " a prime number, and also"
    fallthrough // Explicitly request fallthrough
default:
    description += " an integer."
}
// Result: "The number 5 is a prime number, and also an integer."

Logic Explanation: Since Swift switch matches one case and ends by default, if you wish to execute the code in case 2 (or default) after case 1, you must add fallthrough.

3. C# Developer Perspective
#

Concept Correspondence:

  • continue / break behave consistently with C#.
  • fallthrough corresponds to C#’s goto case or goto default.

C# Comparison Code:

switch (integerToDescribe) {
    case 2:
    case 3:
    // ...
    case 19:
        description += " a prime number, and also";
        goto default; // C# explicit fallthrough
    default:
        description += " an integer.";
        break;
}

Key Difference Analysis:

  • Syntax: Swift’s fallthrough does not require specifying a target. It simply “falls” into the execution block of the next case, and does not check the condition of the next case. This point is crucial and prone to bugs, so use it with caution.

Labeled Statements
#

1. Core Concepts
#

  • Concept Explanation: When dealing with multiple nested loops, you can use a Label to specify which loop a break or continue applies to.
  • Key Syntax: labelName: while ...

2. Example Analysis
#

Documentation Source Code:

gameLoop: while square != finalSquare {
    diceRoll += 1
    switch square + diceRoll {
    case finalSquare:
        break gameLoop // Breaks directly out of the outer while loop, not just the switch
    // ...
    }
}

Logic Explanation: Without the label, break would only exit the switch statement. With the gameLoop label, break gameLoop terminates the entire while loop.

3. C# Developer Perspective
#

Concept Correspondence: C# has goto labels, but does not have this specific syntax for binding break to loop structures.

C# Comparison Code:

// Common C# approach
bool keepPlaying = true;
while (keepPlaying) {
    // ...
    switch (condition) {
        case 1:
            keepPlaying = false; // Use a flag
            break; 
        // Or use goto
        case 2:
            goto EndOfLoop;
    }
}
EndOfLoop: ;

Key Difference Analysis:

  • Syntax: Swift’s label syntax is more structured and readable than C#’s goto, specifically designed for nested loop control.

Early Exit (Guard)
#

1. Core Concepts
#

  • Concept Explanation: guard is a “reverse if”. It requires the condition to be true; otherwise, it executes the else block. The else block must contain an instruction to exit the current scope (e.g., return, break, throw). This avoids “arrow code” (excessive nested ifs) and keeps the “Happy Path” at the top level.
  • Key Syntax: guard condition else { return }

2. Example Analysis
#

Documentation Source Code:

func greet(person: [String: String]) {
    guard let name = person["name"] else {
        return // Condition failed, must exit
    }
    // 'name' can be used directly here, no need to be inside an else block
    print("Hello \(name)!")
    
    guard let location = person["location"] else {
        print("I hope the weather is nice near you.")
        return
    }
    print("I hope the weather is nice in \(location).")
}

Logic Explanation:

  1. Check if person["name"] has a value.
  2. If not, enter else and return.
  3. If it does, the unwrapped name variable can be used directly in the code after the guard statement (this is the biggest difference from if let).

3. C# Developer Perspective
#

Concept Correspondence: C# does not have a guard keyword. The “Guard Clauses” pattern (reverse if) is typically used.

C# Comparison Code:

void Greet(Dictionary<string, string> person) {
    if (!person.TryGetValue("name", out var name)) {
        return;
    }
    // C# out variable leaks to the outer scope, creating a similar effect to guard
    Console.WriteLine($"Hello {name}!");
}

Key Difference Analysis:

  • Syntax: Swift’s guard enforces that the else block must exit, guaranteeing safety at compile time. C# relies on the developer’s coding habits.
  • Scope: The variable bound by guard let is scoped to the entire block containing the guard statement (until the end), making the code flow very smooth.

Deferred Execution (Defer)
#

1. Core Concepts
#

  • Concept Explanation: Code inside a defer block executes “just before the current scope ends”, regardless of whether the scope ends due to successful completion, a return, or a thrown error. Typically used for resource cleanup (e.g., closing files, releasing locks).
  • Key Syntax: defer { ... }

2. Example Analysis
#

Documentation Source Code:

var score = 1
if score < 10 {
    defer {
        print(score)
    }
    score += 5
}
// Prints "6"

Logic Explanation:

  1. Enter if.
  2. Declare defer; the print statement is pushed onto the stack, waiting to execute upon scope exit.
  3. Execute score += 5; score becomes 6.
  4. Prepare to leave the if scope; execute the defer block, printing 6.

3. C# Developer Perspective
#

Concept Correspondence: Similar to C#’s try-finally block or the using statement (IDisposable).

C# Comparison Code:

// C# uses try-finally to simulate
{
    try {
        score += 5;
    } finally {
        Console.WriteLine(score);
    }
}

// Or use using (if resource disposal is involved)
using (var resource = new Resource()) {
    // code
} // Dispose called here

Key Difference Analysis:

  • Flexibility: defer does not require objects to implement an interface, nor does it need nested wrappers like try-finally. You can insert a defer anywhere in the middle of your code to handle cleanup, and multiple defer blocks execute in LIFO (Last In, First Out) order.

Checking API Availability
#

1. Core Concepts
#

  • Concept Explanation: Due to frequent iOS/macOS updates, Swift’s #available is a mechanism that coordinates between compile-time and runtime.
  • Key Syntax: #available, #unavailable

2. Example Analysis
#

Documentation Source Code:

if #available(iOS 10, macOS 10.12, *) {
    // Use new API
} else {
    // Use old API
}

Logic Explanation: * represents the minimum deployment target for other platforms not listed. The compiler uses this to allow you to use newer APIs within the if block.

3. C# Developer Perspective
#

Concept Correspondence: C#’s OperatingSystem class checks (Runtime) or #if preprocessor directives (Compile time).

C# Comparison Code:

// C# Runtime Check (Similar)
if (OperatingSystem.IsIOSVersionAtLeast(10)) {
    // Call new API
}

Key Difference Analysis:

  • Behavior: Swift’s #available is deeply integrated with the compiler. If you call a new API outside of an #available block, the Swift compiler will generate an error, forcing you to add the check. C# checks are typically runtime-based, and the compiler may not prevent you from calling new methods on old systems (unless specific Analyzers are used).