除了参数,ref关键字还能用在哪些场景?

摘要:《老生常谈:值类型 V.S. 引用类型》中花了很大的篇幅介绍ref参数针对值类型和引用类型变量的传递。在C#中,除了方法的ref参数,我们还有很多使用ref关键字传递引用地址的场景,本篇文章作一个简单的总结。 一、参数 二、数组索引 三、
《老生常谈:值类型 V.S. 引用类型》中花了很大的篇幅介绍ref参数针对值类型和引用类型变量的传递。在C#中,除了方法的ref参数,我们还有很多使用ref关键字传递引用/地址的场景,本篇文章作一个简单的总结。 一、参数 二、数组索引 三、方法 四、ref 结构体 五、ref 结构体字段 一、参数 如果在方法的参数(不论是值类型和引用类型)添加了ref关键字,意味着将变量的地址作为参数传递到方法中。目标方法利用ref参数不仅可以直接操作原始的变量,还能直接替换整个变量的值。如下的代码片段定义了一个基于结构体的Record类型Foobar,并定义了Update和Replace方法,它们具有的唯一参数类型为Foobar,并且前置了ref关键字。 static void Update(ref Foobar foobar) { foobar.Foo = 0; } static void Replace(ref Foobar foobar) { foobar = new Foobar(0, 0); } public record struct Foobar(int Foo, int Bar); 基于ref参数针对原始变量的修改和替换体现在如下所示的演示代码中。 var foobar = new Foobar(1, 2); Update(ref foobar); Debug.Assert(foobar.Foo == 0); Debug.Assert(foobar.Bar == 2); Replace(ref foobar); Debug.Assert(foobar.Foo == 0); Debug.Assert(foobar.Bar == 0); C#中的ref + Type(ref Foobar)在IL中会转换成一种特殊的引用类型Type&。如下所示的是上述两个方法针对IL的声明,可以看出它们的参数类型均为Foobar&。 .method assembly hidebysig static void '<<Main>$>g__Update|0_0' ( valuetype Foobar& foobar ) cil managed .method assembly hidebysig static void '<<Main>$>g__Replace|0_1' ( valuetype Foobar& foobar ) cil managed 二、数组索引 我们知道数组映射一段连续的内存空间,具有相同字节长度的元素“平铺”在这段内存上。我们可以利用索引提取数组的某个元素,如果索引操作符前置了ref关键值,那么返回的就是索引自身的引用/地址。与ref参数类似,我们利用ref array[index]不仅可以修改索引指向的数组元素,还可以直接将该数组元素替换掉。 var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) }; Update(ref array[1]); Debug.Assert(array[1].Foo == 0); Debug.Assert(array[1].Bar == 2); Replace(ref array[1]); Debug.Assert(array[1].Foo == 0); Debug.Assert(array[1].Bar == 0); 由于ref关键字在IL中被被转换成“引用类型”,所以对应的“值”也只能存储在对应引用类型的变量上,引用变量同样通过ref关键字来声明。下面的代码演示了两种不同的变量赋值,前者将Foobar数组的第一个元素的“值”赋给变量foobar(类型为Foobar),后者则将第一个元素在数组中的地址赋值给变量foobarRef(类型为Foobar&)。 var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) }; Foobar foobar = array[0]; ref Foobar foobarRef = ref array[0]; 或者 var foobar = array[0]; ref var foobarRef = ref array[0]; 上边这段C#代码将会转换成如下这段IL代码。
阅读全文