默认情况下,C# 中的参数按值传递给函数。这意味着将变量的副本会传递到方法。对于值(struct)类型,值的副本将传递到方法。对于引用(class)类型,引用的副本将传递到方法。参数修饰符可让你按引用传递参数。
因为 struct 是值类型,所以按值将结构传递给方法时,该方法接收结构参数的副本并在其上运行。该方法无法访问调用方法中的原始结构,因此无法对其进行任何更改。它只能更改副本。
class 实例是引用类型,而非值类型。当引用类型通过值传递给方法时,该方法将接收对实例的引用的副本。这两个变量都引用同一对象。参数是引用的副本。调用的方法无法在调用方法中重新分配实例。但是,调用的方法可以使用引用的副本来访问实例成员。如果调用的方法更改实例成员,调用方法也会看到这些更改,因为它引用同一实例。
按值传递并按引用传递
本节中的所有示例都使用以下两种 record 类型来说明 class 类型和 struct 类型之间的差异:
public record struct Dian2 ( int x , int y );
public record class Dian3
{
public int x
{
get;
set;
}
public int y
{
get;
set;
}
public int z
{
get;
set;
}
}
以下示例的输出说明了按值传递结构类型与按值传递类类型之间的差异。这两个 FF变形 方法更改其参数的属性值。当参数是 struct 类型时,这些更改是对参数数据的副本进行的。当参数是 class 类型时,这些更改是对参数所引用的实例所做的更改:
public class Lei传递值类型
{
public static void FF变形 ( D2 点 )
{
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之前:\t\t{点}" );
点 . X = 19;
点 . Y = 23;
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之后:\t\t{点}" );
}
public static void FF变形 ( D3 点 )
{
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之前:\t\t{点}" );
点 . X = 19;
点 . Y = 23;
点 . Z = 42;
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之后:\t\t{点}" );
}
}
static void Main ( string [ ] args )
{
Console . WriteLine ( "===== 值类型 =====" );
D2 D = new ( X : 1 , Y : 2 );
Console . WriteLine ( $"初始化后:\t\t{D}" );
Lei传递值类型 . FF变形 ( D );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF变形 )} 之后:\t\t{D}" );
Console . WriteLine ( "===== 引用类型 =====" );
var d3 = new D3 { X = 1 , Y = 2 , Z = 3 };
Console . WriteLine ( $"初始化后:\t\t{d3}" );
Lei传递值类型 . FF变形 ( d3 );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF变形 )} 之后:\t\t{d3}" );
}
上例输出:
===== 值类型 =====
初始化后: D2 { X = 1, Y = 2 }
FF变形 之前: D2 { X = 1, Y = 2 }
FF变形 之后: D2 { X = 19, Y = 23 }
调用 FF变形 之后: D2 { X = 1, Y = 2 }
===== 引用类型 =====
初始化后: D3 { X = 1, Y = 2, Z = 3 }
FF变形 之前: D3 { X = 1, Y = 2, Z = 3 }
FF变形 之后: D3 { X = 19, Y = 23, Z = 42 }
调用 FF变形 之后: D3 { X = 19, Y = 23, Z = 42 }
修饰符是一种将参数通过引用传递给方法的方法。以下代码遵循前面的示例,但按引用传递参数。通过引用传递结构时,对 struct 类型的修改在调用方法中可见。引用类型通过引用传递时没有语义变化。
public class Lei传递值类型
{
public static void FF变形 ( ref D2 点 )
{
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之前:\t\t{点}" );
点 . X = 19;
点 . Y = 23;
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之后:\t\t{点}" );
}
public static void FF变形 ( ref D3 点 )
{
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之前:\t\t{点}" );
点 . X = 19;
点 . Y = 23;
点 . Z = 42;
Console . WriteLine ( $"\t{nameof ( FF变形 )} 之后:\t\t{点}" );
}
}
static void Main ( string [ ] args )
{
Console . WriteLine ( "===== 值类型 =====" );
D2 D = new ( X : 1 , Y : 2 );
Console . WriteLine ( $"初始化后:\t\t{D}" );
Lei传递值类型 . FF变形 ( ref D );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF变形 )} 之后:\t\t{D}" );
Console . WriteLine ( "===== 引用类型 =====" );
var d3 = new D3 { X = 1 , Y = 2 , Z = 3 };
Console . WriteLine ( $"初始化后:\t\t{d3}" );
Lei传递值类型 . FF变形 ( ref d3 );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF变形 )} 之后:\t\t{d3}" );
}
上例输出:
===== 值类型 =====
初始化后: D2 { X = 1, Y = 2 }
FF变形 之前: D2 { X = 1, Y = 2 }
FF变形 之后: D2 { X = 19, Y = 23 }
调用 FF变形 之后: D2 { X = 19, Y = 23 }
===== 引用类型 =====
初始化后: D3 { X = 1, Y = 2, Z = 3 }
FF变形 之前: D3 { X = 1, Y = 2, Z = 3 }
FF变形 之后: D3 { X = 19, Y = 23, Z = 42 }
调用 FF变形 之后: D3 { X = 19, Y = 23, Z = 42 }
前面的示例修改了参数的属性。方法还可以将参数重新分配给新值。对于按值或按引用传递的结构体和类类型,再分配的行为有所不同。以下示例演示了重新分配值传递的参数时结构类型和类类型的行为方式:
public class Lei传递值类型
{
public static void FF新的 ( D2 点 )
{
Console . WriteLine ( $"\t{nameof ( FF新的 )} 之前:\t\t{点}" );
点 = new ( X : 13 , Y : 29 );
Console . WriteLine ( $"\t{nameof ( FF新的 )} 之后:\t\t{点}" );
}
public static void FF新的 ( D3 点 )
{
Console . WriteLine ( $"\t{nameof ( FF新的 )} 之前:\t\t{点}" );
点 = new D3 { X = 19 , Y = 23 , Z = -42 };
Console . WriteLine ( $"\t{nameof ( FF新的 )} 之后:\t\t{点}" );
}
}
Console . WriteLine ( "===== 值类型 =====" );
D2 D = new ( X : 1 , Y : 2 );
Console . WriteLine ( $"初始化后:\t\t{D}" );
Lei传递值类型 . FF新的 ( D );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF新的 )} 之后:\t\t{D}" );
Console . WriteLine ( "===== 引用类型 =====" );
var d3 = new D3 { X = 1 , Y = 2 , Z = 3 };
Console . WriteLine ( $"初始化后:\t\t{d3}" );
Lei传递值类型 . FF新的 ( d3 );
Console . WriteLine ( $"调用 {nameof ( Lei传递值类型 . FF新的 )} 之后:\t\t{d3}" );
上例输出:
===== 值类型 =====
初始化后: D2 { X = 1, Y = 2 }
FF新的 之前: D2 { X = 1, Y = 2 }
FF新的 之后: D2 { X = 13, Y = 29 }
调用 FF新的 之后: D2 { X = 1, Y = 2 }
===== 引用类型 =====
初始化后: D3 { X = 1, Y = 2, Z = 3 }
FF新的 之前: D3 { X = 1, Y = 2, Z = 3 }
FF新的 之后: D3 { X = 19, Y = 23, Z = -42 }
调用 FF新的 之后: D3 { X = 1, Y = 2, Z = 3 }
前面的示例显示,将参数重新分配给新值时,无论类型是值类型还是引用类型,该更改都不可见于调用方法。若将上例中的 FF新的 参数改为 ref 引用:
===== 值类型 =====
初始化后: D2 { X = 1, Y = 2 }
FF新的 之前: D2 { X = 1, Y = 2 }
FF新的 之后: D2 { X = 13, Y = 29 }
调用 FF新的 之后: D2 { X = 13, Y = 29 }
===== 引用类型 =====
初始化后: D3 { X = 1, Y = 2, Z = 3 }
FF新的 之前: D3 { X = 1, Y = 2, Z = 3 }
FF新的 之后: D3 { X = 19, Y = 23, Z = -42 }
调用 FF新的 之后: D3 { X = 19, Y = 23, Z = -42 }
前面的示例演示如何在调用上下文中重新分配由引用传递的参数的值。
引用和值的安全上下文
方法可以将参数的值存储在字段中。当参数按值传递时,这通常是安全的。值会进行复制,并且当引用类型存储在字段中时,是可以访问的。为了安全地按引用传递参数,需要编译器定义何时可以安全地将引用分配给新变量。对于每个表达式,编译器都会定义安全上下文来限制对表达式或变量的访问。编译器使用两个范围:safe-context 和 ref-safe-context。
- safe-context 定义可以安全地访问任何表达式的范围。
- ref-safe-context 定义可以安全地访问或修改对任何表达式的引用的范围。
在非正式情况下,可以将这些范围视为机制,以确保代码永远不会访问或修改不再有效的引用。只要一个引用指向的是有效的对象或结构,它就有效。safe-context 定义何时可以对变量赋值或重新赋值。ref-safe-context 定义何时可以对 ref 赋值或对 ref 重新赋值。赋值操作会为变量赋一个新值;ref 赋值操作会为变量赋值以引用其他存储位置。
引用参数
将以下修饰符之一应用于参数声明,以按引用而不是按值传递参数:
- ref:在调用方法之前必须初始化参数。该方法可以将新值赋给参数,但不需要这样做。
- out:该调用方法在调用方法之前不需要初始化参数。该方法必须向参数赋值。
- ref readonly:在调用方法之前必须初始化参数。该方法无法向参数赋新值。
- in:在调用方法之前必须初始化参数。该方法无法向参数赋新值。编译器可能会创建一个临时变量来保存 in 参数的自变量副本。
由引用传递的参数是引用变量。它没有自己的价值。相反,它指的是一个不同的变量,称为引用。 引用变量可以重新赋值,这会更改它的引用对象。
类的成员不能具有仅在 ref、ref readonly、in 或 out 方面不同的签名。如果类型的两个成员之间的唯一区别在于其中一个具有 ref 参数,而另一个具有 out、ref readonly 或 in 参数,则会发生编译器错误。但是,当一个方法具有 ref、ref readonly、in 或 out 参数,另一个方法具有值传递的参数时,则可以重载方法,如下面的示例所示。在其他要求签名匹配的情况下(如隐藏或重写),in、ref、ref readonly 和 out 是签名的一部分,相互之间不匹配。
当某个参数具有上述修饰符之一时,相应的自变量可以具有兼容的修饰符:
- 参数 ref 的自变量必须包含 ref 修饰符。
- 参数 out 的自变量必须包含 out 修饰符。
- 参数 in 的自变量可以选择性包含 in 修饰符。如果 ref 修饰符用于自变量,编译器会发出警告。
- ref readonly 参数的自变量应包含 in 或 ref 修饰符,但不能包含两者。如果两个修饰符均未包含,编译器会发出警告。
使用这些修饰符时,它们描述如何使用自变量:
- ref 表示该方法可以读取或写入自变量的值。
- out 表示该方法设置自变量的值。
- ref readonly 表示该方法可以读取但无法写入自变量的值。自变量应按引用传递。
- in 表示该方法可以读取但无法写入自变量的值。自变量将按引用或通过临时变量传递。
不能在以下类型的方法中使用以前的参数修饰符:
- 异步方法,通过使用 async 修饰符定义。
- 迭代器方法,包括 yield return 或 yield break 语句。
扩展方法还限制使用以下自变量关键字:
- 不能对扩展方法的第一个参数使用 out 关键字。
- 当自变量不是 ref 或是不被约束为结构的泛型类型时,不能对扩展方法的第一个自变量使用 struct 关键字。
- 除非第一个自变量是 ref readonly,否则无法使用 in 和 struct 关键字。
- 即使约束为结构,也不能对任何泛型类型使用 ref readonly 和 in 关键字。
属性不是变量。它们是方法。属性不能是 ref 参数的自变量。
ref 参数修饰符
若要使用 ref 参数,方法定义和调用方法均必须显式使用 ref 关键字,如下面的示例所示(除了在进行 COM 调用时,调用方法可忽略 ref)。
static void Main ( string [ ] args )
{
int z = 1;
FFJ24 ( ref z );
Console . WriteLine ( z );
}
public static void FFJ24 ( ref int 整数 )
{
整数 += 24;
}
上例输出:25
传递到 ref 参数的自变量必须先经过初始化,然后才能传递。
out 参数修饰符
若要使用 out 参数,方法定义和调用方法均必须显式使用 out 关键字。例如:
static void Main ( string [ ] args )
{
FFOut ( out int Z方法内初始化 );
Console . WriteLine ( Z方法内初始化 ); // 输出:120
}
public static void FFOut ( out int 整数 )
{
整数 = 120;
}
作为 out 自变量传递的变量在方法调用中传递之前不必进行初始化。但是,被调用的方法需要在返回之前赋一个值。
析构方法使用 out 修饰符声明其参数以返回多个值。其他方法可以为多个返回值返回值元组。
必须先在单独的语句中声明变量,然后才能将其作为 out 参数传递。还可以在方法调用的参数列表而不是单独的变量声明中声明 out 变量。out 变量使代码更简洁可读,还能防止在方法调用之前无意中向该变量赋值。以下示例在调用 number 方法时定义变量。
string zfc整数 = "1945";
if ( Int32 . TryParse ( zfc整数 , out int z ) )
{
Console . WriteLine ( $"转换‘{zfc整数}’到 {z}" );
}
else
{
Console . WriteLine ( $"无法转换‘{zfc整数}’" );
}
还可以声明隐式类型的局部变量。
ref readonly 修饰符
方法声明中必须存在 ref readonly 修饰符。呼叫站点的修饰符是可选的。可以使用 in 或 ref 修饰符。ref readonly 修饰符在调用站点上无效。在调用站点中使用的修饰符有助于描述自变量的特征。仅当自变量为变量且可写时,才能使用 ref。仅当自变量为变量时,才能使用 in。它可能可写或只读。如果自变量不是变量,而是表达式,则不能添加任一修饰符。以下示例显示了这些情况。以下方法使用 ref readonly 修饰符指示,出于性能原因,应按引用传递大型结构:
static void Main ( string [ ] args )
{
JG大 D = new ( );
FFLref ( in D );
FFLref ( ref D );
FFLref ( D ); // 警告 CS9192:应使用“ref”或“in”关键字传递参数 1
FFLref ( new JG大 ( ) ); // 警告 CS9193:参数 1 应为变量,因为它会传递给“ref readonly”参数
}
public static void FFLref ( ref readonly JG大 D )
{
// ……
}
public struct JG大
{
}
如果变量是 readonly 变量,则必须使用 in 修饰符。如果改用 ref 修饰符,编译器将发出错误。
ref readonly 修饰符指示该方法期望自变量是变量,而非不是变量的表达式。不是变量的表达式示例包括常量、方法返回值和属性。如果自变量不是变量,编译器会发出警告。
in 参数修饰符
方法声明中需要 in 修饰符,但在调用站点中不需要。
static void Main ( string [ ] args )
{
int Z只读 = 64;
FFin参数 ( Z只读 );
Console . WriteLine ( Z只读 ); // 输出 64
}
public static void FFin参数 ( in int 整数 )
{
// 整数 = 19; // 警告 CS8331:无法分配给变量“整数”,或将其用作 ref 分配的右侧,因为它是只读变量
}
in 修饰符允许编译器为自变量创建一个临时变量,并传递对该自变量的只读引用。当必须转换自变量、从自变量类型进行隐式转换或自变量为不是变量的值时,编译器始终会创建一个临时变量。例如,当参数是文本值或从属性访问器返回的值时。当 API 要求按引用传递参数时,请选择 ref readonly 修饰符而不是 in 修饰符。
使用 in 参数定义的方法可能会获得性能优化。某些 struct 类型参数可能很大,在紧凑的循环或关键代码路径中调用方法时,复制这些结构的成本很高。方法声明 in 参数以指定参数可能按引用安全传递,因为所调用的方法不修改该参数的状态。按引用传递这些参数可以避免(可能产生的)高昂的复制成本。在调用站点显式添加 in 修饰符以确保参数是按引用传递,而不是按值传递。显式使用 in 有以下两个效果:
- 在调用站点指定 in 会强制编译器选择使用匹配的 in 参数定义的方法。否则,如果两种方法唯一的区别在于是否存在 in,则按值重载的匹配度会更高。
指定 in 会声明你想按引用传递自变量。结合 in 使用的参数必须代表一个可以直接引用的位置。out 和 ref 自变量的相同常规规则适用:不能使用常数、普通属性或其他生成值的表达式。否则,在调用站点省略 in 就会通知编译器你可以创建临时变量,并按只读引用传递至方法。编译器创建临时变量以克服一些 in 参数的限制:
- 临时变量允许将编译时常数作为 in 参数。
- 临时变量允许使用属性或 in 参数的其他表达式。
- 存在从自变量类型到参数类型的隐式转换时,临时变量允许使用自变量。
在前面的所有实例中,编译器创建了临时变量,用于存储常数、属性或其他表达式的值。
以下代码阐释了这些规则:
static void FF ( in int 参数 )
{
// ……
}
FF ( 5 ); // OK,创建临时变量
FF ( 5L ); // 警告 CS1503:从 long 到 int 没有隐式转换
short d = 0;
FF ( d ); // OK,用值 0 创建临时 int
FF ( in d ); // 警告 CS1503:无法将 short 转换为 int
int z = 42;
FF ( z ); // 通过 readonly 传递引用
FF ( in z ); // 通过 readonly 传递引用,显式使用 ‘in’
现在,假设可以使用另一种使用按值自变量的方法。结果的变化如以下代码所示:
static void FF ( in int 参数 )
{
// ……
}
static void FF ( int 参数 )
{
// ……
}
FF ( 5 ); // 调用通过值传递的重载
FF ( 5L ); // 警告 CS1503:从 long 到 int 没有隐式转换
short d = 0;
FF ( d ); // 调用通过值传递的重载
FF ( in d ); // 警告 CS1503:无法将 short 转换为 in int
int z = 42;
FF ( z ); // 调用通过值传递的重载
FF ( in z ); // 通过 readonly 传递引用,显式使用 ‘in’
最后一个是按引用传递参数的唯一方法调用。
备注:为了简化操作,前面的代码将 int 用作参数类型。因为大多数新式计算机中的引用都比 int 大,所以将单个 int 作为只读引用传递没有任何好处。
params 修饰符
在方法声明中的 params 关键字之后不允许有任何其他参数,并且在方法声明中只允许有一个 params 关键字。
params 参数的声明类型必须是集合类型。 识别的集合类型包括:
- 一维数组类型 T [ ],在这种情况下,元素类型为 T。
范围类型:
- System . Span < T >
- System . ReadOnlySpan < T >
此处,元素类型为 T。
- 具有可访问创建方法及相应元素类型的类型。创建方法使用用于集合表达式的相同属性进行标识。
实现 System . Collections . Generic . IEnumerable < T > 的结构或类类型,其中:
- 类型具有一个构造函数,可以在没有参数的情况下调用,并且该构造函数至少与声明成员一样可访问。
类型具有实例(而不是扩展)的 Add 方法,其中:
- 可以使用单个值参数调用该方法。
- 如果方法是泛型方法,则可以从参数推断类型参数。
- 该方法至少与声明成员一样可访问。此处,元素类型是类型的迭代类型。
接口类型:
- System . Collections . Generic . IEnumerable < T >
- System . Collections . Generic . IReadOnlyCollection < T >
- System . Collections . Generic . IReadOnlyList < T >
- System . Collections . Generic . ICollection < T >
- System . Collections . Generic . IList < T >,此处,元素类型为 T。
在 C# 13 之前,参数必须是一维数组。
使用 params 参数调用方法时,可以传入:
- 数组元素类型的参数的逗号分隔列表。
- 指定类型的参数的集合。
- 无参数。如果未发送任何参数,则 params 列表的长度为零。
下面的示例演示可向 params 形参发送实参的各种方法。
static void Main ( string [ ] args )
{
// 可以发送指定类型的参数列表,以逗号分隔
FFint ( 1 , 2 , 3 , 4 );
FFdx ( 3 , 'a' , "红头巾" );
// params参数接受零个或多个参数
// 下面的调用语句只显示一个空行
FFint ( );
FFdx ( );
// 可以传入数组参数,只要数组类型与被调用方法的参数类型相匹配
int [ ] Ints = { 5 , 6 , 7 , 8 , 9 };
object [ ] Dxs = { 2 , 'b' , "铅笔" , DateTime . Now . Day };
FFint ( Ints );
FFdx ( Dxs );
// 下面的调用会导致编译器错误,因为无法将 object 数组转换为 int 数组
// FFint ( Dxs ); // 警告 CS1503:无法从“object [ ]”转换为“int”
// 下面的调用不会导致错误,但是整个 int 数组成为 params 数组的第一个元素
FFdx ( Ints );
}
public static void FFint ( params int [ ] 整数列表 )
{
for ( int z = 0 ; z < 整数列表 . Length ; z++ )
{
Console . Write ( $"{整数列表 [ z ]} " );
}
Console . WriteLine ( );
}
public static void FFdx ( params object [ ] 对象列表 )
{
for ( int z = 0 ; z < 对象列表 . Length ; z++ )
{
Console . Write ( $"{对象列表 [ z ]} " );
}
Console . WriteLine ( );
}
当 params 参数的参数是集合类型时,重载解析可能会导致歧义。实参的集合类型必须可转换为形参的集合类型。当不同的重载为该参数提供更好的转换时,该方法可能更好。但是,如果 params 参数的参数是离散元素或缺失元素,则该参数的所有具有不同 params 参数类型的重载都是相等的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。