在 C# 中,参数传递方式是一个基础却极易产生误解的知识点,尤其是 ref 与 out。
很多初学者会简单地把它们理解为“按引用传递”,却忽略了编译器约束、数据流方向以及与引用类型的真实关系,从而在实际开发中频繁踩坑
本文将从以下三个层面系统梳理 ref 与 out:
- 核心语义与编译器约束
- 基础用法与实战示例
- 引用类型(尤其是 string)的特殊行为与本质原因
一、ref 与 out 的核心区别(从“数据流”理解)
ref 与 out 的共同点是:
方法可以直接操作“调用方变量本身”,而不是它的副本。
但二者的设计目标不同,因此编译器施加了不同的约束。
1️⃣ 核心差异对照表
| 对比维度 | ref | out |
|---|---|---|
| 调用前是否必须初始化 | 必须初始化 | 不要求初始化 |
| 方法内是否必须赋值 | 否 | 是(强制) |
| 数据流方向 | 传入 + 传出 | 仅传出 |
| 语义侧重点 | 修改已有值 | 生成新值 |
2️⃣ 本质总结
- ref:双向传递
-- 调用方负责初始化
-- 方法可以读取、也可以修改 - out:单向输出
-- 调用方不关心初始值
-- 方法必须负责赋值
可以将二者理解为:
ref = “我给你一个值,你可以在它的基础上改”
out = “你来负责给我一个结果”
二、基础用法实战验证
1️⃣ 未带ref 和 out
internal class Program
{
static void Main(string[] args)
{
string a = "123";
List<int> i = new List<int>() { 1, 3, 12, 12, 1 };
Person person = new Person() { Id = 1 ,Name = "邓培"};
aAction(a, i,person);
Console.WriteLine(a);
Console.WriteLine(string.Join(" ", i));
Console.WriteLine(JsonConvert.SerializeObject(person,Formatting.Indented));
Console.ReadKey();
}
static void aAction(string a, List<int> i,Person person)
{
a = "123213";
i = new List<int>() { 1, 21, 312, 312 };
person.Name = "罗倩";
}
}
public class Person
{
public int Id { get; set; }
public string Name { get; set; }
}

注意:整体修改值不会发生改变

2️⃣ ref:典型的“读 + 改”场景
class RefDemo
{
static void Main()
{
int num = 10; // 必须初始化
Modify(ref num);
Console.WriteLine(num); // 20
}
static void Modify(ref int value)
{
value *= 2; // 既能读,也能写
}
}
关键点:
- num 在调用前必须有值
- 方法内部可以使用该值进行计算
- 修改结果会同步回调用方
3️⃣ out:典型的“只负责输出结果”
class OutDemo
{
static void Main()
{
int result; // 可不初始化
CreateValue(out result);
Console.WriteLine(result); // 100
}
static void CreateValue(out int value)
{
value = 100; // 必须赋值,否则编译失败
}
}
关键点:
- 调用方无需提供初始值
- 方法必须在返回前完成赋值
- 编译器会强制检查
三、引用类型的特殊处理:常见误区澄清
很多问题并不来自 ref/out,而是来自对“引用类型传参”的误解。
1️⃣ 重要前提:默认情况下传的是什么?
C# 默认是“值传递”
对引用类型而言,传递的是引用本身的副本
也就是说:
- 外部变量保存的是一个“引用”
- 方法参数拿到的是这个“引用的拷贝”
四、无 ref / out 时的两种典型行为
场景一:修改引用指向 ❌(不生效)
class Demo
{
static void Main()
{
// 初始化原始变量
string s = "old value";
List<int> list = new List<int> { 4, 5, 6 };
// 调用方法尝试修改引用指向
Change(s, list);
// 输出结果:原始变量完全没变化
Console.WriteLine(s); // 输出:old value
Console.WriteLine(string.Join(",", list)); // 输出:4,5,6
}
static void Change(string s, List<int> list)
{
// 修改的是「参数副本」的引用指向
s = "new value"; // 让参数s的副本指向新字符串
list = new List<int> { 1, 2, 3 }; // 让参数list的副本指向新List
}
}
调用后:
string 没变
List 没变
原因:
- 修改的是
参数副本的指向 - 外部变量的引用未被改变
场景二:修改对象内容 ✅(生效)
class Demo
{
static void Main()
{
// 初始化原始Person对象
Person p = new Person { Id = 1, Name = "旧名字" };
// 调用方法修改对象内容
Change(p);
// 输出结果:对象内容被修改
Console.WriteLine(p.Name); // 输出:新名字
}
static void Change(Person p)
{
// 修改的是「对象内部状态」,而非引用本身
p.Name = "新名字";
}
}
class Person
{
public int Id { get; set; }
public string Name { get; set; }
}
原因:
p与外部变量指向同一对象- 修改的是对象内部状态,而非引用本身
五、string 为什么“像值类型一样”?
string 是引用类型,但它是:
不可变(immutable)对象
using System;
// 示例1:无 ref 时,修改 string 引用指向仅作用于方法内副本
class StringImmutableDemo
{
static void Main()
{
// 1. 初始化原始字符串变量
string originalStr = "123";
Console.WriteLine($"【调用方法前】原始变量 originalStr = {originalStr}");
// 输出:【调用方法前】原始变量 originalStr = 123
// 2. 调用方法尝试修改字符串
TryChangeString(originalStr);
// 3. 调用方法后,原始变量无变化
Console.WriteLine($"【调用方法后】原始变量 originalStr = {originalStr}");
// 输出:【调用方法后】原始变量 originalStr = 123
}
/// <summary>
/// 无 ref:修改的是方法内参数副本的引用指向
/// </summary>
static void TryChangeString(string strParam)
{
// 注意:不是修改原有"123"的内容,而是创建新字符串"456"
strParam = "456";
Console.WriteLine($"【方法内部】参数副本 strParam = {strParam}");
// 输出:【方法内部】参数副本 strParam = 456
}
}
运行输出
【调用方法前】原始变量 originalStr = 123
【方法内部】参数副本 strParam = 456
【调用方法后】原始变量 originalStr = 123
这不是“修改字符串”,而是:
- 创建新字符串
"456" - 让变量
s指向新对象
因此:
- 不加
ref→ 只修改局部副本 - 加
ref→ 才能修改外部引用指向
六、加 ref 后:修改引用指向生效
using System;
using System.Collections.Generic;
// 示例2:加 ref 后,修改 string/List 的引用指向生效
class RefChangeReferenceDemo
{
static void Main()
{
// 1. 初始化原始变量
string originalStr = "123";
List<int> originalList = new List<int> { 9, 9 };
Console.WriteLine("【调用方法前】");
Console.WriteLine($"原始字符串 originalStr = {originalStr}");
Console.WriteLine($"原始列表 originalList = [{string.Join(",", originalList)}]");
// 输出:
// 原始字符串 originalStr = 123
// 原始列表 originalList = [9,9]
// 2. 加 ref 调用方法,修改引用指向
ChangeReference(ref originalStr, ref originalList);
Console.WriteLine("\n【调用方法后】");
Console.WriteLine($"原始字符串 originalStr = {originalStr}");
Console.WriteLine($"原始列表 originalList = [{string.Join(",", originalList)}]");
// 输出:
// 原始字符串 originalStr = 456
// 原始列表 originalList = [1,2,3]
}
/// <summary>
/// 加 ref:直接操作原始变量的引用指向
/// </summary>
static void ChangeReference(ref string strParam, ref List<int> listParam)
{
// 直接修改原始字符串变量的指向(新建"456"并让原始变量指向它)
strParam = "456";
// 直接修改原始列表变量的指向(新建List并让原始变量指向它)
listParam = new List<int> { 1, 2, 3 };
}
}
运行输出
【调用方法前】
原始字符串 originalStr = 123
原始列表 originalList = [9,9]
【调用方法后】
原始字符串 originalStr = 456
原始列表 originalList = [1,2,3]
结果:
s指向新字符串list指向新集合
本质:
ref传递的不是“引用的副本”,而是引用变量本身
七、ref 与 out 的典型适用场景
适合使用 ref 的情况
- 需要在原有值基础上修改
- 需要替换引用类型的整体对象
- 典型场景:交换变量、缓存对象复用
适合使用 out 的情况
- 方法职责是“生成结果”
- 不关心调用前的初始值
- 返回多个值但不想定义新类型
bool success = int.TryParse("123", out int value);
八、实践注意事项(非常重要)
ref/ out 必须调用方 + 方法签名同时声明out在 C# 7+ 支持内联声明- 避免滥用
--会降低可读性
--会增加理解成本
--优先考虑返回值或 DTO
总结(核心记忆点)
ref:双向传递,调用前必须初始化out:单向输出,方法内必须赋值- 默认传递的是引用的副本
- 修改对象内容 ≠ 修改引用指向
string因不可变,行为更接近值类型- 修改引用指向 → 必须使用
ref / out
如果你能在脑中始终区分清楚这三点:
值 / 引用 / 引用的副本
那么 ref 与 out 将不再是坑,而是工具。
792

被折叠的 条评论
为什么被折叠?



