对于回归的 .NET 开发人员来说,C# 的新功能非常棒

2025-05-25

对于回归的 .NET 开发人员来说,C# 的新功能非常棒

在Twitter上关注我,很高兴接受您对主题或改进的建议/Chris

所以我暂时脱离了这个圈子一段时间了。实际上,我一直在写全栈 JavaScript 代码,回到 .NET 之后,我注意到 C# 语言新增了一些很棒的特性,这些特性我以前在 JavaScript 和函数式语言中都见过。这让我非常高兴,因为这意味着 C# 能够从不同的范式中汲取经验,最重要的是不断发展,变得越来越好。此外,看到 .NET Core 变得更快、更好也是一件好事 ;) .NET 可以在所有平台上运行,谁能在 2000 年初就想到这一点呢?:)

那么这些特征是什么?

参考

-1-解构

这是我非常喜欢的 JavaScript 特性之一,当然在 JavaScript 中它被称为解构。那么它的具体含义是什么呢?它指的是,当你想引用一个对象的几个属性或一个列表中的项目,但又不想引用它们的全部时。我们来看一个 JS 示例:

// JavaScript example

function getPerson() {
  return {
    name: 'chris',
    company: 'Microsoft',
    title: 'Cloud Advocate' 
  };
}

// without destructuring
const person = getPerson();
console.log(person.name);
console.log(person.company);

// with destructuring
const { name, company } = getPerson();
console.log(name);
console.log(company);

正如你在上面看到的,我person每次想和会员聊天就得打字。好多多余的字符 :/

那么这在 C# 中看起来怎么样?

(var pname, var pcompany) = CreatePerson("chris", "microsoft")

好的,看起来很相似。它到底CreatePerson()给我带来了什么?

在这种情况下,它返回一个对象,但也可以是一个元组。

好的,那么这有效吗?

嗯,这取决于你正在处理什么结构。它目前适用于元组和类,不过对于类,你需要做一些额外的工作才能启动和运行。

元组

我们先来了解一下 Tuple,以便理解它的来龙去脉。首先创建一个生成 Tuple 的方法。如下所示:

public static (string, string) GetPerson(string name, string company) 
{
    return (name, company);
}

我们可以用两种方法来解决这个问题:

  1. 请求元组中所有的变量
  2. 要求尽可能多的,但使用一次性字符_

我们来演示一下第一种情况:

所有变量

var (name, company) = GetPerson("Chris", "Microsoft");
// name = "Chris"
// company = "Microsoft"

丢弃案例

var (name, _) = GetPerson("Chris", "Microsoft");
// name = "Chris"

上面我们用来_表示我们不关心元组中的第二个参数。

如果我们有 3 个或更多参数,它们是否都存储在 中_

嗯,不是。你需要为每个位置声明一个我们不关心的位置,这意味着我们需要像这样输入:

var (name, _, _) = GetPerson("Chris", "Microsoft");

课程

我们也可以对类执行 Destruct 操作,并像对 Tuple 一样访问特定字段。不过,为了实现这一点,我们需要Deconstruct()在类中添加一个方法,如下所示:

public class Person 
{
    public string Name { get; set; }
    public string Company { get; set; }
    public string Country { get; set; }

    public void Deconstruct(out string name, out string company) 
    {
        name = Name;
        company = Company;
    }

    public void Deconstruct(out string name, out string company, out string country)
    {
        name = Name;
        company = Company;
        country = Country;
    }
}

// and access like so
(var name, var company) = new Person(){ Name = "Chris", Company ="Microsoft" };

对于我们不感兴趣的职位,同样的规则也适用,即使用_类似如下的内容:

(var name, var company, _) = new Person(){ Name = "Chris", Company ="Microsoft" };

上面我们说的是,我们正在丢弃country

重载解构

你可能注意到了,我们的类中Person有两个Deconstruct()方法。这是因为我们进行了重载,这使得我们能够像下面这样写:

(var name, var company, _) = new Person(){ Name = "Chris", Company ="Microsoft" };

像这样:

(var name, var company) = new Person(){ Name = "Chris", Company ="Microsoft" };

上面我们实际上删除了我们的丢弃_,因为我们现在使用Deconstruct()带有两个参数的。

但是等一下,那么我什么时候需要丢弃_呢?

在以下情况下,您需要一个参数:

(var name, _ ) = new Person(){ Name = "Chris", Company ="Microsoft" };

上面我们以两个参数为目标,Deconstruct()同时丢弃最后一个参数。

那么为什么我不能直接输入( var name )

它就是这样实现的,我能告诉你什么呢?记住,如果使用重载,请使用两个或更多参数来使用它。

要了解有关 Deconstruct 的更多信息,请阅读此处:

https://docs.microsoft.com/en-us/dotnet/csharp/deconstruct

-2- 模式匹配

模式匹配是我在 F# 等函数式语言中见过的。那么它的目的是什么呢?嗯,如果你问我,那就是为了减少输入,并更精确地比较你真正想要的内容。那么我们究竟在讨论什么呢?Switch-case 以及如何让它们更易于阅读和更有目的性。

请给我看看

好的。我以前在 C# 中支持对字符串、整数和枚举进行 switch case 操作,就是这样。现在别想在对象上使用它了,那样我就得用 if、else-if 语句,然后强制类型转换对象等等。我们来展示一些代码,这样大家就能理解了:

 public abstract class Character 
    {
      private string _name;
      public string Name 
      { 
        get {
            return _name;
        }
      }
      public Character(string name)
      {
          _name = name;
      }
    }
    public class Hero: Character 
    {
      public Hero(string name): base("The Good" + name){}
      public string Healing() 
      {
          return "Performs healing";
      }
    }
    public class Villain: Character 
    {
      public Villain(string name) : base("The Evil "+ name) { }

      public string Lightning()
      {
          return "Performs Force lightning";
      }
    }

现在想象一下我们有一些这样的代码:

var hero = new Hero("Luke");
var villain = new Villain("Darth Vader");
Character character = hero;
var ability = string.Empty;

现在让我们从我记得的 C# 中添加一些切换逻辑:

if(character is Hero) 
{
  var h = (Hero) character;
  ability = h.Healing();
} else {
  var v = (Villain) character;
  ability = v.Lightning();
}

我们对此感到高兴吗?不,真的不高兴。不过好消息是,根据 C# 7,我们可以这样做:

switch(character) 
{
    case Hero hero1:
        ability = hero1.Healing();
        break;
    case Villain villain1:
        ability = villain1.Lightning();
        break;
}

上面我们创建了对象hero1,或者villain1根据具体情况进行创建。这样可以减少一些输入。

拿着我的啤酒,让我们开始 C# 8.0 吧 :)

ability = character switch {
  Hero hero2 => hero2.Healing(),
  Villain villain2 => villain2.Lightning(),
  _ => "Unknown" 
};

看起来确实漂亮又紧凑:)

正确的?

 位置模式

C# 8 简直让我们惊叹不已,至少我是这样的 :) 那么,我们还有什么呢?我们称之为位置模式。这种模式使我们能够检查对象,并根据其设置的属性采取相应的措施。

假设我们有一张发票。根据它的状态,我们可能想以不同的方式处理它。那么,在给定模式匹配的 switch-case 语句中,我们该如何表达呢?

好吧,在我们开始之前,我们先假设我们将使用Destruction来实现这一点。接下来的问题是,哪些值可能值得关注。假设类 , 具有以下属性InvoiceBossSigned我们SkipLevelBossSigned制定一条业务规则,规定如果老板和上级老板都签署了发票,则可以处理该发票。

  • BossSigned,具有价值null或签署日期
  • SkipLevelBossSigned,具有价值null或签署日期
var result = invoice switch 
{
  Invoice (null, null) => "Boss need to sign",
  Invoice (_ , null) => "Skip level boss need to sign",
  _ => "All good, process"
}

属性模式

到目前为止,我们匹配了类型Invoice,但我们也可以添加一些我们不关心类型,只关心形状的东西

var result = shape switch 
{
  { BossSigned: null, SkipLevelBossSigned: null } => "Boss need to sign",
  { SkipLevelBossSigned: null } => "Skip level boss need to sign",
  _ => "All good, process"
}

不过,上面的代码存在风险,如果_值为 null,则会触发 default case。在投入生产之前,你可能需要修复这段代码。

-3- 记录

这东西还没落地,但我真是兴奋极了。它是什么?嗯,简而言之,忘掉字段声明吧。

你是认真的吗?我会得到什么?看起来会怎么样?

好的,对我来说,这得益于 Scala 和 Typescript,它们能够帮你完成繁重的工作,也就是创建字段。在 TypeScript 中如下所示:

class Hero {
  constructor(private name: String, private hp: number, public shout: String)
}

上面的想法是为你创建字段namehpshout。这样你就不必像这样输入它们了:

// the above would compile to this

class Hero {
  constructor(name: String, hp: number, shout: String) {
    this.name = name;
    this.hp = hp;
    this.shout = shout;
  }
}

作为一名 C# 开发人员,这看起来有点落后,类型最后?

它让我想起了这个模因,但是,它就是这样的:)

如果用 C# 实现会怎么样?他们正在努力,但提案应该是这样的:

public class Hero(String name, int hp, String shout);

编译后会变成这样:

public class Hero : IEquatable<Hero>
{
  public String name;
  public int hp;
  public String shout;
  public Hero(string name, int hp, string shout) 
  {
    this.name = name;
    this.hp = hp;
    this.shout = shout;
  }

  public bool Equals(Hero other)
  {
      //
  }

  public override bool Equals(object other)
  {
      //
  }

  public override int GetHashCode()
  {
      //
  }

  public void Deconstruct(out string name, out int hp, out string shout)
  {
      name = name;
      hp = hp;
      shout = shout;
  }

    public Hero With(string name = this.name, int age = this.age, string shout = this.shout) => new Hero(name, hp, shout);
}

因此,我们会获得很多额外的功能,例如 Deconstruct、Equal 以及使用新语法WithWith这也是一个新功能,可以帮助我们创建新的不可变对象。它允许我们像这样输入:

var hero = GetHero(); // get a Hero object from somewhere
var newHero = hero.With(hp = 23)

// alt syntax
var otherHero = hero with { hp = 23 }; 

 概括

本文到此结束。如果你和我一样回归.NET,你会发现它和.NET Core 一样,但又有所不同。哦对了,.NET Core 绝对是个重磅消息。它能够在 Linux/Mac 上运行 .NET,容器化你的代码等等。谁能想到这一天会到来呢?

那么坦白说,我会使用这些结构吗?我肯定会使用模式匹配,即使我很多时候会求助于继承,从而避免使用这种逻辑。

那么解构呢?我的意思是,对于元组来说,确实如此,它不需要额外的工作就能正常工作。对于对象呢?我觉得自己会做以下两件事之一:

  1. 在类上创建一个方法,返回我需要的元组
  2. 是的,Deconstruct()在合适的地方添加方法。不过我会问自己:我应该把字段从类传递到外部上下文吗?字段不是实现细节吗?但是属性呢?我们有包含很多公共字段的数据传输对象,所以

记录,我的生活需要它们。迫不及待地想看到它们最终实现 :)

文章来源:https://dev.to/dotnet/great-new-features-in-c-for-a-returning-net-dev-i2l
PREV
如何使用 GraphQL .Net Core 和 Entity Framework 构建 Web API 使用 GraphQL .Net Core 和 Entity Framework 构建 Web API
NEXT
了解如何在 .Net Core 3.0 中构建您的第一个 Blazor WebAssembly 应用程序