本质上都是指针。
Part 1 ref 参数
考虑一种情况。如果 C 语言里,我们需要让数值发生交换,如果单纯写函数是不行的,因为大家都知道,C 语言的函数是值传递,也就意味着它会把数据本身拿出来赋值,就会产生一个一模一样的副本。当需要发生交换的时候,如果用函数执行,函数最终只交换了副本,因此原本的数据并没有变动。所以,你必须得用指针来表示“函数内的变量用的是和原本数据一样的变量”:
void swap(int *a, int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
我们借用指针才能使得数据成功交换。那么调用方当然就是传入两个地址进去了:
// ...int a = 3, b = 4;
swap(&a, &b);
// ...
这样一波操作后,a 和 b 就可以交换成功了。因为传入的是地址,通过 C 语言的指针间接运算符 * 可以得到内部的数据。那么,如果我们把这个写法类比到 C# 里,那么这种情况就被称为 ref 参数:
void Swap(ref T a, ref T b)
{
var temp = a;
a = b;
b = temp;
}
// 调用方// ...int a = 3, b = 4;
Swap(ref a, ref b);
// ...
所以 ref 本质用于传入一个调用方数据和函数内执行的数据是一个东西的模型。
Part 2 out 参数
接着考虑 out 参数。我们依然从 C 语言角度入手。试想我们要计算一个人的平均分,并返回这个人是否通过考试(平均分大于 60 分)。这个模型可以考虑返回一个 bool 的类型变量。所以写法可以是这样的:
// ↓ 布尔类型(C99 起可用),也可以写别名 bool。_Bool isPassed(float chinese, float english, float math)
{
float total = chinese + english + math;
return total / 3 >= 60;
}
可以从这个函数里看到,这个函数直接执行求和,并返回了平均数是否大于 60 分的行为。那么问题来了。如果我想让这个函数同时也反馈给调用方“这个人平均分是多少”。这个怎么办呢?相信你的第一反应是改写函数的返回值类型。但是实际上这样是不好的,因为我们这个函数目的是求“是不是过了”,而不是“返回多少分”。那么,我们可以考虑从函数内部反馈一个结果出来,从参数传出:
_Bool isPassed(float chinese, float english, float math, float *avg)
{
float total = chinese + english + math;
*avg = total / 3; // 注意这一行。 return *avg >= 60;
}
我们尝试从函数内部为这个指针变量赋值。在调用方的时候可以这么写:
// ...float f; // 一个不用赋值的变量。if (isPassed(70, 80, 90, &f))
{
printf("这个人考试过了!\n");
}
else
{
printf("平均分都没及格,等着重修吧!\n");
}
// ...
那么从外部调用的时候,我们的变量是可以不用赋值的。与其说不用赋值,还不如说成是“赋值没有意义,反正最终都会在这个函数内部执行的过程之中被替换掉”。
这种传参模型就是 out 模式了。写成 C# 便是
// 方法bool IsPassed(float chinese, float english, float math, out float avg)
{
float total = chinese + english + math;
avg = total / 3; // 注意这一行。 return avg >= 60;
}
// 调用方// ...if (IsPassed(70, 80, 90, out float f))
{
Console.WriteLine("过了!");
}
else
{
Console.WriteLine("没过!");
}
// ...
本质上两种传参都是用的指针。但这两个模型的用途和场景不同,所以在 C# 里,编译器可以区分它们。如果返回数值的语句都写了,却没有为 out 参数赋值,那么必然编译器会报错。实际上,out 参数想解决的问题是返回多个数值。显然一个函数只能返回一个对象,虽然后来我们有了 Tuple 泛型和 ValueTuple 泛型可以允许对象返回多个数,也可以用数组来达到这一点,但是老实说,它们都不是最优的解决方案,因为一来是会产生新的对象的内存分配,二来是破坏了代码结构和执行逻辑,毕竟,我们需要的是“这个人是不是过了”。当然,你也不必去咬文嚼字。C# 7 里有值元组的解构,解构的方法用的是多个 out参数,且没有返回值。这会儿并不是为了照顾和强调“返回多个值”,而是想告诉你,因为返回值占一个返回的位置,其它地方只能从参数返回,那么代码看起来就太丑了。所以干脆为了代码的统一,一并把解构的字段都放在参数上。
Part 3 重载
按理说,这个不属于它们的区别。但是前文不是说了那么多知识点吗,这里就顺带提一下。既然它们都被翻译成指针,那么,你怎么区分指针的类型不一样呢?
换言之,如果一个方法传入的参数类型都一样,但只是 ref和 out用得不一样,那么它们构成重载吗?
static void Method(ref int a);
static void Method(out int a);
你可以实践一下,答案是,报错。都被翻译成 int* 了,还哪里能区分得了它们鸭。所以它们不构成重载。但是,不带 ref或 out关键字的方法可以和它们其一构成重载:
static void Method(ref int a);
static void Method(int a);
static void Method2(out int a);
static void Method2(int a);
这样是 OK 的。