在 C# 8.0 中引入的模式匹配,提供了一种优雅的方式来编写更具表现力和简洁的代码。模式匹配在每个新的 C# 版本中都在不断发展。
在这篇文章中,您将探讨如何使用模式匹配以及如何使用模式匹配在 C# 中编写优雅的代码。
C# 中的模式匹配用于根据模式检查值。它可以用于各种场景,例如类型检查、解构元组和处理不同的数据结构。
C# 中的模式匹配包括以下组件:
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也可以与可以为 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用于将值与常量进行比较。与 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"
};
}
在这里,我们使用具有常量模式的开关表达式来根据整数值返回日期的名称。
您可以使用 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"
};
}
您可以对枚举使用模式匹配。让我们定义一个枚举: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用于根据对象的属性匹配对象。
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用于将值解构为其各个部分并匹配它们。
例如,我们可以将 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}).");
}
}
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;
}
}
您可以对开关表达式中的多个值使用模式匹配。例如,根据目的地和重量计算要递送的货件的天数:
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
};
}
当与子句结合使用时,使用 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"
};
}