ref传递已初始化变量的地址,out传递待赋值变量的地址;ref要求调用前初始化、方法内可读可写,out允许调用前未初始化、但方法内必须且仅能赋值一次。

ref 和 out 都能让方法修改调用方的变量,但语义和约束完全不同
直接说结论:ref 传递的是「已初始化变量的地址」,out 传递的是「待赋值变量的地址」。编译器强制检查:用 ref 前变量必须有确定值;用 out 前可以是未赋值状态,但方法内必须写入一次且仅一次。
常见错误现象:
- 编译报错
Use of unassigned local variable 'x'—— 用了ref x但x没初始化 - 编译报错
The out parameter 'y' must be assigned to before control leaves the current method—— 方法末尾前没给y赋值 - 误以为
out可以读取原值 —— 实际上方法体内首次读取out参数会触发编译错误
使用场景:
-
ref:需要双向通信,比如传入一个初始值,方法既读又写(如交换两个数、递增计数器) -
out:纯输出,比如int.TryParse("123", out int result),调用方不关心原值,只想要解析结果
int a = 10;
int b; // 未初始化
TestRef(ref a); // ✅ 合法:a 已赋值
TestOut(out b); // ✅ 合法:b 允许未初始化
// TestRef(ref b); // ❌ 编译失败:b 未赋值
<p>void TestRef(ref int x) {
Console.WriteLine(x); // 可读
x = x * 2; // 可写
}</p><p>void TestOut(out int y) {
// Console.WriteLine(y); // ❌ 编译失败:不能读未赋值的 out 参数
y = 42; // ✅ 必须且只能赋值一次
}</p>
引用类型用 ref 或 out 时,改的是“引用本身”,不是“对象内容”
这是最容易混淆的点。对引用类型(比如 class 实例)默认就是按值传递引用地址——所以你能改对象内部字段,但不能让外部变量指向新对象。加了 ref 或 out,才真正能替换那个“引用变量”本身。
参数差异:
- 不加修饰:
void M(Person p)→ 可改p.Name,但p = new Person()不影响调用方的p - 加
ref:void M(ref Person p)→p = new Person()会让调用方的变量指向新实例 - 加
out:void M(out Person p)→ 同样可替换引用,且调用前p可为null或未声明
性能影响很小,但语义清晰度差别极大。滥用 ref 会让调用方承担“我可能被改”的隐式契约,而 out 明确表示“这个变量即将被你填满”。
Person p1 = new Person { Name = "Alice" };
Person p2 = null;
<p>ChangeName(p1); // 改 Name 字段 → p1.Name 变为 "Bob"
ReplaceRef(ref p1); // 替换整个引用 → p1 指向新对象
CreateOut(out p2); // p2 从 null 变成新 Person</p><p>void ChangeName(Person p) {
p.Name = "Bob"; // ✅ 影响原对象
p = new Person(); // ❌ 不影响调用方的 p1
}</p><p>void ReplaceRef(ref Person p) {
p = new Person { Name = "Charlie" }; // ✅ p1 现在指向这个新实例
}</p><p>void CreateOut(out Person p) {
p = new Person { Name = "David" }; // ✅ p2 被赋值,不再为 null
}</p>
string 是引用类型,但 ref/out 行为受其不可变性约束
string 虽然是引用类型,但它是不可变的(immutable)。这意味着即使你用 ref string,方法内任何“修改”操作(比如 s += "abc")本质都是创建新字符串并让引用指向它——这正好符合 ref 的能力边界。
容易踩的坑:
- 误以为
string按值传递时无法被“修改” → 实际上s = "new"在方法内有效,但不影响外部变量(因为是地址副本) - 对
out string抱有“追加内容”的幻想 →out只保证赋值一次,不能基于原值做拼接(原值根本不可读) - 忽略
string的 GC 开销:频繁用ref替换大字符串,可能比直接返回新字符串更不直观
典型用法就是明确的“重赋值”场景,比如解析逻辑中根据条件决定返回哪个字符串字面量:
string input = "hello";
SetGreeting(ref input);
Console.WriteLine(input); // 输出 "Hi there!"
<p>void SetGreeting(ref string s) {
s = "Hi there!"; // ✅ 合法:替换整个引用
// s += "!"; // ❌ 不推荐:语义模糊,且产生临时字符串
}</p>
ref struct 类型不能作为 ref/out 参数的实参
这是个硬性限制,源于 ref struct 的栈限定生命周期。如果你定义了一个 ref struct S { public int X; },那么它不能出现在 ref S 或 out S 的参数位置——编译器会直接报错 CS8345。
原因很简单:ref 和 out 参数可能被存储到堆上(比如被闭包捕获、或作为异步状态机字段),而 ref struct 严禁逃逸到栈外。
替代方案只有两个:
- 把
ref struct拆成普通struct(如果业务允许) - 用
Span<t></t>或ReadOnlySpan<t></t>包装数据,它们本身就是为栈安全设计的,并支持ref传递元素
这个限制不会影响日常开发,但一旦你在高性能场景(如网络协议解析)中用到 ref struct,就很容易撞上。记住:只要看到 CS8345,第一反应就是检查有没有把 ref struct 往 ref/out 方法里塞。










