"大菜": 源于自己刚踏入猿途混沌时起, 自我感觉不是一般的菜, 因而得名 "大菜", 于自身共勉.
- c# 基础系列 1--- 深入理解 值类型和引用类型 https://note.youdao.com/
- c# 基础系列 2--- 深入理解 String https://note.youdao.com/
在上篇文章深入理解值类型和引用类型的时候, 有的小伙伴就推荐说一说 ref 和 out 关键字, 昨天晚上彻夜难眠在想是否要谈一下呢, 因为可谈的不是太多, 也可能是我理解的不够深刻.
应用场景
out
修饰函数参数, 以传递引用的方式向函数传递参数.
out 关键字也可与泛型类型参数结合使用, 以指定该类型参数是协变参数
ref
修饰函数参数, 以传递引用的方式向函数传递参数.
在方法签名中, 按引用将值返回给调用方. 此功能在 c# 7.0 中新加. 即: 引用返回值. 例如:
- static ref int GetUserId(int[] allUserid)
- {
- return ref allUserid[1];
- }
修饰局部变量.(c#7.0 新增)
- int x = 3;
- ref int x1 = ref x; // 注意这里, 我们通过 ref 关键字 把 x 赋给了 x1
- x1 = 2;
- Console.WriteLine($"改变后的变量 {nameof(x)} 值为: {x}");
运行结果为 2
在 struct 声明中声明 ref struct 或 ref readonly struct, 来实现值类型的引用语义 (c#7.2 新增, 此处不做讨论)
相同之处
ref 和 out 都可以修饰函数的参数, 指示参数是以引用方式传递的.
ref 和 out 在 c# 编译器下生成的 IL 代码是相同的. 生成的元数据是几乎相同的. 有的网络文章说元数据也是一模一样的其实是错误的. 元数据中其实是有一位来标志是 ref 还是 out 的, 仅仅是一个 bit 的不同. 要不然你以为程序执行时怎么区分是 ref 还是 out 的呢 ^ ~ ^
用 ref 或者 out 修饰的函数参数不能设置默认值. 因为没有办法为这些参数传递一个有意义的默认值.
如果一个函数的参数用 ref 或者 out 所修饰, 那这个函数的调用者传递的参数类型必须和函数定义的相同 (继承关系的也不行). 下列的代码是编译不通过的
- static void Main(string[] args)
- {
- MyClass c = new MyClass();
- // 以下语句编译报错的
- Testref(out c);
- Console.Read();
- }
- static void Testref(out object c1)
- {
- c1 = new object();
- }
- class MyClass
- {
- public int Id { get; set; }
- }
不同之处
编译之后的元数据有一个 bit 的差别, 上边已经提及, 此处不过多阐述.
对于方法的重载, c# 编译器是不允许只有 ref 和 out 区别的重载方法. 以下方法的重载形式是不允许的.
- void test(ref int) { }
- void test(out int) { }
ref 和 out 对于 c# 编译器表达的意图是不一样的. ref 告诉编译器调用函数之前必须初始化; out 则反之, 调用函数之前不必初始化. 这其实也证实了 "ref 用于输入, out 用于输出" 的说法是有一点道理的, 但是不是完全正确, 难道我 ref 不能用于方法的输出吗? 呵呵呵
- class Program
- {
- static void Main(string[] args)
- {
- int i = 10;
- Console.WriteLine(i);
- Test(ref i);
- Test(ref i);
- Console.WriteLine(i);
- Console.Read();
- }
- static void Test(ref int i)
- {
- i += 10;
- }
- }
运行结果:
10 30
所以我认为真正的用法是:
ref 是有进有出, 而 out 是只出不进.
out 修饰的函数变量在被调用方法中初始化之前是只写的 (不能读取), 而且函数执行完之前必须被初始化. 以下方法编译是报错的, 因为没有对 out 参数进行初始化
- static void Testref(out MyClass c1)
- {
- }
ref 则是可写可读的.
性能
其实明白了 ref 和 out 的基本原理和作用之后, 怎么用能提高性能其实已经很明了了. 值类型参数如果比较大, 业务又没有复制修改的需求, 当然以引用方式会比较合理. 至于引用类型无特殊需求场景我觉得没有必要添加 ref 或者 out 来画蛇添足.
其他
ref 和 out 修饰引用类型参数
有的同学会问, 引用类型参数本来不就是以引用方式传递的吗, 在加 ref 或者 out 标记有什么意义吗? 的确, 大多数情况下引用类型参数的传递没有必要添加 ref 或者 out 来修饰, 但是如果方法体内我修改了指针的指向的内存地址, 我们的方法调用者又需要这个新的内存地址呢? 例如: 以下为一个连续读取 N 个文件的需求:
- static void Main(string[] args)
- {
- FileStream fs = new FileStream("新文件地址", FileMode.Open);
- // 第一个文件的操作省略
- // 接下来连续读取 N 个文件内容
- bool isHaveFile = true;
- for (; fs!=null; ReadFile(ref fs, isHaveFile))
- {
- fs.Read(.............);
- // 添加判断是否还有文件可读
- // isHaveFile = false;
- }
- Console.Read();
- }
- static void ReadFile(ref FileStream fs,bool isHaveFile=true)
- {
- fs.Close();
- if (!isHaveFile) fs = null;
- else
- {
- fs = new FileStream("新文件地址", FileMode.Open);
- }
- }
可见引用类型添加 ref 或者 out 在一些特定场景下是有必要的.
ref 和 out 修饰值类型参数
为值类型参数指定 ref 或者 out, 效果等同于以传值的方式传递引用类型, 也就是说指向类型实例的指针是复制操作, 实例的值并没有发生复制操作.
关于值类型指针
有的同学会有疑问, 值类型变量其实就是值本身, 为什么也会有指针呢? 这种问题其实自己仔细一想就会知道, 无论值类型还是引用类型值都是分配在内存中, 就会有内存地址的概念. 既然都有内存地址, 那就无所谓值类型和引用类型了
c#7.0+
关于 c# 7.0 + 中对 ref 或者 out 功能的增强请自行查找 msdn.
疑问???
有人说 ref 或者 out 修饰的引用类型参数在传递时候, 指向类型实例的指针并不发生 copy 操作, 也就是说调用方法之前和方法体中的指针的指针是一个, 目前本人学疏才浅并不这么认为, 是否有大神可以留言指点一二?
以上都是非生产环境测试结果, 如果错误, 请及时指正
来源: https://www.cnblogs.com/zhanlang/p/9651132.html