C# 7编程模式与实践(5)

但是对于可变结构体,事情就发生了有意思的变化。首先,这种设计修复了一个老问题,就是会意外地通过属性而修改返回的结构体。但它只是让修改不再产生作用。考虑如下的类:

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集合导致了暂时性中断、弄脏的缓存,并在高度并发系统中引入了高频问题。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/82947a4b5f7533883a344c52de3e78ac.html