← C#: language and application decisions
03 / 10 · 50 MIN

Methods, parameters and patterns

Trace a call’s effects, distinguish parameter contracts and classify inputs with explicit rules.

Does the method receive a value or a variable?

A signature helps explain the effects a call may have. In the usual case, without a modifier, the argument is passed by value. With int queued = 6, calling Adjust(queued) gives the parameter a copy of the number. Adding two to that parameter does not change queued. To expose a new total, the method can return it and the caller can assign it to the intended variable.

For a class, the copied value is a reference. If Change(config) changes config.Limit, the caller may observe that change in the same object. If the method assigns config = new Settings(), it replaces only the local reference. Draw two boxes for the variables and one for the object: moving one arrow differs from changing the object both arrows reach.

This model supports reviewing a routine without running it. First identify what is copied, then what is assigned, and finally who observes that change. A call without ref can affect a shared object; absence of ref is not a general guarantee of purity.

ref, out and in express different contracts

ref lets a method use and change a variable the caller has already initialized. In Increment(ref count), the possibility of changing that variable is visible at the call. out can produce an output without requiring a prior value, but every path returning normally must assign that output. A return false is still a normal return.

In TrySelect(bool available, out int selected), writing if (!available) return false before assigning selected prevents compilation. The failure branch can assign an output and document that it should be consumed only when the Boolean result is true. The output number alone does not replace the success decision.

in limits reassignment or writing through the readonly parameter. It does not promise that the entire program is free of effects. The compiler can use a temporary when the call permits that form. Do not add in merely on the assumption that performance improves: the contract must remain clear and any benefit requires measurement in a real context.

Names associate values; written order determines evaluation

Named arguments help readers understand calls containing several similar values. Archive(name: "batch", copies: 3) makes the roles visible. When both are named, they can appear in a different order from the declaration. An optional parameter supplies a value when its corresponding argument is omitted; it does not remove the need to supply required parameters.

Name-based association does not reorder expression evaluation. In Pair(second: ReadSecond(), first: ReadFirst()), ReadSecond is evaluated before ReadFirst. The second expression’s result is then associated with first. This matters when methods read a cursor, log events or change state. Before determining the outcome, trace evaluations and parameter association separately.

Avoid relying on hidden effects merely to shorten a call. When order has operational meaning, clearly named local variables help expose the steps. A reader should be able to distinguish when a value was obtained from where the called method uses it.

Recognizing a type is not converting a number

A declaration pattern such as value is int amount checks whether the value supports that match and makes amount available in the branch where it does. If value is null, the int pattern does not match. If it contains a boxed long, it also does not match int, even when the number is small. The pattern neither parses text nor numerically converts long to int.

This helps build explicit boundaries for heterogeneous input. If an interface accepts only int, an object holding 8L has another type and can be rejected. If the contract accepts both, express that decision and handle conversion bounds separately. Do not change a pattern merely until one example produces true.

Relational patterns describe conditions such as > 20. They can be combined in a classification, but arm order and guards matter too. Keep decision expressions simple: a classification should explain which rule applied rather than hide state changes in conditions that are difficult to trace.

switch: priority, guards and unknown values

A switch expression selects the first arm whose pattern matches and whose when guard, if present, is true. In a service, > 20 when urgent can precede > 20. A large nonurgent volume fails the first guard and proceeds to the next arm. The presence of the first pattern does not automatically remove the remaining candidates.

Discard _ covers any value reaching that point, including null. Placing it first and then adding a constant case fully subsumes that later case; the compiler rejects the unreachable arm. There is no automatic ordering that prioritizes the most specific pattern.

Covering every value is also a business decision. Using _ => "accepted" may compile yet accept states that were never approved. Decide the outcomes for null and future values deliberately. Removing coverage and expecting an exception does not automatically create a review record or a meaningful response for the caller.

Guided example: classify before producing effects

An application receives a count represented as object?. This example’s contract accepts only int values from 1 through 500; null, other types and out-of-range values are rejected. Among accepted inputs, urgent requests go to priority and the others to standard. These names represent local decisions, not reservations already made.

The method below first recognizes an int within the bounds. Every return path assigns amount and produces a classification. After validation, the switch expression chooses priority using Boolean urgent. In the rejection branch, amount is zero by explicit convention and the return is reject. A caller performing a reservation must consume both outputs and act only on priority or standard. It cannot interpret amount = 0 alone as proof of valid input.

By reading: Classify(30, true, out var n) produces priority and n = 30. With 30 and false, it produces standard and n = 30. With 30L, null, 0 or 501, it produces reject and n = 0. Literal 30L is rejected by type even though its value fits the bounds. None of these outcomes proves stock availability or user authorization.

Reviewing a call

Start with the contract: should the method return a decision, change a variable or modify an existing object? Then follow arguments in evaluation order. Associate values with parameter names or positions and examine every return path. A useful explanation distinguishes compilation errors, normal results and possible exceptions.

When designing overloads, different parameter names do not create different signatures. A by-value parameter can be distinguished from ref, but ref and out alone do not permit two competing overloads with the rest of the signature unchanged. Changing only the return type does not resolve that collision either. Naming operations with different intentions clearly is preferable to making an API difficult to call correctly.

Summary: trace variables and objects separately, assign every out output, distinguish evaluation order from argument association, and make unknown values explicit. The following assessments ask for decisions through reading and selection; they do not require compiling or running the snippets.

static string Classify(object? input, bool urgent, out int amount)
{
    if (input is not int n || n < 1 || n > 500)
    {
        amount = 0;
        return "reject";
    }
    amount = n;
    return urgent switch
    {
        true => "priority",
        false => "standard"
    };
}
IN PRACTICE

Method inside a class with nullable enabled. Classifies without making reservations: 30/true is priority; 30/false is standard; null, 30L, 0 and 501 are reject.

Common pitfalls

Confusing parameter reassignment with caller activation; omitting out assignment on failure; assuming numeric conversion in a pattern; accepting new statuses through the discard.

Related topics: References, nullability and text · Objects, interfaces and records · Exceptions, resources and files

Take this idea with you

A signature describes how values enter and leave; decision order and coverage determine what a call actually means.

Create account

Reference: Method parameters and modifiers · C# 14 / .NET 10; no preview features