方法参数(C# 参考)

在 C# 中,实参可以按值或按引用传递给形参。 请注意,C# 类型可以是引用类型 (class),也可以是值类型 (struct):

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

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

类实例是引用类型,不是值类型。 按值将引用类型传递给方法时,方法接收对类实例的引用的副本。 也就是说,被调用的方法接收实例的地址副本,而调用方法保留实例的原始地址。 调用方法中的类实例具有地址,所调用的方法中的参数具有地址副本,且这两个地址引用同一个对象。 由于参数只包含地址副本,因此所调用的方法不能更改调用方法中类实例的地址。 但是,被调用的方法可以使用该地址的副本来访问原始地址和地址副本引用的类成员。 如果所调用的方法更改了类成员,则调用方法中的原始类实例也会更改。

以下示例输出对差异进行了说明。 调用 ClassTaker 方法更改了类实例的 willIChange 字段的值,因为该方法使用参数中的地址来查找类实例的指定字段。 调用 StructTaker 方法不会更改调用方法中结构的 willIChange 字段,因为该参数的值是结构本身的副本,而不是其地址的副本。 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);

        // Keep the console window open in debug mode.
        Console.WriteLine("Press any key to exit.");
        Console.ReadKey();
    }
}
/* Output:
    Class field = Changed
    Struct field = Not Changed
*/

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

按值传递值类型

按值传递值类型时:

  • 如果方法分配参数以引用其他对象,则这些更改在调用方是不可见的。
  • 如果方法修改参数所引用对象的状态,则这些更改在调用方是不可见的。

下面的示例演示按值传递值-类型参数。 变量 n 按值传递给方法 SquareIt。 在方法内发生的任何更改都不会影响该变量的原始值。

int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(n);  // Passing the variable by value.
System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();

static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
    x *= x;
    System.Console.WriteLine("The value inside the method: {0}", x);
}
/* Output:
    The value before calling the method: 5
    The value inside the method: 25
    The value after calling the method: 5
*/

变量 n 是值类型。 它包含其数据,值为 5。 调用 SquareIt 时,n 的内容复制到参数 x 中,这是该方法内的平方值。 但是在 Main 中,n 的值在调用 SquareIt 方法之后与之前相同。 在方法内发生的更改只影响本地变量 x

按引用传递值类型

按引用传递值类型时:

  • 如果方法分配参数以引用其他对象,则这些更改在调用方是不可见的。
  • 如果方法修改参数所引用对象的状态,则这些更改在调用方是不可见的。

下面的示例与上述示例相同,除了自变量是作为 ref 参数传递的。 x 在方法中更改时,基础自变量的值 n 也更改。

int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);

SquareIt(ref n);  // Passing the variable by reference.
System.Console.WriteLine("The value after calling the method: {0}", n);

// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();

static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
    x *= x;
    System.Console.WriteLine("The value inside the method: {0}", x);
}
/* Output:
    The value before calling the method: 5
    The value inside the method: 25
    The value after calling the method: 25
*/

在此示例中,它传递的不是 n 的值;而是传递 n 的引用。 参数 x 不是 int;它是对 int 的引用,在这种情况下,是对 n 的引用。 因此,当 x 在方法中进行平方计算时,实际求平方值的就是 x 所指的 n

按值传递引用类型

按值传递引用类型时:

  • 如果方法分配参数以引用其他对象,则这些更改在调用方是不可见的。
  • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。

以下示例演示将引用类型参数 arr 按值传递给方法 Change。 由于该参数是对 arr 的引用,因此可以更改数组元素的值。 但是,只有在方法内才能将参数重新分配给其他内存位置,且不会影响原始变量 arr

int[] arr = { 1, 4, 5 };
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);

Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);

static void Change(int[] pArray)
{
    pArray[0] = 888;  // This change affects the original element.
    pArray = new int[5] { -3, -1, -2, -3, -4 };   // This change is local.
    System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
/* Output:
    Inside Main, before calling the method, the first element is: 1
    Inside the method, the first element is: -3
    Inside Main, after calling the method, the first element is: 888
*/

在前面的示例中,数组 arr 属于引用类型,传递给不含 ref 参数的方法。 在这种情况下,将向该方法传递一个指向 arr 的引用副本。 输出表明,此方法可以更改数组元素的内容,在本示例中将 1 更改为了 888。 但是,通过在 Change 方法内使用 new 运算符来分配一份新的内存可使变量 pArray 引用新的数组。 因此,此后的任意更改都不会影响创建于 Main 内的原始数组 arr。 实际上,本示例创建了两个数组,一个在 Main 方法内,另一个在 Change 方法内。

按引用传递引用类型

按引用传递引用类型时:

  • 如果方法分配参数以引用其他对象,则这些更改在调用方是可见的。
  • 如果方法修改参数所引用对象的状态,则这些更改在调用方是可见的。

除了 ref 关键字添加到方法标头和调用,以下示例与上述示例相同。 方法中所作的任何更改都会影响调用程序中的原始变量。

int[] arr = { 1, 4, 5 };
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);

Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);

static void Change(ref int[] pArray)
{
    // Both of the following changes will affect the original variables:
    pArray[0] = 888;
    pArray = new int[5] { -3, -1, -2, -3, -4 };
    System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
/* Output:
    Inside Main, before calling the method, the first element is: 1
    Inside the method, the first element is: -3
    Inside Main, after calling the method, the first element is: -3
*/

方法内所作的所有更改都将影响 Main 中的原始数组。 事实上,将使用 new 运算符重新分配原始数组。 因此,调用 Change 方法后,对 arr 的任何引用都将指向 Change 方法中创建的五元素数组。

引用和值的范围

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

  • safe_to_escape 范围定义了可以安全地访问任何表达式的范围。
  • ref_safe_to_escape 范围定义了可以安全地访问或修改对任何表达式的引用的范围

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

修改键

为不具有 inrefout 的方法声明的参数会按值传递给调用的方法。 refinout 修饰符在分配规则方面有所不同:

  • 必须明确分配 ref 参数的自变量。 被调用的方法可以重新分配该参数。
  • 必须明确分配 in 参数的自变量。 被调用的方法无法重新分配该参数。
  • 无需明确分配 out 参数的自变量。 被调用的方法必须分配该参数。

本部分介绍声明方法参数时可以使用的关键字:

  • params 指定此参数采用可变数量的参数。
  • in 指定此参数由引用传递,但只由调用方法读取。
  • ref 指定此参数由引用传递,可能由调用方法读取或写入。
  • out 指定此参数由引用传递,由调用方法写入。

请参阅