AlgoMaster Logo

ref, out & in Parameters

Last Updated: May 17, 2026

12 min read

By default a C# method gets its arguments by value, which means the method works on a copy of the variable and any change inside the method stays inside the method. The ref, out, and in keywords change that contract. They let the method see, set, or just read the caller's actual storage location instead of a copy, and each keyword spells out a different deal between the caller and the callee about who must do what.

A Quick Recap of Pass-by-Value

The _Method Parameters_ lesson covered the default rule: every argument is passed by value. For a value type like int, that means the method gets its own copy of the number. For a reference type like a List<T>, the method gets its own copy of the reference, which still points at the same object on the heap.

A short refresher to set the contrast with ref:

The method changed price, but price was a copy of cartTotal. The caller's cartTotal is untouched. This is the behavior ref and out are designed to override when you genuinely need the method to write back into the caller's variable.

The ref Keyword

ref tells the compiler to pass a reference to the caller's variable, not a copy. The method and the caller end up working on the same storage slot. A read in the method sees whatever the caller had; a write in the method changes the caller's variable.

Notice two things. The parameter is declared ref int price, and the call site repeats the keyword: ApplyDiscount(ref cartTotal). C# requires the keyword at the call too, on purpose. The caller sees ref cartTotal in the source and knows the method might modify the argument. There's no surprise mutation hiding behind a normal-looking call.

A second rule matters with ref: the variable must be definitely assigned before the call. The compiler treats ref as "the method can read and write," so the caller has to have given it a value first.

Try to pass an uninitialized variable with ref and the compiler stops you with error CS0165. The method might read from it before writing, so passing garbage would be unsafe.

A classic use of ref is swapping two values. Without ref, a swap method has no way to push the new values back to the caller:

The method body reads and writes both parameters, and the changes are visible to the caller because both variables were passed by reference. Without ref, the method would swap two local copies and the caller would see nothing.

The out Keyword

out is closely related to ref but flips the rules around. The caller does not have to initialize the variable, but the method must assign it before returning. It's the right tool when the method's job is to produce a value (or several values) and hand them back through parameters.

Two things to notice. finalPrice and saved were declared inline with the call, using out decimal finalPrice. This is the inline-declared out variable form, added in C# 7. Before C# 7 you had to declare them first and then pass them; now you can do both in one place. The compiler infers the type, the variable comes into scope at the call site, and it's only assigned after the method writes to it.

The second thing: inside ComputeDiscount, the compiler treats discountedPrice and savings as unassigned at the start. If the method reaches its end without assigning both, the compiler refuses to compile:

The compiler error is CS0177: "The out parameter 'savings' must be assigned to before control leaves the current method." Every code path, including early returns, must end with all out parameters assigned.

Fix it by assigning on every path, typically by giving the out parameters a sensible default at the top of the method:

That shape, a bool return plus one or more out results, is so common it has a name: the **Try* pattern**, covered in the next section.

The Try* Pattern

The BCL uses out heavily in a pattern named after methods like int.TryParse, Dictionary<TKey, TValue>.TryGetValue, and Queue<T>.TryDequeue. The shape is always the same: a bool return indicating success, and one or more out parameters carrying the produced value when the call succeeds.

int.TryParse is the everyday example. Parsing a quantity that came in as text from a form or query string:

TryParse returns true and assigns quantity to 3 if the string is a valid integer; it returns false and assigns quantity to 0 otherwise. The big advantage over int.Parse is that no exception is thrown for invalid input, which is the right choice when invalid input is expected (user typed letters into a quantity field, a CSV row has a malformed number, and so on).

A more realistic e-commerce snippet: validating a quantity that arrived as a string before adding to the cart.

Read the call site again: int.TryParse(rawQuantity, out int qty). The variable qty is declared inline, comes into scope at the call, and you can use it immediately in the if condition. This is the inline out form working at full strength: declaration, call, and use in one compact line. Without it, you'd write a separate int qty; declaration first, which adds noise.

Dictionary<TKey, TValue>.TryGetValue is another common one, and avoiding a missing-key exception is the whole point:

When the key exists, TryGetValue returns true and writes the value to the out parameter. When it doesn't, it returns false and writes the default for the value type (0m for decimal). The caller checks the return value before trusting the out.

A handful of other Try* methods you'll meet in production code:

MethodReturnsout parameter
int.TryParse(s, out int n)bool (parse succeeded?)the parsed int, else 0
decimal.TryParse(s, out decimal d)boolthe parsed decimal, else 0m
DateTime.TryParse(s, out DateTime dt)boolthe parsed DateTime, else default
dict.TryGetValue(key, out V value)bool (key existed?)the value, else default of V
queue.TryDequeue(out T item)bool (queue not empty?)the dequeued item, else default of T
stack.TryPop(out T item)bool (stack not empty?)the popped item, else default of T

The shared rule of the pattern: the bool tells you whether the out is meaningful. Always check it before using the value, because on a failure the out is just the type's default and not a real result.

The in Keyword

in is the third modifier, and it's the read-only one. The argument is passed by reference (no copy), but inside the method the parameter is treated as read-only. The method can read it as often as it likes; it cannot assign to it or pass it to another method as ref / out.

in was added in C# 7.2 for one specific job: pass a large struct by reference to avoid the cost of copying it, while still telling readers and the compiler that the method won't modify the value.

Inside ComputeTotal, the parameter order is just a reference to the caller's order. No copy is made, even though Order has six fields. If you tried to assign to a field of order inside the method, the compiler would refuse:

The error CS8332 says the parameter is a "readonly variable." The whole point of in is the read-only by reference contract.

Two more details worth knowing about in.

The keyword at the call site is optional. For ref and out, the caller must repeat the keyword: Swap(ref a, ref b), int.TryParse(s, out int n). For in, it's optional. Both of these compile:

The explicit form is generally a small readability win for someone reading the call cold, but neither is wrong. Most BCL APIs do not use in at call sites, even when the parameter is declared in.

Use `in` mainly with `readonly struct`. If the parameter is a regular (mutable) struct, the compiler has to defensively copy the struct on every member access that could mutate it, to keep its read-only promise. That defensive copy can wipe out the performance win you wanted from in in the first place.

Mark structs you intend to use with in as readonly struct, or mark individual methods on the struct as readonly (a readonly method promises not to mutate the struct). That tells the compiler no defensive copy is needed. The _readonly Struct_ lesson covers it in detail.

ref vs out vs in vs Default (the Comparison Table)

A side-by-side summary of the four ways an argument can flow. Use this as the reference when picking a modifier:

Property(default, by value)refoutin
Pass a reference to the caller's variable?No, copyYesYesYes
Caller must initialize the variable before the call?YesYesNoYes
Method may read the parameter?YesYesOnly after assigning itYes
Method may write the parameter?Yes (changes are local)Yes (visible to caller)Yes (required)No
Method must assign the parameter before returning?NoNoYes (every path)No
Call-site keyword required?NoYesYesOptional
Typical useMost parametersTrue two-way mutation, swapMultiple results, Try* patternAvoid copying large readonly struct

A short way to remember which to reach for:

  • Reads only, ordinary value (or small): default by-value.
  • Method needs to modify an existing variable the caller already set up: ref.
  • Method needs to produce a value (especially a Try*-style result): out.
  • Method needs to read a large struct cheaply without copying: in.

A diagram of the same idea, framed as a choice the caller and method make together:

The diagram says: start at the top with the intent (read only, write only, or both), and the answer drops out. Most parameters live on the left branch as plain by-value. out covers the middle branch when the method's whole point is to produce a result. ref covers the right branch when the value pre-exists and the method mutates it.

Inline-Declared out Variables (C# 7)

This was mentioned alongside int.TryParse, but it's worth its own section because the syntax shows up everywhere in modern C# and reads weirdly the first time. Inline out declarations let you create the variable right at the call site, where it's first used.

Before C# 7, you had to declare the variable first:

From C# 7 onward, you can declare it inline and even use var:

The variable comes into scope at the call site and remains in scope through the enclosing block, so you can use it in the if body and after it. The compiler treats it as definitely assigned only when control reaches a point where the method has assigned it, which for a Try* method means: inside the if (... ) block after a successful parse.

If you don't care about the value (the method has a useful return, and you just need to satisfy the signature), you can use a discard with _:

The discard _ is not a variable, it's a sentinel that says "I'm not going to read this." It compiles to no storage at all. Discards are useful when you only care whether parsing succeeded, not the parsed value, or when a method has multiple out parameters and you only care about some of them.

The discard pattern keeps the call concise without inventing names like unused1, _throwaway, or whatever for values you'll never read.

ref returns and ref locals (Brief Look)

The ref keyword has one more form worth mentioning, though it's an advanced feature you won't reach for often: `ref` returns and `ref` locals. A method can return a reference to a variable (typically into a backing array), and the caller can store that reference in a ref local. Writes through the ref local change the underlying storage.

A small example. A cart that exposes a ref to a slot in its internal array so the caller can edit the slot in place:

The method PriceAt returns ref decimal, a reference to the array slot. The local firstPrice is declared ref decimal and bound to that slot. Assigning to firstPrice changes prices[0] directly, without any setter method.

This is useful in performance-sensitive code that mutates large arrays of value types in place, and in some library APIs where you want to give callers direct access to a slot in a buffer (Span<T> indexers, for instance, return ref T). For everyday e-commerce code, a normal indexer or setter method is clearer. Treat ref returns as a power tool you reach for when profiling proves the copy is the bottleneck, not as a default style.

A read-only counterpart exists too: ref readonly returns, which give callers a non-copying view of the value but forbid them from writing through it. The _ref readonly Parameters_ lesson covers this further.

Common Mistakes

A handful of mistakes show up over and over with these keywords. Each one is easy to fix once you know what you're looking at.

Forgetting to assign every `out` parameter. The compiler is strict: every code path, including early returns, must assign every out before control leaves the method.

The early return false path never assigns discount. Fix it by assigning a default at the top, or on every path.

Fix:

Using `ref` when `out` is clearer. If the method's job is to produce a result, out documents that intent. ref says "I read what you gave me, and I'll write back." A Try* that uses ref forces every caller to declare and initialize a variable that will be overwritten anyway, which is noise.

Fix: use out. The caller doesn't have to initialize, and the inline form drops a declaration line:

Forgetting the keyword at the call site for `ref` or `out`. The compiler reports CS1620 ("Argument 1 must be passed with the 'ref' keyword") or CS1502/CS1503 on a mismatch. Fix is to add the keyword.

Mutating a struct through `in`. The keyword forbids assigning to the parameter or to its fields. The compiler reports CS8332. The fix is usually to drop the in (if mutation is intended) or to make the struct readonly (if it shouldn't have been mutable in the first place).

Using `in` with a non-readonly struct and expecting a perf win. The compiler inserts a defensive copy on every member access that could possibly mutate the struct. The fix is to mark the struct readonly struct (whole struct is immutable) or mark just the accessed members readonly.

Passing a property as `ref` or `out`. Properties are not variables, they're method calls in disguise. The compiler reports CS0206 ("A property or indexer may not be passed as an out or ref parameter"). Either copy the property into a local first and pass the local, or expose the underlying field if you really need byref access.

Putting It Together: A Small E-Commerce Example

A condensed example that uses all three keywords together. A program parses a shopping form (raw strings for quantity and a coupon code), computes a discounted price, and swaps the customer's billing and shipping addresses. Each operation picks the right modifier for the job.

Three different intents, three different modifiers. in for cheap read-only access to the shipping address. out for the parser to produce a quantity without forcing the caller to declare and initialize one. ref for the swap to actually move values around in the caller's variables.

If you find yourself reaching for one of these keywords for a different reason, pause: it's often a sign that a regular return value (or a tuple) would be cleaner.

Summary

  • ref passes a reference to the caller's variable; the method can read and write, and the caller must initialize the variable before the call. The keyword is required at the call site.
  • out also passes a reference, but the caller does not have to initialize the variable, and the method must assign it on every code path before returning. The keyword is required at the call site.
  • in passes a reference but treats the parameter as read-only inside the method. Useful for avoiding copies of large readonly struct values. The keyword is optional at the call site.
  • The Try* pattern (int.TryParse, Dictionary.TryGetValue, Queue.TryDequeue) pairs a bool return with one or more out parameters. Always check the bool before reading the out, because on failure the out is just the type's default.
  • Inline out declarations (C# 7) let you declare the receiving variable right at the call: int.TryParse(s, out int n). Combine with the discard _ (out _) when you don't need the value.
  • Avoid in with non-readonly structs; the compiler inserts defensive copies that defeat the perf goal. Mark the struct or its members readonly to remove the copies.
  • ref returns and ref locals let a method hand back a reference to a slot (typically in an array) for direct in-place edits. Treat them as an advanced power tool, not an everyday default.