دات‌نت

قابلیت‌های جدید C# 14 چیست؟ آموزش کامل با مثال‌های واقعی

‫ راهنمای کامل C# 14 با مثال‌های واقعی: Extension Members، field keyword، Null-conditional Assignment، Span و بقیه‌ی قابلیت‌های زبان C# 14 همراه با .NET 10.‬

C# 14 چیست؟ چه قابلیت‌هایی به این زبان اضافه شده؟ field keyword دقیقاً چه کاری انجام می‌دهد؟ Extension Members چطور کار می‌کنند؟ Null-conditional assignment چیست؟ و C# 14 چه تغییراتی برای Span<T>، Lambda، nameof و Operatorها آورده؟

C# 14 جدیدترین نسخه زبان C# است که همراه با .NET 10 منتشر شده. اگر منتظر یک feature بزرگ و انقلابی مثل چیزی که با Records یا Pattern Matching دیدیم بودید، احتمالاً کمی جا می‌خورید؛ این نسخه بیشتر روی کم‌کردن boilerplate، قوی‌تر کردن Extension، performance، source generation و راحت‌تر شدن کدنویسی روزمره تمرکز کرده تا اضافه‌کردن syntax تازه به معنای واقعی کلمه. تیم زبان C# در دات‌نت بلاگ هم دقیقاً همین را می‌گوید: هدف مشترک همه‌ی این feature‌ها کم‌کردن اصطکاک syntax در کارهای روزمره است تا تمرکز روی منطق دامنه بماند، نه روی تشریفات زبان.

در این مقاله تمام قابلیت‌های اصلی C# 14 را با مثال بررسی می‌کنیم و برای هر کدام جواب می‌دهیم:

  • چه مشکلی را حل می‌کند؟
  • قبل از C# 14 چطور این کار انجام می‌شد؟
  • syntax جدید چه شکلی است؟
  • چه زمانی واقعاً باید ازش استفاده کرد؟
  • آیا برای یک Backend Developer اهمیت واقعی دارد یا صرفاً یک syntax convenience است؟

نکته: C# 14 نسخه‌ای از خودِ زبان C# است و نباید آن را با قابلیت‌های .NET 10 یا ASP.NET Core 10 قاطی کرد. هرچه در این مقاله می‌خوانید مربوط به تغییرات زبان C# است، نه runtime یا فریم‌ورک‌ها. مرجع رسمی همه‌ی این تغییرات صفحه‌ی What's new in C# 14 در Microsoft Learn است.


فهرست قابلیت‌های جدید C# 14

C# 14 این قابلیت‌های اصلی را شامل می‌شود:

  1. Extension Members
  2. field keyword و Field-backed Properties
  3. Null-conditional Assignment
  4. Implicit Span Conversions
  5. nameof برای Unbound Generic Types
  6. Lambda Parameters با Modifier
  7. Partial Events و Partial Constructors
  8. User-defined Compound Assignment Operators
  9. Preprocessor Directives برای File-based Apps

1. Extension Members در C# 14

این احتمالاً مهم‌ترین تغییر C# 14 است، و شخصاً فکر می‌کنم بیشترین تاثیر را روی نحوه‌ی طراحی API و کتابخانه‌ها خواهد گذاشت.

قبل از C# 14، وقتی می‌خواستیم یک type را بدون دست‌زدن به سورس اصلی‌اش گسترش بدهیم، تنها ابزارمان Extension Method بود:

public static class StringExtensions
{
    public static bool IsNullOrEmpty(this string? value)
    {
        return string.IsNullOrEmpty(value);
    }
}

استفاده:

var name = "Mohammad";

if (!name.IsNullOrEmpty())
{
    Console.WriteLine(name);
}

مشکل این مدل این بود که Extension فقط در قالب method امکان‌پذیر بود. اگر می‌خواستی چیزی شبیه property داشته باشی، مجبور بودی یا از یک متد بدون پرانتز استفاده کنی که خیلی هم به property واقعی شبیه نبود، یا کلاً بی‌خیال idea بشوی.

در C# 14 مفهوم Extension Method به Extension Member گسترش پیدا کرده و حالا می‌شود Extension Property هم تعریف کرد.

مثال C# 14

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T> source)
    {
        public bool IsEmpty => !source.Any();

        public bool IsNotEmpty => source.Any();
    }
}

استفاده:

var users = new List<string>
{
    "Ali",
    "Mohammad"
};

Console.WriteLine(users.IsEmpty);
Console.WriteLine(users.IsNotEmpty);

یعنی این property جدید عیناً مثل یک member واقعی خودِ type استفاده می‌شود؛ نه فراخوانی یک متد کمکی جداگانه.

نکته‌ی جالب‌تر این است که C# 14 امکان تعریف static extension members را هم اضافه کرده است:

public static class EnumerableExtensions
{
    extension<T>(IEnumerable<T>)
    {
        public static IEnumerable<T> Empty =>
            Enumerable.Empty<T>();
    }
}

و استفاده:

var result = IEnumerable<int>.Empty;

Extension Members حتی می‌توانند شامل user-defined operators هم باشند. طبق مستندات extension keyword در Microsoft Learn، از C# 14 به بعد کلاس‌های استاتیک top-level و غیر جنریک می‌توانند از extension block برای تعریف این اعضا استفاده کنند و این اعضا می‌توانند instance یا static باشند.

چرا مهم است؟

این قابلیت برای موارد زیر خیلی به‌درد‌بخور است:

  • Domain-specific APIs
  • Utility Libraries
  • Fluent APIs
  • Frameworks
  • Source Generators

اما یک نکته‌ی معماری مهم هست که نباید فراموشش کنیم:

این قابلیت به این معنی نیست که باید همه چیز را تبدیل به Extension Property یا Extension Method کنیم.

اگر یک رفتار واقعاً متعلق به domain object است، بهتر است همان‌جا به‌عنوان member واقعی خودِ کلاس تعریف شود، نه اینکه فقط برای شیک‌تر شدن کد آن را بیرون از کلاس بنویسید. برای کسانی که می‌خواهند عمیق‌تر وارد جزئیات طراحی این feature بشوند، تیم زبان C# یک مقاله‌ی مفصل درباره‌ی دلایل طراحی Extension Members منتشر کرده که پشت‌صحنه‌ی خیلی از تصمیم‌ها را توضیح می‌دهد.


2. field Keyword در C# 14

این یکی از کاربردی‌ترین و به‌احتمال زیاد پرکاربردترین تغییرات C# 14 در کد روزمره است.

فرض کنید property داریم:

public class User
{
    private string _name;

    public string Name
    {
        get => _name;
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException();

            _name = value;
        }
    }
}

فقط به‌خاطر یک validation ساده مجبور شدیم یک backing field دستی بسازیم و کدمان چند خط طولانی‌تر شد.

در C# 14 می‌توانیم از keyword جدید field استفاده کنیم:

public class User
{
    public string Name
    {
        get;
        set => field =
            value ?? throw new ArgumentNullException(nameof(value));
    }
}

field به backing field تولیدشده توسط compiler اشاره می‌کند و دیگر نیازی به تعریف صریح آن نیست.

مثال واقعی‌تر

public class Product
{
    public decimal Price
    {
        get;
        set => field =
            value >= 0
                ? value
                : throw new ArgumentOutOfRangeException(nameof(value));
    }
}

استفاده:

var product = new Product();

product.Price = 100;

Console.WriteLine(product.Price);

اگر:

product.Price = -10;

بنویسید، exception می‌گیرید — دقیقاً همان رفتاری که با backing field دستی هم داشتید، فقط با کد کمتر.

قبل و بعد از C# 14

قبل:

private decimal _price;

public decimal Price
{
    get => _price;
    set => _price = value >= 0
        ? value
        : throw new ArgumentOutOfRangeException();
}

بعد:

public decimal Price
{
    get;
    set => field = value >= 0
        ? value
        : throw new ArgumentOutOfRangeException();
}

طبق مستندات رسمی field-backed properties، این feature دقیقاً برای همین سناریو طراحی شده: اضافه‌کردن یک لایه‌ی validation یا منطق کوچک به یکی از accessorها بدون اینکه مجبور باشید کل property را از حالت auto-implemented خارج کنید.

نکته مهم

اگر در کلاس خودت متغیری به نام field داشته باشی، ممکن است ambiguity ایجاد شود. compiler در این حالت حتی warning اختصاصی (مثل CS9258) صادر می‌کند و پیشنهاد می‌دهد از @field یا this.field استفاده کنی تا منظورت روشن باشد.


3. Null-Conditional Assignment

یکی از syntaxهای کوچک ولی خیلی کاربردی C# 14 همین قابلیت است.

قبلاً اگر می‌خواستیم property را فقط زمانی set کنیم که object null نباشد، این‌طوری می‌نوشتیم:

if (customer is not null)
{
    customer.Order = order;
}

در C# 14 می‌توانیم مستقیم بنویسیم:

customer?.Order = order;

یعنی:

customer == null
        ↓
هیچ assignment انجام نمی‌شود

customer != null
        ↓
Order = order

طبق مستندات conditional member access در Microsoft Learn، عملگرهای ?. و ?[] از این نسخه به بعد می‌توانند در سمت چپ یک assignment هم قرار بگیرند؛ چیزی که تا قبل از C# 14 اصلاً معنا نداشت.


نکته بسیار مهم

سمت راست assignment فقط زمانی evaluate می‌شود که object null نباشد. این جزئیات کوچک ولی حیاتی است، چون خیلی از باگ‌های ظریف دقیقاً همین‌جا اتفاق می‌افتند.

مثلاً:

customer?.Order = GetCurrentOrder();

اگر customer == null باشد، GetCurrentOrder() اصلاً اجرا نمی‌شود.

این موضوع برای متدهایی که expensive هستند — مثلاً یک query به دیتابیس یا یک فراخوانی شبکه‌ای — خیلی مهم است، چون از یک side effect ناخواسته جلوگیری می‌کند.


Compound Assignment

فقط = نیست. می‌توانیم از:

customer?.Balance += 100;

یا:

cart?.Total *= 0.9m;

هم استفاده کنیم.

اما:

customer?.Balance++;

مجاز نیست. ++ و -- در null-conditional assignment پشتیبانی نمی‌شوند، و اگر یک بار این را فراموش کنید، compiler خیلی سریع یادآوری می‌کند.


4. Implicit Span Conversions

یکی از تغییرات مهم‌تر برای performance-oriented programming، بهبود پشتیبانی زبان از رابطه‌ی بین Span<T>، ReadOnlySpan<T> و T[] است.

Span<T> یکی از ابزارهای اصلی .NET برای کار با memory بدون allocation اضافه است. در C# 14، compiler conversionهای implicit بیشتری بین:

T[]
 ↓
Span<T>
 ↓
ReadOnlySpan<T>

را در موقعیت‌های مناسب تشخیص می‌دهد.

مثال

فرض کنیم:

static int Sum(ReadOnlySpan<int> values)
{
    var sum = 0;

    foreach (var value in values)
    {
        sum += value;
    }

    return sum;
}

حالا می‌توانیم یک array را مستقیماً به این API بدهیم:

int[] numbers = [1, 2, 3, 4, 5];

var result = Sum(numbers);

طبق توضیح رسمی Microsoft در مستندات What's new in C# 14، این نوع span از حالا می‌توانند به‌عنوان extension method receiver هم عمل کنند، با conversionهای دیگر ترکیب شوند و به type inference جنریک هم کمک کنند. این یعنی پشتیبانی first-class واقعی، نه فقط چند تا rule پراکنده.

این قابلیت در سناریوهای high-performance مثل parsing، serialization و پردازش bufferها اهمیت بیشتری پیدا می‌کند، جایی که هر allocation اضافه روی throughput اثر می‌گذارد.

نکته

این قابلیت به این معنی نیست که Span<T> ناگهان allocation-free magic شده. باید همچنان تفاوت:

Stack
Heap
Span
ReadOnlySpan
Memory<T>
ReadOnlyMemory<T>

را بشناسید و بدانید کجا کدام یکی به‌درد می‌خورد.


5. nameof و Unbound Generic Types

قبلاً می‌توانستیم بنویسیم:

nameof(List<int>)

اما C# 14 اجازه می‌دهد generic type را بدون type argument هم به nameof بدهیم:

nameof(List<>)

نتیجه:

List

است.

مثال

Console.WriteLine(nameof(Dictionary<,>));

خروجی:

Dictionary

یا:

Console.WriteLine(nameof(Dictionary<,>.KeyCollection));

در سناریوهای reflection، logging و diagnostics این syntax می‌تواند مفید باشد — مثلاً وقتی می‌خواهید نام type را در یک پیام خطا یا log بدون درگیر شدن با type argumentهای مشخص چاپ کنید.


6. Modifier در Lambdaهای ساده

قبل از C# 14 اگر Lambda دارای modifierهایی مثل out، ref یا in بود، باید type پارامترها را explicit می‌نوشتیم، حتی اگر compiler می‌توانست همان type را از context حدس بزند.

مثلاً:

delegate bool TryParse<T>(
    string text,
    out T result);

در نسخه‌های قبلی:

TryParse<int> parser =
    (string text, out int result) =>
        int.TryParse(text, out result);

در C# 14 می‌توانیم بنویسیم:

TryParse<int> parser =
    (text, out result) =>
        int.TryParse(text, out result);

یعنی compiler type پارامترها را از روی delegate استنباط می‌کند و دیگر نیازی به تکرار آن‌ها نیست.

Modifierهای پشتیبانی‌شده

C# 14 اجازه می‌دهد modifierهایی مانند:

scoped
ref
in
out
ref readonly

بدون مشخص کردن type پارامتر استفاده شوند.

مثلاً:

delegate bool Parser(
    ReadOnlySpan<char> text,
    out int value);

Parser parser =
    (text, out value) =>
        int.TryParse(text, out value);

یک استثنا

params همچنان نیازمند parameter list دارای type است و از این miss قاعده جا می‌ماند.


7. Partial Events و Partial Constructors

C# از partial برای تقسیم implementation یک type بین چند فایل استفاده می‌کند. این قابلیت مخصوصاً برای Source Generator، Code Generation، فریم‌ورک‌ها و کدهای تولید-خودکار مهم است، چون یک بخش را دست‌نویس می‌نویسیم و بخش دیگر را ابزار تولید می‌کند.

C# 14 امکان تعریف partial برای Constructor و Event را هم اضافه کرده.

Partial Constructor

تعریف:

public partial class User
{
    public partial User();
}

و implementation:

public partial class User
{
    public partial User()
    {
        Console.WriteLine("User created");
    }
}

در عمل compiler این دو declaration را به یک constructor کامل تبدیل می‌کند. هر partial constructor باید دقیقاً یک defining declaration و یک implementing declaration داشته باشد؛ نه بیشتر و نه کمتر.

Partial Event

تعریف:

public partial class Order
{
    public partial event EventHandler? Completed;
}

implementation:

public partial class Order
{
    public partial event EventHandler? Completed
    {
        add
        {
            // custom add logic
        }

        remove
        {
            // custom remove logic
        }
    }
}

در implementation حتماً باید add و remove هر دو وجود داشته باشند.

چرا این قابلیت مهم است؟

برای یک اپلیکیشن معمولی شاید هیچ‌وقت مستقیماً سراغش نروید. اما اگر با Source Generator یا هر نوع code generation دیگری کار می‌کنید، این قابلیت خیلی منطقی‌تر می‌شود، چون تا قبل از C# 14 نمی‌شد constructor یا event را بین کد دست‌نویس و کد تولیدشده تقسیم کرد.


8. User-defined Compound Assignment Operators

این یکی برای performance و custom value types جالب است.

فرض کنیم یک type داریم:

public class Counter
{
    public int Value { get; private set; }

    public Counter(int value)
    {
        Value = value;
    }
}

در مدل سنتی می‌توانستیم:

public static Counter operator +(Counter left, int value)
{
    return new Counter(left.Value + value);
}

اما این operation هر بار یک object جدید می‌سازد.

C# 14 اجازه می‌دهد compound assignment operator را مستقیماً برای mutation روی instance تعریف کنیم:

public class Counter
{
    public int Value { get; private set; }

    public Counter(int value)
    {
        Value = value;
    }

    public void operator +=(int value)
    {
        Value += value;
    }
}

استفاده:

var counter = new Counter(10);

counter += 5;

Console.WriteLine(counter.Value);

خروجی:

15

تفاوت اصلی این است که operator جدید می‌تواند object موجود را in-place mutate کند، به‌جای اینکه هر بار یک object تازه بسازد. تیم C# این feature را دقیقاً به همین دلیل — جلوگیری از allocation و copy غیرضروری — معرفی کرده است.

چه زمانی ارزش دارد؟

مثلاً در typeهایی مثل:

Vector
Matrix
Large Value Object
Buffer
Numeric Structure
Domain-specific mutable structure

جایی که ساختن object جدید هزینه‌ی واقعی دارد.

اما برای business objectهای معمولی، استفاده از این قابلیت فقط برای اینکه کد «جدیدتر» به نظر برسد، تصمیم خوبی نیست. همان operator معمولی که object جدید می‌سازد، در ۹۰ درصد موارد business logic خواناتر و امن‌تر است.


9. File-based Apps و Preprocessor Directives

یکی از جذاب‌ترین تغییرات اکوسیستم C# 14، ساده‌تر شدن اجرای یک فایل تکی C# بدون نیاز به ساختن یک .csproj سنتی است.

مثلاً:

Console.WriteLine("Hello C# 14");

می‌تواند به‌صورت یک file-based application مستقیم اجرا شود.

در این مدل می‌توان configuration را داخل خودِ فایل C# قرار داد.

#!

در Unix می‌توان از shebang استفاده کرد:

#!/usr/bin/env dotnet run

Console.WriteLine("Hello from C#");

بعد فایل مستقیماً توسط shell قابل اجراست، درست مثل یک script پایتون یا bash.

#: directives

می‌توان package اضافه کرد:

#:package Spectre.Console@*

و سپس استفاده کرد:

using Spectre.Console;

AnsiConsole.MarkupLine(
    "[green]Hello from C#![/]");

همچنین می‌توان project reference، MSBuild property و SDK را هم مشخص کرد:

#:project ../SharedLibrary/SharedLibrary.csproj
#:property PublishAot=false
#:sdk Microsoft.NET.Sdk.Web

این directiveها توسط build tooling برای file-based applications شناخته و پردازش می‌شوند و برای script‌های کوچک، prototype‌های سریع یا حتی automation روزمره خیلی به‌کار می‌آیند.


C# 14 در یک نگاه

اگر بخواهیم همه‌ی قابلیت‌های C# 14 را در یک cheat sheet خلاصه کنیم:

C# 14
│
├── Extension Members
│   ├── Extension Methods
│   ├── Extension Properties
│   ├── Static Extension Members
│   └── Extension Operators
│
├── field keyword
│   └── Field-backed Properties
│
├── Null-conditional Assignment
│   ├── ?.
│   └── ?[]
│
├── First-class Span support
│   ├── Span<T>
│   ├── ReadOnlySpan<T>
│   └── T[]
│
├── nameof
│   └── Unbound Generic Types
│
├── Lambda Parameters
│   ├── ref
│   ├── in
│   ├── out
│   ├── scoped
│   └── ref readonly
│
├── Partial Members
│   ├── partial constructor
│   └── partial event
│
├── User-defined Compound Assignment
│   ├── +=
│   ├── -=
│   ├── *=
│   └── ...
│
└── File-based Apps
    ├── #!
    └── #:

کدام قابلیت‌های C# 14 برای Senior Developer مهم‌تر هستند؟

همه‌ی feature‌ها ارزش یکسانی ندارند. من برای یک Senior .NET Backend Developer این اولویت‌بندی را پیشنهاد می‌کنم:

Feature اهمیت چرا؟
Extension Members بسیار زیاد API Design و Framework Design
field زیاد کاهش Boilerplate در Properties
Null-conditional Assignment زیاد خوانایی و syntax روزمره
Span Conversions زیاد Performance و Memory
Lambda Modifiers متوسط Generic/low-level APIs
Compound Assignment متوسط Performance و custom types
Partial Members متوسط Source Generators
File-based Apps متوسط Tooling و scripting
nameof(List<>) کم Syntax convenience

C# 14 یا .NET 10؟

این دو را نباید با هم اشتباه گرفت.

C# 14 یعنی تغییرات در زبان:

Syntax
Compiler
Type System
Language Semantics

اما .NET 10 شامل تغییرات بسیار بیشتری در:

Runtime
BCL
ASP.NET Core
EF Core
GC
JIT
Networking
Libraries

است.

پس اگر در یک مصاحبه از تو پرسیدند:

What's new in C# 14?

نباید شروع کنی درباره‌ی ASP.NET Core 10 یا APIهای جدید .NET 10 حرف زدن. تمرکز باید روی همین ۹ قابلیت زبان بماند؛ همان چیزی که خودِ Microsoft هم در صفحه‌ی رسمی What's new in C# 14 دقیقاً به همین شکل دسته‌بندی کرده است.


نتیجه‌گیری

C# 14 بیشتر یک release برای refinement و کاهش friction در زبان است تا یک انقلاب syntax.

سه قابلیت از بقیه مهم‌ترند:

1. Extension Members

Extension Method دیگر محدود به Method نیست و می‌تواند Property، Static Member و Operator را هم پوشش دهد. این تغییر شیوه‌ی طراحی API library‌ها را واقعاً عوض می‌کند.

2. field

برای Propertyهایی که نیاز به validation یا custom accessor دارند، دیگر همیشه مجبور نیستیم backing field دستی تعریف کنیم — یک خط کد کمتر، یک کلاس کمتر شلوغ.

3. Null-conditional Assignment

کدهایی مانند:

if (customer != null)
{
    customer.Order = order;
}

می‌توانند به:

customer?.Order = order;

تبدیل شوند.

در کنار این‌ها، پشتیبانی بهتر از Span<T> برای کدهای performance-sensitive و compound assignmentهای user-defined برای typeهای قابل mutation، C# 14 را برای توسعه‌دهندگان سطح بالا جالب‌تر می‌کند.

در نهایت اگر با .NET 10 کار می‌کنی، یادگیری C# 14 فقط یاد گرفتن چند syntax جدید نیست؛ مهم‌تر این است که بفهمی هر قابلیت چه مسئله‌ای را حل می‌کند و چه زمانی نباید ازش استفاده کرد.


منابع و مطالعه‌ی بیشتر

نوشته‌شده توسط محمد نظری من اینجا هستم تا دانشم رو با شما به اشتراک بذارم

دیدگاه‌ها 0

برای ارسال دیدگاه یا پسند، وارد شوید.

ورود / ثبت‌نام

هنوز دیدگاهی نیست — اولین نفر باشید.