如何使用泛型在新的可空上下文中启动健壮的C#项目?

目录

介绍

使用代码

兴趣点


介绍

可以通过C# 项目 (.csproj) 文件中的<Project><PropertyGroup><Nullable>enable</Nullable>元素启用新的可为空上下文。它在编译时为您提供完整的空状态静态分析,并承诺一劳永逸地消除每一个NullReferenceException 。你试过吗?你喜欢它?为什么不?代码质量至关重要,值得您再看一遍。本文旨在提供空值​​和泛型is以及as关键字概念的概述,以便您可以再次爱上新的C#。小事很重要。

使用代码

C#提供了两种类型安全的宏,很像委托作为类型安全的函数指针,第一种是扩展方法,第二种是值类型,两者都足够小以至于它们经常被JIT内联,因此。使用这些小的C#宏,您几乎可以创建任何您想要的东西,并使C#成为您的语言风格。要启动一个新的健壮的C#项目,您需要从一个根接口开始:

using System;
using System.Collections.Generic;
[assembly: CLSCompliant(true)]
namespace Pyramid.Kernel.Up
{
 /// <summary>The extension method <see langword="class"/>.</summary>
 public static partial class It { }
 /// <summary>An <see cref="object"/>.</summary>
 public partial interface JIt
 {
  /// <summary>An <see langword="interface"/> for external 0's.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  partial interface J0<out JOut>
  {
   /// <summary>The 0 <typeparamref name="JOut"/> <see cref="IEnumerator{T}"/>.</summary>
   static readonly IEnumerator<JOut> ZTo = Z.GetEnumerator();
   /// <summary>The 0 <typeparamref name="JOut"/> <see cref="Array"/>.</summary>
   static JOut[] ZArray => Array.Empty<JOut>();
   /// <summary>The 0 <typeparamref name="JOut"/> <see cref="IEnumerable{T}"/>.</summary>
   static IEnumerable<JOut> Z => ZArray;
   /// <summary>The 0 <typeparamref name="JOut"/> <see cref="IReadOnlyCollection{T}"/>.
   /// </summary>
   static IReadOnlyCollection<JOut> ZReadOnlyCollection => ZArray;
   /// <summary>The 0 <typeparamref name="JOut"/> <see cref="IReadOnlyList{T}"/>.</summary>
   static IReadOnlyList<JOut> ZReadOnlyList => ZArray;
  }
 }
 /// <summary>An autonomous <see cref="JIt"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="JIt{J}"/>.</typeparam>
 public partial interface JIt<J> : JIt where J : JIt<J>, new()
 {
  /// <summary>The 0 <typeparamref name="J"/>.</summary>
  static readonly J Z0 = new();
 }
 /// <summary>An autonomous <see cref="JIt{J}"/>.</summary>
 /// <typeparam name="J">Its self-referencing <see cref="It{J}"/>.</typeparam>
 [Serializable]
 public abstract partial class It<J> : object, JIt<J> where J : It<J>, new() { }
}

鉴于空值是邪恶的,我们希望我们创建的每种类型都有一个零对象,但空值必须存在以表示尚未到达的数据,这在异步编程中尤其有用。我们使用C#接口的J-前缀来提醒自己,所有接口实例成员都是virtual,就像Java一样,以及C#常量和只读字段的Z-前缀,以注意它们是final,也像Java。枚举器将被用作非常轻量级的阶梯式线程,而不是异步任务和委托,这甚至比纤程更好,因为枚举器的yieldThread.Yield()便宜得多,大约快10,000多倍,更不用说作为自然循环的简单进度条支持了。这就是空值很重要的原因,因为您总是可以产生空值或特殊对象,例如DBNull.Value发出I/O等待信号。这是以后用的。在本文中,我们要关注缓存中的操作,它通常比内存中的操作快数百倍,而内存中的操作又比磁盘或网络I/O操作快数千或数百万倍,一个I/O层次结构就像一个金字塔,因此我们的新命名空间金字塔。

首先,我们引入空检查:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Is (non-<see langword="null"/>)?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static bool Is<J>(this J it) => it != null;
  /// <summary>Is (non-<see langword="null"/>) for the <paramref name="alias"/>?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="alias">An alias <typeparamref name="J"/>.</param>
  public static bool Is<J>(this J it, [MaybeNullWhen(false), NotNullWhen(true)] out J alias) =>
   (alias = it).Is();
 }
}

它看起来很完美,但第二个方法重载不适用于Nullable<T>,因为调用出来的别名将携带相同的类型,因此仍然可以为空。当然,我们可以使用第三个方法重载Is<J>(this J? it, out J alias) where J : struct,但这会使调用Is(out var o) 不明确,现在需要显式类型说明符而不是var。这是C#需要认真改进的地方,希望能在C# 1011或其他版本中得到解决。模棱两可是万恶之源,说得够多了。然而,既然你 无论如何都必须写作o != null? (O)o: new(),为什么不接受我们的新风格,即o.Is()? o.Be(): new()?好吧,是时候添加一些简洁的方法了:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="JOut"/>
  /// <see cref="IEnumerable{T}"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="JOut"/> <see cref="IEnumerable{T}"/>.</param>
  public static IEnumerable<JOut> Be<JOut>(this IEnumerable<JOut>? it) => it ?? JIt.J0<JOut>.Z;
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="JOut"/>
  /// <see cref="IEnumerator{T}"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="JOut"/> <see cref="IEnumerator{T}"/>.</param>
  public static IEnumerator<JOut> Be<JOut>(this IEnumerator<JOut>? it) =>
   it ?? JIt.J0<JOut>.ZTo;
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="JOut"/>
  /// <see cref="IReadOnlyCollection{T}"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="JOut"/> <see cref="IReadOnlyCollection{T}"/>.
  /// </param>
  public static IReadOnlyCollection<JOut> Be<JOut>(this IReadOnlyCollection<JOut>? it) =>
   it ?? JIt.J0<JOut>.ZReadOnlyCollection;
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="JOut"/>
  /// <see cref="IReadOnlyList{T}"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="JOut"/> <see cref="IReadOnlyList{T}"/>.</param>
  public static IReadOnlyList<JOut> Be<JOut>(this IReadOnlyList<JOut>? it) =>
   it ?? JIt.J0<JOut>.ZReadOnlyList;
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="J"/> to
  /// <see langword="return"/>.</summary>
  /// <typeparam name="J">Its self-referencing <see cref="JIt{J}"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static J Be<J>(this J? it) where J : JIt<J>, new() => it ?? JIt<J>.Z0;
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="J"/> to
  /// <see langword="return"/>.</summary>
  /// <typeparam name="J">Its self-referencing <see cref="ValueType"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static J Be<J>(this J? it) where J : struct => it ?? new();
  /// <summary>Be (non-<see langword="null"/>) a <typeparamref name="JOut"/>
  /// <see cref="Array"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="JOut"/> <see cref="Array"/>.</param>
  public static JOut[] Be<JOut>(this JOut[]? it) => it ?? JIt.J0<JOut>.ZArray;
  /// <summary>Be (non-<see langword="null"/>) a <see cref="string"/> to
  /// <see langword="return"/>.</summary>
  /// <param name="it">A <see cref="string"/>.</param>
  public static string Be(this string? it) => it ?? "";
 }
}

就是这个!每当我们调用o.Be()时,如果类型是不可变的并且线程安全为零,我们将最终得到一个共享的默认空类型对象。如果我们愿意遵循所有类型都是不可变的原则,我们甚至可以利用JIt<J>.Z0.。值得注意的是,Array.Empty<JOut>().GetEnumerator() 为您提供了一个不可变且线程安全的IEnumerator,这是我们家庭的另一个很好的方法重载。空检查很容易。我们现在继续进行类型检查:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Is (<see langword="is"/>) <typeparamref name="JOut"/>?</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  public static bool Is<JOut>(this object? it) => it is JOut;
  /// <summary>Is (<see langword="is"/>) for the <paramref name="alias"/>?</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  /// <param name="alias">An alias <typeparamref name="JOut"/>.</param>
  public static bool Is<JOut>(this object? it, [NotNullWhen(true)] out JOut alias)
   where JOut : notnull => it is JOut o ? (alias = o).So(true) : (alias = default!).So(false);
 }
}

我们正在使用this object通过空检查巧妙地避免调用歧义,知道类型检查总是装箱值类型以获取它们的类型指针。类型检查并没有什么特别之处,它可以被看作是更通用的空检查形式。类型强制更有趣一点,因为as 关键字仅适用于引用类型。我们想概括一下:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Be a <typeparamref name="JOut"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  [return: NotNullIfNotNull("it")] public static JOut Be<JOut>(this object? it) => (JOut)it!;
  /// <summary>Be the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  /// <param name="alias">An alias <typeparamref name="JOut"/>.</param>
  [return: NotNullIfNotNull("it")]
  public static JOut Be<JOut>(this object? it, [NotNullIfNotNull("it")] out JOut alias) =>
   alias = it.Be<JOut>();
 }
}

我们选择Be 作为他们的方法名称来将方法组与as 关键字区分开来,该关键字吞下InvalidCastException 我们不想那样。我们总是希望在发生运行时错误时出现异常。最重要的是,我们希望包括值类型和引用类型,同时允许空值通过类型检查,因为它们应该。为什么不?为什么你可以写return null而不是return (O)null?空值必须通过所有类型检查以确保类型一致性!好吧,事实证明,如果我们让JOut可空,新的C#类型转换正是我们想要的,不再需要as关键字。要使用空检查并行类型检查,我们也非常希望有另一个重载:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Be the <paramref name="alias"/> to <see langword="return"/>.</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="alias">An alias <typeparamref name="J"/>.</param>
  public static J Be<J>(this J it, out J alias) => alias = it;
 }
}

该方法无非是声明一个具有相同值的新变量,事实证明这在单行lambda表达式中非常方便,实际上使所有C#表达式都可以声明内联变量。最后,为了防止引用检查中的意外装箱,我们想添加另一个快捷方法:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Is <paramref name="that"/>?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <typeparam name="JThat">Its other <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="that">A <typeparamref name="JThat"/>.</param>
  public static bool Is<J, JThat>(this J it, JThat that)
   where J : class? where JThat : class? => ReferenceEquals(it, that);
 }
}

通过仅在引用检查中确保引用类型,我们可以在编译时捕获所有意外装箱。无意识的装箱不仅在垃圾收集上缓慢而繁重,而且在产生即使是最有经验的开发人员和专家也可能错过的细微错误方面也很危险。如果可以,请在编译时捕获所有错误!最后,我们包括 补充我们Is 方法的Isnt方法:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>Isn't (<see langword="null"/>)?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  public static bool Isnt<J>(this J it) => it == null;
  /// <summary>Isn't (<see langword="null"/>) for the <paramref name="alias"/>?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="alias">An alias <typeparamref name="J"/>.</param>
  public static bool Isnt<J>(this J it,
   [MaybeNullWhen(true), NotNullWhen(false)] out J alias) => (alias = it).Isnt();
  /// <summary>Isn't <paramref name="that"/>?</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <typeparam name="JThat">Its other <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="that">A <typeparamref name="JThat"/>.</param>
  public static bool Isnt<J, JThat>(this J it, JThat that)
   where J : class? where JThat : class? => !it.Is(that);
  /// <summary>Is (<see langword="is"/>) not <typeparamref name="JOut"/>?</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  public static bool Isnt<JOut>(this object? it) => !it.Is<JOut>();
  /// <summary>Is (<see langword="is"/>) not for the <paramref name="alias"/>?</summary>
  /// <typeparam name="JOut">Its output <see cref="object"/>.</typeparam>
  /// <param name="it">An <see cref="object"/>.</param>
  /// <param name="alias">An alias <typeparamref name="JOut"/>.</param>
  public static bool Isnt<JOut>(this object? it, [NotNullWhen(false)] out JOut alias)
   where JOut : notnull => !it.Is(out alias);
 }
}

这对于现在来说已经足够了,我们将不时重新访问一小部分代码空间,以在全局范围内添加快速和大胜利。所有这些扩展方法都非常小,自动候选JIT内联,在提供代码安全性和质量的同时成本为零。哦,是的, 我们lambda表达式中的那个小So方法是什么?好吧,作为lambda爱好者,我们绝对希望在我们的武器库中有这个快捷方式,它只不过是将表达式链接到一行中:

namespace Pyramid.Kernel.Up
{
 partial class It
 {
  /// <summary>So the <paramref name="next"/> to <see langword="return"/>.</summary>
  /// <typeparam name="J">Its self-referencing <see cref="object"/>.</typeparam>
  /// <typeparam name="JNext">Its next <see cref="object"/>.</typeparam>
  /// <param name="it">A <typeparamref name="J"/>.</param>
  /// <param name="next">A <typeparamref name="JNext"/>.</param>
  [SuppressMessage("", "IDE0060")]
  public static JNext So<J, JNext>(this J it, JNext next) => next;
 }
}

我们的短文到此结束。下一次,我们将讨论索引检查,除了空检查和类型检查,还有另一个关于运行时安全的提示和技巧。之后,我们可以进入更有趣的事情。

兴趣点

  1. 空数组是不可变的和线程安全的,它们的枚举器也是。尽可能利用这一点。
  2. 歧义是代码中万恶之源。我们的微小引用检查方法重载巧妙地避免了这种情况,通过使用两种泛型类型,甚至符合CLS签名要求,其中inout修饰符不能成为方法重载的唯一区别。另外,它可以防止无意的拳击。
  3. [AllowNull]属性标记是现在基本上是不必要的,由于泛型和非空的整合微软最新的改进,因为不受约束的泛型类型可以在默认情况下接受一个可空类型,没有更多的需求,使之明确。如果您的Visual Studio尚未执行此操作,请确保将其升级到最新版本。

https://www.codeproject.com/Tips/5305799/How-to-Start-a-Robust-Csharp-Project-in-the-New-Nu

おすすめ

転載: blog.csdn.net/mzl87/article/details/121238119