首页 / C# 入门教程 / 参数传递(值/ref/out/in)

C# 入门教程

参数传递(值/ref/out/in)

本教程共 100 篇 · 第 50 篇 · 更新于 2026-07-31 · 约 12 分钟阅读

C#C# 入门教程方法参数传递

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 特别适合”既要返回成功与否、又要带出结果”的场景,比如各种 TryParseTryGetValue

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 演示 refout 配合:方法内部累加,并通过 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}");

sumref 改掉,sout 带出,分工清晰。

最佳实践

  1. 优先值传递,保持方法”无副作用”,更易测试。
  2. 需要返回额外结果用 out,而不是用 ref 当输出。
  3. struct 只读传参用 in,省拷贝又不破坏封装。
  4. 只在”必须换掉整个对象”时才对引用类型用 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

这种”必须改原变量”的场景,refout 更合适: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 省拷贝。用对修饰符,方法既清晰又高效。至此,面向对象进阶(继承、多态、抽象、接口、记录、参数)已全部讲完,后面进入方法进阶与委托。