عملگرهای انتسابی ترکیبی سفارشی در C# 14.0
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۲۱ ۱۰:۳۳
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
+=، -= یا *= آشناست. این عملگرها یک راهکار خلاصهنویسی (Syntactic Sugar) برای انجام یک عملیات ریاضی و سپس انتساب نتیجه به همان متغیر هستند. به عنوان مثال، قطعه کد زیر را در نظر بگیرید:a += b;
a = a + b;
int یا double کاملاً بهینه و بدون مشکل است. اما چالش اصلی زمانی آغاز میشود که متغیر a یک نمونه از یک نوع مرجع (Reference Type) پیچیده و حجیم، مانند یک کلاس برای نگهداری تانسورها (Tensors) در یادگیری ماشین یا یک ساختار داده بزرگ باشد.a + b معمولاً به معنای فراخوانی یک سربارگذاری عملگر (Operator Overloading) برای + است. یک پیادهسازی متداول برای عملگر + این است که یک نمونه جدید از آن کلاس را ایجاد کند، نتیجه جمع را در آن قرار دهد و این نمونه جدید را بازگرداند. سپس، این شیء جدید به متغیر a انتساب داده میشود.a به آن اشاره میکرد، اکنون مرجع خود را از دست داده و به زباله (Garbage) تبدیل میشود. در چرخههای بعدی، زبالهروب (Garbage Collector - GC) باید وارد عمل شده و این حافظه را آزاد کند. برای برنامههایی که با دادههای بسیار بزرگ کار میکنند و این عملیاتها را به طور مکرر انجام میدهند، این چرخه مداومِ "ایجاد شیء جدید و رها کردن شیء قدیمی" منجر به پدیدهای به نام سربار حافظه (Memory Churn) میشود. این سربار، فشار بر روی GC را افزایش داده، عملکرد کلی برنامه را کاهش میدهد و میتواند منجر به وقفههای غیرمنتظره برای پاکسازی حافظه شود.a += b را به a = a + b ترجمه کند، توسعهدهنده میتواند یک پیادهسازی مشخص برای خود عملگر += ارائه دهد که دادهها را درجا (In-Place) تغییر میدهد.+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>= و >>>=.
+=, -=, *=, /=) میتوان نسخههای بررسی شده (checked) و بررسی نشده (unchecked) را نیز پیادهسازی کرد تا کنترل دقیقی بر روی سرریزهای حسابی (Arithmetic Overflows) وجود داشته باشد.+=سفارشیDataPayload داریم که یک آرایه بزرگ از بایتها را نگهداری میکند.public class DataPayload
{
public byte[] Data { get; private set; }
public DataPayload(int size)
{
Data = new byte[size];
// فرض کنید با دادههای واقعی پر میشود
}
// رویکرد سنتی: عملگر + یک نمونه جدید ایجاد میکند
public static DataPayload operator +(DataPayload left, DataPayload right)
{
// یک شیء کاملا جدید ایجاد میشود
var newPayload = new DataPayload(left.Data.Length);
// منطق ترکیب دادهها در شیء جدید پیادهسازی میشود
// این بخش برای سادگی، یک جمع ساده را شبیهسازی میکند
for (int i = 0; i < newPayload.Data.Length; i++)
{
newPayload.Data[i] = (byte)(left.Data[i] + right.Data[i]);
}
return newPayload;
}
}payloadA += payloadB استفاده کنیم، یک DataPayload کاملاً جدید ساخته شده و حافظه مربوط به payloadA قدیمی آماده پاکسازی توسط GC میشود.+= سفارشی را ببینید:public class OptimizedDataPayload
{
public byte[] Data { get; private set; }
public OptimizedDataPayload(int size)
{
Data = new byte[size];
// فرض کنید با دادههای واقعی پر میشود
}
// رویکرد جدید: عملگر += سفارشی برای تغییر درجا
// این متد وضعیت شیء فعلی را تغییر داده و خود آن را باز میگرداند
public static OptimizedDataPayload operator +=(OptimizedDataPayload left, OptimizedDataPayload right)
{
// هیچ شیء جدیدی ایجاد نمیشود. دادهها درجا تغییر میکنند.
for (int i = 0; i < left.Data.Length; i++)
{
left.Data[i] += right.Data[i];
}
return left;
}
}operator += نمونه left را مستقیماً تغییر میدهد و هیچ تخصیص حافظه جدیدی رخ نمیدهد. این دقیقاً همان بهینهسازی است که برای انواع داده حجیم به دنبال آن هستیم. نکته مهم این است که این یک تغییر غیرمخرب (Non-Breaking Change) است. اگر شما به عنوان نویسنده یک کتابخانه، عملگر += سفارشی را تعریف نکنید، کدهای موجود مصرفکنندگان کتابخانه شما دقیقاً مانند قبل کار خواهند کرد و کامپایلر از همان الگوی a = a + b پیروی میکند. این ویژگی تنها زمانی فعال میشود که شما به صراحت آن را پیادهسازی کنید.+=) برای یک کلاس حجیم در #C را به بهترین شکل توصیف میکند؟