C# ref 关键字详解
ref 用于引用传递,让方法接收变量的内存地址,而不是变量的值拷贝。
核心区别:默认是值传递(传副本);ref 是引用传递(传原变量本身)
✅ 基础规则
- 调用
ref 参数的方法时,实参必须提前初始化赋值(不能是未赋值的局部变量)
- 方法声明和调用处,两边都要写 ref(C# 7.2 之前强制,新版也建议显式写)
ref 可以用于:值类型、引用类型、数组;不能用于只读变量(readonly、const)
- 方法内修改参数,会直接修改外面原始变量
1. 值类型 + ref(最常用场景)
值类型(int, double, struct)默认传副本,方法内部修改不会影响外部;加 ref 就会修改原值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| using System;
class Program { static void ChangeNum(ref int num) { num = 999; }
static void Main() { int a = 10; ChangeNum(ref a); Console.WriteLine(a); } }
|
如果去掉 ref:输出仍然是 10,方法修改的只是副本。
2. ref 作用于引用类型(class)
引用类型本身传递的是引用副本,不加 ref 也能修改对象内部字段;
加 ref 的效果:可以让参数指向一个全新的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| class Student { public string Name; }
static void ReplaceStudent(ref Student s) { s = new Student { Name = "新同学" }; }
static void Main() { Student stu = new Student { Name = "老王" }; ReplaceStudent(ref stu); Console.WriteLine(stu.Name); }
|
不加 ref 的话,方法里 s = new Student() 只会修改本地副本引用,外部 stu 不变。
3. ref 返回值(C# 7.0+)
方法可以返回一个引用,调用方可以直接修改返回的那个变量。
1 2 3 4 5 6 7 8 9 10 11 12
| static ref int FindItem(int[] arr, int index) { return ref arr[index]; }
static void Main() { int[] arr = { 1, 2, 3 }; ref var item = ref FindItem(arr, 1); item = 200; Console.WriteLine(arr[1]); }
|
4. ref 局部变量
直接定义一个指向已有变量的引用:
1 2 3 4
| int x = 100; ref int rx = ref x; rx = 200; Console.WriteLine(x);
|
5. ref in / ref readonly(C# 7.2+)
in:只读引用传递,禁止方法内修改,适合大结构体,避免拷贝开销
ref readonly:返回只读引用,不能修改返回值
1 2 3 4
| static void ReadOnlyFunc(in Point p) { }
|
6. ref vs out 对比(高频面试)
| ref |
out |
| 实参必须预先赋值 |
实参可以不预先赋值 |
| 方法内可读可写 |
方法内必须给 out 参数赋值,调用前值会被忽略 |
| 声明+调用都写 ref |
声明+调用都写 out |
| 适合:需要修改原有变量 |
适合:方法返回多个结果 |
示例 out:
1 2 3 4 5 6 7
| static void GetXY(out int x, out int y) { x = 1; y = 2; }
GetXY(out int a, out int b);
|
⚠️ 常见坑
- 不能把常量作为 ref 参数:
ChangeNum(ref 10) 编译报错,必须传变量
- ref 不能和 async 一起使用:
async 方法不支持 ref/out 参数
- 不要返回局部变量的 ref(会产生危险的悬空引用)
1 2 3 4 5 6
| static ref int BadReturn() { int tmp = 10; return ref tmp; }
|
📌 什么时候用 ref?
- 方法需要修改外部值类型变量(int、struct)
- 方法需要把外部引用变量指向全新对象
- 超大结构体,用
in 减少内存拷贝
- 需要 ref 返回,直接修改容器内元素(高性能场景)
ref / out / 默认传参 完整对比 Demo
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
| using System;
namespace RefOutDemo { class Program { static void NormalPass(int num) { num = 100; Console.WriteLine($"【普通传参】方法内num = {num}"); }
static void RefPass(ref int num) { num = 200; Console.WriteLine($"【ref传参】方法内num = {num}"); }
static void OutPass(out int num) { num = 300; Console.WriteLine($"【out传参】方法内num = {num}"); }
static void Main(string[] args) { int val = 10; Console.WriteLine($"初始 val = {val}\n");
NormalPass(val); Console.WriteLine($"普通传参后 val = {val}\n");
RefPass(ref val); Console.WriteLine($"ref传参后 val = {val}\n");
OutPass(out val); Console.WriteLine($"out传参后 val = {val}"); } } }
|
运行输出
1 2 3 4 5 6 7 8 9 10
| 初始 val = 10
【普通传参】方法内num = 100 普通传参后 val = 10
【ref传参】方法内num = 200 ref传参后 val = 200
【out传参】方法内num = 300 out传参后 val = 300
|
引用类型(class)下三者对比 Demo
重点理解:class本身传的是引用副本,不加ref也能修改对象成员;只有加ref才可以替换整个对象
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
| using System;
namespace RefOutClassDemo { class User { public string Name { get; set; } }
class Program { static void NormalClass(User u) { u.Name = "张三"; u = new User { Name = "新对象" }; Console.WriteLine($"【普通class】方法内u.Name={u.Name}"); }
static void RefClass(ref User u) { u = new User { Name = "李四" }; Console.WriteLine($"【ref class】方法内u.Name={u.Name}"); }
static void OutClass(out User u) { u = new User { Name = "王五" }; Console.WriteLine($"【out class】方法内u.Name={u.Name}"); }
static void Main(string[] args) { User user = new User { Name = "原始名字" }; Console.WriteLine($"初始:{user.Name}\n");
NormalClass(user); Console.WriteLine($"普通class调用后:{user.Name}\n");
RefClass(ref user); Console.WriteLine($"ref class调用后:{user.Name}\n");
OutClass(out user); Console.WriteLine($"out class调用后:{user.Name}"); } } }
|
运行输出
1 2 3 4 5 6 7 8 9 10
| 初始:原始名字
【普通class】方法内u.Name=新对象 普通class调用后:张三
【ref class】方法内u.Name=李四 ref class调用后:李四
【out class】方法内u.Name=王五 out class调用后:王五
|
核心总结表
| 方式 |
值类型(int/struct) |
引用类型(class) |
实参要求 |
方法内要求 |
| 默认传参 |
传值拷贝,修改不影响外部 |
传引用副本,可以修改对象成员;不能替换外部对象 |
变量可赋值可不赋值 |
无强制赋值要求 |
| ref |
传变量地址,修改直接影响外部 |
传变量地址,可以替换外部对象 |
调用前必须赋值 |
可读可写,无强制赋值 |
| out |
传变量地址,修改直接影响外部 |
传变量地址,可以替换外部对象 |
调用前可以不赋值 |
方法内必须赋值 |
面试一句话记忆
- 普通传参:值类型拷贝值,引用类型拷贝引用
- ref:进来之前要有值,方法里随便读写
- out:进来的值没用,方法里必须给值