نگاهی عمیق به انتسابهای ترکیبی سفارشی در C# 14
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۳۰ ۱۲:۳۹
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
+= یا *= را پیادهسازی کرده و از ساخت بیمورد اشیاء در حافظه جلوگیری کنند. در این مقاله، نحوهی پیادهسازی و استفاده از این قابلیت جدید را بررسی کرده و به نکات و دامهای احتمالی آن میپردازیم..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>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 علامتگذاری میشود.+= را سربارگذاری کنید. این کار به شما اجازه میدهد تا یک عملیات درجا (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 += دقت کنید:static نیست، بلکه یک متد نمونه است که روی شیء فعلی (this) عمل میکند.void: از آنجایی که هیچ شیء جدیدی ساخته نمیشود و تغییرات به صورت درجا اعمال میشوند، نوع خروجی متد void است.+= استفاده میکنید، نسخهی بهینهی operator += فراخوانی میشود و زمانی که از + در یک عبارت انتسابی جدید استفاده میکنید، operator + استاتیک اجرا خواهد شد.+=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
operator += با operator +سازگار باشد. اگرچه از نظر فنی میتوانید دو رفتار کاملاً متفاوت برای آنها تعریف کنید، اما این کار ایدهی بسیار بدی است و باعث سردرگمی شدید توسعهدهندگانی میشود که از کد شما استفاده میکنند. هدف از این قابلیت، ارائهی یک نسخهی بهینهتر از همان عملیات است، نه تعریف یک عملیات جدید.operator + و واگذاری مدیریت عملگر ترکیبی به خود سیشارپ، سادهتر و خواناتر خواهد بود.+) بازگشت (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 تفسیر میکند که منجر به ساخت یک شیء جدید و کاهش کارایی میشود.+ و +=) شود. بنابراین، باید بین مزایای عملکردی و هزینهی نگهداری کد تعادل برقرار کنید.