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

X 对性能敏感的代码中,应避免使用具有委托的闭包。这一原则同样适用于匿名函数和局部函数。

引用返回(Ref Return)、局部引用(Ref Local)和引用属性(Ref Property)

结构体具有一些有意思的性能特性。由于结构体的存储与其父数据结构一致,因此没有正常对象那样的头部开销。这意味着可以将结构体密集地打包到一个数组中,这样很少的或几乎没有空间浪费。这种设计不但降低了整体内存开销,而且提供了极大的本地性,使得CPU的微小缓存得到了很好的利用。这就是结构体颇受高性能应用开发人员喜爱的原因所在。

但是如果结构体过于庞大,这时就必须提高警惕,避免生成不必要的结构体拷贝。Microsoft的指南中给出的建议大小是16个字节,足够存储两个双精度型或是四个整型。16个字节并不多,如有必要可使用位域(Bit-field)进行扩展。

对可变结构体要尤为谨慎。如果在使用可变结构体时想要修改原始结构体中的数据,非常容易意外地更改结构体的拷贝。

局部引用

一种可行的做法是使用智能指针,这样永远不需要生成拷贝。下面给出了一些对性能敏感的代码,来自于我曾开发的一个ORM项目:

for (var i = 0; i < m_Entries.Length; i++) { if (string.Equals(m_Entries[i].Details.ClrName, item.Key, StringComparison.OrdinalIgnoreCase) || string.Equals(m_Entries[i].Details.SqlName, item.Key, StringComparison.OrdinalIgnoreCase)) { var value = item.Value ?? DBNull.Value; if (value == DBNull.Value) { if (!ignoreNullProperties) parts.Add($"{m_Entries[i].Details.QuotedSqlName} IS NULL"); } else { m_Entries[i].ParameterValue = value; m_Entries[i].UseParameter = true; parts.Add($"{m_Entries[i].Details.QuotedSqlName} = {m_Entries[i].Details.SqlVariableName}"); } found = true; keyFound = true; break; } }

你首先会注意到,代码中并没有使用for-each语句。为避免拷贝的开销,代码必须使用旧类型的循环。即便如此,所有的读取和写入也是在m_Entries数组值上直接执行的。

使用C# 7的局部引用,可以在不更改语义的情况下显著地减少混乱。例如:

for (var i = 0; i < m_Entries.Length; i++) { ref Entry entry = ref m_Entries[i]; //创建一个引用 if (string.Equals(entry.Details.ClrName, item.Key, StringComparison.OrdinalIgnoreCase) || string.Equals(entry.Details.SqlName, item.Key, StringComparison.OrdinalIgnoreCase)) { var value = item.Value ?? DBNull.Value; if (value == DBNull.Value) { if (!ignoreNullProperties) parts.Add($"{entry.Details.QuotedSqlName} IS NULL"); } else { entry.ParameterValue = value; entry.UseParameter = true; parts.Add($"{entry.Details.QuotedSqlName} = {entry.Details.SqlVariableName}"); } found = true; keyFound = true; break; } }

这是因为“局部引用”本身就是一个安全的指针。我们称之为“安全”,是因为编译器禁止它指向任何短暂(Ephemeral)类型,例如一般函数的返回结果。

你可能会考虑,是否可以使用“ref var entry = ref m_Entries[i];”。虽然在语法上是合法的,但是你却不能这样做。因为这样会在代码中引发混乱。在声明和表达式中,或者全部使用引用,或者全都不要使用引用。

引用返回

引用返回是对局部引用特性的补充,它允许创建无需拷贝的函数。继续看我们给出的例子,我们将其中的搜索操作抽出,并置入自己的静态函数中。

static ref Entry FindColumn(Entry[] entries, string searchKey) { for (var i = 0; i < entries.Length; i++) { ref Entry entry = ref entries[i]; //创建一个引用 if (string.Equals(entry.Details.ClrName, searchKey, StringComparison.OrdinalIgnoreCase) || string.Equals(entry.Details.SqlName, searchKey, StringComparison.OrdinalIgnoreCase)) { return ref entry; } } throw new Exception("Column not found"); }

在上面的例子中,我们返回了一个对数组元素的引用。当然也可以返回对对象字段、引用属性(参见下节)和引用参数的引用。

ref int Echo(ref int input) { return ref input; } ref int Echo2(ref Foo input) { return ref Foo.Field; }

引用返回具有一个有意思的特性,就是调用者可以选择是否使用它。下面两行代码是同等有效的:

Entry copy = FindColumn(m_Entries, "FirstName"); ref Entry reference = ref FindColumn(m_Entries, "FirstName"); 引用返回和引用属性

你还可以创建具有引用返回风格的属性,这仅适用于只读属性。例如:

public ref int Test { get { return ref m_Test; } }

对于不可变结构体,这个模式看上去非常简单。调用者无需付出额外开销,就可以将其作为一个引用值或是正常值读取,正如在代码中所看到的。

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

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