方法参数和修饰符

默认情况下,C# 按值将参数传递给函数。 此方法将变量的副本传递给方法。 对于值 (struct) 类型,该方法获取 的副本。 对于引用类型(class) 类型,该方法获取 引用的副本。 可以使用参数修饰符 按引用传递参数。

由于结构是 一种值类型,因此按值将结构传递给方法会将参数的副本发送到方法。 该方法适用于此副本。 该方法无法访问调用方法中的原始结构,并且无法更改它。 它只能更改副本。

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

C# 语言参考记录了 C# 语言的最新发布版本。 它还包含即将发布的语言版本公共预览版中功能的初始文档。

本文档标识了在语言的最后三个版本或当前公共预览版中首次引入的任何功能。

小窍门

若要查找 C# 中首次引入功能时,请参阅 有关 C# 语言版本历史记录的文章。

按值传递并按引用传递

本节中的所有示例都使用以下两种 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 readonlyrefin一个out参数,则会发生编译器错误。 但是,如果一个方法具有 refref readonlyin或参数out,另一个方法具有按值传递的参数,则可以重载方法,如以下示例所示。 在其他要求签名匹配的情况下(如隐藏或重写),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
}

可以使用或in修饰符调用该方法ref。 如果省略修饰符,编译器会发出警告。 当自变量是表达式而不是变量时,不能添加 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 修饰符,但在调用站点中不需要。

var largeStruct = new LargeStruct { Value1 = 42, Value2 = 3.14, Value3 = "Hello" };

// Using 'in' avoids copying the large struct and prevents modification
ProcessLargeStruct(in largeStruct);
Console.WriteLine($"Original value unchanged: {largeStruct.Value1}");

// Without 'in', the struct would be copied (less efficient for large structs)
ProcessLargeStructByValue(largeStruct);
Console.WriteLine($"Original value still unchanged: {largeStruct.Value1}");

void ProcessLargeStruct(in LargeStruct data)
{
    // Can read the values
    Console.WriteLine($"Processing: {data.Value1}, {data.Value2}, {data.Value3}");
    
    // Uncomment the following line to see error CS8331
    // data.Value1 = 99; // Compilation error: cannot assign to 'in' parameter
}

void ProcessLargeStructByValue(LargeStruct data)
{
    // This method receives a copy of the struct
    Console.WriteLine($"Processing copy: {data.Value1}, {data.Value2}, {data.Value3}");
    
    // Modifying the copy doesn't affect the original
    data.Value1 = 99;
}

修饰 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# 语法和用法的权威资料。