如何使用 C# 模式匹配编写优雅的代码

作者:微信公众号:【架构师老卢】
8-2 10:3
27

概述:在 C# 8.0 中引入的模式匹配,提供了一种优雅的方式来编写更具表现力和简洁的代码。模式匹配在每个新的 C# 版本中都在不断发展。在这篇文章中,您将探讨如何使用模式匹配以及如何使用模式匹配在 C# 中编写优雅的代码。什么是C#语言中的模式匹配C# 中的模式匹配用于根据模式检查值。它可以用于各种场景,例如类型检查、解构元组和处理不同的数据结构。C# 中的模式匹配包括以下组件:Type PatternsType Patterns with Nullable TypesConstant PatternsProperty PatternsPositional PatternsList Pattern

在 C# 8.0 中引入的模式匹配,提供了一种优雅的方式来编写更具表现力和简洁的代码。模式匹配在每个新的 C# 版本中都在不断发展。

在这篇文章中,您将探讨如何使用模式匹配以及如何使用模式匹配在 C# 中编写优雅的代码。

什么是C#语言中的模式匹配

C# 中的模式匹配用于根据模式检查值。它可以用于各种场景,例如类型检查、解构元组和处理不同的数据结构。

C# 中的模式匹配包括以下组件:

  • Type Patterns
  • Type Patterns with Nullable Types
  • Constant Patterns
  • Property Patterns
  • Positional Patterns
  • List Patterns

Type Patterns

Type Patterns 用于检查值是否为特定类型。它们可以与关键字一起使用。您可以使用此模式为变量分配匹配类型:is

public void Process(object obj)  
{  
    if (obj is string s)  
    {  
        Console.WriteLine($"String: {s}");  
    }  
    else if (obj is int i)  
    {  
        Console.WriteLine($"Integer: {i}");  
    }  
}

在此示例中,我们检查是 a 还是 an,并相应地处理每种情况。objstringinteger

Type Patterns with Nullable Types

Type Patterns with Nullable Types也可以与可以为 null 的类型一起使用。您可以使用 and 关键字来检查值是否为:isis notnull

public void CheckForNull(object? obj)  
{  
    if (obj is null)  
    {  
        Console.WriteLine("Object is null.");  
    }  
    else if (obj is not null)  
    {  
        Console.WriteLine("Object is not null.");  
    }  
}

您可以使用关键字来检查值是否属于特定类型,包括可以为 null 的类型:is

public void ProcessNullable(int? number)  
{  
    if (number is int value)  
    {  
        Console.WriteLine($"Number is {value}.");  
    }  
    else  
    {  
        Console.WriteLine("Number is null.");  
    }  
}

Constant Patterns

Constant Patterns用于将值与常量进行比较。与 switch 表达式一起,模式匹配看起来非常优雅:

public string GetDayType(int day)  
{  
    return day switch  
    {  
        0 => "Sunday",  
        1 => "Monday",  
        2 => "Tuesday",  
        3 => "Wednesday",  
        4 => "Thursday",  
        5 => "Friday",  
        6 => "Saturday",  
        _ => "Invalid day"  
    };  
}

在这里,我们使用具有常量模式开关表达式来根据整数值返回日期的名称。

Range Constant Patterns

您可以使用 pattern 组合器为数字创建范围比较:andor

public string EvaluateNumber(int number)
{
    return number switch
    {
        > 0 and <= 10 => "Number is between 1 and 10",
        > 10 and <= 20 => "Number is between 11 and 20",
        > 20 and <= 30 => "Number is between 21 and 30",
        _ => "Number is out of range"
    };
}

public string CheckSpecialNumber(int number)
{
    return number switch
    {
        5 or 23 or 42 => "Special number",
        _ => "Regular number"
    };
}

您可以使用它来排除一系列数字:not

public string ExcludeNumber(int number)  
{  
    return number switch  
    {  
        not (>= 10 and <= 20) => "Number is not between 10 and 20",  
        _ => "Number is between 10 and 20"  
    };  
}

这种模式匹配不仅限于数字,您还可以使用它来检查字符:

public string EvaluateLetter(char letter)  
{  
    return letter switch  
    {  
        >= 'A' and <= 'Z' => "Uppercase letter",  
        >= 'a' and <= 'z' => "Lowercase letter",  
        _ => "Not a letter"  
    };  
}  
  
public string CheckVowel(char letter)  
{  
    return letter switch  
    {  
        'A' or 'E' or 'I' or 'O' or 'U' or 'a' or 'e' or 'i' or 'o' or 'u' => "Vowel",  
        _ => "Consonant"  
    };  
}

您可以使用它来排除一系列字符:not

public string ExcludeSpecialCharacter(char character)  
{  
    return character switch  
    {  
        not (>= '0' and <= '9') and not (>= 'A' and <= 'Z') and not (>= 'a' and <= 'z') => "Special character",  
        _ => "Alphanumeric character"  
    };  
}

Enum Constant Patterns

您可以对枚举使用模式匹配。让我们定义一个枚举:OrderStatus

public enum OrderStatus  
{  
    Pending,  
    Processing,  
    Shipped,  
    Delivered,  
    Cancelled,  
    Returned  
}

您也可以将 、 用于枚举:andornot

public string GetOrderStatusMessage(OrderStatus status)  
{  
    return status switch  
    {  
        OrderStatus.Pending or OrderStatus.Processing => "Order is in progress.",  
        OrderStatus.Shipped or OrderStatus.Delivered => "Order is on its way or has been delivered.",  
        OrderStatus.Cancelled or OrderStatus.Returned => "Order has been cancelled or returned.",  
        _ => "Unknown order status."  
    };  
}

您可以使用 否定枚举模式。您可以通过以下方式检查订单是否已完成:not

public bool IsOrderFinished(OrderStatus status)  
{  
    return status switch  
    {  
        not (OrderStatus.Delivered or OrderStatus.Cancelled or OrderStatus.Returned) => true,  
        _ => false  
    };  
}

使用 with 时,了解逻辑的应用方式非常重要。运算符否定它后面的整个表达式。notornot

如果需要检查顺序是否为 not 和 not,请为所有应否定的表达式加上括号:ShippedDeliverednot

not (OrderStatus.Shipped or OrderStatus.Delivered)

如果遗漏括号,则以下解释将不正确:

not OrderStatus.Shipped or OrderStatus.Delivered

因为它会检查订单是不是或订单,而不是检查两个选项的“不是”。ShippedDelivered

Property Patterns

Property Patterns用于根据对象的属性匹配对象。

public record Person(string Name, int Age);  
  
public void DisplayPersonInfo(Person person)  
{  
    if (person is { Name: "Anton", Age: 30 })  
    {  
        Console.WriteLine("Anton is 30 years old.");  
    }  
}

在此示例中,我们检查对象的名称是否为“Anton”且年龄为 30 岁。Person

让我们来探讨另一个代码示例,该示例检查一个人是否为空,是否具有专用的姓名和年龄:

public void DisplayPersonInfo(Person? person)  
{  
    if (person != null && person.Name == "Anton" && person.Age == 30)  
    {  
        Console.WriteLine("Anton is 30 years old.");  
    }  
}

通过模式匹配,可以使此代码更加简洁易读:

public void DisplayPersonInfo(Person? person)  
{  
    if (person is { Name: "Anton", Age: 30 })  
    {  
        Console.WriteLine("Anton is 30 years old.");  
    }  
}

如果需要使用与表达式匹配的 person 对象,可以在模式匹配中分配一个变量:

public void DisplayPersonInfo(Person? person)  
{  
    if (person is { Name: "Anton", Age: 30 } p)  
    {  
        Console.WriteLine($"{p.Name} is {p.Age} years old.");  
    }  
}

在这里,您可以使用变量来访问 Person 的属性。pNameAge

您还可以在模式匹配中使用否定表达式来简化代码。而不是使用几个逻辑表达式来匹配不是“Anton”或为 null 的人:

public void DisplayPersonInfo(Person? person)  
{  
    if (person is null || (person.Name != "Anton" && person.Age != 30))  
    {  
        Console.WriteLine("User is not found.");  
    }  
}

您可以使用匹配所有非“Anton”人员的表达式,包括 null 人员:is not

public void DisplayPersonInfo(Person? person)  
{  
    if (person is not { Name: "Anton", Age: 30 })  
    {  
        // Either user is null or not Anton  
        Console.WriteLine("User is not found.");  
    }  
}

您还可以检查并使用空属性模式匹配:nullnot null

public void CheckForNull(object? obj)  
{  
    if (obj is {})  
    {  
        Console.WriteLine("Object is not null.");  
    }  
    else if (obj is not {})  
    {  
        Console.WriteLine("Object is null.");  
    }  
}

Positional Patterns

Positional Patterns用于将值解构为其各个部分并匹配它们。

例如,我们可以将 Point 对象解构为它的 and 坐标并处理不同的情况:xy

public readonly struct 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);  
}  
  
public void DisplayPointInfo(Point point)  
{  
    if (point is (0, 0))  
    {  
        Console.WriteLine("Point is at the origin.");  
    }  
    else if (point is (int x, int y))  
    {  
        Console.WriteLine($"Point is at ({x}, {y}).");  
    }  
}

List Patterns

C# 11 中引入List Patterns在序列上启用模式匹配。例如,您可以根据不同的模式匹配整数列表:

public void AnalyzeList(List<int> numbers)  
{  
    switch (numbers)  
    {  
        case []:  
            Console.WriteLine("The list is empty.");  
            break;  
        case [1, 2, 3]:  
            Console.WriteLine("The list contains 1, 2, 3.");  
            break;  
        case [var first, var second, ..]:  
            Console.WriteLine($"The list starts with {first} and {second}.");  
            break;  
        default:  
            Console.WriteLine("The list has a different pattern.");  
            break;  
    }  
}

Pattern Matching On Multiple Values

您可以对开关表达式中的多个值使用模式匹配。例如,根据目的地和重量计算要递送的货件的天数:

public int CalculateDeliveryDays(string destination, decimal weight)
{
    return (destination, weight) switch
    {
        ("Local", <= 1.0m) => 1,
        ("Local", <= 5.0m) => 2,
        ("Local", > 5.0m) => 3,
        
        ("International", <= 1.0m) => 5,
        ("International", <= 5.0m) => 7,
        ("International", > 5.0m) => 10,

        _ => 10 // Default case for unspecified scenarios
    };
}

Pattern Matching with Switch Expressions and When Clauses

当与子句结合使用时,使用 switch 表达式的模式匹配变得更加强大。该关键字允许您向每种情况添加其他条件,从而可以更好地控制您的逻辑。whenwhen

让我们探讨一个涉及根据类别、价格和会员与非会员客户计算折扣的示例:

public decimal CalculateDiscount(string category, decimal price, bool isMember)
{
    return (category, price) switch
    {
        // 5% discount on electronics under $100
        ("Electronics", <= 100.0m) when !isMember => price * 0.05m,

        // 10% discount on electronics under $500
        ("Electronics", <= 500.0m) when !isMember => price * 0.10m,

        // 15% discount on electronics over $500
        ("Electronics", > 500.0m) when !isMember => price * 0.15m,

        // 7% discount for members on electronics under $100
        ("Electronics", > 100.0m) when isMember => price * 0.07m,

        // 12% discount for members on electronics under $500
        ("Electronics", <= 500.0m) when isMember => price * 0.12m,

        // 18% discount for members on electronics over $500
        ("Electronics", > 500.0m) when isMember => price * 0.18m,

        _ => 0.0m // No discount for unspecified scenarios
    };
}

与普通客户相比,会员在这里可以获得额外的折扣。

使用子句,您甚至可以丢弃所有模式并编写逻辑表达式以使模式匹配:when

public enum MemberType
{
    Bronze,
    Silver,
    Gold
}

public decimal CalculateDiscount(string category, decimal price, MemberType memberType)
{
    return (category, price) switch
    {
        // 3% discount for Bronze members on electronics
        _ when memberType is MemberType.Bronze && category == "Electronics" => price * 0.03m,

        // 5% discount for Silver members on electronics
        _ when memberType is MemberType.Silver && category == "Electronics" => price * 0.05m,

        // 8% discount for Gold members on electronics
        _ when memberType is MemberType.Gold && category == "Electronics" => price * 0.08m,

        _ => 0.0m
    };
}

我最喜欢的一个例子是识别当前温度,说明您可以使用模式匹配和开关表达式编写多么优雅的代码:

public string ClassifyTemperatureImproved(int temperature)
{
    return temperature switch
    {
        < 0 => "Freezing",
        >= 0 and < 10 => "Cold",
        >= 10 and < 20 => "Cool",
        >= 20 and < 30 => "Warm",
        >= 30 => "Hot"
    };
}
阅读排行