C# 按值和按引用传递参数

假设有一个类型A,它有一个 int 类型的属性 X。ChangeA 方法接收类型 A 的参数,把 X 的值改为 2:

public static void changeA(A a)
{     
  a.X = 2;
  }

Main()方法创建类型 A  的实例,把 X 初始化为1,调用 ChangeA 方法: 

static void Main ()
{ 
   A a1 = new A ( X = 1 );
   ChangeA(a1);
   Console.WriteLine($"al.x:{al.A}");
}

输出是什么?1还是2?

答案视情况而定。需要知道 A 是一个类还是结构。下面先假定 A 是结构: 

public struct Ã
{
    public int x { get; set; } 
}

结构按值传递,通过按值传递,ChangeA 方法中的变量 a 得到堆栈中变量 al 的一个副本。在方法 ChangeA 的最后修改并销毁副本。al 的内容从不改变,一直是1。

A 作为一个类时,是完全不同的:

public class A 
{
    public int x { get; set; }
}

类按引用传递。这样,a 变量把堆上的同一个对象引用为变量 al。当 ChangeA 修改 a 的 X 属性值时,把它改为 al.X,因为它是同一个对象。这里,结果是2。

注意:

为了避免在更改成员时类和结构之间的不同行为上出现这种混淆,最好将结构设置为不可变的。如果一个结构体只有不允许改变状态的成员,就不会陷入如此混乱的境地。当然,使 struct 类型不可变的规则总是有例外的。C#7 中新增的 ValueTuple 实现为一个可变结构体。然而,使用 ValueTuple。公共成员就是字段,而不是属性(这是提供公共字段的准则的另一个例外)。由于元组很重要,且以 int 和 float 的方式使用它们,这是违反一些指导原则的好理由。

ref 参数

也可以通过引用传递结构。如果 A 是结构类型,就添加 ref 修饰符,修改 ChangeA  方法的声明,通过引用传递变量:

public static void ChangeA(ref A a)
{
   a.X = 2;
}

从调用端也可以看出这一点,所以给方法参数应用了 ref 修饰符后,在调用方法时需要添加它:

static void Main()
{
   A al  = new A { X = 1 };
   ChangeA(ref al);
   Console.WriteLine($"al.x: {al.X}");
}

现在,与类类型一样,结构也按引用传递,所以结果是2。

类类型如何使用 ref 修饰符?下面修改 ChangeA 方法的实现:

public static void ChangeA(A a)
{
   a.X = 2;
   a = new A { X = 3 };
}

使用 A 类型的类,可以预期什么结果?当然,Main()方法的结果不是1。因为按引用传递是通过类类型实现的。a.X 设置为 2,就改变了原始对象 al。然而,下一行 a = new A {X=3} 现在在堆上创建一个新对象,和一个对新对象的引用。Main()方法中使用的变量 al 仍然引用值为 2 的旧对象。ChangeA 方法结束后,没有引用堆上的新对象,可以回收它。所以这里的结果是2。

把 A 作为类类型,使用 ref 修饰符,传递对引用的引用(在C++术语中,是一个指向指针的指针),它允许分配一个新对象,Main()方法显示了结果3:

public static void changeA(ref A a)
{
     a.X = 21;
     a =  new A { X = 3 };
}

最后,一定要理解,C# 对传递给方法的参数继续应用初始化要求。在任何变量传递给方法之前,必须初始化,无论是按值还是按引用传递。

注意:

在 C#7 中,还可以对局部变量和方法的返回类型使用ref 关键字。

out 参数

如果方法返回一个值,该方法通常声明返回类型,并返回结果。如果方法返回多个值,可能类型还不同,该怎么办?这有不同的选项。一个选项是声明类和结构,把应该返回的所有信息都定义为该类型的成员。另一个选项是使用元组类型,第三个选项是使用 out 关键字。

下面的例子使用通过Int32 类型定义的 Parse 方法。ReadLine 方法获取用户输入的字符串。假设用户输入一个数字,int.Parse 方法把它转换为字符串,并返回该数字:

string input1 = Console.ReadLine(); 3.6 
int result1 = int.Parse(input1);
Console.WriteLine($"result:(result1}");

然而,用户并不总是输入希望他们输入的数据。如果用户没有输入数字,就会抛出一个异常。当然,可以捕获异常,并相应地处理用户,但“正常”情况不这么做。也许可以认为,“正常”情况就是用户输入了错误的数据。

要处理类型错误的数据,更好的方法是使用 Int32 类型的另一个方法::TryParse。TryParse 声明为无论解析成功与否,都返回一个 bool 类型。解析的结果(如果成功)是使用 out 修饰符返回一个参数:

public static bool TryParse(string s, out int result);

调用此方法后,result 变量不需要预先初始化,而是在方法中初始化变量。在 C#7 中,也可以在方法调用时声明变量。与 ref 关键字类似,在调用方法时需要提供 out 关键字,而不仅仅在声明方法时提供:: 

string input2 = ReadLine();
if (int.TryParse(input2, out int result2))
{
   Console.WriteLine($"result:{result2}");
}
else
{
   Console.WriteLine("not a number");
}

注意:

cut var 是 C#7 的一个新特性,在 C#7 之前,需要在调用该方法之前声明一个out 变量。在C#7 中 可以调用方法来实现声明。如果类型是由方法答名明确定义的。则可以使用 var 关键字(这就是为什么 out var 知道这个特性的原因)来声明交量。还可以定义具体的类型。如前面的代码片段所示。该变量的作用域在方法调用之后是有效的。

in 参数

0ddebf3a255bba5b991e16cd5a31c860.png

 C# 7.2 向参数添加了 in 修饰符。out 修饰符允许返回参数指定的值。in 修饰符保证发送到方法中的数据不会更改(在传递值类型时)。

下面定义一个简单的可变结构体。名称为 AValueType,再定义一个公共可变字段:

struct AValueType
{
    public int Data;
}

现在,使用 in 修饰符定义一个方法时,变量就不能更改了。试图更改可变字段 Data,编译器会抱怨不能为只读变量的成员分配值,因为该变量是只读的。in 修饰符使参数设置为只读变量:

static void Cantchange(in AValueType a) 
{
   // a.Data = 43;    //does not compile - readonly variable
     Console.WriteLine(a.Data);
}

当调用方法 CantChange 时,可以通过传递或不传递 in修饰符来调用该方法。这对生成的代码没有影响。 

使用值类型和 in 修饰符,不仅有助于确保不更改内存,编译器还可以创建更好的优化代码。与使用方法调用来复制值类型不同,编译器可以使用引用,从而减少所需的内存并提高性能。

注意:

in 修饰符主要用于值类型。也可以对引用类型使用它。in 修饰符用于引用类型时、可以更改变量的内容,但不能更改变量本身。

往期推荐

8ce9daacda2cc0fb0d2bc270260196a2.png

Love life,love yourself

关注小编不迷路呦~

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值