Menu

C# ref、out 与 in 参数:按引用传递详解

C# 中 ref、out 和 in 参数是如何工作的:传递变量本身而不是副本,用 out 返回额外的结果和 TryParse 模式,out var,只读的 in 参数,明确赋值规则,以及实参是引用类型时会有什么不同。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

默认情况下,C# 按值传递每个实参:方法拿到的是一份副本,给参数赋值不会影响调用方。ref、out 和 in 修饰符则会传递调用方的变量本身。

输出:

by value: 50
by ref:   60

关键字出现了两次:一次在参数列表中(ref int score),一次在调用处(ref score)。调用处必须写,这是有意为之的:任何读到 AddPoints(ref score) 的人都知道 score 可能会被修改。如果漏写,编译器会报 CS1620:Argument 1 must be passed with the 'ref' keyword。

ref:读写调用方的变量

ref 参数是调用方变量的别名。方法可以读取它当前的值,也可以替换它。经典的例子是交换两个变量,这用普通参数是做不到的:

输出:

Linus Ada
volume 100

ref 的规则:

  • 实参必须是一个变量(局部变量、参数、字段或数组元素),而不是字面量或表达式。属性也不行:Clamp(ref player.Volume, 0, 100) 会报错误 CS0206:A non ref-returning property or indexer may not be used as an out or ref value。先把属性复制到一个局部变量中,传入这个局部变量,然后再赋值回去。
  • 变量在调用之前必须已经明确赋值。int n; Clamp(ref n, 0, 10); 会报 CS0165:Use of unassigned local variable 'n'。

out:额外的结果

out 用于方法产生的值。调用方不需要初始化这个变量,而方法在返回之前必须在每条执行路径上给它赋值:

static void Split(string fullName, out string first, out string last)
{
    // error CS0177: The out parameter 'last' must be assigned to before control leaves the current method
    first = fullName.Split(' ')[0];
}

在方法内部,out 参数在你写入之前都被视为未赋值,所以你读不到调用方原来的值。这就是它和 ref 的区别:ref 是双向的,out 是从方法到调用方的单向传递。

TryParse 模式

out 最为人熟知的用法是基础库中的 Try 模式:返回 bool 表示是否成功,把结果放在 out 参数中,这样输入有误时也不会抛出异常:

输出:

"vip25" is a coupon: 25% off
"42" is a quantity: 42
"BOGUS" is not recognized
"" is not recognized

out var quantity(C# 7.0)直接在调用中声明变量,它的类型由参数决定。解析失败时,TryParse 会把它设为 0 并返回 false。如果你只关心解析是否成功,可以用 out _ 丢弃这个值:bool isNumber = int.TryParse(input, out _);。

注意 TryGetDiscount 在两条路径上都给 rate 赋了值:一次是显式赋值,另一次是把它转交给 Dictionary.TryGetValue,而后者本身也是一个 out 调用。

out 与返回元组

在 C# 7 之前,out 是返回多个值的主要方式。现在元组可以更直接地做到这一点:

// with out parameters
static void MinMax(int[] values, out int min, out int max) { ... }
MinMax(temps, out int lo, out int hi);

// with a tuple
static (int Min, int Max) MinMax(int[] values) { ... }
var (lo, hi) = MinMax(temps);

普通的多个返回值,用元组(或一个小类)。out 留给 Try 模式,因为返回 bool 能让调用直接放进 if 条件中。

引用类型:修改对象与替换对象

按值传递一个类的实例,本来就能让方法修改这个对象,因为复制的值是指向同一个对象的引用。ref 额外增加了一种能力:替换调用方变量所指向的对象。

输出:

after AddItem:      2 items
after ResetByValue: 2 items
after ResetByRef:   0 items

ResetByValue 让它自己那份引用副本指向了一个新列表,调用方根本看不到。ResetByRef 修改的则是调用方的变量本身。在日常代码中,你很少需要给类类型的参数加 ref:改为返回新对象即可。

对于结构体,区别就更大了,因为结构体实参会被逐个字段复制。一个必须修改调用方结构体(一个 Point,或一个大型设置结构体)的方法就需要 ref。

in:只读的按引用传递

C# 7.2 新增了 in,它按引用传递,但禁止赋值。它的目的是提升性能:大型结构体不会在每次调用时被复制,同时调用方可以确定它不会被修改。

// C# 7.2 and later
struct Matrix4
{
    public double M11, M12, M13, M14, M21, M22, M23, M24,
                  M31, M32, M33, M34, M41, M42, M43, M44;
}

static double Trace(in Matrix4 m) => m.M11 + m.M22 + m.M33 + m.M44;

static void Reset(in Matrix4 m)
{
    m.M11 = 0;
    // error CS8332: Cannot assign to a member of variable 'm' or use it as the right hand side of a ref assignment because it is a readonly variable
}

在调用处 in 是可选的:Trace(matrix) 和 Trace(in matrix) 都可以。对于 int、double、DateTime 和其他小类型,in 没有任何收益,甚至可能稍慢一些;请把它用在经常传递、包含多个字段的结构体上。

限制

  • ref 和 out 参数不能是可选参数(CS1741)。in 参数可以:static void Connect(in int retries = 3) 是合法的。
  • 重载之间不能只靠 ref 和 out(或 in)来区分,因为它们在运行时层面是一样的。F(int x) 和 F(ref int x) 可以共存。
  • async 方法和迭代器(使用 yield return 的方法)不能有 ref、out 或 in 参数。这些方法请改为返回元组或结果对象。
  • lambda 或局部函数不能捕获外层方法的 ref、out 或 in 参数;先把它复制到一个局部变量中。

ref、out 与 in 对比

调用方必须先赋值方法必须赋值方法可以读取调用处的关键字
(无修饰符)是否是(副本)无
ref是否是必须写
out否是,在每条路径上只能在赋值之后必须写
in是不允许是可选

常见问题

C# 中 ref 和 out 有什么区别?

两者传递的都是调用方的变量本身,而不是副本。使用 ref 时,变量必须在调用前赋值,方法可以读取并修改它。使用 out 时,变量不必事先赋值,方法在写入之前不能读取它,并且必须在返回之前给它赋值(否则报 CS0177)。out 的意思是“这是一个额外的结果”。

C# 中的 out var 是什么意思?

从 C# 7.0 起,你可以在调用中直接声明 out 变量:if (int.TryParse(text, out var number))。变量的类型会根据参数推断出来,而且在这条语句之后它仍然在作用域内。out _ 可以丢弃你不需要的结果。

C# 中对象是按引用传递的吗?

不是。除非你写了 ref、out 或 in,否则每个实参都是按值传递的。对于类,被复制的值是一个引用,所以方法可以修改对象的字段,调用方也能看到这些修改。没有 ref 时它做不到的,是让调用方的变量指向另一个对象。

C# 中的 in 参数修饰符是什么?

in(C# 7.2 及以后)按引用传递实参,但是只读的:方法不能给它赋值。它的作用是避免每次调用时都复制大型结构体。对于 int 或 DateTime 这样的小类型,它没有任何好处。

为什么会出现“Argument 1 must be passed with the 'ref' keyword”错误?

方法声明了一个 ref 参数,而调用时漏写了这个关键字。C# 要求在调用处也写上 ref(以及 out),让读代码的人能看出变量可能会被修改:要写 Increment(ref count);,而不是 Increment(count);。

Coddy 编程语言插图

和 Coddy 一起学编程

开始