参数传递(值/ref/out/in)
本教程共 100 篇 · 第 50 篇 · 更新于 2026-07-31 · 约 12 分钟阅读
50. 参数传递(值/ref/out/in)
本节目标:分清值传递与
ref/out/in引用传递,知道何时该用哪种。
默认是值传递
C# 默认按值传递(pass by value)。对于值类型,方法拿到的是副本,改了不影响原值。
namespace CSharpDemo;
void AddOne(int x) => x += 1;
int n = 5;
AddOne(n);
Console.WriteLine(n); // 仍是 5
引用类型传值时,传的是”引用的副本”——你能改对象内部,却不能让原变量指向新对象。这点常被误解:很多人以为”引用类型传引用”,其实传的是”引用的拷贝”。
namespace CSharpDemo;
void Rename(List<string> list)
{
list.Add("新项"); // 改得到内部,外界可见
// list = new(); // 只改局部变量,外界的变量不受影响
}
List<string> names = ["旧项"];
Rename(names);
Console.WriteLine(string.Join(",", names)); // 旧项,新项
ref:把变量本身传进去
ref 让方法操作原变量,而不是副本。调用处和定义处都要写 ref。
namespace CSharpDemo;
void AddOne(ref int x) => x += 1;
int n = 5;
AddOne(ref n);
Console.WriteLine(n); // 变成 6
ref 也能让方法”换掉整个对象”,这是普通值传递做不到的。
namespace CSharpDemo;
void Replace(ref StringBuilder sb)
{
sb = new StringBuilder("新的");
}
StringBuilder s = new StringBuilder("旧的");
Replace(ref s);
Console.WriteLine(s); // 新的
Note
ref参数在传入前必须先赋值,方法内部读到的是调用方的原值。这与out的”调用前可不赋值”正好相反。
out:只用来”输出结果”
out 专门用于从方法”带出”结果,调用前不必赋值,方法里必须赋值后才能返回。
namespace CSharpDemo;
bool TryParse(string s, out int result)
{
return int.TryParse(s, out result);
}
if (TryParse("42", out int value))
Console.WriteLine(value); // 42
out 的语义是”这个是输出参数”,一眼看出它不是输入。C# 还支持内联声明 out int value,变量作用域正好限制在需要的地方。
out 特别适合”既要返回成功与否、又要带出结果”的场景,比如各种 TryParse、TryGetValue。
in:只读引用,避免拷贝
in 传入引用以避免大结构体(struct)被拷贝,但方法内不能修改它,是”只读引用(readonly reference)”。
namespace CSharpDemo;
struct BigData
{
public double A, B, C, D;
}
void Print(in BigData data)
{
// data.A = 1; // 报错:in 参数不可改
Console.WriteLine(data.A);
}
BigData d = new() { A = 3.14 };
Print(in d);
in 的卖点是性能:对几十字节的大 struct,每次传值都要整块拷贝,in 改成传引用就省了这份拷贝。又因为只读,调用方不用担心被改动。
Tip普通小数据用值传递即可;大
struct又不想拷贝,且保证不被改,才用in。小数组或 class 用in收益很小。
四种方式怎么选
| 方式 | 是否影响外部 | 传入前需赋值 | 典型用途 |
|---|---|---|---|
| 值传递 | 否 | 是 | 一般输入 |
ref | 是(含换对象) | 是 | 需修改原变量 |
out | 是(只输出) | 否 | 返回额外结果 |
in | 否(只读) | 是 | 大值类型免拷贝 |
Warning
ref/out/in会拉紧调用方与被调方的耦合,可读性下降。默认值传递就好;只有当确实需要”影响外部”或”免拷贝”时,才升级到这些修饰符。不要为了”返回多个值”就随手ref。
一个综合小例子
用 Calculator 演示 ref 与 out 配合:方法内部累加,并通过 out 返回步数。
namespace CSharpDemo;
void Accumulate(ref int total, int add, out int steps)
{
total += add;
steps = 1;
}
int sum = 0;
Accumulate(ref sum, 10, out int s);
Console.WriteLine($"总和 {sum},步数 {s}");
sum 被 ref 改掉,s 由 out 带出,分工清晰。
最佳实践
- 优先值传递,保持方法”无副作用”,更易测试。
- 需要返回额外结果用
out,而不是用ref当输出。 - 大
struct只读传参用in,省拷贝又不破坏封装。 - 只在”必须换掉整个对象”时才对引用类型用
ref。
Note如果你发现一个方法需要大量
ref/out才能工作,往往说明该用返回一个对象(如(bool success, int value)元组,或自定义结果类型)来替代。元组返回比多个out更清爽,也更不容易遗漏赋值。
新手常见坑
第一,忘记调用处也写 ref/out/in,编译会报错。定义和调用必须配对。
第二,用 ref 只为”返回多个值”,其实 out 语义更贴切,也更清晰。
第三,对引用类型过度使用 ref。传引用类型本身已经能改对象内容,ref 只在”要换掉整个对象”时才需要。
第四,滥用 ref/out/in 让调用关系变复杂。可读性优先,默认值传递,确需影响外部再升级。
引用类型到底传了什么
很多初学者卡在”引用类型传参到底传了什么”。记住一句话:引用类型按值传递时,传的是”指向对象的钥匙的复印件”。你拿着复印件仍能打开同一扇门(改对象内部),但配一把新钥匙(让变量指向新对象)并不会影响原主人的钥匙。这正是前面 Rename 示例的行为——能加项,却换不掉外面的变量。想换掉整个对象,才需要 ref。
Note
out支持内联声明(如out int value),变量的作用域正好限制在if块内,外面访问不到。这既省了一行提前声明,又避免”声明了却忘了用”的游离变量,可读性更干净。
典型用途:用 ref 交换两个变量
ref 最常见的正当用法之一,是实现”交换”。因为传入的是原变量,方法里互换会反映到外面。
namespace CSharpDemo;
void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
int x = 1, y = 2;
Swap(ref x, ref y);
Console.WriteLine($"x={x}, y={y}"); // x=2, y=1
这种”必须改原变量”的场景,ref 比 out 更合适:out 强调”只输出”,而这里既要读原值又要写回。
不过要提醒:多数业务方法并不需要 ref。如果一个方法频繁靠 ref/out 才能工作,往往是”要返回的东西太多”。这时用元组 (结果, 状态) 或定义一个结果类更清爽,调用方也更容易理解。修饰符只是工具,默认值传递才是常态——先写清晰,遇到真实性能或语义需求再升级,别为了”灵活”提前把接口复杂化。
回到开头那句话:C# 默认值传递,这是最安全、最易推理的默认。其余三种修饰符都是”特例开关”,按下它们是为了满足特定需求——要么要改外部,要么要免拷贝。把它们当例外,而非习惯,代码会更好懂也好维护。
常见疑问解答
问:引用类型还要用 ref 吗?
多数情况不用。传引用类型本身就能改对象内容;只有”要把变量整个换成新对象”时才需 ref。滥用只会让调用关系更难懂。
问:out 和 ref 怎么选?
out 专用于”输出结果”,调用前不必赋值;ref 用于”既要读又要改原值”。若只是为了多返回值,out 语义更贴切。
问:in 是不是总比值传递快?
不是。只有大 struct(几十字节以上)省下的拷贝才明显;小数据用 in 反而多一层间接,收益可忽略。
问:想返回多个值,一定要 ref/out 吗?
不必。用元组 (bool success, int value) 或自定义结果类型往往更清晰,也避免遗漏 out 赋值。ref/out 是手段不是目的。
问:ref 能提高性能吗?
在确实要改原值或换对象时,ref 省去拷贝或新建。但可读性优先,默认值传递,确需影响外部再升级。
小结
参数传递是方法章节的收尾:默认值传递最安全,ref 改原值、out 带结果、in 省拷贝。用对修饰符,方法既清晰又高效。至此,面向对象进阶(继承、多态、抽象、接口、记录、参数)已全部讲完,后面进入方法进阶与委托。