عنوان:

‫نگاهی عمیق به انتساب‌های ترکیبی سفارشی در C# 14


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۳۰ ۱۲:۳۹
آدرس: www.dntips.ir
سی‌شارپ ۱۴ که همراه با دات‌نت ۱۰ عرضه خواهد شد، قابلیت جدید و کارآمدی به نام انتساب‌های ترکیبی سفارشی (User-Defined Compound Assignments) را معرفی می‌کند. این ویژگی به توسعه‌دهندگان اجازه می‌دهد تا نسخه‌های بهینه‌تری از عملگرهای انتسابی مانند += یا *= را پیاده‌سازی کرده و از ساخت بی‌مورد اشیاء در حافظه جلوگیری کنند. در این مقاله، نحوه‌ی پیاده‌سازی و استفاده از این قابلیت جدید را بررسی کرده و به نکات و دام‌های احتمالی آن می‌پردازیم.

پیش‌نیازها برای شروع

کدهای این مقاله با C# 14 نوشته شده‌اند که در زمان نگارش این متن، هنوز در مرحله پیش‌نمایش قرار دارد. این یعنی ممکن است سینتکس نهایی تا زمان انتشار رسمی دات‌نت ۱۰ دستخوش تغییرات جزئی شود.
برای اجرای کدها، باید آخرین نسخه پیش‌نمایش SDK دات‌نت ۱۰ را نصب کنید. سپس یک پروژه جدید (در اینجا Console App) ایجاد کرده و مطمئن شوید که .net10.0 را هدف قرار داده است. در نهایت، برای فعال‌سازی ویژگی‌های پیش‌نمایش زبان، فایل *.csproj پروژه خود را باز کرده و تگ را به آن اضافه کنید:
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <LangVersion>preview</LangVersion>
  </PropertyGroup>
</Project>
اکنون همه چیز برای تست این قابلیت جدید آماده است.

انتساب‌های ترکیبی در C# 13 و قبل از آن

برای درک بهتر قابلیت جدید، ابتدا باید ببینیم انتساب‌های ترکیبی در نسخه‌های فعلی سی‌شارپ چگونه کار می‌کنند. به طور سنتی، عبارتی مانند x += y تنها یک میانبر نوشتاری (syntactic sugar) برای x = x + y است. کلاس‌ها و ساختارها (structs) می‌توانند عملگر + را سربارگذاری (overload) کنند و این پیاده‌سازی به طور خودکار در زمان استفاده از عملگر += نیز فراخوانی می‌شود.
به مثال ساختار Vector3 زیر توجه کنید:
public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    // سربارگذاری عملگر استاتیک +
    public static Vector3 operator +(Vector3 left, Vector3 right)
    {
        Console.WriteLine("operator + called. Creating a new Vector3.");
        return new Vector3(
            left.X + right.X,
            left.Y + right.Y,
            left.Z + right.Z
        );
    }
}

var one = new Vector3(1, 2, 3);
var two = new Vector3(4, 5, 6);

// این خط عملگر + سربارگذاری شده را فراخوانی می‌کند
one = one + two;

// این خط نیز دقیقاً همان عملگر + را فراخوانی می‌کند
one += two;
همانطور که می‌بینید، استفاده از += کاملاً منطقی به نظر می‌رسد، اما یک هزینه‌ی پنهان عملکردی به همراه دارد.

هزینه‌ی پنهان انتساب‌های ترکیبی

هر بار که عملگر + فراخوانی می‌شود، یک شیء Vector3جدید در داخل متد ساخته شده و برگردانده می‌شود. این یعنی حتی وقتی می‌نویسید one += two، در پشت صحنه یک شیء سوم (نتیجه‌ی one + two) ایجاد شده و سپس به متغیر one اختصاص داده می‌شود. شیء قبلی که در one قرار داشت، برای پاک‌سازی توسط Garbage Collector علامت‌گذاری می‌شود.
این سربار عملکردی معمولاً جزئی است، اما در سناریوهایی که با داده‌های حجیم سروکار داریم یا عملیات در یک حلقه‌ی فشرده (tight loop) تکرار می‌شود، می‌تواند تأثیر قابل توجهی بر عملکرد و مصرف حافظه برنامه بگذارد. خوشبختانه، C# 14 برای این مشکل راه‌حلی ارائه می‌دهد.

راه‌حل بهینه‌تر در C# 14: سربارگذاری مستقیم عملگرهای ترکیبی

در C# 14 شما می‌توانید به طور مستقیم عملگرهای ترکیبی مانند += را سربارگذاری کنید. این کار به شما اجازه می‌دهد تا یک عملیات درجا (in-place) و بسیار بهینه‌تر را پیاده‌سازی کنید.
به نسخه‌ی به‌روز شده‌ی ساختار Vector3 نگاه کنید:
public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    // عملگر استاتیک + که یک نمونه جدید برمی‌گرداند (برای عملیات غیرتخصیصی)
    public static Vector3 operator +(Vector3 left, Vector3 right)
    {
        Console.WriteLine("Static operator + called. Creating a new Vector3.");
        return new Vector3(
            left.X + right.X,
            left.Y + right.Y,
            left.Z + right.Z
        );
    }

    // عملگر ترکیبی بهینه که به صورت درجا عمل می‌کند
    public void operator +=(Vector3 right)
    {
        Console.WriteLine("Instance operator += called. Modifying in-place.");
        X += right.X;
        Y += right.Y;
        Z += right.Z;
    }
}

var one = new Vector3(1, 2, 3);
var two = new Vector3(4, 5, 6);

// عملگر ترکیبی سربارگذاری شده را فراخوانی می‌کند (بهینه)
// Output: Instance operator += called. Modifying in-place.
one += two;

// عملگر + را فراخوانی می‌کند (یک شیء جدید می‌سازد)
// Output: Static operator + called. Creating a new Vector3.
var three = one + two;
به تفاوت‌های کلیدی در پیاده‌سازی operator += دقت کنید:
  • متد نمونه (Instance Method): این عملگر دیگر static نیست، بلکه یک متد نمونه است که روی شیء فعلی (this) عمل می‌کند.
  • خروجی void: از آنجایی که هیچ شیء جدیدی ساخته نمی‌شود و تغییرات به صورت درجا اعمال می‌شوند، نوع خروجی متد void است.
  • انتخاب هوشمند کامپایلر: کامپایلر سی‌شارپ به اندازه کافی هوشمند است تا در هر موقعیت، مناسب‌ترین متد را انتخاب کند. وقتی از += استفاده می‌کنید، نسخه‌ی بهینه‌ی operator += فراخوانی می‌شود و زمانی که از + در یک عبارت انتسابی جدید استفاده می‌کنید، operator + استاتیک اجرا خواهد شد.

نکته تکمیلی: کدام عملگرها را می‌توان سربارگذاری کرد؟

این قابلیت فقط به += محدود نمی‌شود. شما می‌توانید تمام عملگرهای ترکیبی زیر را به صورت سفارشی پیاده‌سازی کنید:
+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
این انعطاف‌پذیری به شما اجازه می‌دهد تا برای طیف وسیعی از عملیات، بهینه‌سازی‌های مشابهی را اعمال کنید.

نکات مهم و دام‌های احتمالی

هنگام استفاده از این ویژگی، چند نکته را باید در نظر داشته باشید.

۱. حفظ ثبات منطقی

بسیار مهم است که منطق پیاده‌سازی شده در operator += با operator +سازگار باشد. اگرچه از نظر فنی می‌توانید دو رفتار کاملاً متفاوت برای آن‌ها تعریف کنید، اما این کار ایده‌ی بسیار بدی است و باعث سردرگمی شدید توسعه‌دهندگانی می‌شود که از کد شما استفاده می‌کنند. هدف از این قابلیت، ارائه‌ی یک نسخه‌ی بهینه‌تر از همان عملیات است، نه تعریف یک عملیات جدید.

۲. چه زمانی از این قابلیت استفاده کنیم؟

این ویژگی برای بهینه‌سازی طراحی شده است. بیشترین سود را زمانی خواهید برد که:
  • با کلاس‌ها یا ساختارهایی سروکار دارید که حجم زیادی داده را مدیریت می‌کنند (مانند ماتریس‌ها، تصاویر یا بافرهای بزرگ).
  • در حال توسعه‌ی یک کتابخانه (Library) هستید که عملکرد در آن یک اولویت کلیدی است.
در سناریوهای دیگر، تفاوت عملکرد ممکن است ناچیز باشد. در این موارد، پیاده‌سازی operator + و واگذاری مدیریت عملگر ترکیبی به خود سی‌شارپ، ساده‌تر و خواناتر خواهد بود.

۳. مراقب حالت‌های جایگزین (Fallback) باشید

سی‌شارپ به شما اجازه می‌دهد عملگرها را برای کار با انواع داده مختلف سربارگذاری کنید. این موضوع در مورد عملگرهای ترکیبی نیز صادق است. با این حال، باید مراقب باشید که به طور ناخواسته به نسخه‌ی غیراصلی (مبتنی بر عملگر +) بازگشت (fallback) نکنید.
به مثال زیر توجه کنید. ما برای ضرب در int هر دو عملگر * و *= را پیاده‌سازی کرده‌ایم، اما برای decimal فقط عملگر * را داریم:
public record struct Vector3(decimal X, decimal Y, decimal Z)
{
    // نسخه‌ی بهینه برای int
    public void operator *=(int right) { /* ... */ }
    public static Vector3 operator *(Vector3 left, int right) { /* ... */ }

    // فقط نسخه‌ی غیراصلی برای decimal
    public static Vector3 operator *(Vector3 left, decimal right) { /* ... */ }
}

var vecInt = new Vector3(1, 2, 3);
var vecDec = new Vector3(4, 5, 6);

// از متد بهینه‌ی *‍= برای int استفاده می‌کند
vecInt *= 2;

// چون *‍= برای decimal تعریف نشده، به سراغ عملگر * می‌رود
// و یک شیء جدید می‌سازد (Fallback)
vecDec *= 3.4m;
در خط vecInt *= 2، متد بهینه‌ی operator *= فراخوانی می‌شود. اما در خط vecDec *= 3.4m، چون نسخه‌ی operator *=(decimal right) وجود ندارد، سی‌شارپ به حالت قبل بازگشته و عبارت را به صورت vecDec = vecDec * 3.4m تفسیر می‌کند که منجر به ساخت یک شیء جدید و کاهش کارایی می‌شود.

نتیجه‌گیری

انتساب‌های ترکیبی سفارشی یک ویژگی کوچک اما قدرتمند در C# 14 است. اگر با ساختارهای داده بزرگ کار می‌کنید یا به عنوان یک توسعه‌دهنده کتابخانه به دنبال حداکثر کارایی هستید، این قابلیت قطعاً ارزش بررسی را دارد. با این حال، همیشه به خاطر داشته باشید که این ویژگی ممکن است منجر به تکرار کد (مثلاً بین منطق + و +=) شود. بنابراین، باید بین مزایای عملکردی و هزینه‌ی نگهداری کد تعادل برقرار کنید.


مشاهده مطلب اصلی