方法参数和修饰符

默认情况下,C# 中的参数按值传递给函数。 这意味着将变量的副本会传递到方法。 对于值 (struct) 类型,的副本将传递到方法。 对于引用 (class) 类型,引用的副本将传递到方法。 参数修饰符可让你按引用传递参数。

因为结构是值类型,所以按值将结构传递给方法时,该方法接收结构参数的副本并在其上运行。 该方法无法访问调用方法中的原始结构,因此无法对其进行任何更改。 它只能更改副本。

类实例是引用类型,而非值类型。 当引用类型通过值传递给方法时,该方法将接收对实例的引用的副本。 这两个变量都引用同一对象。 参数是引用的副本。 调用的方法无法在调用方法中重新分配实例。 但是,调用的方法可以使用引用的副本来访问实例成员。 如果调用的方法更改实例成员,调用方法也会看到这些更改,因为它引用同一实例。

按值传递并按引用传递

本节中的所有示例都使用以下两种 record 类型来说明 class 类型和 struct 类型之间的差异:

public record struct Point(int X, int Y);
// This doesn't use a primary constructor because the properties implemented for `record` types are 
// readonly in record class types. That would prevent the mutations necessary for this example.
public record class Point3D
{
    public int X { get; set; }
    public int Y { get; set; }
    public int Z { get; set; }
}

以下示例的输出说明了按值传递结构类型与按值传递类类型之间的差异。 这两个 Mutate 方法更改其参数的属性值。 当参数是 struct 类型时,这些更改是对参数数据的副本进行的。 当参数是 class 类型时,这些更改是对参数所引用的实例所做的更改:

public class PassTypesByValue
{
    public static void Mutate(Point pt)
    {
        Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
        pt.X = 19;
        pt.Y = 23;

        Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
    }
    public static void Mutate(Point3D pt)
    {
        Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
        pt.X = 19;
        pt.Y = 23;
        pt.Z = 42;

        Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
    }

    public static void TestPassTypesByValue()
    {
        Console.WriteLine("===== Value Types =====");

        var ptStruct = new Point { X = 1, Y = 2 };
        Console.WriteLine($"After initialization:\t\t{ptStruct}");

        Mutate(ptStruct);

        Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptStruct}");

        Console.WriteLine("===== Reference Types =====");

        var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };

        Console.WriteLine($"After initialization:\t\t{ptClass}");

        Mutate(ptClass);
        Console.WriteLine($"After called {nameof(Mutate)}:\t\t{ptClass}");

        // Output:
        // ===== Value Types =====
        // After initialization:           Point { X = 1, Y = 2 }
        //         Enter Mutate:           Point { X = 1, Y = 2 }
        //         Exit Mutate:            Point { X = 19, Y = 23 }
        // After called Mutate:            Point { X = 1, Y = 2 }
        // ===== Reference Types =====
        // After initialization:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Enter Mutate:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Exit Mutate:            Point3D { X = 19, Y = 23, Z = 42 }
        // After called Mutate:            Point3D { X = 19, Y = 23, Z = 42 }
    }
}

修饰符是一种通过引用 将参数 传递给方法的方法。 以下代码遵循前面的示例,但按引用传递参数。 通过引用传递结构时,对 struct 类型的修改在调用方法中可见。 引用类型通过引用传递时没有语义变化。

public class PassTypesByReference
{
    public static void Mutate(ref Point pt)
    {
        Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
        pt.X = 19;
        pt.Y = 23;

        Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
    }
    public static void Mutate(ref Point3D pt)
    {
        Console.WriteLine($"\tEnter {nameof(Mutate)}:\t\t{pt}");
        pt.X = 19;
        pt.Y = 23;
        pt.Z = 42;

        Console.WriteLine($"\tExit {nameof(Mutate)}:\t\t{pt}");
    }

    public static void TestPassTypesByReference()
    {
        Console.WriteLine("===== Value Types =====");

        var pStruct = new Point { X = 1, Y = 2 };
        Console.WriteLine($"After initialization:\t\t{pStruct}");

        Mutate(ref pStruct);

        Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pStruct}");

        Console.WriteLine("===== Reference Types =====");

        var pClass = new Point3D { X = 1, Y = 2, Z = 3 };

        Console.WriteLine($"After initialization:\t\t{pClass}");

        Mutate(ref pClass);
        Console.WriteLine($"After called {nameof(Mutate)}:\t\t{pClass}");

        // Output:
        // ===== Value Types =====
        // After initialization:           Point { X = 1, Y = 2 }
        //         Enter Mutate:           Point { X = 1, Y = 2 }
        //         Exit Mutate:            Point { X = 19, Y = 23 }
        // After called Mutate:            Point { X = 19, Y = 23 }
        // ===== Reference Types =====
        // After initialization:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Enter Mutate:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Exit Mutate:            Point3D { X = 19, Y = 23, Z = 42 }
        // After called Mutate:            Point3D { X = 19, Y = 23, Z = 42 }
    }
}

前面的示例修改了参数的属性。 方法还可以将参数重新分配给新值。 对于按值或按引用传递的结构体和类类型,再分配的行为有所不同。 以下示例演示了重新分配值传递的参数时结构类型和类类型的行为方式:

public class PassByValueReassignment
{
    public static void Reassign(Point pt)
    {
        Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
        pt = new Point { X = 13, Y = 29 };

        Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
    }

    public static void Reassign(Point3D pt)
    {
        Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
        pt = new Point3D { X = 13, Y = 29, Z = -42 };

        Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
    }

    public static void TestPassByValueReassignment()
    {
        Console.WriteLine("===== Value Types =====");

        var ptStruct = new Point { X = 1, Y = 2 };
        Console.WriteLine($"After initialization:\t\t{ptStruct}");

        Reassign(ptStruct);

        Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");

        Console.WriteLine("===== Reference Types =====");

        var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };

        Console.WriteLine($"After initialization:\t\t{ptClass}");

        Reassign(ptClass);
        Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");

        // Output:
        // ===== Value Types =====
        // After initialization:           Point { X = 1, Y = 2 }
        //         Enter Reassign:         Point { X = 1, Y = 2 }
        //         Exit Reassign:          Point { X = 13, Y = 29 }
        // After called Reassign:          Point { X = 1, Y = 2 }
        // ===== Reference Types =====
        // After initialization:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Enter Reassign:         Point3D { X = 1, Y = 2, Z = 3 }
        //         Exit Reassign:          Point3D { X = 13, Y = 29, Z = -42 }
        // After called Reassign:          Point3D { X = 1, Y = 2, Z = 3 }
    }
}

前面的示例显示,将参数重新分配给新值时,无论类型是值类型还是引用类型,该更改都不可见于调用方法。 以下示例显示了重新分配通过引用传递的参数时的行为:

public class PassByReferenceReassignment
{
    public static void Reassign(ref Point pt)
    {
        Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
        pt = new Point { X = 13, Y = 29 };

        Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
    }

    public static void Reassign(ref Point3D pt)
    {
        Console.WriteLine($"\tEnter {nameof(Reassign)}:\t\t{pt}");
        pt = new Point3D { X = 13, Y = 29, Z = -42 };

        Console.WriteLine($"\tExit {nameof(Reassign)}:\t\t{pt}");
    }

    public static void TestPassByReferenceReassignment()
    {
        Console.WriteLine("===== Value Types =====");

        var ptStruct = new Point { X = 1, Y = 2 };
        Console.WriteLine($"After initialization:\t\t{ptStruct}");

        Reassign(ref ptStruct);

        Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptStruct}");

        Console.WriteLine("===== Reference Types =====");

        var ptClass = new Point3D { X = 1, Y = 2, Z = 3 };

        Console.WriteLine($"After initialization:\t\t{ptClass}");

        Reassign(ref ptClass);
        Console.WriteLine($"After called {nameof(Reassign)}:\t\t{ptClass}");

        // Output:
        // ===== Value Types =====
        // After initialization:           Point { X = 1, Y = 2 }
        //         Enter Reassign:         Point { X = 1, Y = 2 }
        //         Exit Reassign:          Point { X = 13, Y = 29 }
        // After called Reassign:          Point { X = 13, Y = 29 }
        // ===== Reference Types =====
        // After initialization:           Point3D { X = 1, Y = 2, Z = 3 }
        //         Enter Reassign:         Point3D { X = 1, Y = 2, Z = 3 }
        //         Exit Reassign:          Point3D { X = 13, Y = 29, Z = -42 }
        // After called Reassign:          Point3D { X = 13, Y = 29, Z = -42 }
    }
}

前面的示例演示如何在调用上下文中重新分配由引用传递的参数的值。

引用和值的安全上下文

方法可以将参数的值存储在字段中。 当参数按值传递时,这通常是安全的。 值会进行复制,并且当引用类型存储在字段中时,是可以访问的。 为了安全地按引用传递参数,需要编译器定义何时可以安全地将引用分配给新变量。 对于每个表达式,编译器都会定义安全上下文来限制对表达式或变量的访问。 编译器使用两个范围:safe-contextref-safe-context

  • safe-context 定义可以安全地访问任何表达式的范围。
  • ref-safe-context 定义可以安全地访问或修改对任何表达式的引用的范围。

在非正式情况下,可以将这些范围视为机制,以确保代码永远不会访问或修改不再有效的引用。 只要一个引用指向的是有效的对象或结构,它就有效。 safe-context 定义何时可以对变量赋值或重新赋值。 ref-safe-context 定义何时可以对 ref 赋值对 ref 重新赋值。 赋值操作会为变量赋一个新值;ref 赋值操作会为变量赋值以引用其他存储位置。

引用参数

将以下修饰符之一应用于参数声明,以按引用而不是按值传递参数:

  • ref:在调用方法之前必须初始化参数。 该方法可以将新值赋给参数,但不需要这样做。
  • out:该调用方法在调用方法之前不需要初始化参数。 该方法必须向参数赋值。
  • ref readonly:在调用方法之前必须初始化参数。 该方法无法向参数赋新值。
  • in:在调用方法之前必须初始化参数。 该方法无法向参数赋新值。 编译器可能会创建一个临时变量来保存 in 参数的自变量副本。

由引用传递的参数是 引用变量。 它没有自己的价值。 相反,它指的是一个不同的变量,称为其 引用。 引用变量可以重新赋值,这会更改它的引用对象。

类的成员不能具有仅在 refref readonlyinout 方面不同的签名。 如果类型的两个成员之间的唯一区别在于其中一个具有 ref 参数,而另一个具有 outref readonlyin 参数,则会发生编译器错误。 但是,当一个方法具有 refref readonlyinout 参数,另一个方法具有值传递的参数时,则可以重载方法,如下面的示例所示。 在其他要求签名匹配的情况下(如隐藏或重写),inrefref readonlyout 是签名的一部分,相互之间不匹配。

当某个参数具有上述修饰符之一时,相应的自变量可以具有兼容的修饰符:

  • 参数 ref 的自变量必须包含 ref 修饰符。
  • 参数 out 的自变量必须包含 out 修饰符。
  • 参数 in 的自变量可以选择性包含 in 修饰符。 如果 ref 修饰符用于自变量,编译器会发出警告。
  • ref readonly 参数的自变量应包含 inref 修饰符,但不能包含两者。 如果两个修饰符均未包含,编译器会发出警告。

使用这些修饰符时,它们描述如何使用自变量:

  • ref 表示该方法可以读取或写入自变量的值。
  • out 表示该方法设置自变量的值。
  • ref readonly 表示该方法可以读取但无法写入自变量的值。 自变量按引用传递。
  • in 表示该方法可以读取但无法写入自变量的值。 自变量将按引用或通过临时变量传递。

不能在以下类型的方法中使用以前的参数修饰符:

  • 异步方法,通过使用 async 修饰符定义。
  • 迭代器方法,包括 yield returnyield break 语句。

扩展方法还限制使用以下自变量关键字:

  • 不能对扩展方法的第一个参数使用 out 关键字。
  • 当自变量不是 ref 或是不被约束为结构的泛型类型时,不能对扩展方法的第一个自变量使用 struct 关键字。
  • 除非第一个自变量是 ref readonly ,否则无法使用 instruct关键字。
  • 即使约束为结构,也不能对任何泛型类型使用 ref readonlyin 关键字。

属性不是变量。 它们是方法。 属性不能是 ref 参数的自变量。

ref 参数修饰符

若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字,如下面的示例所示。 (除了在进行 COM 调用时,调用方法可忽略 ref。)

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

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

传递到 ref 参数的自变量必须先经过初始化,然后才能传递。

out 参数修饰符

若要使用 out 参数,方法定义和调用方法均必须显式使用 out 关键字。 例如:

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

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

作为 out 自变量传递的变量在方法调用中传递之前不必进行初始化。 但是,被调用的方法需要在返回之前赋一个值。

析构方法使用 out 修饰符声明其参数以返回多个值。 其他方法可以为多个返回值返回值元组

必须先在单独的语句中声明变量,然后才能将其作为 out 参数传递。 还可以在方法调用的参数列表而不是单独的变量声明中声明 out 变量。 out 变量使代码更简洁可读,还能防止在方法调用之前无意中向该变量赋值。 以下示例在调用 number 方法时定义 变量。

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

还可以声明隐式类型的局部变量。

ref readonly 修饰符

方法声明中必须存在 ref readonly 修饰符。 呼叫站点的修饰符是可选的。 可以使用 inref 修饰符。 ref readonly 修饰符在调用站点上无效。 在调用站点中使用的修饰符有助于描述自变量的特征。 仅当自变量为变量且可写时,才能使用 ref。 仅当自变量为变量时,才能使用 in。 它可能可写或只读。 如果自变量不是变量,而是表达式,则不能添加任一修饰符。 以下示例显示了这些情况。 以下方法使用 ref readonly 修饰符指示,出于性能原因,应按引用传递大型结构:

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

可以使用 refin 修饰符调用该方法。 如果省略修饰符,编译器会发出警告。 当自变量是表达式而不是变量时,不能添加 inref 修饰符,因此应禁止显示警告:

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

如果变量是 readonly 变量,则必须使用 in 修饰符。 如果改用 ref 修饰符,编译器将发出错误。

ref readonly 修饰符指示该方法期望自变量是变量,而非不是变量的表达式。 不是变量的表达式示例包括常量、方法返回值和属性。 如果自变量不是变量,编译器会发出警告。

in 参数修饰符

方法声明中需要 in 修饰符,但在调用站点中不需要。

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;
}

in 修饰符允许编译器为自变量创建一个临时变量,并传递对该自变量的只读引用。 当必须转换自变量、从自变量类型进行隐式转换或自变量为不是变量的值时,编译器始终会创建一个临时变量。 例如,当参数是文本值或从属性访问器返回的值时。 当 API 要求按引用传递参数时,请选择 ref readonly 修饰符而不是 in 修饰符。

使用 in 参数定义的方法可能会获得性能优化。 某些 struct 类型参数可能很大,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本很高。 方法声明 in 参数以指定参数可能按引用安全传递,因为所调用的方法不修改该参数的状态。 按引用传递这些参数可以避免(可能产生的)高昂的复制成本。 在调用站点显式添加 in 修饰符以确保参数是按引用传递,而不是按值传递。 显式使用 in 有以下两个效果:

  • 在调用站点指定 in 会强制编译器选择使用匹配的 in 参数定义的方法。 否则,如果两种方法唯一的区别在于是否存在 in,则按值重载的匹配度会更高。
  • 指定 in 会声明你想按引用传递自变量。 结合 in 使用的参数必须代表一个可以直接引用的位置。 outref 自变量的相同常规规则适用:不能使用常数、普通属性或其他生成值的表达式。 否则,在调用站点省略 in 就会通知编译器你可以创建临时变量,并按只读引用传递至方法。 编译器创建临时变量以克服一些 in 参数的限制:
    • 临时变量允许将编译时常数作为 in 参数。
    • 临时变量允许使用属性或 in 参数的其他表达式。
    • 存在从自变量类型到参数类型的隐式转换时,临时变量允许使用自变量。

在前面的所有实例中,编译器创建了临时变量,用于存储常数、属性或其他表达式的值。

以下代码阐释了这些规则:

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`

现在,假设可以使用另一种使用按值自变量的方法。 结果的变化如以下代码所示:

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`

最后一个是按引用传递参数的唯一方法调用。

注意

为了简化操作,前面的代码将 int 用作参数类型。 因为大多数新式计算机中的引用都比 int 大,所以将单个 int 作为只读引用传递没有任何好处。

params 修饰符

在方法声明中的 params 关键字之后不允许有任何其他参数,并且在方法声明中只允许有一个 params 关键字。

params 参数的声明类型必须是集合类型。 识别的集合类型包括:

在 C# 13 之前,参数必须是一维数组。

使用 params 参数调用方法时,可以传入:

  • 数组元素类型的参数的逗号分隔列表。
  • 指定类型的参数的集合。
  • 无参数。 如果未发送任何参数,则 params 列表的长度为零。

下面的示例演示可向 params 形参发送实参的各种方法。

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[]
*/

当参数的参数 params 为集合类型时,重载解析可能会导致歧义。 参数的集合类型必须可转换为参数的集合类型。 当不同的重载为该参数提供更好的转换时,该方法可能更好。 但是,如果参数的参数 params 是离散元素或缺失,则具有不同 params 参数类型的所有重载都等于该参数。

有关详细信息,请参阅 C# 语言规范参数列表 部分。 该语言规范是 C# 语法和用法的权威资料。