方法参数

默认情况下,C# 中的参数按值传递给函数。 这意味着将变量的副本会传递到方法。 对于值 (struct) 类型,的副本将传递到方法。 对于引用 (class) 类型,引用的副本将传递到方法。 参数修饰符可让你按引用传递参数。 以下概念可帮助你了解这些区别以及如何使用参数修饰符:

  • 按值传递就是将变量副本传递给方法。
  • 按引用传递就是将对变量的访问传递给方法。
  • 引用类型的变量包含对其数据的引用。
  • 值类型的变量直接包含其数据。

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

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

以下示例输出对差异进行了说明。 方法 ClassTaker 更改 willIChange 字段的值,因为该方法使用参数中的地址查找类实例的指定字段。 调用方法中结构的 willIChange 字段不会更改调用 StructTaker,因为参数的值是结构本身的副本,而不是其地址的副本。 StructTaker 更改副本,对 StructTaker 的调用完成时,副本丢失。

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

形参类型和实参模式的组合

一个自变量是如何传递的,以及它是引用类型还是值类型,控制着对自变量的哪些修改在调用方是可见的:

  • 按值传递值类型时
    • 如果方法分配参数以引用其他对象,则这些更改在调用方是不可见的。
    • 如果方法修改参数所引用对象的状态,则这些更改在调用方是不可见的。
  • 按值传递引用类型时
    • 如果方法分配参数以引用其他对象,则这些更改在调用方是不可见的。
    • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。
  • 按引用传递值类型时
    • 如果方法分配参数来引用其他对象 ref =,则这些更改 在调用方中不 可见。
    • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。
  • 按引用传递引用类型时
    • 如果方法分配参数以引用其他对象,则这些更改在调用方是可见的。
    • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。

按引用传递引用类型使所调用方能够替换调用方中引用参数引用的对象。 对象的存储位置按引用参数的值传递到方法。 如果更改参数存储位置中的值(以指向新对象),你还可以将存储位置更改为调用方所引用的位置。 下面的示例将引用类型的实例作为 ref 参数传递。

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-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 关键字。
  • 当自变量不是 struct 或是不被约束为结构的泛型类型时,不能对扩展方法的第一个自变量使用 ref 关键字。
  • 除非第一个自变量是 struct ,否则无法使用 ref readonlyin关键字。
  • 即使约束为结构,也不能对任何泛型类型使用 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 变量使代码更简洁可读,还能防止在方法调用之前无意中向该变量赋值。 以下示例在调用 Int32.TryParse 方法时定义 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[]
*/