Method Parameters

By default, arguments in C# are passed to functions by value. That means a copy of the variable is passed to the method. For value (struct) types, a copy of the value is passed to the method. For reference (class) types, a copy of the reference is passed to the method. Parameter modifiers enable you to pass arguments by reference. The following concepts help you understand these distinctions and how to use the parameter modifiers:

  • Pass by value means passing a copy of the variable to the method.
  • Pass by reference means passing access to the variable to the method.
  • A variable of a reference type contains a reference to its data.
  • A variable of a value type contains its data directly.

Because a struct is a value type, the method receives and operates on a copy of the struct argument when you pass a struct by value to a method. The method has no access to the original struct in the calling method and therefore can't change it in any way. The method can change only the copy.

A class instance is a reference type not a value type. When a reference type is passed by value to a method, the method receives a copy of the reference to the class instance. Both variables refer to the same object. The parameter is a copy of the reference. The called method can't reassign the instance in the calling method. However, the called method can use the copy of the reference to access the instance members. If the called method changes an instance member, the calling method also sees those changes since it references the same instance.

The output of the following example illustrates the difference. The method ClassTaker changes the value of the willIChange field because the method uses the address in the parameter to find the specified field of the class instance. The willIChange field of the struct in the calling method doesn't change from calling StructTaker because the value of the argument is a copy of the struct itself, not a copy of its address. StructTaker changes the copy, and the copy is lost when the call to StructTaker is completed.

class TheClass
{
    public string? willIChange;
}

struct TheStruct
{
    public string willIChange;
}

class TestClassAndStruct
{
    static void ClassTaker(TheClass c)
    {
        c.willIChange = "Changed";
    }

    static void StructTaker(TheStruct s)
    {
        s.willIChange = "Changed";
    }

    public static void Main()
    {
        TheClass testClass = new TheClass();
        TheStruct testStruct = new TheStruct();

        testClass.willIChange = "Not Changed";
        testStruct.willIChange = "Not Changed";

        ClassTaker(testClass);
        StructTaker(testStruct);

        Console.WriteLine("Class field = {0}", testClass.willIChange);
        Console.WriteLine("Struct field = {0}", testStruct.willIChange);
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

Combinations of parameter type and argument mode

How an argument is passed, and whether it's a reference type or value type controls what modifications made to the argument are visible from the caller:

  • When you pass a value type by value:
    • If the method assigns the parameter to refer to a different object, those changes aren't visible from the caller.
    • If the method modifies the state of the object referred to by the parameter, those changes aren't visible from the caller.
  • When you pass a reference type by value:
    • If the method assigns the parameter to refer to a different object, those changes aren't visible from the caller.
    • If the method modifies the state of the object referred to by the parameter, those changes are visible from the caller.
  • When you pass a value type by reference:
    • If the method assigns the parameter to refer to a different object, those changes aren't visible from the caller.
    • If the method modifies the state of the object referred to by the parameter, those changes are visible from the caller.
  • When you pass a reference type by reference:
    • If the method assigns the parameter to refer to a different object, those changes are visible from the caller.
    • If the method modifies the state of the object referred to by the parameter, those changes are visible from the caller.

Passing a reference type by reference enables the called method to replace the object to which the reference parameter refers in the caller. The storage location of the object is passed to the method as the value of the reference parameter. If you change the value in the storage location of the parameter (to point to a new object), you also change the storage location to which the caller refers. The following example passes an instance of a reference type as a ref parameter.

class Product
{
    public Product(string name, int newID)
    {
        ItemName = name;
        ItemID = newID;
    }

    public string ItemName { get; set; }
    public int ItemID { get; set; }
}

private static void ChangeByReference(ref Product itemRef)
{
    // Change the address that is stored in the itemRef parameter.
    itemRef = new Product("Stapler", 12345);
}

private static void ModifyProductsByReference()
{
    // Declare an instance of Product and display its initial values.
    Product item = new Product("Fasteners", 54321);
    System.Console.WriteLine("Original values in Main.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);

    // Pass the product instance to ChangeByReference.
    ChangeByReference(ref item);
    System.Console.WriteLine("Calling method.  Name: {0}, ID: {1}\n",
        item.ItemName, item.ItemID);
}

// This method displays the following output:
// Original values in Main.  Name: Fasteners, ID: 54321
// Calling method.  Name: Stapler, ID: 12345

Safe context of references and values

Methods can store the values of parameters in fields. When parameters are passed by value, that's usually safe. Values are copied, and reference types are reachable when stored in a field. Passing parameters by reference safely requires the compiler to define when it's safe to assign a reference to a new variable. For every expression, the compiler defines a safe context that bounds access to an expression or variable. The compiler uses two scopes: safe-context and ref-safe-context.

  • The safe-context defines the scope where any expression can be safely accessed.
  • The ref-safe-context defines the scope where a reference to any expression can be safely accessed or modified.

Informally, you can think of these scopes as the mechanism to ensure your code never accesses or modifies a reference that's no longer valid. A reference is valid as long as it refers to a valid object or struct. The safe-context defines when a variable can be assigned or reassigned. The ref-safe-context defines when a variable can be ref assigned or ref reassigned. Assignment assigns a variable to a new value; ref assignment assigns the variable to refer to a different storage location.

Reference parameters

You apply one of the following modifiers to a parameter declaration to pass arguments by reference instead of by value:

  • ref: The argument must be initialized before calling the method. The method can assign a new value to the parameter, but isn't required to do so.
  • out: The calling method isn't required to initialize the argument before calling the method. The method must assign a value to the parameter.
  • readonly ref: The argument must be initialized before calling the method. The method can't assign a new value to the parameter.
  • in: The argument must be initialized before calling the method. The method can't assign a new value to the parameter. The compiler might create a temporary variable to hold a copy of the argument to in parameters.

Members of a class can't have signatures that differ only by ref, ref readonly, in, or out. A compiler error occurs if the only difference between two members of a type is that one of them has a ref parameter and the other has an out, ref readonly, or in parameter. However, methods can be overloaded when one method has a ref, ref readonly, in, or out parameter and the other has a parameter that is passed by value, as shown in the following example. In other situations that require signature matching, such as hiding or overriding, in, ref, ref readonly, and out are part of the signature and don't match each other.

When a parameter has one of the preceding modifiers, the corresponding argument can have a compatible modifier:

  • An argument for a ref parameter must include the ref modifier.
  • An argument for an out parameter must include the out modifier.
  • An argument for an in parameter can optionally include the in modifier. If the ref modifier is used on the argument instead, the compiler issues a warning.
  • An argument for a ref readonly parameter should include either the in or ref modifiers, but not both. If neither modifier is included, the compiler issues a warning.

When you use these modifiers, they describe how the argument is used:

  • ref means the method can read or write the value of the argument.
  • out means the method sets the value of the argument.
  • ref readonly means the method reads, but can't write the value of the argument. The argument should be passed by reference.
  • in means the method reads, but can't write the value of the argument. The argument will be passed by reference or through a temporary variable.

Properties aren't variables. They're methods, and can't be passed to ref parameters. You can't use the previous parameter modifiers in the following kinds of methods:

  • Async methods, which you define by using the async modifier.
  • Iterator methods, which include a yield return or yield break statement.

Extension methods also have restrictions on the use of these argument keywords:

  • The out keyword can't be used on the first argument of an extension method.
  • The ref keyword can't be used on the first argument of an extension method when the argument isn't a struct, or a generic type not constrained to be a struct.
  • The ref readonly and in keywords can't be used unless the first argument is a struct.
  • The ref readonly and in keywords can't be used on any generic type, even when constrained to be a struct.

ref parameter modifier

To use a ref parameter, both the method definition and the calling method must explicitly use the ref keyword, as shown in the following example. (Except that the calling method can omit ref when making a COM call.)

void Method(ref int refArgument)
{
    refArgument = refArgument + 44;
}

int number = 1;
Method(ref number);
Console.WriteLine(number);
// Output: 45

An argument that is passed to a ref parameter must be initialized before it's passed.

out parameter modifier

To use an out parameter, both the method definition and the calling method must explicitly use the out keyword. For example:

int initializeInMethod;
OutArgExample(out initializeInMethod);
Console.WriteLine(initializeInMethod);     // value is now 44

void OutArgExample(out int number)
{
    number = 44;
}

Variables passed as out arguments don't have to be initialized before being passed in a method call. However, the called method is required to assign a value before the method returns.

Deconstruct methods declare their parameters with the out modifier to return multiple values. Other methods can return value tuples for multiple return values.

You can declare a variable in a separate statement before you pass it as an out argument. You can also declare the out variable in the argument list of the method call, rather than in a separate variable declaration. out variable declarations produce more compact, readable code, and also prevent you from inadvertently assigning a value to the variable before the method call. The following example defines the number variable in the call to the Int32.TryParse method.

string numberAsString = "1640";

if (Int32.TryParse(numberAsString, out int number))
    Console.WriteLine($"Converted '{numberAsString}' to {number}");
else
    Console.WriteLine($"Unable to convert '{numberAsString}'");
// The example displays the following output:
//       Converted '1640' to 1640

You can also declare an implicitly typed local variable.

ref readonly modifier

The ref readonly modifier must be present in the method declaration. A modifier at the call site is optional. Either the in or ref modifier can be used. The ref readonly modifier isn't valid at the call site. Which modifier you use at the call site can help describe characteristics of the argument. You can only use ref if the argument is a variable, and is writable. You can only use in when the argument is a variable. It might be writable, or readonly. You can't add either modifier if the argument isn't a variable, but is an expression. The following examples show these conditions. The following method uses the ref readonly modifier to indicate that a large struct should be passed by reference for performance reasons:

public static void ForceByRef(ref readonly OptionStruct thing)
{
    // elided
}

You can call the method using the ref or in modifier. If you omit the modifier, the compiler issues a warning. When the argument is an expression, not a variable, you can't add the in or ref modifiers, so you should suppress the warning:

ForceByRef(in options);
ForceByRef(ref options);
ForceByRef(options); // Warning! variable should be passed with `ref` or `in`
ForceByRef(new OptionStruct()); // Warning, but an expression, so no variable to reference

If the variable is a readonly variable, you must use the in modifier. The compiler issues an error if you use the ref modifier instead.

The ref readonly modifier indicates that the method expects the argument to be a variable rather than an expression that isn't a variable. Examples of expressions that aren't variables are constants, method return values, and properties. If the argument isn't a variable, the compiler issues a warning.

in parameter modifier

The in modifier is required in the method declaration but unnecessary at the call site.

int readonlyArgument = 44;
InArgExample(readonlyArgument);
Console.WriteLine(readonlyArgument);     // value is still 44

void InArgExample(in int number)
{
    // Uncomment the following line to see error CS8331
    //number = 19;
}

The in modifier allows the compiler to create a temporary variable for the argument and pass a readonly reference to that argument. The compiler always creates a temporary variable when the argument must be converted, when there's an implicit conversion from the argument type, or when the argument is a value that isn't a variable. For example, when the argument is a literal value, or the value returned from a property accessor. When your API requires that the argument be passed by reference, choose the ref readonly modifier instead of the in modifier.

Methods that are defined using in parameters potentially gain performance optimization. Some struct type arguments might be large in size, and when methods are called in tight loops or critical code paths, the cost of copying those structures is substantial. Methods declare in parameters to specify that arguments can be passed by reference safely because the called method doesn't modify the state of that argument. Passing those arguments by reference avoids the (potentially) expensive copy. You explicitly add the in modifier at the call site to ensure the argument is passed by reference, not by value. Explicitly using in has the following two effects:

  • Specifying in at the call site forces the compiler to select a method defined with a matching in parameter. Otherwise, when two methods differ only in the presence of in, the by value overload is a better match.
  • By specifying in, you declare your intent to pass an argument by reference. The argument used with in must represent a location that can be directly referred to. The same general rules for out and ref arguments apply: You can't use constants, ordinary properties, or other expressions that produce values. Otherwise, omitting in at the call site informs the compiler that it's fine to create a temporary variable to pass by read-only reference to the method. The compiler creates a temporary variable to overcome several restrictions with in arguments:
    • A temporary variable allows compile-time constants as in parameters.
    • A temporary variable allows properties, or other expressions for in parameters.
    • A temporary variable allows arguments where there's an implicit conversion from the argument type to the parameter type.

In all the preceding instances, the compiler creates a temporary variable that stores the value of the constant, property, or other expression.

The following code illustrates these rules:

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // OK, temporary variable created.
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // OK, temporary int created with the value 0
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // passed by readonly reference
Method(in i); // passed by readonly reference, explicitly using `in`

Now, suppose another method using by-value arguments was available. The results change as shown in the following code:

static void Method(int argument)
{
    // implementation removed
}

static void Method(in int argument)
{
    // implementation removed
}

Method(5); // Calls overload passed by value
Method(5L); // CS1503: no implicit conversion from long to int
short s = 0;
Method(s); // Calls overload passed by value.
Method(in s); // CS1503: cannot convert from in short to in int
int i = 42;
Method(i); // Calls overload passed by value
Method(in i); // passed by readonly reference, explicitly using `in`

The only method call where the argument is passed by reference is the final one.

Note

The preceding code uses int as the argument type for simplicity. Because int is no larger than a reference in most modern machines, there is no benefit to passing a single int as a readonly reference.

params modifier

No other parameters are permitted after the params keyword in a method declaration, and only one params keyword is permitted in a method declaration.

The declared type of the params parameter must be a collection type. Recognized collection types are:

Before C# 13, the parameter must be a single dimensional array.

When you call a method with a params parameter, you can pass in:

  • A comma-separated list of arguments of the type of the array elements.
  • A collection of arguments of the specified type.
  • No arguments. If you send no arguments, the length of the params list is zero.

The following example demonstrates various ways in which arguments can be sent to a params parameter.

public static void ParamsModifierExample(params int[] list)
{
    for (int i = 0; i < list.Length; i++)
    {
        System.Console.Write(list[i] + " ");
    }
    System.Console.WriteLine();
}

public static void ParamsModifierObjectExample(params object[] list)
{
    for (int i = 0; i < list.Length; i++)
    {
        System.Console.Write(list[i] + " ");
    }
    System.Console.WriteLine();
}

public static void TryParamsCalls()
{
    // You can send a comma-separated list of arguments of the
    // specified type.
    ParamsModifierExample(1, 2, 3, 4);
    ParamsModifierObjectExample(1, 'a', "test");

    // A params parameter accepts zero or more arguments.
    // The following calling statement displays only a blank line.
    ParamsModifierObjectExample();

    // An array argument can be passed, as long as the array
    // type matches the parameter type of the method being called.
    int[] myIntArray = { 5, 6, 7, 8, 9 };
    ParamsModifierExample(myIntArray);

    object[] myObjArray = { 2, 'b', "test", "again" };
    ParamsModifierObjectExample(myObjArray);

    // The following call causes a compiler error because the object
    // array cannot be converted into an integer array.
    //ParamsModifierExample(myObjArray);

    // The following call does not cause an error, but the entire
    // integer array becomes the first element of the params array.
    ParamsModifierObjectExample(myIntArray);
}
/*
Output:
    1 2 3 4
    1 a test

    5 6 7 8 9
    2 b test again
    System.Int32[]
*/