怎么做网站的地图页,网站改版的方式大致有,wordpress中文字体,门窗设计软件免费版原文地址:https://www.infoq.com/articles/Patterns-Practices-CSharp-7 关键点 遵循 .NET Framework 设计指南#xff0c;时至今日#xff0c;仍像十年前首次出版一样适用。API 设计至关重要#xff0c;设计不当的API大大增加错误#xff0c;同时降低可重用性。始终保持时至今日仍像十年前首次出版一样适用。API 设计至关重要设计不当的API大大增加错误同时降低可重用性。始终保持成功之道只做正确的事避免犯错。去除 line noise 和 boilerplate 类型的代码以保持关注业务逻辑在为了性能牺牲而可读性之前请保持清醒 C# 7 一个主要的更新是带来了大量有趣的新特性。虽然已经有很多文章介绍了 C# 7 可以做哪些事但关于如何用好 C# 7 的文章还是很少。遵循 .NET Framework设计指南中 的原则我们首先通过下面的策略获取这些新特性的最佳做法。 元组返回结果
在 C# 以往的编程中从一个函数中返回多个结果可是相当的乏味。Output 关键词是一种方法但如果对于异步方法不适用。TupleT元组 尽管啰嗦又要分配内存同时对于其字段又不能有描述性名称。自定义的结构优于元组但在一次性代码中滥用会产生垃圾代码。最后匿名类型和动态类型(dynamic) 的组合非常慢又缺乏静态类型检查。所有的这一切问题在新的元组返回语法中得到了解决。下面是旧语法的例子
public (string, string) LookupName(long id) // tuple return type
{ return (John, Doe); // tuple literal
}
var names LookupName(0);
var firstName names.Item1;
var lastName names.Item2;
这个函数真是的返回类型是 ValueTuplestring, string。顾名思义这是类似 TupleT 类的轻量级结构。这解决了类型膨胀的问题但和 TupleT 同样缺失了描述性名称。
public (string First, string Last) LookupName(long id)
var names LookupName(0);var firstName names.First;var lastName names.Last;
返回的类型仍然是 ValueTuplestring, string但现在编译器为函数添加了TupleElementNames 属性允许代码使用描述性名称而不是 Item1/Item2。
警告TupleElementNames 属性只能被编译器使用。如果在返回类型上使用反射则只能看到 ValueTupleT 结构。因为这些属性在函数返回结果的时候才会出现相关的信息是不存在的。
编译器尽所能地为这些临时的类型维持一种幻觉。例如考虑下面这些声明:
var a LookupName(0);
(string First, string Last) b LookupName(0);
ValueTuplestring, string c LookupName(0);
(string make, string model) d LookupName(0);
从编译器来看a 是一种像 b 的 (string First, string Last) 类型。 由于 c 明确声明为 ValueTuplestring, string类型所以没有 c.First 的属性。d 说明了这种设计带来的破坏导致失去类型安全。很容易不小心重命名字段会将一个元组分配给一个恰好具有相同形状的元组。重申一下这是因为编译器不会认为 (string First, string Last) 和 (string make, string model) 是不同的类型。 ValueTuple 是可变的
关于 ValueTuple 的一个有趣的看法它是可变的。Mads Torgersen 解释了原因: 下面的原因解释了可变结构为何经常是坏的设计请不要用于元组。如果您以常规方式封装可变结构体使用私有、公共的访问器那么您将遇到一些意外惊吓。原因是尽管这些结构体被保存在只读变量中访问器将悄悄在结构体的副本中生效 然而元组只有公共的、可变的字段。由于这种设计没有访问器因此不会有上述现象带来的风险。 再且因为它们是结构体当它们被传递时会被复制。线程之间不直接共享也不会有 “共享可变状态” 的风险。这与 System.Tuple 系列的类型相反为了线程安全需要保证其不可变。 [译者]Mutator的翻译参考https://en.wikipedia.org/wiki/Mutator_method#C.23_example为 C# 中的访问器 注意他说的是“字段”而不是“属性”。这可能会导致基于反射的库会有问题这将对返回元组结果的方法造成毁灭。
元组返回结果指南
✔ 当返回结果的列表字段很小且永不会改变时考虑使用元组返回结果而不是 out 参数。✔ 在元组返回结果中使用帕斯卡(PascalCase)来命名描述性字段。这使得元组字段看起来像普通类和结构体上的属性。✔ 在读取元组返回值时不要使用var来解构(deconstructing) 避免意外搞错字段。✘ 期望的返回值中用到反射的避免使用元组。✘ 在公开的 APIs 中请不要使用元组返回结果如果在将来的版本中需要返回其他字段将字段添加到元组返回结果具有破坏性。
(译者deconstructing 的翻译参考 https://zhuanlan.zhihu.com/p/25844861 中对deconstructing的翻译下面的部分名词也是如此) 解构多值返回结果
回到 LookupName 的示例 创建一个名称变量似乎有点恼人只能在被局部变量单独替换之前立即使用它。C7 也使用所谓的 “解构” 来解决这个问题。语法有几种变形
(string first, string last) LookupName(0);
(var first, var last) LookupName(0);var (first, last) LookupName(0);
(first, last) LookupName(0);
在上面示例的最后一行假定变量 first 和 last 已经事先被声明了。 解构器
尽管名字很像 “析构(destructor)”但解构器与对象销毁无关。正如构造函数将独立的值组合成一个对象一样解构器同样是组合和分解对象。解构器允许任何类提供上述的解构语法。让我们来分析一下 Rectangle 类它有这样的构造函数:
public Rectangle(int x, int y, int width, int height)
当你在一个新的实例中调用 ToString 时你会得到{X0,Y0,Width0,Height0}。结合这两个事实我们知道了在自定义的解构函数中对字段排序。
public void Deconstruct(out int x, out int y, out int width, out int height)
{ x X; y Y; width Width; height Height;
} var (x, y, width, height) myRectangle;
Console.WriteLine(x);
Console.WriteLine(y);
Console.WriteLine(width);
Console.WriteLine(height);
你可能会好奇为什么使用 output 参数而不是元组。一部分原因是性能这样就减少了需要复制的数量。但最主要的原因是微软还为重载打开了一道门。继续我们的研究注意到 Rectangle 还有第二个构造函数:
public Rectangle(Point location, Size size);
我们同样为它匹配一个解构方法:
public void Deconstruct(out Point location, out Size size);var (location, size) myRectangle;
有多少个不同数量的构造参数就有多少个解构函数。即使你显式地指出类型编译器也无法确定有哪些解构方法可以使用。在 API 设计中结构通常能从解构中受益。类特别是模型或者DTOs如 Customer 和 Employee 可能不应该有解构方法它们没有方法解决诸如:应该是 (firstName, lastName, phoneNumber, email) 还是 (firstName, lastName, email, phoneNumber) 的问题。某种程度来说大家都应该开心。 解构器指南
✔ 考虑在读取元组返回值时使用解构但要注意避免搞错顺序的错误。✔ 为结构提供自定义的解构方法。✔ 记得匹配类的构造函数中字段的顺序重写 ToString 。✔ 如果结构具有多个构造函数考虑提供对应的解构方法。✔ 考虑立即解构大值元组。大值元组的总大小超过16个字节这可能带来多次复制的昂贵代价。请注意引用类型的变量在32位操作系统中的大小总是4字节而在64位操作系统是8字节。✘ 当不知道在类中字段应以何种方式排序时请不要使用解构方法。✘ 不要声明多个具有同等数量参数的解构方法。
Out 变量
C# 7 为 带有 out 变量的调用函数提供了两种新的语法选择。现在可以在函数调用中这样声明变量。
if (int.TryParse(s, out var i))
{Console.WriteLine(i);
}
另一种选择是完全使用下划线忽略out 变量。
if (int.TryParse(s, out _))
{Console.WriteLine(success);
}
如果你使用过 C# 7 预览版可能会注意到一点对被忽略的参数使用星号(*)已被更改为用下划线。这样做的部分原因是在函数式编程中通常出于同样的目的使用了下划线。其他类似的选择包括诸如void 或者 ignore 的关键字。使用下划线很方便同时意味着 API中的设计缺陷。在大多数情况中更好的方法是对忽视的 out 参数简单地提供一个方法重载。 Out 变量指南
✔ 考虑用元组返回值替代 out参数。✘ 尽量避免使用 out 或者 ref 参数。[详情见 框架设计指南 ]✔ 考虑对忽视的 out 参数提供重载这样就不需要用下划线了。
局部方法和迭代器
局部方法是一个有趣的概念。乍一看就像是创建匿名方法的一种更易读的语法。下面看看他们的不同。
public DateTime Max_Anonymous_Function(IListDateTime values)
{ FuncDateTime, DateTime, DateTime MaxDate (left, right) { return (left right) ? left : right; }; var result values.First(); foreach (var item in values.Skip(1)) result MaxDate(result, item); return result;
} public DateTime Max_Local_Function(IListDateTime values)
{ DateTime MaxDate(DateTime left, DateTime right) { return (left right) ? left : right; } var result values.First(); foreach (var item in values.Skip(1)) result MaxDate(result, item); return result;
}
然而一旦你开始深入了解一些有趣的内容将会浮现。 匿名方法 vs. 局部方法
当你创建一个普通的匿名方法时总是会创建一个对应的隐藏类来存储该匿名方法。该隐藏类的实例将被创建并存储在该类的静态字段中。因此一旦创建没有额外的开销。反观局部方法不需要隐藏类。相反局部方法表现为其静态父方法。
闭包
如果您的匿名方法或局部方法引用了外部变量则产生闭包。下面是示例:
public DateTime Max_Local_Function(IListDateTime values)
{ int callCount 0; DateTime MaxDate(DateTime left, DateTime right) { callCount; --The variable callCount is being closed over. return (left right) ? left : right; } var result values.First(); foreach (var item in values.Skip(1)) result MaxDate(result, item); return result;
}
对于匿名方法来说隐藏类每次创建新实例时都要求外部父方法被调用。这确保每次调用时会在父方法和匿名方法共享数据副本。这种设计的缺点是每次调用匿名方法需要实例化一个新对象。这就带来了昂贵的使用成本同时加重垃圾回收的压力。反观局部方法使用隐藏结构取代了隐藏类。这就允许继续存储上一次调用的数据避免了每次都要实例化对象。与匿名方法一样本地方法实际存储在隐藏结构中。 委托
创建匿名方法或局部方法时通常会将其封装到委托以便在事件处理程序或者 LINQ 表达式中调用。根据定义匿名方法是匿名的。所以为了使用它往往需要当成委托存储在一个变量或参数。委托不可以指向结构(除非他们被装箱了那就是奇怪的语义)。所以如果你创建了一个委托并指向一个局部方法编译器将会创建一个隐藏类代替隐藏结构。如果该本地方法是一个闭包那么每次调用父方法时都会创建一个隐藏类的新实例。 迭代器
在C中使用 yield 返回的 IEnumerableT 不能立即验证其参数。相反直到在匿名枚举器中调用 MoveNext才可以对其参数进行验证。这在 VB 中不是问题因为它支持 匿名迭代器。下面有一个来自MSDN的示例:
Public Function GetSequence(low As Integer, high As Integer) _
As IEnumerable Validate the arguments. If low 1 Then Throw New ArgumentException(low is too low) If high 140 Then Throw New ArgumentException(high is too high) Return an anonymous iterator function. Dim iterateSequence Iterator Function() As IEnumerable For index low To high Yield index Next End Function Return iterateSequence()
End Function
在当前的 C# 版本中GetSequence的迭代器需要完全独立的方法。而在 C# 7中可以使用本地方法实现。
public IEnumerableint GetSequence(int low, int high)
{ if (low 1) throw new ArgumentException(low is too low); if (high 140) throw new ArgumentException(high is too high); IEnumerableint Iterator() { for (int i low; i high; i) yield return i; } return Iterator();
}
迭代器需要构建一个状态机所以它们的行为就像在隐藏类中作为委托返回闭包。
匿名方法和本地方法指南
✔ 当不需要委托时使用局部方法代替匿名方法尤其是涉及到闭包。✔ 当返回一个需要验证参数的 IEnumerator 时使用局部迭代器。✔ 考虑将局部方法放到方法的开头或结尾处以便与父方法区分来。✘ 避免在性能敏感的代码中使用带委托的闭包这适用于匿名方法和局部方法。 引用返回、局部引用以及引用属性
结构具有一些有趣的性能特性。由于他们与其父数据结构一起存储没有普通类的头开销。这意味着你可以非常密集地存储在数组中很少或不浪费空间。除了减少内存总体开销外还带来了极大的优势使 CPU 缓存更高效。这就是为什么构建高性能应用程序的人喜欢结构。但是如果结构太大的话需要避免不必要的复制。微软的指南建议为16个字节足够存储2个 doubles 或者 4 个 integers。这不是很多尽管有时可以使用位域 (bit-fields)来扩展。
局部引用
这样做的一个方法是使用智能指针所以你永远不需要复制。这里有一些我仍然使用的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。为了避免复制仍然使用旧式的 for 循环。即使如此所有的读和写操作都是直接在 m_Entries 数组中操作。使用 C# 7 的局部引用明显地减少混乱而不改变语义。
for (var i 0; i m_Entries.Length; i)
{ ref Entry entry ref m_Entries[i]; //create a reference 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; }
}
这是因为 局部引用 真的是一个安全的指针。我们之所以说它 “安全” 是因为编译器指向不允许任何临时变量诸如普通方法的结果。如果你很想知道 ref var entry ref m_Entries[i]; 是不是有效的语法(是的)无论如何也不能这么做会造成混乱。 ref 既是用于声明又不会被用到。(译者这里应该是指 entry 的 ref 修饰吧) 引用返回
引用返回丰富了本地方法允许创建无副本的方法。继续之前的示例我们可以将搜索结果输出推到其静态方法。
对于匿名方法来说隐藏类每次创建新实例时都要求外部父方法被调用。这确保每次调用时会在父方法和匿名方法共享数据副本。这种设计的缺点是每次调用匿名方法需要实例化一个新对象。这就带来了昂贵的使用成本同时加重垃圾回收的压力。反观局部方法使用隐藏结构取代了隐藏类。这就允许继续存储上一次调用的数据避免了每次都要实例化对象。与匿名方法一样本地方法实际存储在隐藏结构中。 委托
创建匿名方法或局部方法时通常会将其封装到委托以便在事件处理程序或者 LINQ 表达式中调用。根据定义匿名方法是匿名的。所以为了使用它往往需要当成委托存储在一个变量或参数。委托不可以指向结构(除非他们被装箱了那就是奇怪的语义)。所以如果你创建了一个委托并指向一个局部方法编译器将会创建一个隐藏类代替隐藏结构。如果该本地方法是一个闭包那么每次调用父方法时都会创建一个隐藏类的新实例。 迭代器
在C中使用 yield 返回的 IEnumerableT 不能立即验证其参数。相反直到在匿名枚举器中调用 MoveNext才可以对其参数进行验证。这在 VB 中不是问题因为它支持 匿名迭代器。下面有一个来自MSDN的示例: 在当前的 C# 版本中GetSequence的迭代器需要完全独立的方法。而在 C# 7中可以使用本地方法实现。 迭代器需要构建一个状态机所以它们的行为就像在隐藏类中作为委托返回闭包。
匿名方法和本地方法指南
✔ 当不需要委托时使用局部方法代替匿名方法尤其是涉及到闭包。✔ 当返回一个需要验证参数的 IEnumerator 时使用局部迭代器。✔ 考虑将局部方法放到方法的开头或结尾处以便与父方法区分来。✘ 避免在性能敏感的代码中使用带委托的闭包这适用于匿名方法和局部方法。
引用返回、局部引用以及引用属性
结构具有一些有趣的性能特性。由于他们与其父数据结构一起存储没有普通类的头开销。这意味着你可以非常密集地存储在数组中很少或不浪费空间。除了减少内存总体开销外还带来了极大的优势使 CPU 缓存更高效。这就是为什么构建高性能应用程序的人喜欢结构。但是如果结构太大的话需要避免不必要的复制。微软的指南建议为16个字节足够存储2个 doubles 或者 4 个 integers。这不是很多尽管有时可以使用位域 (bit-fields)来扩展。
局部引用
这样做的一个方法是使用智能指针所以你永远不需要复制。这里有一些我仍然使用的ORM性能敏感代码。 你会注意到的第一件事是没有使用 for-each。为了避免复制仍然使用旧式的 for 循环。即使如此所有的读和写操作都是直接在 m_Entries 数组中操作。使用 C# 7 的局部引用明显地减少混乱而不改变语义。 这是因为 局部引用 真的是一个安全的指针。我们之所以说它 “安全” 是因为编译器指向不允许任何临时变量诸如普通方法的结果。如果你很想知道 ref var entry ref m_Entries[i]; 是不是有效的语法(是的)无论如何也不能这么做会造成混乱。 ref 既是用于声明又不会被用到。(译者这里应该是指 entry 的 ref 修饰吧)
引用返回
引用返回丰富了本地方法允许创建无副本的方法。继续之前的示例我们可以将搜索结果输出推到其静态方法。 static ref Entry FindColumn(Entry[] entries, string searchKey)
{ for (var i 0; i entries.Length; i){ ref Entry entry ref entries[i]; //create a referenceif (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; } }
对于不可变结构来说这种模式似乎毫不伤脑。调用者不需要花费额外的功夫就可以将其视为引用值或普通值。对于可变的结构事情变得有趣起来。首先这修复了一不小心就会通过修改属性而改变结构返回值的老问题只与值变化共进退。考虑以下的类
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; } }
引用返回和索引器
对于引用返回和局部引用最大的限制可能就是需要一个固定的指针。考虑这行代码
ref int x ref myList[0];
这样的代码无效因为列表不像数组在读取其值时会创建一个副本结构。下面是对 ListT 实现 引用的源码
public T this[int index] { get { // Following trick can reduce the range check by oneif ((uint) index (uint)_size) {ThrowHelper.ThrowArgumentOutOfRangeException();}Contract.EndContractBlock(); return _items[index]; -- return makes a copy}
这同样适用于 ImmutableArrayT 和 访问 IListT 接口的普通数组。但是您可以实现自己的ListT将其索引定义为引用返回。
public ref T this[int index] { get { // Following trick can reduce the range check by oneif ((uint) index (uint)_size) {ThrowHelper.ThrowArgumentOutOfRangeException();}Contract.EndContractBlock(); return ref _items[index]; -- return ref makes a reference}
如果你这么做需要明确实现 IListT 和 IReadOnlyListT 接口。这是因为引用返回具有与普通返回值不同的签名因此不能满足接口的要求。由于索引器实际上只是专用属性它们与引用属性具有相同的限制; 这意味着您无法显式定义 setter而索引器却是可写的。
引用返回、局部引用和引用属性指南
✔ 在使用数组的方法中考虑使用引用返回而不是索引值✔ 在拥有结构的自定义集合类中对索引器考虑使用引用返回代替一般的返回结果。✔ 将包含可变结构体的属性暴露为引用属性。✘ 不要将包含不可变结构的属性暴露为引用属性。✘ 不要在不可变或只读类上暴露引用属性。✘ 不要在不可变或只读集合类上暴露引用索引器。
ValueTask 和通用异步返回类型
当Task类被创建时它的主要角色是简化多线程编程。它创建一种将长时间运行的操作推入线程池的通道并在 UI线程上推迟读取结果。而当你使用 fork-join 模式并发时效果显著。随着.NET 4.5中引入了 async/await 一些缺陷也开始显现。正如我们在2011年的反馈(详见 Task Parallel Library Improvements in .NET 4.5)创建一个 Task对象所花费的时间比可接受的时间长因此必须重写其内部结果是创建TaskInt32 所需的时间缩短了49至55并在大小上减小了52。这是很好的一步但 Task 仍然分配了内存。所以当你在紧凑循环中使用它如下所示将产生大量的垃圾。
while (await stream.ReadAsync(buffer, offset, count) ! 0)
{ //process buffer}
而且如前所述 C# 高性能代码的关键在于减少内存分配和随后的GC循环。微软的Joe Duffy在 Asynchronous Everything 的文章中写到 首先请记住Midori 被整个操作系统用于内存垃圾回收。我们必须学到了一些必要的经验教训以便充分发挥作用。但我想说的主要是避免不必要的分配分配越多麻烦越多特别是短命对象。早期 .NET世界中流传着一句口头禅Gen0 集合是无代价的。不幸的是这形成了很多.NET的库代码滥用。Gen0 集合存在着中断、弄脏缓存以及在高并发的系统中有高频问题。 这里的真正解决方案是创建一个基于结构的 task而不是使用堆分配的版本。这实际上是以System.Threading.Tasks.Extensions 中的 ValueTaskT创建。并且因为 await 已经任何暴露的方法中工作了,所以你可以使用它。
手动暴露ValueTaskT
ValueTaskT的基本用例是预期结果在大部分时间是同步的并且想要消除不必要的内存分配。首先假设你有一个传统的基于 task 的异步方法。
public async TaskCustomer ReadFromDBAsync(string key)
然后我们将其封装到一个缓存方法中:
public ValueTaskCustomer ReadFromCacheAsync(string key)
{Customer result; if (_Cache.TryGetValue(key, out result)) return new ValueTaskCustomer(result); //no allocationelsereturn new ValueTaskCustomer(ReadFromCacheAsync_Inner(key));
}
并添加一个辅助方法来构建异步状态机。
async TaskCustomer ReadFromCacheAsync_Inner(string key)
{ var result await ReadFromDBAsync(key);_Cache[key] result; return result;
}
有了这一点调用者可以使用与 ReadFromDBAsync 完全相同的语法来调用ReadFromCacheAsync;
async Task Test()
{ var a await ReadFromCacheAsync(aaa); var b await ReadFromCacheAsync(bbb);
}
通用异步
虽然上述模式并不困难但实施起来相当乏味。而且我们知道编写代码越繁琐出现简单的错误就越有可能。所以目前 C# 7 的提议是提供通用异步返回结果。根据目前的设计你只能使用异步关键字并且方法返回 Task、TaskT或者 void。一旦实现通用异步返回结果将会扩展到任何 tasklike 方法上去。一些人认为 tasklike 需要有一个 AsyncBuilder 属性。这表明辅助类被用于创建 tasklike 对象。在这个设计的注意事项中微软估计大概有五种人实际上会创建 tasklike 类从而被普遍接受。其他人都很可能也像这五分之一。这是我们上面使用新语法的例子
public async ValueTaskCustomer ReadFromCacheAsync(string key)
{Customer result; if (_Cache.TryGetValue(key, out result)){ return result; //no allocation} else{result await ReadFromDBAsync(key);_Cache[key] result; return result;}
}
如您所见我们已经去除了辅助方法除了返回类型它看起来像任何其他异步方法一样。
何时使用 ValueTaskT
所以应该使用 ValueTaskT 代替 TaskT? 完全不必要这可能有点难以理解所以我们将引用相关文档 方法可能会返回一个该值类型的实例当它们的操作可以同时执行同时被频繁唤起(invoked)。这时对于TaskTResult每一次调用都是昂贵的成本应该被禁止。 使用 ValueTaskTResult 代替 TaskTResult 需要权衡利弊。例如虽然 ValueTaskTResult 可以避免分配并且成功返回结果是可以同步返回的。然而它需要两个字段而 TaskTResult 作为引用类型只是一个字段。这意味着调用方法最终返回的是两个数据而不是一个数据这就会有更多的数据被复制。同时意味着如果在异步方法中需要等待时只返回其中一个这会导致该异步方法的状态机变得更大。因为要存储两个字段的结构而不是一个引用。 再进一步使用者通过 await 来获取异步操作的结果ValueTaskTResult 可能会导致更复杂的模型实际上就会导致分配更多的内存。例如考虑到一个方法可能返回一个普通的已缓存 task 的结果TaskTResult或者是一个 ValueTaskTResult。如果调用者的预期结果是 TaskTResult可以被诸如 Task.WhenAll 和 Task.WhenAny 的方法调用那么 ValueTaskTResult 首先需要使用 ValueTaskTResult.AsTask 将其自身转换为 TaskTResult 如果 TaskTResult 在第一次使用没有被缓存了将导致分配。 因此Task的任何异步方法的默认选择应该是返回一个 Task 或TaskTResult。除非性能分析证明使用 ValueTaskTResult 优于TaskTResult。Task.CompletedTask 属性可能被单独用于传递任务成功执行的状态 ValueTaskTResult 并不提供泛型版本。 这是一段相当长的段落所以我们在下面的指南中总结了这一点。
ValueTask T指南
✔ 当结果经常被同步返回时请考虑在性能敏感代码中使用 ValueTaskT。✔ 当内存压力是个问题且 Tasks 不能被缓存时考虑使用 ValueTaskT。✘ 避免在公共API中暴露 ValueTaskT除非有显著的性能影响。✘ 不要在调用 Task.WhenAll 或 WhenAny 中调用 ValueTaskT。
表达式体成员
表达式体成员允许消除简单函数的括号。这通常是将一个四行函数减少到一行。例如
public override string ToString()
{ return FirstName LastName;
}public override string ToString() FirstName LastName;
必须注意不要过分。例如假设当 FirstName 为空时您需要避免产生空格。你可能会这么写:
public override string ToString() !string.IsNullOrEmpty(FirstName) ? FirstName LastName : LastName;
但是你可能会遇到 last name 同时为空。
public override string ToString() !string.IsNullOrEmpty(FirstName) ? FirstName LastName : (!string.IsNullOrEmpty(LastName) ? LastName : No Name);
如您所见很容易得意忘形地使用这个功能。所以当你遇到有多分支条件或者 null合并操作时请克制使用。
表达式体属性
表达式体属性是 C# 6 的新特性。在使用 Get/Set 方法处理 MVVM风格的模型之类时非常有用。这是C6代码
public string FirstName
{ get { return Getstring(); } set { Set(value); }
}
还有 C# 7的替代方案
public string FirstName
{ get Getstring(); set Set(value);
}
虽然没有减少代码行数但大部分 line-noise 代码已经消失了。而且每个属性都能这么做积少成多。有关 Get/Set 在这些示例中的工作原理的更多信息请参阅 C#, VB.NET To Get Windows Runtime Support, Asynchronous Methods。
表达式体构造函数
表达式体构造函数是C# 7 的新特性。下面有一个例子:
class Person
{ public Person(string name) Name name; public string Name { get; }
}
这里的用法非常有限。它只有在零个或者一个参数的情况下才有效。一旦需要将其他参数分配给字段/属性时则必须用回传统的构造函数。同时也无法初始化其他字段解析事件处理程序等参数验证是可能的请参见下面的“抛出表达式”。所以我们的建议是简单地忽略这个功能。它只是将单参数构造函数看起来与一般的构造函数不同而已同时让代码大小减少而已。
析构表达式
为了使 C# 更加一致析构被允许写成和表达式的成员一样就像用在方法和构造函数一样。对于那些忘记析构的人来说C# 中的析构是在 Finalize 方法上重写System.Object。虽然 C# 不这样表达
~UnmanagedResource()
{ReleaseResources();
}
这种语法的一个问题是它看起来很像一个构造函数因此可以很容易地被忽略。另一个问题是它模仿 C 中的析构语法却是完全不同的语义。但是已经被使用了这么久所以我们只好转向新的语法:
~UnmanagedResource() ReleaseResources();
现在我们有一行孤立的、容易忽略的代码用于终结对象生命周期。这不是一个简单的 属性 或 ToString 方法而是很重大的操作需要显眼一些。所以我建议不要使用它。
表达式体成员指南
✔ 为简单的属性使用表达式体成员。✔ 为方法重载使用表达式体成员。✔ 简单的方法考虑使用表达式体成员。✘ 不要在表达式体成员使用多分支条件abc或 null 合并运算符x ?? y。✘ 不要为 构造函数 和 析构函数 中使用表达式成员。
抛出表达式
表面上编程语言一般可以分为两种
一切都是表达式语句、声明和表达式都是独立的概念
Ruby是前者的一个实例甚至其声明也是表达式。相比之下Visual Basic代表后者语句和表达式之间有很强的区别。例如对于 if 而言当它独立存在时以及作为表达式中的一部分时是完全不同的语法。C主要是第二阵营但存在着 C语言的遗产允许你处理语句当成表达式一样。可以编写如下代码
while ((current stream.ReadByte()) ! -1)
{ //do work;}
首先C7 允许使用非赋值语句作为表达式。现在可以在表达式的任何地方放置 “throw” 语句不用对语法做任何更改。以下是Mads Torgersen 新闻稿中的一些例子 class Person
{ public string Name { get; } public Person(string name) Name name ?? throw new ArgumentNullException(name); public string GetFirstName(){ var parts Name.Split( ); return (parts.Length 0) ? parts[0] : throw new InvalidOperationException(No name!);} public string GetLastName() throw new NotImplementedException();
}
在这些例子中很容易看出会发生什么情况。但是如果我们移动抛出表达式的位置呢
return (parts.Length 0) ? throw new InvalidOperationException(No name!) : parts[0];
这样看来就不够易读了。而左右的语句是相关的中间的语句与他们无关。从第一个版本看左边是预期分支右边是错误分支。第二个版本的错误分支将预期分支分成两半打破整条流程。
我们来看另一个例子。这里我们掺入一个函数调用。
void Save(IListCustomer customers, User currentUser)
{ if (customers null || customers.Count 0) throw new ArgumentException(No customers to save);_Database.SaveEach(dbo.Customer, customers, currentUser);
}void Save(IListCustomer customers, User currentUser)
{_Database.SaveEach(dbo.Customer, (customers null || customers.Count 0) ? customers : throw new ArgumentException(No customers to save), currentUser);
}
我们已经可以看到写到一块是有问题的尽管它的LINQ并不难看。但是为了更好地阅读代码我们使用橙色标记条件蓝色标记函数调用黄色标记函数参数红色标记错误分支。 这样可以看到随着参数改变位置上下文如何变化。
抛出表达式指南
✔ 在分支/返回语句中考虑将抛出表达式放在条件abc和 null 合并运算符x ?? y的右侧。✘ 避免将抛出表达式放到条件运算的中间位置。✘ 不要将抛出表达式放在方法的参数列表中。有关异常如何影响 API设计的更多信息请参阅 Designing with Exceptions in .NET。
模式匹配 和 加强 Switch 语句
模式匹配加强了 Switch 语句对API设计没有任何影响。所以虽然可以使异构集合的处理变得更加容易但最好的情况还是尽可能地使用共享接口和多态性。也就是说有些细节还是要注意的。考虑这个八月份发布的例子 switch(shape)
{ case Circle c:WriteLine($circle with radius {c.Radius}); break; case Rectangle s when (s.Width s.Height):WriteLine(${s.Width} x {s.Height} square); break; case Rectangle r:WriteLine(${r.Width} x {r.Height} rectangle); break; default:WriteLine(unknown shape); break; case null: throw new ArgumentNullException(nameof(shape));
}
以前case的顺序并不重要。在 C# 7 中像 Visual Basic一样switch语句几乎严格按顺序执行。对于 when 表达式同样适用。实际上您希望最常见的情况是 switch 语句中的第一种情况就像在一系列 if-else-if 语句块中一样。同样如果任何检查特别昂贵那么它应该越靠近底部只在必要时才执行。顺序规则的例外是默认情况。它总是被最后处理不管它的实际顺序是什么。这会使代码更难理解所以我建议将默认情况放在最后。
模式匹配表达式
虽然 switch 语句可能是 C# 中最常用的模式匹配; 但并不是唯一的方式。在运行时求值的任何布尔表达式都可以包含模式匹配表达式。下面有一个例子它判断变量 o 是否是一个字符串如果是这样则尝试将其解析为一个整数。
if (o is string s int.TryParse(s, out var i))
{Console.WriteLine(i);
}
注意如何在模式匹配中创建一个名为s的新变量然后再用于TryParse。这种方法可以链式组合构建更复杂的表达式
if ((o is int i) || (o is string s int.TryParse(s, out i)))
{Console.WriteLine(i);
}
为了方便比较 将上述代码重写成 C# 6 风格:
if (o is int)
{Console.WriteLine((int)o);
}else if (o is string int.TryParse((string) o, out i))
{Console.WriteLine(i);
}
现在还不知道新的模式匹配代码是否比以前的方式更有效但它可能会消除一些冗余的类型检查。
一起维护这个在线文档
C# 7 的新特性仍然很新鲜而且关于它们在现实世界中如何运行还需要多多了解。所以如果你看到一些你不同意的东西或者这些指南中没有的话请让我们知道。
关于作者
乔纳森·艾伦Jonathan Allen在90年代末期开始从事卫生诊所的MIS项目从 Access 和 Excel 到企业解决方案。在为金融部门编写自动化交易系统五年之后他成为各种项目的顾问包括机器人仓库的UI癌症研究软件的中间层以及房地产保险公司的大数据需求。在空闲时间他学习和书写16世纪以来的武术知识。
原文地址http://www.cnblogs.com/chenug/p/6803649.html .NET社区新闻深度好文微信中搜索dotNET跨平台或扫描二维码关注