我如何在C#8中度过夏天

在最近发布的DotNet&More Blazor播客,NetCore 3.0 Preview,C#8中,我们不仅随便提到了诸如C#8这样的热门话题。 有关使用C#8的经验的故事还不够大,无法单独解决,因此决定与它分享书信体裁的方式。


在本文中,我想谈谈我在生产环境中使用C#8的4个月的经验。 您可以在下面找到以下问题的答案:


  • 如何在新的C#中“拼写”
  • 哪些功能真正有用
  • 什么令人失望

可以从Microsoft官方文档中找到C#8功能的完整列表。 在本文中,我将忽略由于某种原因而无法尝试的那些机会,即:


  • 只读成员
  • 默认界面成员
  • 一次性引用结构
  • 异步流
  • 指数和范围

我建议从一种在我看来似乎最美味的方式开始。


切换表达式


在我们的梦想中,我们非常乐观地呈现此功能:


int Exec(Operation operation, int x, int y) => operation switch { Operation.Summ => x + y, Operation.Diff => x - y, Operation.Mult => x * y, Operation.Div => x / y, _ => throw new NotSupportedException() }; 

但是,不幸的是,现实会自行调整。
首先,不可能合并条件:


  string TrafficLights(Signal signal) { switch (signal) { case Signal.Red: case Signal.Yellow: return "stop"; case Signal.Green: return "go"; default: throw new NotSupportedException(); } } 

实际上,这意味着在一半情况下,必须将switch表达式转换为常规switch,以避免复制粘贴。


其次,新语法不支持语句,即 不返回值的代码。 看起来似乎很好,并且没有必要,但是当我意识到在测试中使用断言(例如与模式匹配结合使用)的频率时,我自己感到惊讶。


第三,最后一段后面的switch表达式不支持多行处理程序。 在添加日志时,我们了解到多么可怕:


  int ExecFull(Operation operation, int x, int y) { switch (operation) { case Operation.Summ: logger.LogTrace("{x} + {y}", x, y); return x + y; case Operation.Diff: logger.LogTrace("{x} - {y}", x, y); return x - y; case Operation.Mult: logger.LogTrace("{x} * {y}", x, y); return x * y; case Operation.Div: logger.LogTrace("{x} / {y}", x, y); return x / y; default: throw new NotSupportedException(); } } 

我不想说新开关不好。 不,他很好,只是还不够好。


属性和位置模式


一年前,在我看来,他们似乎是“改变发展的机会”头衔的主要候选人。 而且,正如预期的那样,为了充分利用位置和财产模式的力量,您需要更改开发方法。 即,必须模仿代数数据类型。
看来是什么问题了:进入标记界面并继续。 不幸的是,这种方法在大型项目中有一个严重的缺点:没有人能保证在设计时跟踪您的代数类型的扩展。 因此,很可能随着时间的流逝,对代码的更改将在最意想不到的地方导致很多“默认失败”。


元组模式


但是事实证明,与样本进行比较的新可能性的“年轻兄弟”确实做得很好。 事实是,元组模式不需要在我们熟悉的代码架构中进行任何更改,它只是简化了一些情况:


  Player? Play(Gesture left, Gesture right) { switch (left, right) { case (Gesture.Rock, Gesture.Rock): case (Gesture.Paper, Gesture.Paper): case (Gesture.Scissors, Gesture.Scissors): return null; case (Gesture.Rock, Gesture.Scissors): case (Gesture.Scissors, Gesture.Paper): case (Gesture.Paper, Gesture.Rock): return Player.Left; case (Gesture.Paper, Gesture.Scissors): case (Gesture.Rock, Gesture.Paper): case (Gesture.Scissors, Gesture.Rock): return Player.Right; default: throw new NotSupportedException(); } } 

但是最好的部分是,此功能可以预知,可以与Deconstruct方法一起使用。 只需传递带有已实现的Deconstruct的类即可切换和使用元组模式的功能。


使用声明


这似乎是一个很小的机会,但是却带来了很多欢乐。 在所有促销活动中,Microsoft都在谈论减少嵌套的方面。 但是说实话,没有那么重要。 但是真正严重的是排除一个代码块的副作用:


  • 通常,在添加using时,我们必须使用copy-paste方法将代码“放入”该块。 现在我们只是不考虑它
  • 在using内部声明并在using对象的Dispose之后使用的变量确实让人头疼。 一个少的问题
  • 在需要频繁进行Dispose调用的类中,每个方法长2行。 这看起来有些琐事,但是在许多小的方法的情况下,此琐事不允许在一个屏幕上显示足够数量的这些方法

结果,使用声明之类的简单操作极大地改变了编码的感觉,以至于您根本不想返回到c#7.3。


静态局部功能


老实说,如果没有代码分析的帮助,我什至不会注意到这种可能性。 尽管如此,她还是坚定地执行了我的代码:毕竟,静态局部函数非常适合小型纯函数的角色,因为它们无法支持方法变量的关闭。 这样一来,您就可以轻松上手,因为您知道代码中的潜在错误少了一个。


可空引用类型


对于甜点,我想提到C#8的最重要功能。 实际上,解析可为空的引用类型值得单独撰写。 我只想描述这些感觉。


  • 首先,这很棒。 我本来可以描述我明确声明要声明一个可为空的字段或属性的意图,但是现在此功能已内置在语言中。
  • 其次,它根本不保存NullReferenceException中的内容。 我不是在谈论警告时臭名昭著的“堵塞”。 只是在运行时没有人为您生成任何空参数检查,所以不要急于抛出诸如throw new ArgumentNullException()的代码
  • 第三,DTO存在严重问题。 例如,您用Required属性注释属性。 因此,具有100%not null属性的对象将进入您的WebAPI控制器。 但是,不可能将此属性和所有类似属性与可为空的引用类型检查相关联。 问题是,如果您声明标准的MyProperty {get; set;}具有NotNull类型属性,您将收到警告: “ [CS8618]不可空的属性'MyProperty'未初始化。请考虑将该属性声明为可空的” 。 这很公平,因为您不能保证在初始化过程中不存在null语义。 此功能的唯一结果是无法在任何DTO中使用非null属性。 但是有个好消息,有一个简单的解决方法-只需使用默认值初始化字段即可:
     public string MyProperty { get; set; } = ""; 
  • 第四,处理复杂情况的属性(例如TryGetValue)本身非常复杂。 结果,很有可能不是非常有意识的开发人员会滥用运算符(!),从而使可空引用类型的功能达到水平。 分析仪的希望之一。
  • 第五点,也是最重要的一点,就我个人而言,这个机会已经使我从NullReferenceException错误中救了很多遍。 事实证明,这样可以节省时间,在编译阶段会捕获很多错误,而不会进行测试或调试。 这不仅在开发复杂的业务逻辑的过程中尤其如此,在使用外部库,DTO和其他依赖项(可能包含空值)的琐碎工作中也是如此。

总结


当然,所提供的机会并没有完全成熟,但是C#和F#/ Scala之间的差距越来越小。 无论好坏,时间都会证明一切。


在发布本文时,C#8可能已经纳入您的项目,所以我想知道您对我们最喜欢的语言的新版本有何看法?

Source: https://habr.com/ru/post/zh-CN467655/


All Articles