哎呀,这个两个关键字就写篇 blog 是不是有点小题大做了呀,但是还是记录一下吧
突然想到的这两个速记和区分用法,说不定能帮到正在学习 C# 的你

快速说明

看着比较理论枯燥,可以直接跳到 举例说明

refout 都是 C# 中用于按引用传递参数的关键字,它们的主要区别在于对参数初始化的要求和使用场景。

相同点

两者都按引用传递参数,这意味着方法内部对参数的修改会直接影响到原始变量。
在方法声明和调用时都需要显式使用关键字。

不同点

  • 初始化要求:
    • 传递给 ref 的变量必须在传递之前初始化。
    • 传递给 out 的变量不必在传递之前初始化,但必须在方法内部赋值。
  • 使用场景:
    • 需要在方法内部修改调用方传递的变量时。
    • 主要用于方法返回多个值,或者需要在方法内部初始化参数时。
  • 侧重点:
    • ref 侧重于修改传入的参数。
    • out 侧重于输出参数,将方法内部的值传递给调用方。

更详细的解释

ref

ref 就像是给原始变量取了一个别名,方法内部通过这个别名操作的就是原始变量本身。
由于操作的是原始变量,因此必须保证变量在传递之前已经有值。
可以理解为“有进有出”,既可以传递值给方法,也可以从方法中获取修改后的值。

out

out 更像是方法用来“输出”数据的通道。
方法内部必须对 out 参数进行赋值,否则会导致编译错误。
即使在方法外部初始化了变量,在方法内部也会被覆盖。
可以理解为“只出不进”,方法只负责给参数赋值,并将值传递给调用方。

代码举例说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
using System;

public class Example
{
public static void RefExample(ref int num)
{
// 首先!通过参数列表获取到了传进来的引用,并将其命名为 num,本质上还是 a
// 然后! 对这个获取到的变量直接进行一顿操作,比如下面的 num += 10
num += 10;
// 最后! 方法执行完毕,回到 Main 方法中去吧 ~
}

public static void OutExample(out int num)
{
// 进入方法!参数列表中可以理解为:新建了一个 int 变量 num,并且和方法外部传进来的变量是关联的
// 怎么个关联法呢?那就是最终方法结束时,会把这个 num 传出去,就跟方法 return 一个值类似
// 所以!既然是新建的变量,肯定要初始化呀(声明已经在参数列表中声明过了哦)也就是为什么要赋值,不能直接操作

num = 20; // 赋值!

// 方法结束了,这个变量会输出给方法外部,也就是赋值给 b
}

// 喂!直接从这里开始看说明哦!
public static void Main(string[] args)
{
// 首先来看 ref 的用法

int a = 5;

// 这里调用 RefExample 方法,!可以理解为:传入参数是 a 的引用
// 因为是 a 的一个引用,所以 a 本身肯定要已经存在了对吧,所以需要初始化,也就是上面的 int a = 5;
RefExample(ref a); // 接着跳到这个方法内部看说明哦 ヾ(•ω•`)
// 因为在 RefExample 方法内部对 a 进行了操作,所以 a 的值已经被改变啦

Console.WriteLine($"使用 ref 后 a 的值为:{a}"); // 所以输出:15

// 因为是传递一个变量的引用,所以这个变量肯定得先存在,所以要在这里先初始化
// 在方法内部不需要对这个变量赋值(或者也可以说初始化),因为已经知道这个变量是什么了(毕竟传递的是引用,不是无名变量)

// ---------------------------------- 分割线 ---------------------------------------

// 接着来看 out 的用法

int b;

// 这里调用 OutExample 方法,!可以理解为:执行一个方法,获取了一个返回值并传递赋值给 b
// 因为 b 是要被赋值(初始化)的,所以当然一开始不需要初始化,只需要声明(声明基本所有情况肯定是必须的哟)
OutExample(out b); // 接着跳到这个方法内部看说明吧 ヾ(•ω•`)
// 此时!b 等于是接受了方法执行后的输出,也就是方法内部的 num 这个存在

Console.WriteLine($"使用 out 后 b 的值为:{b}"); // 输出:20

// 这也就是为什么,这里不需要初始化(会接受到方法内部的输出,无论如何都会覆盖掉原本的值)
// 但是方法内部需要进行赋值或者说初始化(因为此时才要让变量存在,然后去输出出去)

// ---------------------------------- 分割线 ---------------------------------------

// 接着看看错误情况

// 1. 尝试不初始化 ref 参数,会导致编译错误
int c;
RefExample(ref c); // 编译错误:使用了未赋值的局部变量“c”
// 可这样理解:此时编译器都不知道 c 的值是多少,其在内存中的位置是薛定谔的猫,那怎么知道其引用是什么呢

// 2. out 尝试在赋值前使用 out 参数会导致编译错误
int d;
OutExample2(out d); // 方法实现写下面了
Console.WriteLine($"使用 out 后 d 的值为:{d}"); // 会导致编译错误,因为 d 在此时尚未赋值
// 可这样理解:此时编译器中新建的变量 num 是薛定谔的猫,其值是未知的,那怎么知道增加后的值是多少呢
}

public static void OutExample2(out int num)
{
// 需要在方法内部对 d 进行赋值后才能使用,如先: num = 20;
num += 10;
}
}

其他记忆方法

1. 从英文单词的含义出发:

ref (reference 的缩写): 意味着“引用”。可以理解为“通过引用传递”,就像给变量取了一个别名,方法内部操作的就是原始变量本身。因此,原始变量必须先“存在”(即初始化)。
out (output 的缩写): 意味着“输出”。可以理解为方法用来“输出”数据的通道。方法负责给 out 参数赋值,并将值传递给调用方。因此,原始变量可以“不存在”(即不初始化),方法会负责“创造”它。

2. 类比生活中的场景:

ref: 想象你把家里的钥匙(原始变量)交给你的朋友(方法),让他帮你去家里拿东西。你的朋友必须先有钥匙才能进门(初始化)。他拿到东西后,你家里就有了那个东西(修改原始变量)。
out: 想象你委托快递员(方法)去商店买东西。你不需要事先给快递员任何东西(不初始化)。快递员到商店买好东西后,会把东西交给你(方法给 out 参数赋值)。

3. 关注初始化要求:

这是 ref 和 out 最关键的区别,也是最容易记住的点:

ref 需要“先有后用”: 传递 ref 参数前必须初始化。
out 可以“先用后有”: 传递 out 参数前可以不初始化,但方法内部必须赋值。

4. 从方法的功能角度考虑:

ref 侧重于“修改”: 方法需要修改调用方传入的变量。
out 侧重于“输出”: 方法需要返回多个值,或者需要在方法内部初始化参数。

5. 口诀记忆法:

可以尝试用一些简单的口诀来记忆:

ref: “引用传递先赋值,方法内外都变化。”
out: “输出参数不强求,方法内部必须赋。”