AlgoMaster Logo

Method Parameters

Last Updated: May 17, 2026

12 min read

A method is a named, reusable block of code, and this chapter is about the values you pass into one. C# gives you several ways to declare parameters and several ways to supply arguments at the call site, and the rules for what gets copied versus shared at the boundary trip up almost every learner at some point. This chapter walks through positional arguments, named arguments, optional parameters with default values, the order rules between them, and the difference between passing a value type and a reference type. By the end you should be able to read a method signature and predict, without running the code, what the caller can and cannot change.

Parameters vs Arguments

Two words that get mixed up constantly. A parameter is the variable listed in a method's declaration. An argument is the actual value you pass when you call that method.

In the declaration Total(decimal price, int quantity), price and quantity are parameters. In the call Total(29.99m, 3), 29.99m and 3 are arguments. The parameter is the slot, the argument is what fills it.

Most C# developers use the two words interchangeably in casual conversation. The compiler and the official documentation don't, so it pays to know the distinction when you read error messages like CS1503: Argument 1: cannot convert from 'string' to 'int'.

Positional Arguments

The default way to call a method is to pass arguments in the same order the parameters were declared. The first argument fills the first parameter, the second fills the second, and so on. This is called positional because position is what matches argument to parameter.

Order matters. Swap any two arguments and you'll either get a compile error (if the types differ) or, worse, a silently wrong result (if the types match).

This doesn't even compile, because int 2 is implicitly convertible to decimal price but decimal 49.99m is not implicitly convertible to int quantity. The compiler error is CS1503: Argument 2: cannot convert from 'decimal' to 'int'. That's a friendly accident here, but consider this version:

The number happens to look right because multiplication is commutative, but the meaning is wrong. The function was supposed to take a price of $49.99 and a quantity of 2, not a price of $2 and a quantity of 49.99. This is the kind of bug that lives in code forever, because the output looks plausible. Named arguments, coming up next, are one tool for preventing exactly this class of mistake.

Named Arguments

Instead of relying on position, you can name each argument at the call site. The syntax is parameterName: value.

The call is more verbose, but you've documented the meaning at the call site. A reader doesn't have to jump to the declaration to understand what 49.99m, 2, and 5m are supposed to mean. The compiler matches each argument to its parameter by name, not by position.

With named arguments you can also reorder them however you like:

Same result, different order. This is occasionally useful when you want to group related arguments visually, but most of the time you'd just stick with the declaration order for readability.

When Named Arguments Earn Their Keep

The wins show up in three cases. First, methods with several bool parameters where the call site reads as a parade of true, false, true:

The first call works but tells you nothing. The second call is self-documenting. For booleans especially, named arguments are worth the extra characters.

Second, methods where two parameters have the same type and could plausibly be confused (like from and to, or width and height):

Both parameters are decimal. Without names, swapping them silently changes the meaning, and the compiler can't help. With names, the call is unambiguous.

Third, when you want to skip an optional parameter in the middle of the parameter list. We'll get to that with the next concept.

Optional Parameters with Default Values

Sometimes a parameter has a sensible default and most callers don't want to specify it. You can give a parameter a default value in the method declaration, and callers can leave it out of the call.

When the second call passes 5, quantity is 5. When the first call passes nothing, quantity defaults to 1. The method has one declaration but supports two call shapes.

The default value must be a compile-time constant: a literal, a const field, a default expression, or a few specific things like null. You cannot use a method call, a new object, or anything computed at runtime.

For reference type defaults, the common ones are null and string literals:

Defaults Are Baked at the Call Site

This is the subtle one. When you compile a call that omits an optional parameter, the compiler inserts the default value into the call site itself, not into the method body. This sounds like a distinction without a difference, until you ship a library.

Imagine you publish a package with this method:

A caller's code compiles against version 1.0 and writes Total(29.99m). The compiler embeds 1 into the caller's binary, effectively turning the call into Total(29.99m, 1). Now you release version 2.0 of your library and change the default to 2. If the caller doesn't recompile, they still get quantity = 1, because that value is hard-coded into their compiled binary. Only after they recompile do they pick up the new default.

This rarely bites a single project where everything compiles together, but it's a real gotcha for library authors. The standard advice is to avoid changing default values across published versions. If you need different behavior, add an overload (the _Method Overloading_ lesson) instead.

Parameter Order Rules

C# enforces a few rules about how you order parameters in a declaration and how you supply arguments in a call. Get these wrong and you'll get a compile error before you can run anything.

Required Parameters Come Before Optional Ones

In the declaration, every required parameter must appear before any optional parameter. You can't sprinkle defaults wherever you like.

The reason is straightforward. If quantity is optional but discount is required, a caller writing Bad(29.99m, 5m) would have no way to indicate whether 5m is quantity or discount. The compiler avoids the ambiguity by forbidding the declaration outright.

Positional Arguments Come Before Named Arguments

At the call site, positional arguments must appear before named arguments. You can mix them, but the positional ones come first.

The rule above used to be strict: once you named one argument, you couldn't go back to positional. C# 7.2 relaxed this to allow non-trailing named arguments, as long as they're in the right position. In practice, this means you can name an argument in the middle and let the surrounding positional arguments fill the others:

It compiles, but it reads strangely. Most teams prefer the simpler rule: all positional first, then all named.

Skipping an Optional Parameter in the Middle

When you want to override a default that comes after an optional you'd rather leave alone, named arguments are the only clean way to do it.

Without the named argument, you'd have to write PlaceOrder("Alice", false, false, false), which is both noisier and more error-prone.

Pass-by-Value: What Actually Gets Copied

C# passes arguments by value by default. The word "value" here causes confusion, because it's used in two different ways: the language-level rule (everything is copied) and the type system (value types vs reference types). The two combine in a way that's not obvious until you see it spelled out.

The key idea: when you pass an argument, the value of the variable is copied into the parameter. For a value type, that value is the data itself. For a reference type, that value is a reference (effectively, an address) to a heap object. In both cases, the parameter inside the method is a fresh, independent variable, but what it holds differs.

Value Types: The Data Is Copied

int, double, decimal, bool, char, struct, and enum types are value types. When you pass one as an argument, the value itself is copied into the parameter. The method works on its own copy, and changes to the parameter have no effect on the caller's variable.

cartPrice is 100m. The method receives a copy of that value (also 100m) in its price parameter. The method reassigns price to 90m, but that only affects the method's own variable. The caller's cartPrice is unchanged.

A small picture of the stack frames involved:

The arrow is dashed because no link survives the call. The caller's cartPrice and the method's price are separate slots that happen to start with the same value. After the method returns, the parameter's stack slot is gone and the caller continues with its original 100m.

Reference Types: The Reference Is Copied

This is where it gets interesting. A reference type variable doesn't hold the object itself, it holds a reference to an object on the heap. When you pass that variable as an argument, the reference is copied, not the object. The parameter inside the method is a fresh variable that holds a copy of the same reference. Both names now point to the same heap object.

The caller passed shoppingCart. The method received a parameter cart that holds a copy of the same reference. When the method called cart.Add("Mouse"), it mutated the object on the heap, and the caller sees that mutation because shoppingCart is pointing at the same object.

Here's the picture:

Two stack slots, one heap object. Both arrows point to the same object, so any mutation through either name is visible through the other.

This is the most common pattern in C# code that uses collections, custom classes, and most BCL types. It's exactly what you want for things like a shopping cart: pass the cart to a helper, have the helper modify it, and the changes stick.

Reassigning the Parameter Doesn't Touch the Caller

Now the trap. Mutating the object through the parameter is visible to the caller. But reassigning the parameter to point at a different object is not.

Inside the method, cart was redirected to point at a brand new list. But cart is a copy of the reference. Updating the copy doesn't update the caller's shoppingCart. The caller still points at the original two-item list.

The picture before reassignment:

And after cart = new List<string> { "Mouse" }:

The caller's shoppingCart is still pointing at the original list. The method's cart parameter is now pointing at a new list, but that's a local variable on the method's stack frame and goes away when the method returns.

The rule, said plainly:

  • Mutating the object (calling methods on it, setting its properties, adding to it if it's a collection) is visible to the caller. The caller sees the same object you mutated.
  • Reassigning the parameter to a new object is not visible to the caller. The caller still has the original reference.

C# does have a way to opt into truly reference-like semantics where reassignment also affects the caller, and that's what ref parameters are for. For now, the rule above describes 99% of the parameter passing you'll do.

A Note on string

string is a reference type, but it's also immutable. Strings have no mutating methods, so you can't observe the "mutate-through-parameter" behavior with a string. You can reassign a string parameter all day inside a method, and the caller's string is untouched, just like any other reassignment. In practice, strings feel exactly like value types when passed around, even though they aren't.

name.ToUpper() returns a brand new string and name = ... reassigns the parameter to it. The caller's product still references the original. This is the same rule as before, with no surprises.

nameof(parameter)

One small but useful tool when working with parameters: the nameof operator. It returns the name of a variable, type, or member as a string, evaluated at compile time. The most common use with parameters is in argument validation.

nameof(percent) produces the string "percent". The advantage over writing the literal string "percent" is that if you rename the parameter (most IDEs will offer this as a refactor), nameof updates automatically. The literal string would silently fall out of sync, leaving an error message that names a parameter the method no longer has.

nameof is evaluated at compile time, so it has zero runtime cost. It's purely a typed reference that becomes a string. Use it anywhere you'd otherwise hard-code a parameter or member name into an exception message, a log line, or a data-binding string.

Putting It All Together

A method signature that uses several of the features in this chapter:

A few things worth noticing:

  • cart is a List<string>, a reference type. Every call mutates the same list, so all the items end up in shoppingCart. No ref keyword needed.
  • quantity and discount are optional. The four call sites use different combinations of positional and named arguments to pass exactly what they want.
  • nameof(quantity) keeps the validation error message tied to the parameter name. If we rename quantity to qty tomorrow, the error message follows.
  • The required parameters (cart and product) come first in the declaration, followed by the optional ones. C# requires this order.

A summary table of the call shapes we used:

CallStyleNotes
AddItem(cart, "Headphones")Positional, both optionals defaultedShortest form.
AddItem(cart, "Keyboard", 2)Positional, one optional suppliedDefault for discount.
AddItem(cart, "Mouse", quantity: 3, discount: 5m)Positional then namedNames document intent.
AddItem(cart, product: "USB Cable", discount: 1m)Named, skipping quantityNamed args let you skip middle optionals.

Summary

  • A parameter is the slot declared in a method signature; an argument is the value passed at the call site. The compiler's error messages use these terms precisely.
  • Positional arguments match by order. Type mismatches catch some swaps at compile time, but same-typed parameters can swap silently and produce wrong-but-plausible results.
  • Named arguments (paramName: value) make calls self-documenting and prevent same-type swap bugs. They're especially valuable for boolean parameters and for skipping optional parameters in the middle of the list.
  • Optional parameters use = defaultValue in the declaration. The default must be a compile-time constant (literal, const, default, or null). Required parameters must come before optional ones.
  • Default values are baked at the call site by the compiler. Changing a default in a library is a binary-compatibility break for callers who don't recompile.
  • C# passes arguments by value. For value types, the data is copied. For reference types, the reference is copied, but both names point at the same heap object.
  • Mutating an object through a parameter is visible to the caller. Reassigning the parameter to a new object is not.
  • nameof(parameter) returns the parameter's name as a string at compile time. Use it in exception messages and logs so renames stay in sync.

The _ref, out & in Parameters_ lesson covers the three keywords that let you opt into different parameter-passing semantics. ref lets the caller's variable see reassignments inside the method, out lets a method produce one or more output values without using the return slot, and in passes a read-only reference for performance without allowing mutation.