Course outline

C# / Basic: Typed Programs and Object Contracts / Lesson 3

Control Flow and Method Contracts

Use branches, loops, and methods to expose domain decisions and prove bounded progress without hidden side effects.

About 5 min readContent reviewed
On this page

Lesson goals

  • Trace Boolean decisions and loop boundaries.
  • Explain ordinary, ref, and out parameter roles.
  • State preconditions, results, and failure state.

Branches encode ordered policy

An if condition must be Boolean. Relational comparisons express a domain boundary, while && and

short-circuit and can protect a later operation. Parentheses control expression grouping; they do not justify packing unrelated state changes into one clever expression. Keep validation and mutation easy to inspect.

An else-if chain selects the first matching branch, so overlapping thresholds must appear in a deliberate order. A switch statement or switch expression can express alternatives, including patterns, but must still define how invalid or unmatched domain values are handled. A default arm is a policy, not permission to ignore missing requirements.

Test adjacent threshold values

The first program classifies a known score after validating its permitted interval. The highest threshold is tested first. For score74, high fails and pass succeeds. A real input reader should feed this domain function rather than mixing terminal parsing into every branch.

Test immediately below, exactly at, and immediately above each threshold. The difference between >= and > often hides when tests use only typical middle values. Keep invalid-score rejection separate from ordinary retry classification so a score outside zero through one hundred is not accidentally treated as a valid category.

csharp An ordered score policy
using System;

int score = 74;
if (score < 0 || score > 100)
{
    Console.WriteLine("invalid");
}
else if (score >= 85)
{
    Console.WriteLine("high");
}
else if (score >= 60)
{
    Console.WriteLine("pass");
}
else
{
    Console.WriteLine("retry");
}
Expected output
pass

The valid score74 belongs to the pass interval60 through84.

A loop needs a valid starting state and progress

Before each iteration, state which work is already complete. For a sum of one through five, total initially represents an empty sum. Before iteration i it contains all earlier positive integers, and adding i preserves that claim for the next iteration. Increasing i reaches a condition that becomes false.

Use half-open ranges for indexed collections: zero through Count minus one. for, while, and foreach have different syntax but the same obligations for bounds, valid state, and termination. break and continue change control flow, so include those paths when an iteration owns resources or maintains several counters.

Pass values and report success explicitly

Ordinary parameters receive copied values. Copying an object reference still lets a method reach the same object, while reassigning the local reference does not reassign the callerโ€™s variable. ref explicitly aliases a caller variable; out requires assignment before normal return. These features express different contracts, not a single general pass-by-reference default.

The second example implements a small Try-style helper using out. Its documented domain is nonnegative operands whose sum fits int. It assigns the output on every return path, checks before addition, and returns false on failure. C# out does not promise unchanged output on failure, so callers must use the Boolean result.

csharp A bounded Try-style calculation
using System;

if (TryAddNonnegative(12, 8, out int result))
{
    Console.WriteLine($"result={result}");
}

static bool TryAddNonnegative(int a, int b, out int result)
{
    result = 0;
    if (a < 0 || b < 0 || a > int.MaxValue - b) return false;
    result = a + b;
    return true;
}
Expected output
result=20

The guard permits12 plus8, and out supplies twenty only on the successful call.

Keep dependencies and scope visible

A method contract should state accepted inputs, effects, result meaning, and failure policy. Prefer parameters over hidden mutable globals when that improves testability. Local functions can keep a helper near its use, but capturing outer variables introduces dependencies that deserve review. Overloads should have distinct meaningful shapes instead of surprising conversions.

Recursion needs a base case and progress, and the runtime does not promise unlimited stack depth. For large or externally supplied work, an iterative algorithm with explicit limits may be easier to justify. Short methods are useful when they clarify responsibilities, not when they scatter one invariant across many hidden state changes.

Common errors and fixes

A threshold value enters the wrong branch.

Why it happens
The comparison or branch order does not match the domain intervals.
How to fix it
Write disjoint intervals and test adjacent values.
Prevent it next time
Keep a boundary table beside the contract.

The caller uses an out value after a false result.

Why it happens
Failure data was mistaken for a successful result.
How to fix it
Inspect the returned Boolean before use.
Prevent it next time
Document the Try-style failure output.

A recursive helper fails on large input.

Why it happens
Depth is unbounded despite a terminating mathematical definition.
How to fix it
Use a bounded iterative strategy or validate a safe depth.
Prevent it next time
Include stack and work limits in the input contract.

Check your understanding

1. Classify scores59,60,84,85 and identify one invalid value test.

Show worked solution

The outputs are retry, pass, pass, and high. The exact boundaries enter the higher category because comparisons use >=. A negative score or101 must be rejected before classification. Tests should distinguish invalid input from a valid low score, rather than merely checking that every path prints some label.

2. Explain why the addition guard checks a > int.MaxValue - b before evaluating a + b.

Show worked solution

For nonnegative b, int.MaxValue - b is representable. Rejecting a above that remaining capacity proves the later sum fits. Checking a sum after overflow would be too late, and the one-sided guard is valid only under the documented nonnegative-domain checks. For arbitrary signs, design both upper and lower bound cases or use a checked operation with a suitable failure policy.

Key takeaways

  • Branches encode domain intervals and deserve boundary tests.
  • Method parameters copy values unless an explicit ref/out contract says otherwise.
  • Every loop needs invariant, bounds, and a progress argument.

Official references