目录
Ref(引用参数)
ref 可以将参数声明为引用参数,被声明为引用参数的形参的参数名将作为实参变量的别名,指向相同的内存位置。
使用 ref 需要注意以下几点:
- 使用引用参数时,必须在方法的声明和调用中都使用ref修饰符
- 实参必须是变量,在用作实参前必须被赋值。如果是引用类型变量,可以赋值为一个引用或null
为什么实参必须是变量:因为形参作为实参的别名需要内存空间,如果传入参数的是一个表达式那么形参的指向不知道指向哪里因为实参是表达式不会分配内存。
class MyClass
{
public int Val = 20;
}
internal class Program
{
static void Method(ref MyClass f1,ref int f2)
{
f1.Val = f1.Val + 5;
f2 = f2 + 5;
Console.WriteLine($"f1.Val:{f1.Val};f2:{f2}"); //f1.Val:25;f2:15
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
int a2 = 10;
Method(ref a1,ref a2);
Console.WriteLine($"f1.Val:{a1.Val};f2:{a2}"); //f1.Val:25;f2:15
}
}
- 在方法调用之前,将要被用作实参的变量a1和a2分配在栈上
- 在方法的开始,形参名被设置为实参的别名。变量a1和f1引用相同的内存位置,a2和f2引用相同的内存位置
- 在方法的结束位置,f2和f1的对象的值都被加上了5
- 方法执行完毕,形参的名称失效,但是值类型a2的值和引用类型a1所指向的对象的值都被方法内的行为改变了
引用类型作为值参数和引用参数
当值类型分别作为值参数和引用参数时我们比较好理解。如果值类型作为值参数时传递给形参的只是值类型的副本,在方法中对形参进行修改并不会影响实参的值。如果值类型作为引用参数时传递给形参的就是实参的地址,在方法中对形参进行修改会影响实参的值。
那么对于引用类型,无论是作为值参数还是引用参数,都可以在方法内部修改它的成员,这两者主要的区别在于设置形参本身。
- 将引用类型对象作为值参数传递 如果在方法内创建一个新对象并赋值给形参,将切断形参与实参之间的关联,并且方法调用结束后,新对象也将不复存在。
- 将引用类型对象作为引用参数传递 如果在方法内创建一个新对象并赋值给形参,在方法结束后该对象依然存在,并且是实参所引用的值。
下面代码展示第一种情况——将引用类型对象作为值参数传递:
class MyClass
{
public int Val = 20;
}
internal class Program
{
static void Method(MyClass f1)
{
f1.Val = 50;
Console.WriteLine($"After member assignment: {f1.Val}"); //After member assignment: 50
f1 = new MyClass();
Console.WriteLine($"After new object creation: {f1.Val}"); //After new object creation: 20
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call: {a1.Val}"); //Before method call: 20
Method(a1);
Console.WriteLine($"After method call: {a1.Val}"); //After method call: 50
}
}
- 在方法开始时,实参和形参指向堆中相同的对象
- 在为对象的成员赋值之后,它们仍指向堆中相同的对象
- 当方法为形参赋值新对象时,实参仍指向原始对象,而形参指向的是新对象
- 在方法调用之后,实参指向原始对象,形参和新对象都会消失
下面代码展示第二种情况——将引用类型对象作为引用参数传递:
class MyClass
{
public int Val = 20;
}
internal class Program
{
static void Method(ref MyClass f1)
{
f1.Val = 50;
Console.WriteLine($"After member assignment: {f1.Val}"); //After member assignment: 50
f1 = new MyClass();
Console.WriteLine($"After new object creation: {f1.Val}"); //After new object creation: 20
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Console.WriteLine($"Before method call: {a1.Val}"); //Before method call: 20
Method(ref a1);
Console.WriteLine($"After method call: {a1.Val}"); //After method call: 20
}
}
- 在方法调用时,形参和实参指向堆中相同的对象
- 对成员值的修改会同时影响到形参和实参
- 当方法创建新的对象并赋值给形参时,形参和实参的引用都指向该新对象
- 在方法结束后,实参指向在方法内创建的新对象
Out(输出参数)
输出参数用于从方法体内把数据传出到调用代码,与引用参数类似,输出参数的形参充当实参的别名。
使用 out 需要注意以下几点:
- 使用输出参数时,必须在方法的声明和调用中都使用 out 修饰符
- 实参必须是变量而不能是表达式,方法需要内存位置来保存返回值
- 在方法内部,给输出参数赋值之后才能读取它,这意味着参数的初始值是无关的所以没有必要在方法调用之前给实参赋值
- 在方法返回之前,代码中的每条可能的路径都必须为所有输出参数赋值
class MyClass
{
public int Val = 20;
}
internal class Program
{
static void Method(out MyClass f1,out int f2)
{
f1 = new MyClass();
f1.Val = 25;
f2 = 15;
}
static void Main(string[] args)
{
Method(out MyClass a1,out int a2);
Console.WriteLine($"a1.Val: {a1.Val};a2: {a2}"); //a1.Val: 25;a2: 15
}
}
- 在方法调用之前,将要被用作实参的变量a1和a2已经在栈中
- 在方法的开始,形参的名称被设置为实参的别名
- 在方法内部,对形参进行赋值
- 方法指向完毕,形参的名称失效,但输出参数的值都被方法内的行为改变了
使用场景:返回多个值,通过将输出参数声明为out,可以将其看做为方法返回的值,这比使用 ref 参数更明确,因为调用者知道不需要为输出参数提供初始值。优化性能,当方法需要返回大量数据时,通过将结果作为 out 参数传递,可以避免生成返回值的赋值。
In(输入参数)
In 关键字用于指示方法参数是只读的,不可以在方法内部进行修改,与引用参数类似,输出参数的形参充当实参的别名。
使用 In 需要注意以下几点:
- 使用输出参数时,必须在方法的声明和调用中都使用 in 修饰符
- 实参必须是变量而不能是表达式,不能在方法调用语句中进行声明
- 不能在方法内部修改 In 声明的参数,但如果是引用类型,可以修改变量所指向的对象的成员
class MyClass
{
public int Val = 20;
}
internal class Program
{
static void Method(in MyClass f1)
{
//f1 = new MyClass(); 报错:无法分配给 变量“f1”,或将其用作 ref 分配的右侧,因为它是只读变量
f1.Val = 50;
}
static void Main(string[] args)
{
MyClass a1 = new MyClass();
Method(in a1);
Console.WriteLine($"a1.Val: {a1.Val};"); //a1.Val: 50;
}
}
使用场景: 传递大型对象,当需要将大型对象作为方法的参数传递时,使用 in 参数可以避免不必要的内存复制。参数不可变性,确保方法内部不会修改方法参数的值,避免一些意外的修改。