撰写了文章 更新于 2020-04-20 16:17:35
从C#开始的编程入门——值类型与按引用传递
我们来思考一个经典编程问题。
要求编写一个方法,接受两个int类型的变量,交换两个变量的值。
也就是说如果a为1,b为2。执行方法后a为2,b为1。
经典错解
根据直觉,我们很自然地会写出这样的代码。交换嘛,把值给对方就是了。
void Swap(int a, int b)
{
var temp = a;
a = b;
b = temp;
}这个思路完全没有问题。但是如果你检查结果。
会发现a和b的值并没有改变。这是为什么呢?
先说结论。C#中,值类型作为参数传递给方法时默认按值传递。
值类型
C#中的所有结构都是值类型(Value Type)。从名字中我们可以略知一二。对于值类型我们大部分时候关心的是它的值。
比如基本类型中的各种数值类型,我们尝试用一个int类型变量给另一个赋值时会发生值本身的复制。
int a = 1;
int b = a;
b = 3;
System.Console.WriteLine($”a:{a} b:{b}”); // a:1 b:3完成赋值后,两个变量的值不会相互影响。这实际上已经解释了Swap的错解为什么错了。 那么调用方法传递参数的时候又会发生什么呢?
按值传递
传递参数的过程中,实际上就相当于把实参复制给方法中的形参,这就是所谓的按值传递(pass by value)。也就是说,以刚才写的Swap方法为例,对于两个参数,我们可以这样理解:
int a = arg1; int b = arg2;
那么,回忆上面讲的,既然发生了类似于这样的值复制,那么自然,即便交换了值,也影响不到方法外面的变量。
那么,如何绕过这样的机制以达成我们的目的呢?
按引用传递
尽管默认为按值传递,但是我们可以按引用传递(pass by reference)一个变量。
按值传递,我们相当于复制了变量的值;按引用传递,我们就好比直接把变量搬进了方法,而形参只是给它取了一个别名,形参会直接代表外面的实参。
如果需要按引用传递值类型变量,需要在参数类型前加上ref关键字,即reference。
我们直接在错解上面修改即可。你看,代码逻辑完全不变。
void Swap(ref int a, ref int b)
{
var temp = a;
a = b;
b = temp;
}
对于按引用传递的参数,在调用时也需要加上ref关键字。
int a = 1; int b = 2; Swap(ref a, ref b);
检查结果,和期望的一样。
事实上,在C中,解决这样的问题的方法是使用指针。而C++中除了指针还可以使用引用。这两种方法一定程度上都是间接操作一个变量的方法。C#的变量在使用时没有直接的指针和引用的概念,但是实际上这种按引用传递的方法在思路上是类似的。在定义中,可以把ref当成*或者&看。调用时的ref也就相当于&。
in和out
除了ref之外,C#还提供了in和out关键字。使用这两个关键字修饰的参数也是按引用传递。
in
in关键字表示“我只是单纯用一下”。in传入的参数可以被方法使用,但是不允许方法修改它的值。传入的参数必须被初始化。道理很显然,如果允许传入一个未被初始化的变量,我又不能给它赋值,那有个什么用。
void Print(in int n)
{
System.Console.WriteLine(n);
}实际上这就相当于C++中的const int &参数。
out
out和in相反,它表示“你给我一个变量,我会产出它的值”。 可能你已经想到了,out传入的变量就不用初始化了,因为在方法中,我们会负责为它赋值。
void GetItDone(out string s)
{
s = “Done”;
}对于out参数,如果没在方法返回前为其赋值,会在编译时发生异常。 out参数仍然是按引用传递的。因为我们的目的是在方法内直接为一个外部传来的变量赋值,并且希望在方法结束后仍然维持这个变量被赋值的状态。
这一篇我们介绍了结构的值类型特性以及按引用传递。下一篇我们介绍一种天生通过引用传递的类型:类。这也是C#中面向对象特性最强大的类型。
