مقالات › C#

قابلیت‌های مدرن C#؛ Record، Pattern Matching، Primary Constructor و بیشتر

مروری کاربردی بر C# مدرن (نسخه‌ی ۹ تا ۱۳): Record، Propertyهای init، تطبیق الگو، Switch Expression، Primary Constructor، Collection Expression و Raw String.

C# در سال‌های اخیر به‌شدت تکامل پیدا کرده است. کدی که با C# 12 و 13 نوشته می‌شود با C# 7 خیلی فرق دارد: کوتاه‌تر، امن‌تر و گویاتر. اگر چند سال پیش C# را یاد گرفته‌اید، این مرور قابلیت‌هایی را نشان می‌دهد که ارزش دارد همین امروز به کار بگیرید؛ با مثال‌هایی که مستقیم می‌توانید در پروژه‌هایتان استفاده کنید.

Record؛ داده با مقایسه‌ی مقداری

record نوعی است که برای داده طراحی شده. کامپایلر برای آن برابری مقداری، ToString() خوانا، Deconstruction و تغییر غیرمخرب تولید می‌کند.

public record Money(decimal Amount, string Currency);

var a = new Money(100, "USD");
var b = new Money(100, "USD");

Console.WriteLine(a == b);   // True — مقایسه بر اساس مقدار، نه ارجاع
Console.WriteLine(a);        // Money { Amount = 100, Currency = USD }

var c = a with { Amount = 250 };   // کپی با یک تغییر
var (amount, currency) = c;        // Deconstruction

از Record برای DTOها، درخواست و پاسخ API، Value Objectها و پیام‌ها استفاده کنید. record struct همین مزایا را برای انواع مقداری کوچک فراهم می‌کند.

نکته: Record برای DTOهای EF Core عالی است، اما Entityهایی که EF Core ردیابی می‌کند معمولاً به‌صورت کلاس بهتر کار می‌کنند، چون هویتشان کلیدشان است نه مقادیرشان.

Propertyهای init و اعضای required

public class Settings
{
    public required string ApiUrl { get; init; }
    public int TimeoutSeconds { get; init; } = 30;
}

var s = new Settings { ApiUrl = "https://api.example.com" };
// s.ApiUrl = "...";       ❌ بعد از مقداردهی اولیه قابل تغییر نیست
// new Settings();         ❌ خطای کامپایل: ApiUrl الزامی است

init اشیا را بعد از ساخت تغییرناپذیر می‌کند و required فراخواننده را مجبور می‌کند Propertyهای مهم را مقداردهی کند.

تطبیق الگو (Pattern Matching)

تطبیق الگو اجازه می‌دهد شکل داده را بررسی کنید و هم‌زمان مقادیر را استخراج کنید.

object value = 42;

if (value is int number and > 0)
    Console.WriteLine($"Positive int: {number}");

// الگوی Property
if (order is { Status: OrderStatus.Paid, Total: > 1_000_000 })
    ApplyVipDiscount(order);

// بررسی null
if (customer is not null) { /* ... */ }

Switch Expression

Switch Expression جای دستورهای پرحجم switch را با یک شکل کوتاه و مبتنی بر عبارت می‌گیرد:

decimal Shipping(Order order) => order switch
{
    { Country: "IR", Total: >= 2_000_000 } => 0,
    { Country: "IR" }                       => 60_000,
    { IsExpress: true }                     => 25,
    _                                       => 12
};

الگوهای ترکیبی باعث می‌شوند قوانین بیزینس مثل یک مشخصات فنی خوانده شوند:

string Classify(int temperature) => temperature switch
{
    < 0            => "freezing",
    >= 0 and < 15  => "cold",
    >= 15 and < 28 => "pleasant",
    _              => "hot"
};

الگوهای لیستی (C# 11)

string Describe(int[] numbers) => numbers switch
{
    []              => "empty",
    [var single]    => $"one item: {single}",
    [1, ..]         => "starts with 1",
    [.., var last]  => $"ends with {last}"
};

Primary Constructor (C# 12)

کلاس‌ها و Structها حالا می‌توانند پارامترهای سازنده را مستقیم در تعریف نوع بنویسند؛ ایده‌آل برای تزریق وابستگی:

// قبل
public class OrderService
{
    private readonly IOrderRepository _orders;
    private readonly ILogger<OrderService> _logger;

    public OrderService(IOrderRepository orders, ILogger<OrderService> logger)
    {
        _orders = orders;
        _logger = logger;
    }
}

// C# 12
public class OrderService(IOrderRepository orders, ILogger<OrderService> logger)
{
    public async Task<Order?> GetAsync(int id) =>
        await orders.FindAsync(id);
}

توجه: پارامترهای Primary Constructor در کلاس‌ها متغیرهای Capture‌شده هستند، نه فیلدهای readonly. اگر تغییرناپذیری مهم است، آن‌ها را صریحاً در یک فیلد readonly قرار دهید.

Collection Expression (C# 12)

یک نحو واحد برای ساخت آرایه، لیست و Span:

int[] primes = [2, 3, 5, 7];
List<string> tags = ["csharp", "dotnet"];
Span<byte> buffer = [0, 0, 0, 0];

int[] more = [..primes, 11, 13];   // Spread
List<int> empty = [];

Raw String Literal (C# 11)

عالی برای JSON، SQL و Regex؛ بدون نیاز به Escape:

var json = """
    {
      "name": "Ali",
      "skills": ["C#", "React"]
    }
    """;

var name = "Ali";
var template = $$"""
    { "greeting": "Hello, {{name}}" }
    """;

Namespace فایلی و Global Using

namespace Shop.Application.Orders;   // بدون تورفتگی اضافه

// GlobalUsings.cs
global using Microsoft.EntityFrameworkCore;
global using Shop.Domain.Entities;

با <ImplicitUsings>enable</ImplicitUsings>، Namespaceهای رایج به‌طور خودکار Import می‌شوند.

Nullable Reference Types

این قابلیت را در همه‌ی پروژه‌ها فعال کنید:

<Nullable>enable</Nullable>
public string Name { get; set; } = string.Empty;  // هرگز null نیست
public string? MiddleName { get; set; }           // ممکن است null باشد

int length = person.MiddleName?.Length ?? 0;

کامپایلر قبل از اینکه NullReferenceException در زمان اجرا رخ دهد به شما هشدار می‌دهد.

بهبودهای کوچک دیگر

// new با نوع هدف
Dictionary<string, List<int>> map = new();

// Lambdaهای static (بدون Capture تصادفی)
Func<int, int> square = static x => x * x;

// رشته‌های UTF-8
ReadOnlySpan<byte> header = "Content-Type"u8;

// params برای کالکشن‌ها (C# 13)
void Log(params ReadOnlySpan<string> messages) { }

یک مثال قبل و بعد

// سبک قدیمی
public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

public string GetLabel(ProductDto p)
{
    if (p == null) return "unknown";
    if (p.Price == 0) return "free";
    if (p.Price < 100) return "cheap";
    return "premium";
}

// C# مدرن
public record ProductDto(int Id, string Name, decimal Price);

public static string GetLabel(ProductDto? p) => p switch
{
    null              => "unknown",
    { Price: 0 }      => "free",
    { Price: < 100 }  => "cheap",
    _                 => "premium"
};

جمع‌بندی

C# مدرن اجازه می‌دهد منظور خود را با تشریفات بسیار کمتری بیان کنید: Record برای داده، init و required برای ساخت امن، تطبیق الگو و Switch Expression برای قوانین بیزینس شفاف، Primary Constructor برای DI، Collection Expression و Raw String برای مقادیر ثابت و Nullable Reference Types برای امنیت. آن‌ها را به‌تدریج به کار بگیرید؛ کدتان هم‌زمان کوتاه‌تر و درست‌تر می‌شود.

پرسش‌های متداول

آیا برای این قابلیت‌ها به آخرین نسخه‌ی دات‌نت نیاز دارم؟

بیشتر قابلیت‌ها به نسخه‌ی C# وابسته‌اند که با Target Framework گره خورده است (‎.NET 8 ← C# 12 و ‎.NET 9 ← C# 13). برخی قابلیت‌ها با تنظیم LangVersion روی نسخه‌های قدیمی‌تر هم کار می‌کنند، اما قابلیت‌های وابسته به Runtime نه.

آیا همه‌ی کلاس‌ها را به Record تبدیل کنم؟

نه. Record را برای داده و Value Object استفاده کنید. سرویس‌ها و Entityهایی که هویت و رفتار دارند را کلاس نگه دارید.

آیا Switch Expression از if کندتر است؟

نه. کامپایلر کد بهینه‌ای تولید می‌کند که اغلب با شرط‌های دستی یکسان است.