从一开始,C#就支持通过值或引用传递参数。 但是在版本7之前,C#编译器仅支持一种从方法(或属性)返回值的方法-按值返回。 在C#7中,情况发生了变化,引入了两个新功能:ref return和ref locals。 有关它们及其性能的更多信息-已删减。

原因
就公共语言运行时而言,数组与其他集合之间有许多差异。 从一开始,CLR就支持数组,并且可以将它们视为内置功能。 CLR环境和JIT编译器可以使用数组,它们还具有另一个功能:数组索引器按引用而不是按值返回元素。
为了证明这一点,我们将不得不转向禁止方法-使用可变值类型:
public struct Mutable { private int _x; public Mutable(int x) => _x = x; public int X => _x; public void IncrementX() { _x++; } } [Test] public void CheckMutability() { var ma = new[] {new Mutable(1)}; ma[0].IncrementX();
测试将成功,因为数组索引器与列表索引器明显不同。
C#编译器为数组索引器ldelema提供了一条特殊指令,该数组返回指向该数组元素的托管链接。 本质上,数组索引器通过引用返回元素。 但是,List不能以相同的方式工作,因为在C#中不可能*返回内部状态的别名。 因此,列表索引器按值返回一个元素,即返回该元素的副本。
*如我们将很快看到的,列表索引器仍然无法通过引用返回元素。
这意味着ma [0] .IncrementX()调用修改数组第一个元素的方法,而ml [0] .IncrementX()调用修改该元素的副本而不影响原始列表的方法。
返回值和参考局部变量:基础
这些函数的含义非常简单:声明返回的引用值使您可以返回现有变量的别名,而引用局部变量可以存储此类别名。
1.一个简单的例子:
[Test] public void RefLocalsAndRefReturnsBasics() { int[] array = { 1, 2 };
2.返回的参考值和只读修饰符
返回的参考值可以返回实例字段的别名,从C#版本7.2开始,您可以返回别名而无需使用ref readonly修饰符写入相应的对象:
class EncapsulationWentWrong { private readonly Guid _guid; private int _x; public EncapsulationWentWrong(int x) => _x = x;
- 方法和属性可以返回内部状态的“别名”。 在这种情况下,不得为该属性定义task方法。
- 通过引用返回中断封装,因为客户端可以完全控制对象的内部状态。
- 通过只读链接返回避免不必要地复制值类型,同时不允许客户端更改内部状态。
- 只读链接可用于引用类型,尽管在非标准情况下这没有多大意义。
3.现有限制。 返回别名可能很危险:在方法完成后对放置在堆栈上的变量使用别名会使应用程序崩溃。 为了使此函数安全,C#编译器应用了各种限制:
- 无法返回链接到局部变量。
- 无法在结构中返回对此的引用。
- 您可以返回指向堆上变量的链接(例如,指向类成员)。
- 您可以返回指向ref / out参数的链接。
有关更多信息,我们建议您检查出色的出版物《
安全的返回规则》,以获取ref返回 。 这篇文章的作者Vladimir Sadov是C#编译器的返回引用函数的创建者。
现在,我们对返回的参考值和参考的局部变量有了一个大致的了解,让我们看一下如何使用它们。
在索引器中使用返回的参考值
为了测试这些功能对性能的影响,我们将创建一个唯一的,不变的集合,称为NaiveImmutableList <T>,并将其与T []和List进行比较,以获取不同大小(4、16、32和48)的结构。
public class NaiveImmutableList<T> { private readonly int _length; private readonly T[] _data; public NaiveImmutableList(params T[] data) => (_data, _length) = (data, data.Length); public ref readonly T this[int idx]
对所有集合执行性能测试,并将每个项目的所有N个属性值相加:
private const int elementsCount = 100_000; private static LargeStruct_48[] CreateArray_48() => Enumerable.Range(1, elementsCount).Select(v => new LargeStruct_48(v)).ToArray(); private readonly LargeStruct_48[] _array48 = CreateArray_48(); [BenchmarkCategory("BigStruct_48")] [Benchmark(Baseline = true)] public int TestArray_48() { int result = 0;
结果如下:

显然,出了点问题! NaiveImmutableList <T>集合的性能与List相同。 怎么了
使用只读修饰符返回值:工作原理
如您所见,NaiveImmutableList <T>索引器使用ref readonly修饰符返回只读链接。 这是完全合理的,因为我们想限制客户更改不可变集合的基础状态的能力。 但是,我们在性能测试中使用的结构不仅可读。
该测试将帮助我们了解基本行为:
[Test] public void CheckMutabilityForNaiveImmutableList() { var ml = new NaiveImmutableList<Mutable>(new Mutable(1)); ml[0].IncrementX();
测试失败! 但是为什么呢? 因为“只读链接”的结构与in修饰符和关于结构的只读字段的结构相似,所以:每次使用结构元素时,编译器都会生成保护性副本。 这意味着ml [0]。 仍会创建第一个元素的副本,但这不是索引器完成的:该副本是在调用点创建的。
这种行为实际上是有道理的。 C#编译器支持使用in修饰符按值,按引用和“只读链接”传递参数(有关详细信息,请参见
C#中的in修饰符和
只读结构 (“
C#中的in修饰符和只读结构“))。 现在,编译器支持三种不同的方法返回值的方法:按值,按引用和按只读链接。
只读链接与常规链接非常相似,因此编译器使用相同的InAttribute区分它们的返回值:
private int _n; public ref readonly int ByReadonlyRef() => ref _n;
在这种情况下,ByReadonlyRef方法可以有效地编译为:
[InAttribute] [return: IsReadOnly] public int* ByReadonlyRef() { return ref this._n; }
in修饰符和只读链接之间的相似性意味着这些函数不太适合常规结构,并且可能导致性能问题。 考虑一个例子:
public struct BigStruct {
在为bigStruct声明变量时,除了语法异常外,代码看起来还不错。 目标很明确:出于性能原因,BigStruct通过引用返回。 不幸的是,由于BigStruct结构是可写的,因此每次访问该项目时都会创建一个保护副本。
在索引器中使用返回的参考值。 尝试次数2
让我们为不同大小的只读结构尝试相同的测试集:

现在,结果变得更有意义了。 对于大型结构,处理时间仍在增加,但这是可以预期的,因为处理10万多个大型结构需要更长的时间。 但是现在NaiveimmutableList <T>的运行时非常接近时间T [],并且比List的运行时好得多。
结论
- 返回的参考值应小心处理,因为它们可能破坏封装。
- 带readonly修饰符的返回参考值仅对只读结构有效。 在常规结构的情况下,可能会出现性能问题。
- 使用可写结构时,每次使用该变量时,带有readonly修饰符的返回参考值都会创建一个保护副本,这可能会导致性能问题。
返回的参考值和参考的局部变量对于库创建者和基础结构代码开发者而言是有用的功能。 但是,在库代码中使用它们非常危险:为了使用通过只读链接有效地返回项目的集合,每个库用户都必须记住:指向可写结构的只读链接会在调用时创建保护性副本”。 在最好的情况下,这将否定生产率的提高,并且在最坏的情况下,如果同时对一个引用局部变量(只读)进行大量请求,则将导致生产率的严重下降。
PS只读链接将出现在BCL中。 在以下请求中提出了用于访问不可变集合中项目的只读ref方法,以包括corefx存储库(
实现ItemRef API提案 (“包括ItemRef API的提案”))中的更改。 因此,每个人都必须了解使用这些功能的功能以及应如何以及何时使用它们,这一点非常重要。