🔬 تکنیکهای پیشرفته #C :
آمادهاید که به بخشهای عمیق و کمتر دیدهشده #C سفر کنیم؟ امروز میخوایم در مورد دو تا قابلیت قدرتمند ولی خاص صحبت کنیم که برای بهینهسازیهای سطح پایین و کارهای حرفهای استفاده میشن: ref locals و ref returns.
اینها ابزارهای روزمره شما نیستن، ولی شناختنشون شما رو به درک عمیقتری از قدرت و انعطاف #C میرسونه.
1️⃣این ref local چیست؟ یک نام مستعار برای حافظه
یه ref local، یه متغیر محلیه که یک کپی از مقدار نیست، بلکه یک ارجاع یا نام مستعار مستقیم به حافظهی یک متغیر دیگهست (مثل یک عنصر آرایه یا یک فیلد).
این یعنی هر تغییری روی این متغیر، مستقیماً روی مقدار اصلی اعمال میشه!
2️⃣حالا ref return چیست؟ برگرداندن آدرس به جای مقدار 💥
یه متد میتونه به جای برگردوندن مقدار یک متغیر، رفرنس یا آدرس حافظهی اون رو برگردونه. این کار به کدی که متد رو صدا زده، اجازه دسترسی مستقیم و تغییر مقدار اصلی رو میده.
3️⃣نوبت ref readonly: ترکیب پرفورمنس و ایمنی 🛡
گاهی وقتا شما میخواید از مزیت پرفورمنس برگردوندن رفرنس (جلوگیری از کپی شدن دادههای بزرگ) استفاده کنید، ولی نمیخواید کدی که اون رو دریافت کرده، بتونه مقدار اصلی رو تغییر بده.
اینجاست که ref readonly وارد میشه. با این قابلیت، شما رفرنس رو برمیگردونید، ولی اون رفرنس فقط-خواندنی خواهد بود.
4️⃣هشدار و کاربرد اصلی ⚠️
این قابلیتها ابزارهای روزمره شما نیستن. اونها برای micro optimization های خاص طراحی شدن، به خصوص در سناریوهای پرفورمنس بالا مثل کار با <Span<T یا در بازیسازی که هر نانوثانیه مهمه.
در ۹۹٪ مواقع، شما به اینها نیاز ندارید و کد سادهتر، همیشه بهتره.
آشنایی با این مفاهیم، حتی اگه ازشون استفاده نکنید، دید شما رو نسبت به قابلیتهای سطح پایین و قدرت #C عمیقتر میکنه.
آیا تا حالا از ref locals یا ref returns تو پروژهای استفاده کردید؟ یا اصلاً از وجودشون خبر داشتید؟
خب، اینجا که نمیشه همه حرفا رو زد! 😉
ادامهی بحث، سوالات، غر زدنها و گپ و گفتهای خودمونی، فقط تو گروه.
[C# Geeks Hangout]
شیرجه عمیق در ref locals و ref returns
آمادهاید که به بخشهای عمیق و کمتر دیدهشده #C سفر کنیم؟ امروز میخوایم در مورد دو تا قابلیت قدرتمند ولی خاص صحبت کنیم که برای بهینهسازیهای سطح پایین و کارهای حرفهای استفاده میشن: ref locals و ref returns.
اینها ابزارهای روزمره شما نیستن، ولی شناختنشون شما رو به درک عمیقتری از قدرت و انعطاف #C میرسونه.
1️⃣این ref local چیست؟ یک نام مستعار برای حافظه
یه ref local، یه متغیر محلیه که یک کپی از مقدار نیست، بلکه یک ارجاع یا نام مستعار مستقیم به حافظهی یک متغیر دیگهست (مثل یک عنصر آرایه یا یک فیلد).
این یعنی هر تغییری روی این متغیر، مستقیماً روی مقدار اصلی اعمال میشه!
int[] numbers = { 0, 1, 2, 3, 4 };
// numRef حالا یک نام دیگر برای numbers[2] است
ref int numRef = ref numbers[2];
// هر تغییری روی numRef، مستقیماً روی عنصر آرایه اعمال میشه
numRef *= 10;
Console.WriteLine(numRef); // خروجی: 20
Console.WriteLine(numbers[2]); // خروجی: 20 (عنصر اصلی هم تغییر کرد!)2️⃣حالا ref return چیست؟ برگرداندن آدرس به جای مقدار 💥
یه متد میتونه به جای برگردوندن مقدار یک متغیر، رفرنس یا آدرس حافظهی اون رو برگردونه. این کار به کدی که متد رو صدا زده، اجازه دسترسی مستقیم و تغییر مقدار اصلی رو میده.
class Program
{
static string x = "Old Value";
// این متد، رفرنس به فیلد x رو برمیگردونه
static ref string GetX() => ref x;
static void Main()
{
// xRef یک رفرنس مستقیم به x میشه
ref string xRef = ref GetX();
// تغییر xRef، خودِ متغیر استاتیک x رو تغییر میده
xRef = "New Value";
Console.WriteLine(x); // خروجی: New Value
}
}
3️⃣نوبت ref readonly: ترکیب پرفورمنس و ایمنی 🛡
گاهی وقتا شما میخواید از مزیت پرفورمنس برگردوندن رفرنس (جلوگیری از کپی شدن دادههای بزرگ) استفاده کنید، ولی نمیخواید کدی که اون رو دریافت کرده، بتونه مقدار اصلی رو تغییر بده.
اینجاست که ref readonly وارد میشه. با این قابلیت، شما رفرنس رو برمیگردونید، ولی اون رفرنس فقط-خواندنی خواهد بود.
4️⃣هشدار و کاربرد اصلی ⚠️
این قابلیتها ابزارهای روزمره شما نیستن. اونها برای micro optimization های خاص طراحی شدن، به خصوص در سناریوهای پرفورمنس بالا مثل کار با <Span<T یا در بازیسازی که هر نانوثانیه مهمه.
در ۹۹٪ مواقع، شما به اینها نیاز ندارید و کد سادهتر، همیشه بهتره.
🤔 حرف حساب و تجربه شما
آشنایی با این مفاهیم، حتی اگه ازشون استفاده نکنید، دید شما رو نسبت به قابلیتهای سطح پایین و قدرت #C عمیقتر میکنه.
آیا تا حالا از ref locals یا ref returns تو پروژهای استفاده کردید؟ یا اصلاً از وجودشون خبر داشتید؟
خب، اینجا که نمیشه همه حرفا رو زد! 😉
ادامهی بحث، سوالات، غر زدنها و گپ و گفتهای خودمونی، فقط تو گروه.
[C# Geeks Hangout]
🔖 هشتگها:
#Ref
#CSharp
#AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
تو پست قبلی، با اصول اولیه و کاربردی structها آشنا شدیم. امروز وقتشه که کلاه غواصی رو سرمون کنیم و به دو تا از عمیقترین و تخصصیترین مباحث مربوط به structها شیرجه بزنیم: رفتار عجیب سازندهها و قابلیت ref struct.
این یکی از گیجکنندهترین بخشهای کار با structهاست. یک struct همیشه یک سازنده پیشفرض بدون پارامتر داره که تمام فیلدها رو صفر میکنه (همون default).
حالا اگه شما خودتون یه سازنده بدون پارامتر بنویسید (که از 10 #C به بعد ممکنه)، اون سازنده پیشفرض حذف نمیشه و هنوز از راههای دیگهای مثل ساختن آرایه، قابل دسترسه!
این کد رو ببینید تا کامل متوجه بشید:
توصیه حرفهای: بهترین کار اینه که
این یه قابلیت خیلی خاص و پیشرفته برای بهینهسازیهای سطح پایینه. یه ref struct، نوعی از struct هست که کامپایلر تضمین میکنه فقط و فقط روی Stack زندگی کنه و هرگز به Heap منتقل نشه.
چرا این خوبه؟ چون به ما اجازه میده با حافظه Stack به صورت خیلی بهینه کار کنیم و از فشار روی Garbage Collector کم کنیم، مثل کاری که <Span<T انجام میده.
محدودیتهای ref struct:
چون ref struct هرگز نباید روی هیپ قرار بگیره، محدودیتهای زیر رو داره:
🚫 نمیتونه عضو یک class باشه.
🚫 نمیتونه عنصر یک آرایه باشه.
🚫 نمیتونه Boxed بشه (به object تبدیل بشه).
🚫 نمیتونه اینترفیس پیادهسازی کنه.
🚫 نمیتونه در متدهای async استفاده بشه.
🤔 حرف حساب و تجربه شما
این دو مفهوم، نهایت عمق و قدرت structها در #C رو نشون میدن.
🚀 شیرجه عمیق در structها: سازندههای گیجکننده و ref struct
تو پست قبلی، با اصول اولیه و کاربردی structها آشنا شدیم. امروز وقتشه که کلاه غواصی رو سرمون کنیم و به دو تا از عمیقترین و تخصصیترین مباحث مربوط به structها شیرجه بزنیم: رفتار عجیب سازندهها و قابلیت ref struct.
1️⃣ تلهی سازندهها: دوگانگی new() و default 🤯
این یکی از گیجکنندهترین بخشهای کار با structهاست. یک struct همیشه یک سازنده پیشفرض بدون پارامتر داره که تمام فیلدها رو صفر میکنه (همون default).
حالا اگه شما خودتون یه سازنده بدون پارامتر بنویسید (که از 10 #C به بعد ممکنه)، اون سازنده پیشفرض حذف نمیشه و هنوز از راههای دیگهای مثل ساختن آرایه، قابل دسترسه!
این کد رو ببینید تا کامل متوجه بشید:
struct Point
{
int x = 1;
int y;
// سازنده سفارشی بدون پارامتر
public Point() => y = 1;
}
// --- نتایج عجیب ---
// سازنده صریح و سفارشی ما صدا زده میشه
Point p1 = new Point();
Console.WriteLine($"p1: ({p1.x}, {p1.y})");
// خروجی: p1: (1, 1)
// سازنده پیشفرضِ صفرکننده صدا زده میشه
Point p2 = default;
Console.WriteLine($"p2: ({p2.x}, {p2.y})");
// خروجی: p2: (0, 0)
// آرایهها هم از سازنده پیشفرض و صفرکننده استفاده میکنن
Point[] points = new Point[1];
Console.WriteLine($"points[0]: ({points[0].x}, {points[0].y})");
// خروجی: points[0]: (0, 0)
توصیه حرفهای: بهترین کار اینه که
structهاتون رو جوری طراحی کنید که حالت پیشفرض و صفر شدهشون، یک حالت معتبر و قابل استفاده باشه.2️⃣ ref struct: زندگی فقط روی Stack! ⚡️
این یه قابلیت خیلی خاص و پیشرفته برای بهینهسازیهای سطح پایینه. یه ref struct، نوعی از struct هست که کامپایلر تضمین میکنه فقط و فقط روی Stack زندگی کنه و هرگز به Heap منتقل نشه.
چرا این خوبه؟ چون به ما اجازه میده با حافظه Stack به صورت خیلی بهینه کار کنیم و از فشار روی Garbage Collector کم کنیم، مثل کاری که <Span<T انجام میده.
محدودیتهای ref struct:
چون ref struct هرگز نباید روی هیپ قرار بگیره، محدودیتهای زیر رو داره:
🚫 نمیتونه عضو یک class باشه.
🚫 نمیتونه عنصر یک آرایه باشه.
🚫 نمیتونه Boxed بشه (به object تبدیل بشه).
🚫 نمیتونه اینترفیس پیادهسازی کنه.
🚫 نمیتونه در متدهای async استفاده بشه.
🤔 حرف حساب و تجربه شما
این دو مفهوم، نهایت عمق و قدرت structها در #C رو نشون میدن.
🔖 هشتگها:
#AdvancedCSharp #Struct #Performance #MemoryManagement
📖 سری آموزشی کتاب C# 12 in a Nutshell
در دو پست قبلی، با قیدها و وراثت در دنیای جنریکها آشنا شدیم. امروز در قسمت آخر این سری، به دو تا از نکات خیلی ظریف و فنی شیرجه میزنیم که رفتار جنریکها رو در سطح حافظه و مقادیر پیشفرض نشون میده.
این نکات، درک شما رو از جنریکها کامل میکنه.
چطور میتونیم مقدار پیشفرض یک پارامتر جنریک T رو بدست بیاریم؟ چون T میتونه هر چیزی باشه (value type یا reference type)، ما نمیتونیم مستقیماً null یا 0 بهش بدیم.
راه حل #C، کلمه کلیدی default هست.
default(T)
به ما مقدار پیشفرض T رو میده:
🔹 اگه T یک Reference Type باشه، مقدارش null میشه.
🔹 اگه T یک Value Type باشه، مقدارش صفر میشه (نتیجه صفر کردن بیتهای حافظه).
این یکی از اون نکات خیلی مهمه که خیلیها رو غافلگیر میکنه.
قانون اینه: دادههای استاتیک (Static Fields/Properties) برای هر نوع بستهی جنریک (Closed Type)، منحصر به فرد و جداگانه هستن.
یعنی static فیلدِ <Bob<int هیچ ربطی به static فیلدِ <Bob<string نداره و هر کدوم در حافظه، جای خودشون رو دارن.
مثال کتاب برای درک بهتر این موضوع:
این جزئیات فنی، نشون میده که جنریکها در #C فقط یه جایگزینی ساده متن نیستن، بلکه یه مکانیزم قدرتمند در سطح CLR هستن که تایپهای کاملاً جدیدی رو در زمان اجرا تولید میکنن.
🔬 نکات عمیق جنریکها: Static Data و مقدار default
در دو پست قبلی، با قیدها و وراثت در دنیای جنریکها آشنا شدیم. امروز در قسمت آخر این سری، به دو تا از نکات خیلی ظریف و فنی شیرجه میزنیم که رفتار جنریکها رو در سطح حافظه و مقادیر پیشفرض نشون میده.
این نکات، درک شما رو از جنریکها کامل میکنه.
1️⃣ مقدار پیشفرض T (default(T))
چطور میتونیم مقدار پیشفرض یک پارامتر جنریک T رو بدست بیاریم؟ چون T میتونه هر چیزی باشه (value type یا reference type)، ما نمیتونیم مستقیماً null یا 0 بهش بدیم.
راه حل #C، کلمه کلیدی default هست.
default(T)
به ما مقدار پیشفرض T رو میده:
🔹 اگه T یک Reference Type باشه، مقدارش null میشه.
🔹 اگه T یک Value Type باشه، مقدارش صفر میشه (نتیجه صفر کردن بیتهای حافظه).
static void Zap<T>(T[] array)
{
for (int i = 0; i < array.Length; i++)
{
// مقدار پیشفرض T را در آرایه قرار میدهد
array[i] = default(T);
}
}
// از #C 7.1 به بعد، میتونیم خلاصهتر بنویسیم:
// array[i] = default;
2️⃣ تلهی دادههای استاتیک در جنریکها ⚠️
این یکی از اون نکات خیلی مهمه که خیلیها رو غافلگیر میکنه.
قانون اینه: دادههای استاتیک (Static Fields/Properties) برای هر نوع بستهی جنریک (Closed Type)، منحصر به فرد و جداگانه هستن.
یعنی static فیلدِ <Bob<int هیچ ربطی به static فیلدِ <Bob<string نداره و هر کدوم در حافظه، جای خودشون رو دارن.
مثال کتاب برای درک بهتر این موضوع:
class Bob<T>
{
public static int Count;
}
// --- نتایج ---
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص int)
Console.WriteLine(++Bob.Count); // خروجی: 2 (شمارنده مخصوص int)
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص string، کاملاً جداست!)
Console.WriteLine(++Bob.Count); // خروجی: 1 (شمارنده مخصوص object، این هم جداست!)
🤔 حرف حساب و تجربه شما
این جزئیات فنی، نشون میده که جنریکها در #C فقط یه جایگزینی ساده متن نیستن، بلکه یه مکانیزم قدرتمند در سطح CLR هستن که تایپهای کاملاً جدیدی رو در زمان اجرا تولید میکنن.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
به این کد نگاه کنید. به نظر کاملاً منطقی میاد، ولی کامپایل نمیشه! چرا؟
امروز میخوایم یه نکته خیلی عمیق و فنی در مورد کست کردن پارامترهای جنریک (T) رو یاد بگیریم.
مشکل اینه که کامپایلر در زمان کامپایل، نمیدونه T قراره چه نوعی باشه. وقتی شما مینویسید (SomeType)arg، کامپایلر نمیدونه منظور شما کدوم یکی از ایناست:
🔹 تبدیل عددی (Numeric)
🔹 تبدیل رفرنس (Reference)
🔹 تبدیل Boxing/Unboxing
🔹 تبدیل سفارشی (Custom)
این ابهام باعث میشه کامپایلر جلوی شما رو بگیره تا از خطاهای پیشبینی نشده جلوگیری کنه.
برای رفع این ابهام، باید به کامپایلر بگیم دقیقاً منظورمون چه نوع تبدیلیه. دو راه حل اصلی وجود داره:
اپراتور as فقط و فقط برای تبدیلهای رفرنس استفاده میشه. چون ابهامی نداره، کامپایلر قبولش میکنه. اگه تبدیل شکست بخوره، null برمیگردونه.
راه حل عمومیتر که برای Value Typeها (Unboxing) هم کار میکنه، اینه که اول متغیر رو به object کست کنید. این کار به کامپایلر میگه "منظور من یه تبدیل رفرنس یا unboxing هست، نه عددی یا سفارشی".
برای Reference Type:
این یه نکته ظریف ولی خیلی مهمه که نشون میده کامپایلر #C چقدر به ایمنی نوع و رفع ابهام اهمیت میده.
🤯 تلهی کستینگ در جنریکها: چرا کامپایلر گیج میشود و راه حل چیست؟
به این کد نگاه کنید. به نظر کاملاً منطقی میاد، ولی کامپایل نمیشه! چرا؟
StringBuilder Foo<T>(T arg)
{
if (arg is StringBuilder)
return (StringBuilder)arg; // ❌ خطای زمان کامپایل!
// ...
}
امروز میخوایم یه نکته خیلی عمیق و فنی در مورد کست کردن پارامترهای جنریک (T) رو یاد بگیریم.
1️⃣ مشکل کجاست؟ ابهام در نوع تبدیل
مشکل اینه که کامپایلر در زمان کامپایل، نمیدونه T قراره چه نوعی باشه. وقتی شما مینویسید (SomeType)arg، کامپایلر نمیدونه منظور شما کدوم یکی از ایناست:
🔹 تبدیل عددی (Numeric)
🔹 تبدیل رفرنس (Reference)
🔹 تبدیل Boxing/Unboxing
🔹 تبدیل سفارشی (Custom)
این ابهام باعث میشه کامپایلر جلوی شما رو بگیره تا از خطاهای پیشبینی نشده جلوگیری کنه.
2️⃣ راه حلها: شفافسازی برای کامپایلر
برای رفع این ابهام، باید به کامپایلر بگیم دقیقاً منظورمون چه نوع تبدیلیه. دو راه حل اصلی وجود داره:
راه حل اول: استفاده از اپراتور as (برای Reference Typeها) ✅
اپراتور as فقط و فقط برای تبدیلهای رفرنس استفاده میشه. چون ابهامی نداره، کامپایلر قبولش میکنه. اگه تبدیل شکست بخوره، null برمیگردونه.
StringBuilder Foo<T>(T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb != null)
return sb;
// ...
}
راه حل دوم: کست کردن به object (راه حل عمومی) 🚀
راه حل عمومیتر که برای Value Typeها (Unboxing) هم کار میکنه، اینه که اول متغیر رو به object کست کنید. این کار به کامپایلر میگه "منظور من یه تبدیل رفرنس یا unboxing هست، نه عددی یا سفارشی".
برای Reference Type:
// کامپایلر میفهمه که منظور، تبدیل رفرنس است
return (StringBuilder)(object)arg;
برای Value Type:
______
// کامپایلر میفهمه که منظور، Unboxing است
int Foo<T>(T x) => (int)(object)x;
🤔 حرف حساب و تجربه شما
این یه نکته ظریف ولی خیلی مهمه که نشون میده کامپایلر #C چقدر به ایمنی نوع و رفع ابهام اهمیت میده.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
ولی این کد خطا میده:
جواب این سوال تو یه مفهوم پیشرفته و خیلی مهم به اسم Covariance نهفتهست.
به زبان ساده، Covariance میگه شما میتونید یه نوع جنریک با پارامتر فرزند (مثل <string>) رو به همون نوع جنریک با پارامتر پدر (مثل <object>) تبدیل کنید. (چون string یک نوع از object است).
این قابلیت در #C فقط برای اینترفیسها و دلیگیتها فعاله، نه برای کلاسها.
چرا نمیتونیم یه <List<Bear رو به <List<Animal تبدیل کنیم؟ چون اگه این کار مجاز بود، میتونستیم یه فاجعه منطقی به بار بیاریم و ایمنی نوع (Type Safety) رو که #C بهش افتخار میکنه، از بین ببریم:
برای جلوگیری از این خطای منطقی، کلاسهای جنریک مثل <
حالا فرض کنید یه متد Wash داریم که میخواد یه لیستی از حیوانات رو بشوره. اگه ورودی رو <List<Animal بذاریم، نمیتونیم بهش <List<Bear پاس بدیم.
راه حل، جنریک کردن خود متد با استفاده از قیدهاست:
Covariance
یه مفهوم عمیقه که اساس کارکرد خیلی از اینترفیسهای مهم داتنت مثل <IEnumerable<T هست. درک اون، شما رو به درک عمیقتری از سیستم تایپینگ #C میرسونه.
🧬 جنریکهای پیشرفته: Covariance و راز تبدیل <IEnumerable<string به <IEnumerable<objectتا حالا براتون سوال شده چرا این کد در #C درسته:
IEnumerable<object> list = new List<string>();
ولی این کد خطا میده:
List<object> list = new List<string>();
جواب این سوال تو یه مفهوم پیشرفته و خیلی مهم به اسم Covariance نهفتهست.
1️⃣ Covariance چیست؟
به زبان ساده، Covariance میگه شما میتونید یه نوع جنریک با پارامتر فرزند (مثل <string>) رو به همون نوع جنریک با پارامتر پدر (مثل <object>) تبدیل کنید. (چون string یک نوع از object است).
این قابلیت در #C فقط برای اینترفیسها و دلیگیتها فعاله، نه برای کلاسها.
2️⃣ چرا کلاسها Covariant نیستن؟ (تلهی Type Safety) ⚠️
چرا نمیتونیم یه <List<Bear رو به <List<Animal تبدیل کنیم؟ چون اگه این کار مجاز بود، میتونستیم یه فاجعه منطقی به بار بیاریم و ایمنی نوع (Type Safety) رو که #C بهش افتخار میکنه، از بین ببریم:
class Animal { }
class Bear : Animal { }
class Camel : Animal { }
// لیستی از خرسها
List bears = new List();
// ❌ خطای زمان کامپایل! اگر این خط مجاز بود...
// List animals = bears;
// ...اونوقت میتونستیم یه شتر رو تو لیست خرسها بریزیم!
// animals.Add(new Camel()); // 💥 فاجعه در زمان اجرا!برای جلوگیری از این خطای منطقی، کلاسهای جنریک مثل <
List<T که متدهای Add دارن (یعنی "نوشتنی" هستن)، به صورت پیشفرض Covariant نیستن. اما اینترفیسی مثل <IEnumerable<T که فقط "خواندنی" هست، میتونه Covariant باشه.3️⃣ پس چطور با این محدودیت کار کنیم؟
حالا فرض کنید یه متد Wash داریم که میخواد یه لیستی از حیوانات رو بشوره. اگه ورودی رو <List<Animal بذاریم، نمیتونیم بهش <List<Bear پاس بدیم.
راه حل، جنریک کردن خود متد با استفاده از قیدهاست:
public class ZooCleaner
{
// این متد، هر نوع لیستی رو قبول میکنه که T اون از Animal ارث برده باشه
public static void Wash<T>(List<T> animals) where T : Animal
{
foreach (T animal in animals)
{
Console.WriteLine($"Washing a {animal.GetType().Name}");
}
}
}
// --- نحوه استفاده ---
List bears = new List();
ZooCleaner.Wash(bears); // ✅ حالا درسته!
🤔 حرف حساب و تجربه شما
Covariance
یه مفهوم عمیقه که اساس کارکرد خیلی از اینترفیسهای مهم داتنت مثل <IEnumerable<T هست. درک اون، شما رو به درک عمیقتری از سیستم تایپینگ #C میرسونه.
🔖 هشتگها:
#CSharp #DotNet #OOP #Generics #AdvancedCSharp
📖 سری آموزشی کتاب C# 12 in a Nutshell
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
مثال کامل:
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
💡 مفهوم Delegates (دلیگیتها): شیئی که متدها را صدا میزند
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
delegate int Transformer (int x);
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
int Square (int x) { return x * x; }
// یا به شکل کوتاهتر:
int Square (int x) => x * x;🤝 ایجاد و فراخوانی دلیگیت
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
Transformer t = Square; // ایجاد نمونه دلیگیت
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
int answer = t(3); // answer برابر 9 میشود
مثال کامل:
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Delegate type declaration
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
🔌 نوشتن متدهای Plug-in با دلیگیتها
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
int[] values = { 1, 2, 3 };
Transform (values, Square); // Hook in the Square method
foreach (int i in values) Console.Write (i + " "); // خروجی: 1 4 9
// پیادهسازی متد Transform
void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
int Square (int x) => x * x;
int Cube (int x) => x * x * x;
delegate int Transformer (int x);ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
🤔 جمعبندی و تجربه شما
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
🔖 هشتگها:
#CSharp #Delegates #AdvancedCSharp #OOP #Callback
📖 سری آموزشی کتاب C# 12 in a Nutshell
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
مثال زیر یک متد هدف Static را نشان میدهد:
مثال زیر یک متد هدف Instance را نشان میدهد:
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
🎯 متدهای هدف (Target Methods): Static یا Instance؟
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
🔹 متد هدف Static
مثال زیر یک متد هدف Static را نشان میدهد:
Transformer t = Test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public static int Square (int x) => x * x; }
delegate int Transformer (int x);
🔸 متد هدف Instance
مثال زیر یک متد هدف Instance را نشان میدهد:
Test test = new Test();
Transformer t = test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public int Square (int x) => x * x; }
delegate int Transformer (int x);
🧠 نحوه عملکرد با Instance Methodها
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";
ProgressReporter p = r.ReportProgress;
p(99); // خروجی: %Complete: 99
Console.WriteLine (p.Target == r); // خروجی: True
Console.WriteLine (p.Method); // خروجی: Void ReportProgress(Int32)
r.Prefix = "";
p(99); // خروجی: 99
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
public string Prefix = "";
public void ReportProgress (int percentComplete)
=> Console.WriteLine (Prefix + percentComplete);
}
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
⛓️ دلیگیتهای چندپخشی (Multicast Delegates)
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
➕ ترکیب (Combine) دلیگیتها
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
➖ حذف (Remove) دلیگیتها
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
d -= SomeMethod1;
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
💡 نکات تکمیلی
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
⚠️ مقادیر بازگشتی در Multicast
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
🖥 مثال عملی از Multicast Delegate
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile; // ترکیب دو متد
Util.HardWork (p);
void WriteProgressToConsole (int percentComplete)
=> Console.WriteLine (percentComplete);
void WriteProgressToFile (int percentComplete)
=> System.IO.File.WriteAllText ("progress.txt",
percentComplete.ToString());
🔖 هشتگها:
#CSharp #Delegates #Multicast #OOP #AdvancedCSharp