MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#中的模式匹配与switch表达式

2022-08-086.8k 阅读

模式匹配基础

模式匹配是 C# 7.0 引入的一项强大功能,它允许开发人员以更简洁、更具表现力的方式检查对象的结构和类型。模式匹配通过将输入值与特定模式进行比较,根据匹配结果执行不同的操作。这种功能在处理复杂的数据结构和条件逻辑时非常有用,能使代码更易读、更易于维护。

模式的类型

C# 中有几种不同类型的模式,包括:

  • 常量模式:匹配一个常量值。例如,在switch语句中,你可以匹配一个具体的数字或字符串。
int number = 5;
switch (number)
{
    case 5:
        Console.WriteLine("The number is 5");
        break;
    default:
        Console.WriteLine("The number is not 5");
        break;
}

在这个例子中,case 5就是一个常量模式。如果number的值等于5,就会执行相应的代码块。

  • 类型模式:匹配特定的类型。这在处理多态对象时非常有用。
object obj = "Hello";
switch (obj)
{
    case string s:
        Console.WriteLine($"It's a string: {s}");
        break;
    case int i:
        Console.WriteLine($"It's an integer: {i}");
        break;
    default:
        Console.WriteLine("Unknown type");
        break;
}

这里case string scase int i是类型模式。obj对象首先被检查是否为string类型,如果是,s会被赋值为该字符串,然后执行相应代码块。

  • var 模式:匹配任何值,并将该值绑定到一个新的变量。
object value = 10;
switch (value)
{
    case var v:
        Console.WriteLine($"The value is {v}");
        break;
}

case var v匹配任何类型的值,并将值赋给v。在这个例子中,v将被赋值为10。

switch 表达式

switch表达式是 C# 8.0 引入的,它是对传统switch语句的一种增强,允许switch返回一个值。这使得代码更加简洁,尤其是在需要根据不同条件返回不同值的场景中。

基本语法

int number = 3;
string result = number switch
{
    1 => "One",
    2 => "Two",
    3 => "Three",
    _ => "Other"
};
Console.WriteLine(result);

在这个例子中,number switchswitch表达式的开始。每个case使用=>语法,右边是匹配该case时返回的值。_表示默认情况,类似于传统switch语句中的default

与传统 switch 语句的对比

传统的switch语句主要用于执行不同的代码块,而switch表达式专注于返回值。例如,假设我们要根据月份返回该月的天数:

// 传统 switch 语句
int GetDaysInMonth(int month, int year)
{
    int days;
    switch (month)
    {
        case 1:
        case 3:
        case 5:
        case 7:
        case 8:
        case 10:
        case 12:
            days = 31;
            break;
        case 4:
        case 6:
        case 9:
        case 11:
            days = 30;
            break;
        case 2:
            days = DateTime.IsLeapYear(year)? 29 : 28;
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(month), "Month must be between 1 and 12");
    }
    return days;
}

使用switch表达式可以更简洁地实现相同功能:

// switch 表达式
int GetDaysInMonth(int month, int year) =>
    month switch
    {
        1 or 3 or 5 or 7 or 8 or 10 or 12 => 31,
        4 or 6 or 9 or 11 => 30,
        2 => DateTime.IsLeapYear(year)? 29 : 28,
        _ => throw new ArgumentOutOfRangeException(nameof(month), "Month must be between 1 and 12")
    };

switch表达式使用or关键字来组合多个匹配条件,使代码更紧凑。

模式匹配与 switch 表达式的结合

模式匹配和switch表达式结合使用,可以实现非常强大和灵活的条件逻辑。

类型模式与 switch 表达式

假设我们有一个形状基类Shape,以及它的子类CircleRectangle

public abstract class Shape { }
public class Circle : Shape
{
    public double Radius { get; set; }
}
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }
}

我们可以使用类型模式和switch表达式来计算不同形状的面积:

double CalculateArea(Shape shape) =>
    shape switch
    {
        Circle c => Math.PI * c.Radius * c.Radius,
        Rectangle r => r.Width * r.Height,
        _ => throw new ArgumentException("Unsupported shape type", nameof(shape))
    };

这里shape switch使用类型模式Circle cRectangle r,如果shapeCircle类型,c将被赋值为该Circle对象,然后计算圆的面积;如果是Rectangle类型,r将被赋值,计算矩形面积。

复合模式

C# 还支持复合模式,即可以在一个模式中组合多个条件。例如,我们可以匹配一个特定范围内的整数:

int num = 15;
string rangeResult = num switch
{
    >= 1 and <= 10 => "In the range 1 - 10",
    >= 11 and <= 20 => "In the range 11 - 20",
    _ => "Outside the range"
};
Console.WriteLine(rangeResult);

在这个例子中,>= 1 and <= 10>= 11 and <= 20是复合模式,使用and关键字组合了多个条件。如果num的值在相应范围内,就会返回对应的字符串。

位置模式

位置模式在解构对象时非常有用。例如,假设我们有一个表示二维点的结构体Point

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

我们可以使用位置模式来匹配Point的不同位置:

Point point = new Point { X = 1, Y = 2 };
string pointResult = point switch
{
    (0, 0) => "Origin",
    (_, 0) => "On X-axis",
    (0, _) => "On Y-axis",
    _ => "Elsewhere"
};
Console.WriteLine(pointResult);

这里(0, 0)(_, 0)(0, _)是位置模式。(_, 0)表示Y坐标为0,X坐标任意(_表示忽略该值)。

在集合处理中的应用

模式匹配和switch表达式在集合处理中也有很好的应用场景。

处理列表中的元素

假设我们有一个包含不同类型形状的列表:

List<Shape> shapes = new List<Shape>
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};
foreach (var shape in shapes)
{
    string description = shape switch
    {
        Circle c => $"Circle with radius {c.Radius}",
        Rectangle r => $"Rectangle with width {r.Width} and height {r.Height}",
        _ => "Unknown shape"
    };
    Console.WriteLine(description);
}

在这个循环中,通过switch表达式和类型模式,我们可以根据形状的具体类型生成相应的描述。

处理数组元素

对于数组,我们可以使用位置模式和switch表达式。例如,假设有一个包含三个整数的数组,我们想根据数组元素的值进行不同操作:

int[] numbers = { 1, 2, 3 };
string arrayResult = numbers switch
{
    [1, _, 3] => "First is 1 and third is 3",
    [_, 2, _] => "Second is 2",
    _ => "Other combination"
};
Console.WriteLine(arrayResult);

这里[1, _, 3][_, 2, _]是针对数组的位置模式。[1, _, 3]表示数组第一个元素为1,第三个元素为3,第二个元素忽略。

模式匹配的高级特性

关系模式

关系模式允许你使用比较运算符(如<><=>=)来匹配值的范围。除了前面提到的整数范围匹配,还可以用于其他可比较类型。例如,对于日期:

DateTime date = new DateTime(2023, 7, 1);
string dateResult = date switch
{
    >= DateTime.Parse("2023-01-01") and <= DateTime.Parse("2023-06-30") => "First half of 2023",
    >= DateTime.Parse("2023-07-01") and <= DateTime.Parse("2023-12-31") => "Second half of 2023",
    _ => "Not in 2023"
};
Console.WriteLine(dateResult);

这里使用关系模式匹配date是否在2023年的上半年或下半年。

逻辑模式

逻辑模式使用andornot关键字来组合其他模式。例如,我们可以组合类型模式和常量模式:

object data = "Hello";
string dataResult = data switch
{
    string s and { Length: >= 5 } => $"Long string: {s}",
    string s => $"Short string: {s}",
    _ => "Not a string"
};
Console.WriteLine(dataResult);

在这个例子中,string s and { Length: >= 5 }是一个逻辑模式,它首先检查data是否为string类型,然后检查字符串长度是否大于等于5。

模式匹配与性能

虽然模式匹配和switch表达式提供了强大的功能和更简洁的代码,但在性能关键的场景中,需要考虑其性能影响。

简单模式匹配的性能

对于简单的常量模式和类型模式匹配,性能通常与传统的if - elseswitch语句相当。例如,在前面的整数常量匹配和类型匹配的例子中,编译器会进行优化,生成高效的代码。

复杂模式匹配的性能

然而,当模式匹配变得复杂,尤其是包含多个复合模式或逻辑模式时,性能可能会受到影响。例如,在包含多个andor组合的逻辑模式中,每次匹配都需要评估多个条件,这可能会增加计算量。在这种情况下,开发人员需要权衡代码的可读性和性能。如果性能是关键因素,可以考虑使用更传统的条件判断语句来优化性能。

性能测试示例

为了直观地了解性能差异,我们可以编写一个简单的性能测试。假设我们要测试一个根据整数范围进行匹配的功能,分别使用switch表达式和传统if - else语句:

using System;
using System.Diagnostics;

class Program
{
    static void Main()
    {
        const int iterations = 10000000;
        Stopwatch stopwatch = new Stopwatch();

        // 使用 switch 表达式测试
        stopwatch.Start();
        for (int i = 0; i < iterations; i++)
        {
            int num = i % 100;
            string result = num switch
            {
                >= 0 and < 20 => "Low",
                >= 20 and < 40 => "Medium - Low",
                >= 40 and < 60 => "Medium",
                >= 60 and < 80 => "Medium - High",
                >= 80 and < 100 => "High",
                _ => "Unknown"
            };
        }
        stopwatch.Stop();
        Console.WriteLine($"switch 表达式时间: {stopwatch.ElapsedMilliseconds} ms");

        // 使用 if - else 语句测试
        stopwatch.Restart();
        for (int i = 0; i < iterations; i++)
        {
            int num = i % 100;
            string result;
            if (num >= 0 && num < 20)
            {
                result = "Low";
            }
            else if (num >= 20 && num < 40)
            {
                result = "Medium - Low";
            }
            else if (num >= 40 && num < 60)
            {
                result = "Medium";
            }
            else if (num >= 60 && num < 80)
            {
                result = "Medium - High";
            }
            else if (num >= 80 && num < 100)
            {
                result = "High";
            }
            else
            {
                result = "Unknown";
            }
        }
        stopwatch.Stop();
        Console.WriteLine($"if - else 语句时间: {stopwatch.ElapsedMilliseconds} ms");
    }
}

通过这个性能测试,我们可以看到在大量迭代的情况下,switch表达式和if - else语句的性能差异。在实际应用中,应根据具体需求和场景选择合适的方式。

模式匹配在实际项目中的应用场景

错误处理

在处理错误码或异常类型时,模式匹配和switch表达式可以使错误处理代码更清晰。例如,假设我们有一个方法可能抛出不同类型的异常:

public static void ProcessData()
{
    try
    {
        // 模拟可能抛出异常的代码
        throw new ArgumentNullException();
    }
    catch (Exception ex)
    {
        string errorMessage = ex switch
        {
            ArgumentNullException => "Argument cannot be null",
            ArgumentOutOfRangeException => "Argument is out of range",
            _ => "Unknown error"
        };
        Console.WriteLine(errorMessage);
    }
}

这里通过switch表达式和类型模式,我们可以根据不同的异常类型生成相应的错误消息,使错误处理逻辑更简洁。

状态机实现

在实现状态机时,模式匹配和switch表达式可以很好地模拟状态转换。例如,假设我们有一个简单的游戏角色状态机,角色有IdleWalkingJumping三种状态:

public enum CharacterState
{
    Idle,
    Walking,
    Jumping
}

public class Character
{
    public CharacterState State { get; set; }

    public void Update()
    {
        // 简单模拟状态转换逻辑
        State = State switch
        {
            CharacterState.Idle => CharacterState.Walking,
            CharacterState.Walking => CharacterState.Jumping,
            CharacterState.Jumping => CharacterState.Idle,
            _ => CharacterState.Idle
        };
    }
}

Update方法中,通过switch表达式,我们可以根据当前状态轻松地转换到下一个状态,使状态机的实现更直观。

数据解析

在解析数据时,模式匹配和switch表达式也非常有用。例如,假设我们从外部源获取到的数据可能是不同类型的,如整数、字符串或布尔值,我们可以使用模式匹配来解析数据:

object dataValue = "123";
int parsedValue = dataValue switch
{
    int i => i,
    string s when int.TryParse(s, out int num) => num,
    _ => throw new FormatException("Unable to parse data")
};
Console.WriteLine(parsedValue);

这里string s when int.TryParse(s, out int num)是一个带when子句的模式。首先检查dataValue是否为string类型,然后使用when子句尝试将其解析为整数,如果解析成功则返回解析后的整数。

与其他语言特性的结合

与 LINQ 的结合

模式匹配和switch表达式可以与 LINQ(Language Integrated Query)结合使用,增强查询的灵活性。例如,假设我们有一个包含不同形状的列表,我们想查询出面积大于某个值的形状描述:

List<Shape> shapesList = new List<Shape>
{
    new Circle { Radius = 5 },
    new Rectangle { Width = 4, Height = 6 }
};
var queryResult = shapesList.Select(shape => shape switch
{
    Circle c when CalculateArea(c) > 50 => $"Large circle with radius {c.Radius}",
    Rectangle r when CalculateArea(r) > 50 => $"Large rectangle with width {r.Width} and height {r.Height}",
    _ => "Small or unknown shape"
});
foreach (var result in queryResult)
{
    Console.WriteLine(result);
}

在这个例子中,通过switch表达式和when子句,我们在Select操作中根据形状的类型和面积进行筛选和转换,生成相应的描述。

与异步编程的结合

在异步编程中,模式匹配和switch表达式也可以发挥作用。例如,假设我们有一个异步方法可能返回不同类型的结果,我们可以在异步操作完成后使用模式匹配来处理结果:

public async Task<object> GetDataAsync()
{
    // 模拟异步操作
    await Task.Delay(1000);
    return 42;
}

public async Task ProcessDataAsync()
{
    object result = await GetDataAsync();
    string processedResult = result switch
    {
        int i => $"The result is an integer: {i}",
        string s => $"The result is a string: {s}",
        _ => "Unknown result type"
    };
    Console.WriteLine(processedResult);
}

这里在异步获取数据后,通过switch表达式根据结果的类型进行相应的处理。

模式匹配和 switch 表达式的未来发展

随着 C# 语言的不断发展,模式匹配和switch表达式有望得到进一步增强。可能的发展方向包括:

  • 更丰富的模式类型:未来可能会引入更多类型的模式,如针对复杂数据结构的模式,进一步简化对这些结构的操作和匹配。
  • 更好的性能优化:编译器和运行时可能会针对模式匹配和switch表达式进行更多性能优化,使开发人员在享受其简洁性的同时,无需过多担心性能问题。
  • 与新语言特性的融合:随着 C# 引入更多新特性,模式匹配和switch表达式可能会与这些特性更紧密地结合,提供更强大的功能。例如,与新的类型系统特性或并发编程模型相结合,为开发人员提供更高效的编程方式。

模式匹配和switch表达式是 C# 中非常强大且实用的功能,它们不仅使代码更简洁、易读,还能提高开发效率。在实际开发中,合理运用这些功能可以解决许多复杂的条件判断和数据处理问题,同时开发人员也需要关注性能和与其他语言特性的结合,以充分发挥它们的优势。无论是处理简单的常量匹配,还是复杂的对象结构和逻辑组合,模式匹配和switch表达式都为 C# 开发带来了更多的灵活性和表现力。