但是对于可变结构体,事情就发生了有意思的变化。首先,这种设计修复了一个老问题,就是会意外地通过属性而修改返回的结构体。但它只是让修改不再产生作用。考虑如下的类:
public class Shape { Rectangle m_Size; public Rectangle Size { get { return m_Size; } } } var s = new Shape(); s.Size.Width = 5;在C# 1中,Size类不能更改。在C# 6中,代码会触发一个编译器错误。而在C# 7中,只需添加ref就能正常运行。代码如下:
public ref Rectangle Size { get { return ref m_Size; } }第一眼看去,代码像是会立刻阻止覆写Size。但事实上,你依然可以编写如下的代码:
var rect = new Rectangle(0, 0, 10, 20); s.Size = rect;虽然属性是“只读”的,但是代码会按预期运行。编译器能理解代码并不会返回一个Rectangle对象,而是返回一个指向保存Rectangle对象位置的指针。
现在还有一个问题,就是其中的不可变结构体不再是不可变了。尽管我们不能更改单个字段,但是可以通过引用属性替换整个值。C#禁止该语法并给出警告。例如:
readonly int m_LineThickness; public ref int LineThickness { get { return ref m_LineThickness; } }鉴于C#并没有提供类似于只读引用返回的定义,因此不能创建指向只读字段的引用。
引用返回和索引器(Indexer)引用返回和局部引用都需要给定一个固定的引用点,这可能是它们的最大局限性所在。考虑下面的代码:
ref int x = ref myList[0];该代码是无效的。因为列表不同于数组,在读取列表值时,会创建结构体的一个副本。下面是List<T>的实际实现,引用自:
public T this[int index] { get { // 下面的编码技巧可以减少一次范围检查。 if ((uint) index >= (uint)_size) { ThrowHelper.ThrowArgumentOutOfRangeException(); } Contract.EndContractBlock(); return _items[index]; <-- 返回做了一个拷贝。 }这同样适用于ImmutableArray<T>,以及通过IList<T>接口访问正常数组。但是,你可以实现自己的List<T> ,将索引声明为引用返回。代码如下:
public ref T this[int index] { get { // 下面的编码技巧可以减少一次范围检查。 if ((uint) index >= (uint)_size) { ThrowHelper.ThrowArgumentOutOfRangeException(); } Contract.EndContractBlock(); return ref _items[index]; <-- 以指针形式返回引用。 }如果采取这一做法,需要显式地实现IList<T>和IReadOnlyList<T>接口。因为引用返回的签名不同于普通返回值,并不能满足接口的要求。
鉴于索引器事实上只是一种特殊的属性,因此具有和引用属性一样的限制。这意味着,你不能显式地声明名称以set为开头的函数(即setter)。同时,索引器也是可写的。
引用返回、局部引用和引用属性的指导原则考虑对操作数组的函数使用引用返回,而不是索引值。
考虑在具有结构体的自定义集合类中使用引用返回,而不是正常的返回。
要将包含可变结构体的属性暴露为引用属性。
X 不要将包含不可变结构体的属性暴露为引用属性。
X 不要在不可变类或只读类上暴露引用属性。
X 不要在不可变或只读集合类上暴露引用索引器。
ValueTask和通用异步返回类型(Generalized Async Return Type)创建Task类主要针对简化多线程编程。Task类创建了一个通道,使得开发人员可以将耗时长的操作推入线程池中,并稍后在UI线程中读回结果。Task类在fork-join风格的并发编程中效果显著。
但是随着.NET 4.5中引入了async/await,Task类的一些缺陷开始显现。正如我们曾在2011年就撰文指出的(参见“.NET 4.5中任务并行类库的改进”一文),创建Task对象所需时间会超出我们可接受的范围,需要对Task类的内部实现机制进行重写。重写后达到了“Task<Int32>的创建时间降低了49-55%,对象的大小减少了52%。”
这一步非常好,但Task类依然需要分配内存。如果在更紧凑的循环中使用Task类,依然会生成大量的垃圾。下面给出一个这样的例子:
while (await stream.ReadAsync(buffer, offset, count) != 0) { //处理缓存。 }在前文中多次提及,高性能C#代码的关键在于降低内存分配,并减少随后的GC循环。Microsoft的Joe Duffy在博客文章“异步化所有事情”中是这样写的���
首先,大家是否还记得曾经的Midori项目。Midori要实现的是一个完整的操作系统,有效地使用垃圾回收所得到的内存。从该项目中,我们学到了适当运作此类项目的关键经验教训。我要强调的一点,应该像避免瘟疫一样避免夸大的内存分配,即使是短生命的内存分配。早期在.NET领域有一个广泛传播的口头禅:“Gen0集合是无价的”。不幸的是,这句话影响了很多的.NET库代码,完全驴头不对马嘴。Gen0集合导致了暂时性中断、弄脏的缓存,并在高度并发系统中引入了高频问题。