22 мая 2025

Сопоставление с образцом в C#

Современный C# претерпел тихую революцию в том, как мы обрабатываем условную логику. Прошли те времена, когда проверки типов и сравнения значений требовали многословных каскадов if-else или неуклюжих операторов switch. Введение сложных возможностей сопоставления с образцом, особенно начиная с C# 8.0, коренным образом изменило подход разработчиков к написанию потока управления — делая код одновременно более выразительным, лаконичным и безопасным.

Улучшение потока управления с помощью сопоставления с образцом в C#

Сопоставление с образцом в C# предлагает лаконичный синтаксис для проверки выражения и выполнения действий, когда это выражение соответствует определённому образцу. Оно позволяет разработчикам проверять значения по различным критериям, включая их тип, значение, свойства или даже структуру сложных объектов. Эта возможность в основном реализуется через выражение is и выражение switch (или оператор switch). Хотя базовая проверка типов существовала и раньше, C# 8.0 представил более богатый набор образцов, значительно расширив их полезность и влияние на повседневную практику кодирования. Это улучшение имеет решающее значение для эффективного сопоставления с образцом в switch и if в C#, превращая многословную условную логику в компактные, понятные конструкции.

Давайте рассмотрим некоторые из доступных ключевых типов образцов и то, как они упрощают общие задачи программирования, предоставляя чёткие примеры сопоставления с образцом в C#, которые демонстрируют их преимущества.

Практические примеры сопоставления с образцом в C#

Истинная мощь сопоставления с образцом становится очевидной при сопоставлении с традиционными подходами. Разработчики получают значительные улучшения в читабельности и лаконичности кода.

Образец объявления

Образец объявления проверяет тип выражения во время выполнения и, если он совпадает, присваивает результат новой переменной. Это устраняет необходимость в явном приведении типов и проверках на null во многих сценариях.

До:

public void ProcessAssetOld(object asset)
{
    if (asset is SoftwareLicense)
    {
        SoftwareLicense license = (SoftwareLicense)asset;
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent)
    {
        HardwareComponent hardware = (HardwareComponent)asset;
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

public class SoftwareLicense { public string ProductName { get; set; } public DateTime ExpirationDate { get; set; } }
public class HardwareComponent { public string ComponentName { get; set; } public string SerialNumber { get; set; } }

После (использование образца объявления):

public void ProcessAssetNew(object asset)
{
    if (asset is SoftwareLicense license)
    {
        Console.WriteLine($"Software License: {license.ProductName}, Expiration: {license.ExpirationDate.ToShortDateString()}");
    }
    else if (asset is HardwareComponent hardware)
    {
        Console.WriteLine($"Hardware Component: {hardware.ComponentName}, Serial: {hardware.SerialNumber}");
    }
    else
    {
        Console.WriteLine("Unknown asset type.");
    }
}

Переменные license и hardware находятся в области видимости и получают значение только в том случае, если образец совпадает, что повышает типовую безопасность и уменьшает потенциальные ошибки во время выполнения.

Образец типа

Подобно образцу объявления, образец типа проверяет тип выражения во время выполнения. Он часто используется в выражениях switch, где объявление новой переменной не является строго необходимым для самого образца.

До:

public decimal CalculateShippingCostOld(Shipment shipment)
{
    if (shipment == null)
        throw new ArgumentNullException(nameof(shipment));
    if (shipment.GetType() == typeof(StandardShipment))
        return 5.00m;
    if (shipment.GetType() == typeof(ExpressShipment))
        return 15.00m;
    if (shipment.GetType() == typeof(InternationalShipment))
        return 25.00m;
    return 0.00m; // Default for unknown types
}

public abstract class Shipment { }
public class StandardShipment : Shipment { }
public class ExpressShipment : Shipment { }
public class InternationalShipment : Shipment { }

После (использование образца типа в выражении switch):

public decimal CalculateShippingCostNew(Shipment shipment) => shipment switch
{
    StandardShipment => 5.00m,
    ExpressShipment => 15.00m,
    InternationalShipment => 25.00m,
    null => throw new ArgumentNullException(nameof(shipment)),
    _ => 0.00m // Handle unknown shipment types
};

Это демонстрирует лаконичное сопоставление с образцом switch case в C# для различных типов.

Образец константы

Образец константы проверяет, равно ли результат выражения указанному константному значению. Это упрощает сравнение дискретных значений.

До:

public string GetOrderStateOld(OrderStatus status)
{
    if (status == OrderStatus.Pending)
        return "Order is awaiting processing.";
    else if (status == OrderStatus.Shipped)
        return "Order has been dispatched.";
    else if (status == OrderStatus.Delivered)
        return "Order has been delivered.";
    else if (status == OrderStatus.Cancelled)
        return "Order has been cancelled.";
    else
        return "Unknown order state.";
}

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

После (использование образца константы):

public string GetOrderStateNew(OrderStatus status) => status switch
{
    OrderStatus.Pending => "Order is awaiting processing.",
    OrderStatus.Shipped => "Order has been dispatched.",
    OrderStatus.Delivered => "Order has been delivered.",
    OrderStatus.Cancelled => "Order has been cancelled.",
    _ => "Unknown order state."
};

Это чистый способ обработки сопоставления с образцом строк в C# или значений перечислений.

Реляционные образцы

Реляционные образцы сравнивают результат выражения с константой, используя операторы сравнения (<, >, <=, >=). Это делает проверки диапазонов гораздо более читабельными.

До:

public string GetEmployeePerformanceLevelOld(int score)
{
    if (score < 50)
        return "Needs Improvement";
    else if (score >= 50 && score < 70)
        return "Meets Expectations";
    else if (score >= 70 && score < 90)
        return "Exceeds Expectations";
    else if (score >= 90)
        return "Outstanding";
    else
        return "Invalid Score"; // Should not happen with int, but for completeness
}

После (использование реляционных образцов):

public string GetEmployeePerformanceLevelNew(int score) => score switch
{
    < 50 => "Needs Improvement",
    >= 50 and < 70 => "Meets Expectations",
    >= 70 and < 90 => "Exceeds Expectations",
    >= 90 => "Outstanding",
    _ => "Invalid Score" // Catches any unexpected integer values
};

Это демонстрирует мощь объединения реляционных образцов с логическими образцами для сопоставления с образцом в C#.

Образец свойства

Образец свойства позволяет сопоставлять свойства или поля выражения с вложенными образцами. Это невероятно полезно для проверки состояния объектов.

До:

public decimal CalculateOrderDiscountOld(CustomerOrder order)
{
    if (order == null) return 0m;
    if (order.TotalAmount >= 500 && order.Customer.IsPremium)
    {
        return 0.15m; // 15% for premium customers with large orders
    }
    else if (order.TotalAmount >= 100)
    {
        return 0.05m; // 5% for general large orders
    }
    return 0m;
}

public class Customer { public bool IsPremium { get; set; } }
public class CustomerOrder { public decimal TotalAmount { get; set; } public Customer Customer { get; set; } }

После (использование образца свойства):

public decimal CalculateOrderDiscountNew(CustomerOrder order) => order switch
{
    { TotalAmount: >= 500, Customer.IsPremium: true } => 0.15m,
    { TotalAmount: >= 100 } => 0.05m,
    null => 0m,
    _ => 0m
};

Это демонстрирует мощное сопоставление с образцом свойства в C#, упрощая проверки состояния объектов.

Позиционный образец

Позиционные образцы деконструируют результат выражения (например, кортежи или записи с методами Deconstruct) и сопоставляют полученные значения с вложенными образцами.

До:

public string DescribeTransactionOld(Transaction transaction)
{
    if (transaction.Type == TransactionType.Deposit && transaction.Amount > 1000)
        return "Large Deposit";
    else if (transaction.Type == TransactionType.Withdrawal && transaction.Amount > 500)
        return "Significant Withdrawal";
    else if (transaction.Type == TransactionType.Fee && transaction.Amount > 0)
        return "Applied Fee";
    else
        return "Standard Transaction";
}

public record Transaction(TransactionType Type, decimal Amount);
public enum TransactionType { Deposit, Withdrawal, Fee, Transfer }

После (использование позиционного образца):

public string DescribeTransactionNew(Transaction transaction) => transaction switch
{
    (TransactionType.Deposit, > 1000m) => "Large Deposit",
    (TransactionType.Withdrawal, > 500m) => "Significant Withdrawal",
    (TransactionType.Fee, > 0m) => "Applied Fee",
    _ => "Standard Transaction"
};

Эта лаконичная форма повышает читабельность сопоставления с образцом в C# для деконструируемых типов.

Логические образцы (and, or, not)

Логические образцы комбинируют другие образцы, используя ключевые слова and, or и not, что позволяет выполнять сложные условные проверки в рамках одного образца.

До:

public bool CanGrantAccessOld(UserRole role, int securityLevel, bool hasTwoFactorAuth)
{
    if ((role == UserRole.Administrator || role == UserRole.Manager) && securityLevel > 7 && hasTwoFactorAuth)
        return true;
    else if (role == UserRole.Developer && securityLevel > 5)
        return true;
    else if (role != UserRole.Guest && securityLevel >= 3 && securityLevel <= 10)
        return true;
    else
        return false;
}

public enum UserRole { Guest, User, Developer, Manager, Administrator }

После (использование логических образцов):

public bool CanGrantAccessNew(UserRole role, int securityLevel, bool hasTwoFactorAuth) => (role, securityLevel, hasTwoFactorAuth) switch
{
    (UserRole.Administrator or UserRole.Manager, > 7, true) => true,
    (UserRole.Developer, > 5, _) => true,
    (not UserRole.Guest, >= 3 and <= 10, _) => true,
    _ => false
};

Сопоставление с образцом is в C# в сочетании с логическими операторами становится очень выразительным.

Образец var

Образец var всегда соответствует выражению и присваивает его результат новой локальной переменной. Это особенно полезно, когда вам нужно захватить все входное значение выражения switch (или часть сложного образца) для выполнения дополнительных, более сложных проверок внутри условия when, особенно тех, которые включают вызовы методов или свойства, которые не могут быть выражены другими образцами напрямую.

До:

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceOld(IEnumerable<int> numbers)
{
    if (numbers == null)
        return "Invalid sequence (null)";
    if (numbers.Count() == 0)
        return "Empty sequence";
    if (numbers.Count() > 5 && numbers.Average() > 10.0)
        return "Long sequence with high average";
    return "Normal sequence";
}

После (использование образца var с условием when):

using System.Collections.Generic;
using System.Linq;

public string AnalyzeNumberSequenceNew(IEnumerable<int> numbers) => numbers switch
{
    null => "Invalid sequence (null)", 
    var list when list.Count() == 0 => "Empty sequence",
    var list when list.Count() > 5 && list.Average() > 10.0 => "Long sequence with high average",
    _ => "Normal sequence"
};

Здесь var list захватывает экземпляр IEnumerable<int>. Это позволяет выполнять последующие сложные LINQ-запросы (например, Count() и Average()) непосредственно на переменной list внутри условия when, без необходимости явных проверок типов с помощью is.

Образцы списков (C# 11)

Введённые в C# 11, образцы списков позволяют сопоставлять элементы в последовательности (например, массивы или List<T>). Они невероятно мощны для проверки структуры и содержимого коллекций.

До:

public bool ValidateLogEntryOld(string[] logParts)
{
    if (logParts == null || logParts.Length < 2) return false;

    if (logParts[0] == "ERROR" && logParts.Length >= 3 && logParts[1] == "AUTH" && logParts[2] == "FAILED")
        return true;
    if (logParts[0] == "INFO" && logParts.Length >= 2 && logParts[1] == "CONNECTED")
        return true;
    if (logParts[0] == "WARN" && logParts.Length >= 4 && logParts[1] == "DISK" && logParts[2] == "SPACE" && int.TryParse(logParts[3], out int space) && space < 10)
        return true;
    return false;
}

После (использование образцов списков):

public bool ValidateLogEntryNew(string[] logParts) => logParts switch
{
    ["ERROR", "AUTH", "FAILED", ..] => true, // Error authentication failed
    ["INFO", "CONNECTED", ..] => true,     // Client connected
    ["WARN", "DISK", "SPACE", var size, ..] when int.TryParse(size, out int space) && space < 10 => true, // Low disk space
    _ => false // Any other log entry
};

Это мощная функция для сопоставления с образцом последовательностей, особенно полезная в сценариях, таких как анализ аргументов командной строки или записей журнала с различной структурой. .. (образец среза) соответствует нулю или более элементам, обеспечивая гибкость.

За пределами основ: более чистый код с сопоставлением с образцом

Преимущества сопоставления с образцом выходят за рамки простого синтаксического сахара. Принимая эти возможности, разработчики изначально пишут код, который является:

  • Более выразительный: Образцы напрямую передают намерение, делая код легче для чтения и понимания с первого взгляда. Сложная условная логика становится интуитивно понятной.
  • Более лаконичный: Устранение шаблонных блоков if-else и явных приведений типов уменьшает объём кода, что приводит к меньшему объёму кода для поддержки и меньшему количеству возможностей для ошибок.
  • Безопаснее: Выражения switch, в частности, часто требуют исчерпывающего сопоставления с образцом. Компилятор C# помогает, предупреждая разработчиков, если выражение switch не охватывает все возможные входные значения, помогая предотвратить исключения во время выполнения. Эта компилятором обеспечиваемая исчерпываемость улучшает корректность программного обеспечения.
  • Легче рефакторить: С более чистой, более модульной условной логикой, рефакторинг становится менее пугающим. Разработчики могут изменять или расширять поведение с минимальным воздействием на другие части системы.

Сопоставление с образцом превратило C# в язык, который предлагает элегантные решения для сложной обработки данных и управления потоком. От базовых проверок типов до деконструкции объектов и проверки последовательностей — возможности, предоставляемые C# 8.0+, являются важными инструментами для любого современного разработчика программного обеспечения. Использование этих мощных функций не только повышает качество кода, но и оптимизирует процесс разработки, способствуя созданию более надёжных и поддерживаемых приложений. Разработчики, всё ещё полагающиеся на старые стили операторов if-else или switch, могут значительно выиграть от внедрения этих образцов, делая свои программы на C# более надёжными и приятными в работе.

Связанные статьи