目录

介绍

背景

经典类型系统

剖析C#的类型系统

现代方式

生成迭代器

丢弃(Discards)

处理异步代码

模式匹配

可空类型

见解

结论

兴趣点


介绍

近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单个问题的许多潜在(语言)解决方案。这既好又坏。很好是因为它给了我们成为开发人员的自由和权力(不会影响向后兼容性),并且由于与决策相关的认知负荷而导致不好。

在本系列中,我们希望了解存在哪些选项以及这些选项的不同之处。当然,在某些条件下,某些人可能有优点和缺点。我们将探索这些场景,并提出一个指南,以便在翻新现有项目时让我们的生活更轻松。

这是本系列的第四部分。您可以在我的博客中找到第一部分,第二部分以及第三部分。

背景

我觉得重要的是要讨论新的语言特征在何处闪耀,以及旧的——我们称之为已建立的——特征在哪里仍然是首选。我可能并不总是对的(特别是,因为我的一些观点肯定会更主观/更具品位)。像往常一样留下评论讨论将不胜感激!

让我们从一些历史背景开始。

经典类型系统

历史上,已经引入类型来告诉开发人员分配将保留多少字节。此外,一些简单的事情,如加,也可以由编译器计算出来。此前,在纯汇编程序中,开发人员必须决定哪种操作适合给定的值。现在,编译器能够知道这两个值不仅保留了4个字节,而且还应将它们视为整数。一个Int32类型加将更适用。

后来,就需要引入自定义类型进行了沟通。最初的结构已经诞生。尽管从机器的角度来看标准操作可能没有多大意义,但分配和结构(因此而得名)才是关键。我们不仅可以拥有“名称”(通常在编译时将其删除,即,为方便开发人员,编译器才知道),而且所有部分均已通过位置和类型正确指定。

随着面向对象程序设计的引入及其最初的解释,我们已经看到了类型(主要是与类相关联)及其与函数的关系(后来称为方法)的重要性。类型信息检查/访问在运行时的相关性增加,导致诸如反射的功能。尽管经典的本机系统通常具有非常有限的运行时功能(例如C ++),但是托管系统的出现却具有很大的可能性(例如JVM或.NET)。

现在,如今使用此方法的问题之一是,许多类型最初不再来自底层系统,它们来自一些数据的反序列化(例如,对Web API的传入请求)。虽然基本的验证和反序列化可能来自系统中定义的类型,但通常仅来自这种类型的派生(例如,省略某些属性,添加新属性,更改某些属性的类型等)。就目前而言,处理此类数据时会出现重复和限制。因此,需要动态编程语言,它需要在这方面提供更大的灵活性——以开发的类型安全为代价。

每个问题都有解决方案,在过去的十年中,我们看到了对类型系统和类型理论的新热爱。诸如TypeScript之类的流行语言将多年研究的结果和其他(更具异国情调的)编程语言带入了主流。希望一些更经典和历史性的编程语言也能够从这些进步中学到东西。

剖析C#的类型系统

这也可以称为.NET的类型系统,但是,尽管肯定有一些来自.NET的通用基础层,但许多构造和可能性仅来自该语言。在另一方面,如果我们看看F#如何使用.NET的类型系统,我们知道.NET并没有自然的限制——系统可以扩展很远。

C#喜欢使用静态类型系统。“静态”一词在这里意味着某些东西。假设我们具有以下类型:

public class Person
{public string FirstName { get; set; }public string LastName { get; set; }public DateTime Birthday { get; set; }
}

如果我们要强制所有属性为可选属性怎么办?好吧,实际上从某种意义上讲,它们已经没有人强迫我们设置它们了。但是,让我们假设已经在本文中介绍了可空类型(它们将在以后介绍),我们所追求的是:

public class PartialPerson
{public string? FirstName { get; set; }public string? LastName { get; set; }public DateTime? Birthday { get; set; }
}

现在我们有一个问题。当第一个类更改后,我们还需要对第二个类进行一些更改。如果我们可以改成这样的话该怎么样:

public type PartialPerson = Partial<Person>;

这实际上就是TypeScript的工作方式。在TypeScript中,Partial<T>只是用于迭代所有属性并在每个属性上放置一个可选(?)的别名。

好吧,所以C#不喜欢这样。C#更加面向运行时。因此,我们应该对此进行反思。

在运行时,可能如下所示:

var PartialPersonType = Partial<Person>();

其中,Partial方法可以用更直接的方式实现。

public static Type Partial<T>()
{var type = typeof(T);var builder = GetTypeBuilder<T>();foreach (var property in type.GetProperties()){CreateProperty(builder, property);}return builder.CreateType();
}private static TypeBuilder GetTypeBuilder<T>()
{var name = $"Partial<{typeof(T).Name}>";var an = new AssemblyName(name);var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");return moduleBuilder.DefineType(name,TypeAttributes.Public |TypeAttributes.Class |TypeAttributes.AutoClass |TypeAttributes.AnsiClass |TypeAttributes.BeforeFieldInit |TypeAttributes.AutoLayout,null);
}private static void CreateProperty(TypeBuilder tb, PropertyInfo property, string? ignore = null)
{var propertyName = property.Name;var propertyType = property.PropertyType;var attributes = property.Attributes;var customAttributes = property.CustomAttributes;var addNullable = false;if (propertyType.IsInterface || propertyType.IsClass){// we require the custom attributeaddNullable = true;}else if (propertyType.IsValueType && (!propertyType.IsGenericType || propertyType.GetGenericTypeDefinition() != typeof(Nullable<>))){// for values there is no attribute but the Nullable type// we only apply it if its not yet wrapped in such a typepropertyType = typeof(Nullable<>).MakeGenericType(propertyType);}var fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);var propertyBuilder = tb.DefineProperty(propertyName, attributes, propertyType, null);foreach (var customAttribute in customAttributes){// Append all custom attributes (as beforehand) except the Nullable oneif (customAttribute.Constructor.ReflectedType.Name != "NullableAttribute"){AppendAttribute(customAttribute, propertyBuilder);}}// if the nullable attribute should be added we can abuse some magic ...if (addNullable){var customAttribute = MethodBase.GetCurrentMethod().GetParameters().Last().CustomAttributes.Last();AppendAttribute(customAttribute, propertyBuilder);}var getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);var getIl = getPropMthdBldr.GetILGenerator();getIl.Emit(OpCodes.Ldarg_0);getIl.Emit(OpCodes.Ldfld, fieldBuilder);getIl.Emit(OpCodes.Ret);var setPropMthdBldr = tb.DefineMethod("set_" + propertyName,MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig,null, new[] { propertyType });var setIl = setPropMthdBldr.GetILGenerator();var modifyProperty = setIl.DefineLabel();var exitSet = setIl.DefineLabel();setIl.MarkLabel(modifyProperty);setIl.Emit(OpCodes.Ldarg_0);setIl.Emit(OpCodes.Ldarg_1);setIl.Emit(OpCodes.Stfld, fieldBuilder);setIl.Emit(OpCodes.Nop);setIl.MarkLabel(exitSet);setIl.Emit(OpCodes.Ret);propertyBuilder.SetGetMethod(getPropMthdBldr);propertyBuilder.SetSetMethod(setPropMthdBldr);
}private static void AppendAttribute(CustomAttributeData customAttribute, PropertyBuilder propertyBuilder)
{var args = customAttribute.ConstructorArguments.Select(m => m.Value).ToArray();var cab = new CustomAttributeBuilder(customAttribute.Constructor, args);propertyBuilder.SetCustomAttribute(cab);
}

虽然可以如上所示即时创建类型,但事实是这是一种运行时机制。这样,许多潜在的类型转换用例要么很难完成,要么不可能。

但是,存在诸如Fody之类的社区项目来操纵程序集和/或IL代码,以在编译时添加此类内容。这些方面的主要问题是编译器帮助/工具。看到真正的情况通常并不那么容易。

现代方式

类型仍然是类型。可是等等!还有更多的东西。我们具有很多功能,这些功能要么直接与C#一起提供,要么与.NET一起提供,或者由生态系统提供。在本节中,我们将尝试探索所有这些。

实际上,尽管C#中使用的许多语法直接用于某些IL代码或代码构造,但C#的某些部分(大部分是更新的,但正如我们将看到的很旧)往往会在自然层次上与类型系统紧密配合。他们使用现有的接口,类型或其他元素——有时甚至在我们没有意识到的情况下创建新类型。我们已经看到例如代理的类(例如Func<T>)或正在创建的局部函数。让我们看看还有什么可用的!

生成迭代器

从C#的第一个版本开始,我们能够即时生成类型。使用yield,我们可以启动自己的迭代器。这样的迭代器由实现IEnumerable接口的类型表示。事实证明,这里要做的唯一事情就是以某种方式创建IEnumerator实例。然后,所有逻辑(和状态)都包含在IEnumerator实例中。

首先让我们编写自己的实现代码。我们想要的是前三个数字(1、2、3)的可枚举。

class MyEnumerable : IEnumerable<int>
{public IEnumerator<int> GetEnumerator() => new MyIterable();IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator();class MyIterable : IEnumerator<int>{public int Current => current;object IEnumerator.Current => current;private int current = 0;public void Dispose() {}public bool MoveNext() => ++current < 4;public void Reset() => current = 0;}
}

C#语言具有许多不错的功能来处理枚举器。当然,最常用的是foreach循环构造:

var enumerable = new MyEnumerable();foreach (var item in enumerable)
{item.Dump(); // 1, 2, 3
}

显然,这只是以下代码的语法糖:

var enumerable = new MyEnumerable();
var iterable = enumerable.GetEnumerator();while (iterable.MoveNext())
{var item = iterable.Current;item.Dump();
}

快速比较MSIL代码可以很容易地确认这一点。使用foreach循环的隐式版本如下所示:

IL_0000:  nop
IL_0001:  newobj      UserQuery+MyEnumerable..ctor
IL_0006:  stloc.0     // enumerable
IL_0007:  nop
IL_0008:  ldloc.0     // enumerable
IL_0009:  callvirt    UserQuery+MyEnumerable.GetEnumerator
IL_000E:  stloc.1
IL_000F:  br.s        IL_0021
IL_0011:  ldloc.1
IL_0012:  callvirt    System.Collections.Generic.IEnumerator<System.Int32>.get_Current
IL_0017:  stloc.2     // item
IL_0018:  nop
IL_0019:  ldloc.2     // item
IL_001A:  call        LINQPad.Extensions.Dump<Int32>
IL_001F:  pop
IL_0020:  nop
IL_0021:  ldloc.1
IL_0022:  callvirt    System.Collections.IEnumerator.MoveNext
IL_0027:  brtrue.s    IL_0011
IL_0029:  leave.s     IL_0036
IL_002B:  ldloc.1
IL_002C:  brfalse.s   IL_0035
IL_002E:  ldloc.1
IL_002F:  callvirt    System.IDisposable.Dispose
IL_0034:  nop
IL_0035:  endfinally
IL_0036:  ret

为了进行比较,显式版本编译为:

IL_0000:  nop
IL_0001:  newobj      UserQuery+MyEnumerable..ctor
IL_0006:  stloc.0     // enumerable
IL_0007:  ldloc.0     // enumerable
IL_0008:  callvirt    UserQuery+MyEnumerable.GetEnumerator
IL_000D:  stloc.1     // iterable
IL_000E:  br.s        IL_0020
IL_0010:  nop
IL_0011:  ldloc.1     // iterable
IL_0012:  callvirt    System.Collections.Generic.IEnumerator<System.Int32>.get_Current
IL_0017:  stloc.2     // item
IL_0018:  ldloc.2     // item
IL_0019:  call        LINQPad.Extensions.Dump<Int32>
IL_001E:  pop
IL_001F:  nop
IL_0020:  ldloc.1     // iterable
IL_0021:  callvirt    System.Collections.IEnumerator.MoveNext
IL_0026:  stloc.3
IL_0027:  ldloc.3
IL_0028:  brtrue.s    IL_0010
IL_002A:  ret

由于IEnumerator实现了IDisposable接口,我们还应该正确配置资源。foreach语法确实是为我们。始终使用生成的代码的另一个原因——通过做正确的事情而无需记住,这只会使我们的生活更轻松。

尽管如此,让我们印象深刻的不是foreach部分,而是IEnumerable/ IEnumerator实现的类的生成。

让我们使用yield关键字来做到这一点。

static IEnumerable<int> GetNumbers()
{var current = 0;while (++current < 4){yield return current;}
}

有趣的是,这小段代码已经代表了上面指定的完整迭代器。C#编译器会生成所有必需的类型,以使我们能够正常工作。用法也一样,只是我们只是调用函数(GetNumbers())而不是显式的构造函数调用(new MyEnumerable())。

让我们回顾一下迭代器的优点。

有用的

应避免的

  • 自定义迭代
  • 广义枚举
  • 状态机
  • 迭代数组(如果已知)

丢弃(Discards)

C#编译器对开发人员施加了很多限制。这些限制中的一些限制用于防止明显的错误,而其他一些限制则用于防止用户运行潜在的无用代码。这些限制之一是禁止在未分配的情况下使用某些表达式。

考虑以下代码:

static void Main()
{2 + 3;
}

现在,这是一个奇怪的代码。它将计算2 + 3的结果,但不会执行任何操作。简而言之,要么编译器会优化该语句,要么我们只会浪费一些CPU周期。

我个人认为这是一个奇怪的限制。是的,上面的代码是没有用的,但是由于C#允许运算符重载,因此在某些情况下,简单的add表达式实际上会产生有意义的副作用。

这种设计选择的(负面的或恼人的)含义可以更实际地看到的场景是一个简单的可空性测试。

static void Run(Action action)
{action ?? throw new ArgumentNullException(nameof(action));action();
}

这可能行不通。但可以执行以下操作:

static void Run(Action action)
{(action ?? throw new ArgumentNullException(nameof(action)))();
}

设计上认为调用表达式是OK的。显然,方法调用的副作用趋势非常高——特别是对于操作员的“不可能”等级。

但是,我们仍然希望保持版本1的可读性。因此,为了减轻这种历史性设计选择的后果,引入了一种特殊的构造:丢弃。

丢弃背后的想法很简单:引入一个始终可以分配给的特殊变量_。它永远不会被读取——这是一个只写变量,无论如何编译器都会对其进行优化。

使用此变量,我们可以回到版本1:

static void Run(Action action)
{_ = action ?? throw new ArgumentNullException(nameof(action));action();
}

_是一种特殊的构造,可以在多种场合看到。假设我们有多项检查:

static void Run<T>(Action<T> action, T arg)where T : class
{_ = action ?? throw new ArgumentNullException(nameof(action));_ = arg ?? throw new ArgumentNullException(nameof(arg));action(arg);
}

很明显,类型action和arg将最有可能是不同的。无论如何,都可以接受分配。使用不必要的out参数也可以这样做:

if (int.TryParse(str, out _))
{// parsed successfully, but we don't care about the result
}

另一个有用的例子是“解雇”任务。在这之前,我通常介绍一种扩展方法,如下所示:

public static class TaskExtensions
{public static void Forget(this Task task){// Empty on purpose; maybe log something?}
}

这样做的好处是,现在我可以很容易地通知C#编译器,一个已用过的任务已经被故意不用了:

public Task StartTask()
{// ...
}public void OnClick()
{StartTask().Forget();
}

使用丢弃,我们不需要额外的扩展方法来传输此类信息。而且,用户已经知道任务会发生什么(提示:答案是“无”)。

public void OnClick()
{_ = StartTask();
}

我们还将在模式匹配部分中使用丢弃。

有用的

应避免的

  • 扔掉信息
  • 运行任何表达式
  • “忘记”任务
  • 储存资讯
  • 无副作用的表达

处理异步代码

在上一节中,我们已经简短地涉及了异步代码的主题。从.NET 4开始,我们有了这种Task类型,可以轻松地处理多个工作流。与任务并行库(TPL)和async / await(C#5 / .NET 4.5)一起,我们拥有了功能强大的工具带,这些工具带多年来一直没有得到改善。

但是,为什么async/ await需要特定版本的.NET?这不仅仅是语言功能吗?像往常一样(例如,内插字符串,元组),如果我们需要特定版本的基类库(BCL),我们会立即知道生成了一些代码,这些代码使用了BCL的类型。如果将方法装饰为async,它将生成一个实现IAsyncStateMachine接口的新类。

IAsyncStateMachine接口如下所示:

public interface IAsyncStateMachine
{void MoveNext();void SetStateMachine(IAsyncStateMachine stateMachine);
}

足够有趣的是,它具有与IEnumerator接口类似的MoveNext方法。实际上,我们可以使用一个特殊的IEnumerator版本来编写自己的async/ await实现。从JavaScript中,我们知道生成器(枚举器/yield语法糖的JavaScript名称)已经被(AB)用来在特性到达语言之前引入async/ await功能。即使在今天,polyfills仍使用此填充(或在生成器不可用的情况下,甚至再降一级)。

让我们来看一个使用async和await的方法的简单示例:

async static Task Run(Func<Task> action)
{await action();
}

这个小片段生成了一个类,如下所示:

在MSIL中,然后在给定的Run方法中使用生成的类:

IL_0000:  newobj      UserQuery+<Run>d__1..ctor
IL_0005:  stloc.0
IL_0006:  ldloc.0
IL_0007:  ldarg.0
IL_0008:  stfld       UserQuery<Run>d__1.action
IL_000D:  ldloc.0
IL_000E:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Create
IL_0013:  stfld       UserQuery+<Run>d__1.<>t__builder
IL_0018:  ldloc.0
IL_0019:  ldc.i4.m1
IL_001A:  stfld       UserQuery+<Run>d__1.<>1__state
IL_001F:  ldloc.0
IL_0020:  ldfld       UserQuery+<Run>d__1.<>t__builder
IL_0025:  stloc.1
IL_0026:  ldloca.s    01
IL_0028:  ldloca.s    00
IL_002A:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder.Start<<Run>d__1>
IL_002F:  ldloc.0
IL_0030:  ldflda      UserQuery+<Run>d__1.<>t__builder
IL_0035:  call        System.Runtime.CompilerServices.AsyncTaskMethodBuilder.get_Task
IL_003A:  ret

简而言之,我们实例化生成的类,存储使用的参数(捕获)并设置要在异步状态机内使用的状态。异步任务方法构建器的静态Create方法用于构建关联的构建器状态。然后,我们运行异步任务方法构建器以为此构造一个任务。最后,我们从构建器返回Task属性。

不用说,上面代码的简单版本是:

static Task Run(Func<Task> action)
{return action();
}

这两个变体并不完全等效。事先,我们返回了一个新生成的任务,“包装”了原始任务。现在,我们返回原始的。在性能方面,它们肯定是不同的。在此版本中,我们省略了完整的类生成。同样,用于运行该方法的MSIL相较而言非常短:

IL_0000:  nop
IL_0001:  ldarg.0
IL_0002:  callvirt    System.Func<System.Threading.Tasks.Task>.Invoke
IL_0007:  stloc.0
IL_0008:  br.s        IL_000A
IL_000A:  ldloc.0
IL_000B:  ret

显然,仅当我们具有多个await或复杂的结构(例如,仅await在某个if块中)时,才使用async方法。在任何其他情况下,我们都应尝试更轻量一些。编译时和运行时都将感谢我们更快的执行速度。

对于将标准项目包装在任务中的情况更是如此。考虑我们创建一个实现需要以下方法的接口的类:

public Task<Task> GetNameAsync()
{// ...
}

如果我们已经知道名称,则可以直接将其返回,但是如何将其包装在任务中呢?最简单的情况是用async来修饰方法,但我们也可以使用Task.FromResult:

public Task<Task> GetNameAsync() => Task.FromResult("constant name");

根据经验,始终先寻找非生成的解决方案。

在这一点上,我们可以写一些关于某些好处的文章,例如何时使用ConfigureAwait(false)和所有我们现在可以使用async/ await做的所有事情(例如在try- catch块中),但我觉得很多的文章(包括我自己)这样做已经做到了这一点。相反,我想触摸awaits迭代器的主题。

在C#8之前,我们没有处理异步流的好方法。等待流等同于仅在流完成时做出反应。另一种选择是等待直到第一个数据来自流。但是,由于不存在数据,因此这也不能解决问题。我们想要的是一个隐式循环,该循环可能await直到某些数据块可用为止。流结束时,循环结束。

所有这些听起来都像是对迭代器的增强。同样,以下不是解决方案:

foreach (var item in await GetItems())
{// ...
}

潜在地,我们可以将流包装成:

foreach (var getNextItem in GetItems())
{var item = await getNextItem();// ...
}

但是,如果没有下一项?现在,我们放置一个回调。如果没有,我们可以接收null或引发异常。两种情况都有明显的缺点。因此,让我们来看一个自定义数据类型。

foreach (var getNextItem in GetItems())
{var state = await getNextItem();if (state.Finished){break;}var item = state.Current;// ...
}

仍然有些混乱,特别是从样板的角度来看。因此,我们现在有await foreach。可以与新IAsyncEnumerable接口结合使用。

public async IAsyncEnumerable<int> GetNumbersAsync()
{var current = 0;while (++current < 4){await Task.Delay(500);yield return current;}
}

现在,我想为您提供有关如何生成(以及究竟生成了什么)的详细信息,但是您可以猜测它与我们之前检查的结构类似。最后,它只是async/ await与迭代器结合的状态机。

通过检查async可枚举的,可以直接看出所有这些相似之处——我们看到这很像“正常”的可枚举的。现在它仅依赖于IAsyncEnumerator(谁会猜到?)称为async的枚举器:

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{T Current { get; }ValueTask<bool> MoveNextAsync();
}

太好了,因此语法糖看起来如何?

await foreach (var item in GetNumbersAsync())
{// ...
}

太好了,所以现在这个距离也缩小了!async迭代器可以是超级强大尤其是对于事件流。

有用的

应避免的

  • 复杂的任务逻辑
  • 驯服多个工作流
  • 处理异步事件/流
  • 包装任务

模式匹配

近年来,C#的方向肯定发生了变化。它吸收了越来越多的功能概念。模式匹配是更有趣的概念之一。C#中的模式匹配有多种方式,例如,改进的is运算符。正式地,他们称其为“模式表达式”。

事先,我们必须使用各种不同的运算符来实现诸如类型转换和后续检查之类的操作。对于类,我们可以使用as:

var element = node as IElement;if (element != null)
{// ...
}

但是有了is操作符的新功能,就不再需要此类片段/临时变量。我们可以编写能读得很好的代码。

if (node is IElement element)
{// ...
}

完美吧?无聊的。好吧,所以也许新的switch控制结构更符合您的口味。就个人而言,我希望看到一个新的match结构,但是,我可以看到为什么避免使用新的保留关键字,并且我喜欢渐进式方法。

switch (node)
{case IElement element:// ...break;
}

让我们检查为该构造生成的MSIL代码:

IL_0000:  nop
IL_0001:  ldarg.1
IL_0002:  stloc.3
IL_0003:  ldloc.3
IL_0004:  stloc.0
IL_0005:  ldloc.0
IL_0006:  brtrue.s    IL_000A
IL_0008:  br.s        IL_0016
IL_000A:  ldloc.0
IL_000B:  isinst      IElement
IL_0010:  dup
IL_0011:  stloc.1
IL_0012:  brfalse.s   IL_0016
IL_0014:  br.s        IL_0018
IL_0016:  br.s        IL_001E
IL_0018:  ldloc.1
IL_0019:  stloc.2     // element
IL_001A:  br.s        IL_001C
IL_001C:  br.s        IL_001E
IL_001E:  ret

好吧,这里没有魔术。几乎和我们写的一样:

if (node is IElement)
{var element = (IElement)node;// ...
}

虽然有一些细微的差异。最值得注意的是,我们以生成的MSIL形式进行了显式转换。使用前面提到的模式表达式,我们将更接近使用新switch结构生成的MSIL代码。因此,我们真的可以说switch纯粹是语法糖来避免重复。

它仅仅是语法表达式之上的语法糖吗?好吧,至少,它是很好的甜糖。特别是,因为它带有扩展名。在switch分支的上下文中,我们可以使用when关键字引入更多条件。

C#文档列出了一个很好的示例:

switch (shape)
{case Square s when s.Side == 0:case Circle c when c.Radius == 0:return 0;case Square s:return s.Side * s.Side;case Circle c:return c.Radius * c.Radius * Math.PI;
}

这样很棒,我们避免了需要使用复杂函数goto或局部函数来避免重复的结构。

C#团队甚至更进一步。除了显式类型(它将检查是否可以进行强制转换)之外,我们还可以隐式使用当前类型。像往常一样,var是触发类型推断的关键字。

官方文档中提到以下示例:

switch (shapeDescription)
{case "circle":return new Circle(2);case "square":return new Square(4);case "large-circle":return new Circle(12);case var o when (o?.Trim().Length ?? 0) == 0:return null;
}

因此,特定的空白情况使用隐式类型来触发使用when的其他检查。或者,我们可以写成case string o when。但是,var应该首选它,因为它在重构的情况下也会经受时间的考验。此外,它将传送给读者“嘿,我不想在这里检查演员表,我只想介绍更多条件”。毕竟,将意图传达给读者很重要。

有用的

应避免的

  • 避免强制重复
  • 简化许多分支拆分
  • 汇集分散但相关的逻辑块
  • 只是替换简单的if语句
  • 合并不相关逻辑的整个块

可空类型

最后,关于类型的一些事!潜在地,多年来(或曾经)C#开发中最重要的变化已经到来。可空类型!

什么?我的意思是,每个类都代表一个堆分配的对象,必须首先创建该对象,否则,它指向称为“null指针”或简单地称为的默认地址null。null引用异常可能是最引人注目的一个,它谈的是它不属于默认的类型系统的语言(或框架)的年龄。幸运的是,C#语言团队中有一些非常聪明的人,他们提出了既可行又适合的解决方案。

以前,我们只是从我们一直在调用的方法中收到一些类型信息。下面的代码是一个示例:

var element = document.QuerySelector("a");
// element is of IElement, but can it be null ?

对于可为null的类型,每种类型T都是非 null。现在,这与值类型保持一致,这要求包装器(伪)可为空(T?或Nullable<T>)。对于引用类型,不存在这样的包装器,但是,信息是通过元数据传输的。

由于这是一个非常敏感的功能,因此需要首先启用它。以下行必须出现在我们要引入可为空类型的项目的csproj文件中。

<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>

现在,我们不仅可以返回类型,还可以使用问号来修饰它,以表示可能返回null。

var element = document.QuerySelector("a");
// element is of IElement?, we should introduce checks!

方法签名也是如此。让我们考虑以下签名:

public void Insert(IElement element);

IElement?不允许将方法与实例一起使用。相反,我们必须引入类型保护。

public void InsertMaybe(IElement? element)
{if (!(element is null)){// type transformed to IElement from IElement?Insert(element);}
}

长话短说:Nullable通过检测需要保护的地方和不需要保护的地方,使我们的生活更轻松。我们应该将违反可空性行为视为错误。

在可为空的上下文中工作的每个方法都将相应地带有一个NonNullTypes属性。此外,可为空的引用也明确标记为Nullable。结果,C#编译器还能够从没有提供源代码的第三方库或BCL中推断出正确的用法。

除了生成的元数据,其他所有内容都照常进行。没有MSIL的含义。这仅仅是使所有应用程序更强大和更好的升级。

有用的

应避免的

  • 运输意图
  • 警告/代码健壮性
  • 避免过多的防护
  • 信任非注释代码

重要:对于引用类型(即类),这是一种只在编译时使用的机制,与Nullable<T>无关,它表示可以赋值为null的值类型(即结构)。

见解

这是本系列的最后一部分。

关于类型,C#的发展似乎尚未完成。F#(基于CLR)或TypeScript(带有JS转译)之类的语言显示了可行的方式。我们可以预期,在未来几年内,生态系统将会有更多的改善。

结论

C#的发展并未停止于使用和生成的类型。我们看到C#为我们提供了一些更先进的技术,以在没有外部工具太多帮助的情况下获得灵活性。但是,外部工具的帮助为我们提供了更多的可能性,而无需付出很多牺牲。

我个人希望TypeScript的灵活类型系统可以用作一个角色模型,以便为我们的工具带带来一些高级的编译时操纵、创建和类型评估。

兴趣点

我总是展示未优化的MSIL代码。一旦MSIL代码得到优化(甚至正在运行),它看起来可能会有所不同。在这里,实际观察到的不同方法之间的差异可能会消失。但是,由于本文重点关注开发人员的灵活性和效率(而不是应用程序性能),因此所有建议仍然有效。

使C#代码现代化——第四部分:类型相关推荐

  1. netbeans代码提示_Java代码现代化的七个NetBeans提示

    netbeans代码提示 在" 七个不可或缺的NetBeans Java提示"一文中 ,我谈到了一般使用NetBeans提示的问题,然后重点介绍了七个提示. 接下来列出了该帖子中强 ...

  2. 使C#代码现代化——第二部分:方法

    目录 介绍 背景 什么是方法? 现代方式 扩展方法 委托 Lambda表达式 LINQ表达式 方法表达式 局部函数 结论 介绍 近年来,C#已经从一种具有一个功能的语言发展成为一种语言,其中包含针对单 ...

  3. 使C#代码现代化——第一部分:属性

    目录 介绍 背景 什么是属性? 从Java到C# 经典方式 现代方式 自动属性 分配的属性 只读属性 属性表达式 Get和Set表达式 结论 介绍 近年来,C#已经从一种具有一个功能的语言发展成为一种 ...

  4. mybatis 鉴别其_MyBatis之Mapper XML 文件详解(四)-JDBC 类型和嵌套查询

    MyBatis之Mapper XML 文件详解(四)-JDBC 类型和嵌套查询 白玉 IT哈哈 支持的 JDBC 类型 为了未来的参考,MyBatis 通过包含的 jdbcType 枚举型,支持下面的 ...

  5. WebServer代码实现第四版

    WebServer代码实现第四版:JDBC重构WebServer业务功能二 前言 1.JDBC批处理与返回自动主键 JDBC批处理:JDBCDEMO_batch.java 代码实现: 题外话 返回自动 ...

  6. C#中方法的参数的四种类型(转)

    转自:http://www.cnblogs.com/netlyf/p/3822956.html C#中方法的参数有四种类型: 1. 值参数类型  (不加任何修饰符,是默认的类型) 2. 引用型参数   ...

  7. 【整理】Python中的re.search和re.findall之间的区别和联系 + re.finall中带命名的组,不带命名的组,非捕获的组,没有分组四种类型之间的区别

    之前自己曾被搞晕过很多次. 后来使用这些函数次数多了之后,终于比较清楚的弄懂了两者之间的区别和关系了. 尤其是一些细节方面的注意事项了. 在看下面的总结和代码之前,请先确保你对如下基本概念已经有所了解 ...

  8. 【翻译】四种类型的为什么:产品背后的驱动力是什么?

    作者:Catherine (Kit) Ulrich 四种类型的为什么:产品背后的驱动力是什么? 最近我写了一篇我提出的叫做思维阶梯的框架的文章,一个简单的小工具为产品人创造出惊艳的愿景.它结合了Sim ...

  9. java abc 979899_商品标题由关键词组成,关键词主要包括核心词、类目词、属性词以及长尾词四种类型。其中属性词是指...

    商品标题由关键词组成,关键词主要包括核心词.类目词.属性词以及长尾词四种类型.其中属性词是指 答:商品属性 网签备案具有创设权利的功能,能产生物权变动的效力. 答:错 下列程序执行后输出的结果是\nx ...

最新文章

  1. OVS技术介绍(四十一)
  2. 使用Jquery+EasyUI进行框架项目开发案例解说之中的一个---员工管理源代码分享
  3. 常见的数据库连接字符串收集
  4. 新一批国产游戏版号下发:共53款 腾讯、网易在列
  5. win32开发(窗口类和窗口)
  6. ES6 iterator 迭代器
  7. python之路--面向对象之封装
  8. 计算机应用基础选择题占多少分,计算机应用基础练习题(选择题部分)..doc
  9. 【疲劳检测】基于matlab行为特征疲劳驾驶检测【含Matlab源码 944期】
  10. 分享两款在线教育教学管理系统源码
  11. 如何解决Vosviewer关键词共现分析出现的Incorrect number of columns错误
  12. Sosoapi环境搭建
  13. 使用Docker部署ONLYOFFICE Document Server
  14. Python函数初识
  15. 【上海落户-个人感受】想到一个城市落户,需要提前打算。
  16. 华为手机怎么录屏?十分简单,轻松学会
  17. 医学遗传学词汇英语术语英文(Glossary) 3
  18. 【文献翻译】MDC-Checker:一种新的多域配置网络风险评估框架
  19. JavaSE02-JVM、JRE、JDK
  20. Navicat新建查询系统找不到指定路径怎么办?

热门文章

  1. c语言400行小游戏,400行代码编C语言控制台界版2048游戏,编写疯子一样的C语言代码...
  2. 单纯形表的matlab输出,自编MATLAB版单纯性算法 可以列出单纯形表以及其他相关数据...
  3. 设计灵感|C4D卡通角色设计作品,你想要的模型集设都有
  4. 设计灵感|独具中国韵味的海报设计
  5. 高清壁纸|海贼王漫画名场面
  6. 淘宝京东设计师来看,电商Banner设计策略!
  7. html.textboxfor属性,label标签中的for属性与form属性
  8. lambert(兰伯特)投影 应用工具_全息投影技术,在哪些场地可以用到
  9. java许愿墙_18.JavaScript实现许愿墙效果
  10. QTableWidget简单使用