使用C#8.0中的模式做更多事情

   原文

使用C#8.0中的模式做更多事情

Visual Studio 2019预览版2已经发布!有了它,还有几个C#8.0功能可供您试用。它主要是关于模式匹配,但我会在最后触及其他一些新闻和变化。

更多地方有更多模式

当C#7.0引入模式匹配时,我们说我们希望将来在更多地方添加更多模式。那个时候到了!我们正在添加我们称之为递归模式的东西 ,以及称为(你猜对了!) 切换表达式的更紧凑的switch语句表达形式。

这是一个简单的C#7.0模式示例来启动我们:

 class Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y) => (X, Y) = (x, y);
    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Display(object o)
{
    switch (o)
    {
        case Point p when p.X == 0 && p.Y == 0:
            return "origin";
        case Point p:
            return $"({p.X}, {p.Y})";
        default:
            return "unknown";
    }
}
 

切换表达式

首先,让我们观察一下,许多switch语句在case确实没有做太多有趣的工作。通常它们都只是通过将其赋值给变量或通过返回它来生成一个值(如上所述)。在所有这些情况下,switch语句坦率地说是笨重的。感觉就像它具有五十年历史的语言特征,有很多仪式。

我们决定是时候添加一个表达形式的switch 。在这里,应用于上面的例子:

 static string Display(object o)
{
    return o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };
}
 

这里有几件事从switch语句改变了。我们列出来吧:

  • switch关键字是测试值和{...}案例列表之间的“中缀”。这使得它与其他表达式更具组合性,并且更容易在视觉上区分开关语句。
  • 为简洁起见, case关键字和:已被lambda arrow =>替换。
  • 为简洁起见, default已替换为_ discard模式。
  • 身体是表达!所选主体的结果将成为切换表达式的结果。

由于表达式需要具有值或抛出异常,因此在没有匹配的情况下到达末尾的switch表达式将引发异常。在可能出现这种情况时,编译器会很好地警告你,但不会强迫你用一个全能的方式结束所有的开关表达式:你可能知道的更好!

当然,由于我们的Display方法现在包含一个return语句,我们可以将它简化为表达式:

     static string Display(object o) => o switch
    {
        Point p when p.X == 0 && p.Y == 0 => "origin",
        Point p                           => $"({p.X}, {p.Y})",
        _                                 => "unknown"
    };
 

说实话,我不确定我们将在这里给出什么样的格式化指导,但应该清楚的是,这更加简洁和清晰,特别是因为简洁通常允许您以“表格”方式格式化开关,如上所述,图案和身体在同一条线上,并且=> s排列在彼此之下。

顺便说一句,我们计划允许一个尾随逗号,在最后一个案例之后与C#中所有其他“花括号中的逗号分隔列表”保持一致,但预览2还不允许这样。

物业模式

说到简洁,这些模式突然变成了上面切换表达式中最重要的元素!让我们为此做些什么。

请注意,switch表达式使用类型模式 Point p (两次),以及when子句为第一种case添加其他条件。

在C#8.0中,我们在类型模式中添加了更多可选元素,这允许模式本身进一步深入到模式匹配的值。您可以通过添加包含嵌套模式的{...}来使其成为属性模式 ,以应用于值的可访问属性或字段。这让我们重写switch表达式如下:

 static string Display(object o) => o switch
{
    Point { X: 0, Y: 0 }         p => "origin",
    Point { X: var x, Y: var y } p => $"({x}, {y})",
    _                              => "unknown"
};
 

两种情况都仍然检查o是一个Point 。然后第一种情况递归地将常量模式0应用于pXY属性,检查它们是否具有该值。因此,我们可以在这个和许多常见情况中消除when子句。

第二种情况将var模式应用于XY每一个。回想一下C#7.0中的var模式总是成功,并且只是声明一个新变量来保存该值。因此xy包含pXpY的int值。

我们从不使用p ,事实上可以省略它:

     Point { X: 0, Y: 0 }         => "origin",
    Point { X: var x, Y: var y } => $"({x}, {y})",
    _                            => "unknown"
 

包括属性模式在内的所有类型模式仍然存在的一点是,它们要求值为非null。这开启了“空”属性模式{}被用作紧凑的“非空”模式的可能性。例如,我们可以用以下两种情况替换后备案例:

     {}                           => o.ToString(),
    null                         => "null"
 

{}处理剩余的非null对象, null获取空值,因此切换是穷举的,编译器不会抱怨值掉落。

位置模式

属性模式并没有使第二个Point案例更短 ,并且看起来不值得那么麻烦,但还有更多可以做的事情。

请注意, Point类有一个Deconstruct方法,即所谓的解构函数 。在C#7.0中,解构器允许在赋值时解构一个值,以便您可以编写例如:

 (int x, int y) = GetPoint(); // split up the Point according to its deconstructor
 

C#7.0没有将解构与模式结合起来。这随位置模式而变化,这是我们在C#8.0中扩展类型模式的另一种方式。如果匹配类型是元组类型或具有解构函数,我们可以使用位置模式作为应用递归模式的紧凑方式,而无需命名属性:

 static string Display(object o) => o switch
{
    Point(0, 0)         => "origin",
    Point(var x, var y) => $"({x}, {y})",
    _                   => "unknown"
};
 

将对象作为Point匹配后,将应用解构函数,并将嵌套模式应用于结果值。

解构主义并不总是合适的。它们只应添加到真正清楚哪个值是哪个类型的类型中。例如,对于Point类,假设第一个值为X而第二个值为Y是安全且直观的,因此上述开关表达式直观且易于阅读。

元组模式

位置模式的一个非常有用的特殊情况是它们应用于元组时。如果将switch语句直接应用于元组表达式,我们甚至允许省略额外的括号集,如switch (x, y, z)而不是switch ((x, y, z))

元组模式非常适合同时测试多个输入。这是一个状态机的简单实现:

 static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition) switch
    {
        (Opened, Close)              => Closed,
        (Closed, Open)               => Opened,
        (Closed, Lock)   when hasKey => Locked,
        (Locked, Unlock) when hasKey => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };
 

当然我们可以选择在开启的元组中包含hasKey而不是使用when子句 - 这真的是一个品味问题:

 static State ChangeState(State current, Transition transition, bool hasKey) =>
    (current, transition, hasKey) switch
    {
        (Opened, Close,  _)    => Closed,
        (Closed, Open,   _)    => Opened,
        (Closed, Lock,   true) => Locked,
        (Locked, Unlock, true) => Closed,
        _ => throw new InvalidOperationException($"Invalid transition")
    };
 

总而言之,我希望您能看到递归模式和切换表达式可以带来更清晰,更具声明性的程序逻辑。

预览2中的其他C#8.0功能

虽然模式功能是在VS 2019预览2中上线的主要功能,但有一些较小的功能,我希望您也会觉得它们既实用又有趣。我不会在这里详细介绍,但只是简单介绍一下。

使用声明

在C#中, using语句总是会导致嵌套级别,这可能非常烦人并且会损害可读性。对于您只想在范围结束时清理资源的简单情况,您现在可以使用声明 。使用声明只是前面带有using关键字的局部变量声明,它们的内容放在当前语句块的末尾。所以代替:

 static void Main(string[] args)
{
    using (var options = Parse(args))
    {
        if (options["verbose"]) { WriteLine("Logging..."); }
        ...
    } // options disposed here
}
 

你可以简单地写

 static void Main(string[] args)
{
    using var options = Parse(args);
    if (options["verbose"]) { WriteLine("Logging..."); }

} // options disposed here
 

一次性参考结构

引用结构是在C#7.2中引入的,这不是重申其有用性的地方,但作为回报它们有一些严重的限制,例如无法实现接口。 Ref结构现在可以是一次性的,无需实现IDisposable接口,只需在其中使用Dispose方法即可。

静态局部函数

如果要确保本地函数不会产生与从封闭范围“捕获”(引用)变量相关联的运行时成本,则可以将其声明为static 。然后编译器将阻止引用封闭函数中声明的任何东西 - 除了其他静态局部函数!

预览1后的更改

预览1的主要功能是可以为空的引用类型和异步流。两者都在预览2中有所改进,所以如果您已经开始使用它们,请注意以下几点。

可空的引用类型

我们在源代码(通过#nullable#pragma warning指令)和项目级别添加了更多选项来控制可空警告。我们还将项目文件opt-in更改为<NullableContextOptions>enable</NullableContextOptions>

异步流

我们改变了编译器期望的IAsyncEnumerable<T>接口的形状!这使编译器与.NET Core 3.0 Preview 1中提供的接口不同步,这可能会给您带来一些麻烦。但是,.NET Core 3.0 Preview 2即将发布,这将使接口重新同步。

有它!

一如既往,我们热衷于您的反馈!请特别使用新的图案功能。你碰到砖墙吗?有什么烦人的事吗?您找到了哪些酷炫有用的场景?点击反馈按钮告诉我们!

快乐的黑客,

Mads Torgersen,C#的设计负责人