环球精选!除了参数,ref关键字还可以用在什么地方?
2023-07-03 11:12:22 来源: 博客园
《老生常谈:值类型 V.S. 引用类型》中花了很大的篇幅介绍ref参数针对值类型和引用类型变量的传递。在C#中,除了方法的ref参数,我们还有很多使用ref关键字传递引用/地址的场景,本篇文章作一个简单的总结。
一、参数一、参数二、数组索引三、方法四、ref 结构体五、ref 结构体字段
如果在方法的参数(不论是值类型和引用类型)添加了ref关键字,意味着将变量的地址作为参数传递到方法中。目标方法利用ref参数不仅可以直接操作原始的变量,还能直接替换整个变量的值。如下的代码片段定义了一个基于结构体的Record类型Foobar,并定义了Update和Replace方法,它们具有的唯一参数类型为Foobar,并且前置了ref关键字。
(相关资料图)
static void Update(refFoobar foobar){ foobar.Foo = 0;}static void Replace(refFoobar 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 staticvoid "<二、数组索引$>g__Update|0_0" (valuetype Foobar&foobar) cil managed.method assembly hidebysig staticvoid "< $>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];refFoobar foobarRef = ref array[0];或者var foobar = array[0];refvar foobarRef = ref array[0];
上边这段C#代码将会转换成如下这段IL代码。我们不仅可以看出foobar和foobarRef声明的类型的不同(Foobar和Foobar&),还可以看到array[0]和ref array[0]使用的IL指令的差异,前者使用的是ldelem(Load Element)后者使用的是ldelema(Load Element Addess)。
.method private hidebysig staticvoid "三、方法$" (string[] args) cil managed{// Method begins at RVA 0x209c// Header size: 12// Code size: 68 (0x44).maxstack 5.entrypoint.locals init ([0] valuetype Foobar[] "array",[1] valuetype Foobarfoobar,[2] valuetype Foobar&foobarRef)// {IL_0000: ldc.i4.3// (no C# code)IL_0001: newarr FoobarIL_0006: dupIL_0007: ldc.i4.0// Foobar[] array = new Foobar[3]// {// new Foobar(1, 1),// new Foobar(2, 2),// new Foobar(3, 3)// };IL_0008: ldc.i4.1IL_0009: ldc.i4.1IL_000a: newobj instance void Foobar::.ctor(int32, int32)IL_000f: stelem FoobarIL_0014: dupIL_0015: ldc.i4.1IL_0016: ldc.i4.2IL_0017: ldc.i4.2IL_0018: newobj instance void Foobar::.ctor(int32, int32)IL_001d: stelem FoobarIL_0022: dupIL_0023: ldc.i4.2IL_0024: ldc.i4.3IL_0025: ldc.i4.3IL_0026: newobj instance void Foobar::.ctor(int32, int32)IL_002b: stelem FoobarIL_0030: stloc.0// Foobar foobar = array[0];IL_0031: ldloc.0IL_0032: ldc.i4.0IL_0033: ldelemFoobarIL_0038: stloc.1// ref Foobar reference = ref array[0];IL_0039: ldloc.0IL_003a: ldc.i4.0IL_003b: ldelemaFoobarIL_0040: stloc.2// (no C# code)IL_0041: nop// }IL_0042: nopIL_0043: ret} // end of method Program::" $"
方法可以通过前置的ref关键字返回引用/地址,比如变量或者数组元素的引用/地址。如下面的代码片段所示,方法ElementAt返回指定Foobar数组中指定索引的地址。由于该方法返回的是数组元素的地址,所以我们利用返回值直接修改对应数组元素(调用Update方法),也可以直接将整个元素替换掉(调用Replace方法)。如果我们查看ElementAt基于IL的声明,同样会发现它的返回值为Foobar&
var array = new Foobar[] { new Foobar(1, 1), new Foobar(2, 2), new Foobar(3, 3) };var copy = ElementAt(array, 1);Update(ref copy);Debug.Assert(array[1].Foo == 2);Debug.Assert(array[1].Bar == 2);Replace(ref copy);Debug.Assert(array[1].Foo == 2);Debug.Assert(array[1].Bar == 2);ref var self = ref ElementAt(array, 1);Update(ref self);Debug.Assert(array[1].Foo == 0);Debug.Assert(array[1].Bar == 2);Replace(ref self);Debug.Assert(array[1].Foo == 0);Debug.Assert(array[1].Bar == 0);static refFoobar ElementAt(Foobar[] array, int index) => ref array[index];四、ref 结构体
如果在定义结构体时添加了前置的ref关键字,那么它就转变成一个ref结构体。ref结构体和常规结构最根本的区别是它不能被分配到堆上,并且总是以引用的方式使用它,永远不会出现“拷贝”的情况,最重要的ref 结构体莫过于Span
public refstruct Foobar{ public int Foo { get; } public int Bar { get; } public Foobar(int foo, int bar) { Foo = foo; Bar = bar; }}
ref结构体具有很多的使用约束。对于这些约束,很多人不是很理解,其实我们只需要知道这些约束最终都是为了确保:ref结构体只能存在于当前线程堆栈,而不能转移到堆上。基于这个原则,我们来具体来看看ref结构究竟有哪些使用上的限制。
1. 不能作为泛型参数
除非我们能够显式将泛型参数约束为ref结构体,对应的方法严格按照ref结构的标准来操作对应的参数或者变量,我们才能够能够将ref结构体作为泛型参数。否则对于泛型结构体,涉及的方法肯定会将其当成一个常规结构体看待,若将ref结构体指定为泛型参数类型自然是有问题。但是针对ref结构体的泛型约束目前还没有,所以我们就不能将ref结构体作为泛型参数,所以按照如下的方式创建一个Wrapper
// ErrorCS0306The type "Foobar" may not be used as a type argumentvar wrapper = new Wrapper(new Foobar(1, 2));public class Wrapper { public Wrapper(T value) => Value = value; public T Value { get; }}
2. 不能作为数组元素类型
数组是分配在堆上的,我们自然不能将ref结构体作为数组的元素类型,所以如下的代码也会遇到编译错误。
//ErrorCS0611Array elements cannot be of type "Foobar"var array = new Foobar[16];
3. 不能作为类型和非ref结构体数据成员
由于类的实例分配在堆上,常规结构体也并没有纯栈分配的约束,ref结构体自然不能作为它们的数据成员,所以如下所示的类和结构体的定义都是不合法的。
public class Foobarbaz{ //ErrorCS8345Field or auto-implemented property cannot be of type "Foobar" unless it is an instance member of a ref struct.public Foobar Foobar { get; } public int Baz { get; } public Foobarbaz(Foobar foobar, int baz) { Foobar = foobar; Baz = baz; }}
或者
public structureFoobarbaz{ //ErrorCS8345Field or auto-implemented property cannot be of type "Foobar" unless it is an instance member of a ref struct.public Foobar Foobar { get; } public int Baz { get; } public Foobarbaz(Foobar foobar, int baz) { Foobar = foobar; Baz = baz; }}
4. 不能实现接口
当我们以接口的方式使用某个结构体时会导致装箱,并最终导致堆分配,所以ref结构体不能实现任意接口。
//Error CS8343 "Foobar": ref structs cannot implement interfacespublic ref struct Foobar : IEquatable{ public int Foo { get; } public int Bar { get; } public Foobar(int foo, int bar) { Foo = foo; Bar = bar; } public bool Equals(Foobar other) => Foo == other.Foo && Bar == other.Bar;}
5. 不能导致装箱
所有类型都默认派生自object,所有值类型派生自ValueType类型,但是这两个类型都是引用类型(ValueType自身是引用类型),所以将ref结构体转换成object或者ValueType类型会导致装箱,是无法通过编译的。
//ErrorCS0029Cannot implicitly convert type "Foobar" to "object"Object obj = new Foobar(1, 2);//ErrorCS0029Cannot implicitly convert type "Foobar" to "System.ValueType"ValueType value = new Foobar(1, 2);
6. 不能在委托中(或者Lambda表达式)使用
ref结构体的变量总是引用存储结构体的栈地址,所以它们只有在创建该ref结构体的方法中才有意义。一旦方法返回,堆栈帧被回收,它们自然就“消失”了。委托被认为是一个待执行的操作,我们无法约束它们必须在某方法中执行,所以委托执行的操作中不能引用ref结构体。从另一个角度来讲,一旦委托中涉及针对现有变量的引用,必然会导致“闭包”的创建,也就是会创建一个类型来对引用的变量进行封装,这自然也就违背了“不能将ref结构体作为类成员”的约束。这个约束同样应用到Lambda表达式和本地方法上。
public class Program{ static void Main() { var foobar = new Foobar(1, 2); //Error CS8175 Cannot use ref local "foobar" inside an anonymous method, lambda expression, or query expressionAction action1 = () => Console.WriteLine(foobar); //Error CS8175 Cannot use ref local "foobar" inside an anonymous method, lambda expression, or query expressionvoid Print() => Console.WriteLine(foobar); }}
7. 不能在async/await异步方法中
这个约束与上一个约束类似。一般来说,一个异步方法执行过程中遇到await语句就会字节返回,后续针对操作具有针对ref结构体引用,自然是不合法的。从另一方面来讲,async/await最终会转换成基于状态机的类型,依然会出现利用自动生成的类型封装引用变量的情况,同样违背了“不能将ref结构体作为类成员”的约束。
async Task InvokeAsync(){ await Task.Yield(); //ErrorCS4012Parameters or locals of type "Foobar" cannot be declared in async methods or async lambda var foobar = new Foobar(1, 2);}
值得一提的是,对于返回类型为Task的异步方法,如果没有使用async关键字,由于它就是一个普通的方法,编译器并不会执行基于状态机的代码生成,所以可以自由地使用ref结构体。
public Task InvokeAsync(){ var foobar = new Foobar(1, 2); ... return Task.CompletedTask;}
8. 不能在迭代器中使用
如果在一个返回IEnumerable
IEnumerable<(int Foo, int Bar)> Deconstruct(Foobar foobar1, Foobar foobar2){ //ErrorCS4013Instance of type "Foobar" cannot be used inside a nested function, query expression, iterator block or async methodyield return (foobar1.Foo, foobar1.Bar); //ErrorCS4013Instance of type "Foobar" cannot be used inside a nested function, query expression, iterator block or async methodyield return (foobar2.Foo, foobar2.Bar);}
9. readonly ref 结构体
顺表补充一下,我们可以按照如下的方式添加前置的readonly关键字定义一个只读的ref结构体。对于这样的结构体,其数据成员只能在被构造或者被初始化的时候进行指定,所以只能定义成如下的形式。
public readonlyrefstruct Foobar{ public int Foo { get; } public int Bar { get; } public Foobar(int foo, int bar) { Foo = foo; Bar = bar; }}
public readonlyrefstruct Foobar{ public int Foo { get; init; } public int Bar { get; init; }}
public readonly refstruct Foobar{ public readonlyint Foo; public readonlyint Bar; public Foobar(int foo, int bar) { Foo = foo; Bar = bar; }}
如果为属性定义了set方法,或者其字段没有设置成“只读”,这样的readonly ref 结构体均是不合法的。
public readonly ref struct Foobar{ //ErrorCS8341Auto-implemented instance properties in readonly structs must be readonly.public int Foo { get; set; } //ErrorCS8341Auto-implemented instance properties in readonly structs must be readonly.public int Bar { get; set; }}
public readonly ref struct Foobar{ //ErrorCS8340Instance fields of readonly structs must be readonly.public int Foo; //ErrorCS8340Instance fields of readonly structs must be readonly.public int Bar;}五、ref 结构体字段
我们可以在ref结构体的字段成员前添加ref关键字使之返回一个引用。除此之外,我们还可以进一步添加readonly关键字创建“只读引用字段”,并且这个readonly关键可以放在ref后面(ref readonly),也可以放在ref前面(readonly ref),还可以前后都放(readonly ref readonly)。如果你之前没有接触过ref字段,是不是会感到很晕?希望一下的内容能够为你解惑。上面的代码片段定义了一个名为RefStruct的ref 结构体,定义其中的四个字段(Foo、Bar、Baz和Qux)都是返回引用的ref 字段。除了Foo字段具有具有可读写的特性外,我们采用上述三种不同的形式将其余三个字段定义成“自读”的。
public ref struct RefStruct{ public ref KV Foo; public ref readonly KV Bar; public readonly ref KV Baz; public readonly ref readonly KV Qux; public RefStruct(ref KV foo, ref KV bar, ref KV baz, ref KV qux) { Foo = ref foo; Bar = ref bar; Baz = ref baz; Qux = ref qux; }}public struct KV{ public int Key; public int Value; public KV(int key, int value) { Key = key; Value = value; }}
1. Writable
在如下的演示代码中,我们针对同一个KV对象的引用创建了RefStruct。在直接修改Foo字段返回的KV之后,由于四个字段引用的都是同一个KV,所以其余三个字段都被修改了。由于Foo字段是可读可写的,所以当我们为它指定一个新的KV后,其他三个字段也被替换了。
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);value.Foo.Key = 1;value.Foo.Value = 1;Debug.Assert(kv.Key == 1);Debug.Assert(kv.Value == 1);Debug.Assert(value.Foo.Key == 1);Debug.Assert(value.Foo.Value == 1);Debug.Assert(value.Bar.Key == 1);Debug.Assert(value.Bar.Value == 1);Debug.Assert(value.Baz.Key == 1);Debug.Assert(value.Baz.Value == 1);Debug.Assert(value.Qux.Key == 1);Debug.Assert(value.Qux.Value == 1);value.Foo = new KV(2, 2);Debug.Assert(kv.Key == 2);Debug.Assert(kv.Value == 2);Debug.Assert(value.Foo.Key == 2);Debug.Assert(value.Foo.Value == 2);Debug.Assert(value.Bar.Key == 2);Debug.Assert(value.Bar.Value == 2);Debug.Assert(value.Baz.Key == 2);Debug.Assert(value.Baz.Value == 2);Debug.Assert(value.Qux.Key == 2);Debug.Assert(value.Qux.Value == 2);
2. ref readonly
第一个字段被定义成“ref readonly”,readonly被置于ref之后,表示readonly并不是用来修饰ref,而是用来修饰引用指向的KV对象,它使我们不能修改KV对象的数据成员。所以如下的代码是不能通过编译的。
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);//ErrorCS8332Cannot assign to a member of field "Bar" or use it as the right hand side of a ref assignment because it is a readonly variablevalue.Bar.Key = 2;//ErrorCS8332Cannot assign to a member of field "Bar" or use it as the right hand side of a ref assignment because it is a readonly variablevalue.Bar.Value = 2;
但是这仅仅能够保证我们不能直接通过字段进行修改而已,我们依然可以通过将字段赋值给另一个变量,利用这个变量依然达到更新该字段的目的。
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);kv = value.Bar;kv.Key = 1;kv.Value = 1;Debug.Assert(value.Baz.Key == 1);Debug.Assert(value.Baz.Value == 1);
由于readonly并不是修饰引用本身,所以我们采用如下的方式通过修改引用达到替换字段的目的。
KV kv = default;KV another = new KV(1,1);var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);value.Bar = ref another;Debug.Assert(value.Bar.Key == 1);Debug.Assert(value.Bar.Key == 1);
3. readonly ref
如果readonly被置于ref前面,就意味着引用本身,所以针对Baz字段的赋值是不合法的。
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);KV another = new KV(1, 1);//ErrorCS0191A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)value.Baz = ref another;
但是引用指向的KV对象是可以直接通过字段进行修改。
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);value.Baz.Key = 1;value.Baz.Value = 1;Debug.Assert(value.Baz.Key == 1);Debug.Assert(value.Baz.Key == 1);
4. readonly ref readonly
现在我们知道了ref前后的readonly分别修饰的是字段返回的引用和引用指向的目标对象,所以对于readonly ref readonly修饰的字段Qux,我们既不能字节将其替换成指向另一个KV的引用,也不能直接利用它修改该字段指向的KV对象。
KV kv = default;var another = new KV(1, 1);var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);//ErrorCS0191A readonly field cannot be assigned to (except in a constructor or init-only setter of the type in which the field is defined or a variable initializer)value.Qux = ref another;
KV kv = default;var value = new RefStruct(ref kv, ref kv, ref kv, ref kv);//ErrorCS8332Cannot assign to a member of field "Qux" or use it as the right hand side of a ref assignment because it is a readonly variablevalue.Qux.Key = 1;//ErrorCS8332Cannot assign to a member of field "Qux" or use it as the right hand side of a ref assignment because it is a readonly variablevalue.Qux.Value = 1;
标签:
[责任编辑:]
猜你喜欢
- (2023-07-03)亚龙湾公主郡PK生资花园升值前景哪个好?在三亚市投资买房划算吗?
- (2023-07-03)消息称新 AirPods Pro 将配备体温计和 USB-C 接口|焦点观察
- (2023-07-03)要闻:想看钢铁侠2(钢铁侠2快播)
- (2023-07-03)9月17日是什么星座农历_9月17日是什么星座|天天报道
- (2023-07-03)【公开市场操作】央行开展50亿7天期逆回购操作,中标利率1.9%,与上期持平(07-03) 微动态
- (2023-07-03)当前要闻:黑客攻防从入门到精通全套_黑客攻防从入门到精通
- (2023-07-03)交通先行 促乡村振兴 前沿热点