بخش دوم: Delegates
متدهای هدف (Target Methods)
همان طور که به خاطر دارید، گفتیم که یک delegate می تواند به یک متد اشاره کند، درست است؟ به زبان ساده، یعنی می توانیم متغیری داشته باشیم که یک متد را در خودش نگه دارد و بعد با صدا زدن آن متغیر، آن متد را فراخوانی کنیم.
در سی شارپ، متدهای محلی (local)، ایستا (static) و نمونه (instance) داریم. حالا delegate به کدام یک می تواند اشاره کند؟ به همه! متد هدف یک delegate می تواند یک متد محلی، ایستا یا نمونه باشد.
// متد هدف ایستا
Transformer transformer = Test.Square;
Console.WriteLine(transformer(10)); // 100
class Test
{
public static int Square(int x) => x * x;
}
delegate int Transformer(int x);
// متد هدف نمونه
Test test = new();
Transformer transformer = test.Square;
Console.WriteLine(transformer(10)); // 100
class Test
{
public int Square(int x) => x * x;
}
delegate int Transformer(int x);
چندپخشی (Multicasting)
یکی از بخش های جالب در делеگیت ها، بحث چندپخشی است. یک متغیر معمولی می تواند داده را نگه دارد، درست است؟ مثلاً یک متغیر از نوع int فقط یک عدد را می تواند در خودش ذخیره کند؛ اگر عدد دیگری به آن نسبت دهید، عدد قبلی با عدد جدید جایگزین می شود. اما در delegateها می توانیم با استفاده از عملگرهای + و +=، چندین متد را به یک نمونه از delegate نسبت دهیم.
Transformer transformer = Square;
transformer += Cube;
فراخوانی transformer، هر دو متد Square و Cube را به ترتیبی که اضافه شده اند، صدا می زند.
حالا اگر بخواهیم یک متد را از delegate حذف کنیم، از عملگرهای - و -= استفاده می کنیم.
transformer -= Square;
delegateها تغییرناپذیر (immutable) هستند، بنابراین وقتی از
-=یا+=استفاده می کنید، در واقع یک نمونه جدید از delegate ساخته می شود و به همان متغیر قبلی نسبت داده می شود.
اگر چندین متد با نوع خروجی غیر از void را به یک delegate نسبت دهیم، چه اتفاقی می افتد؟ کدام نتیجه برگردانده می شود؟ فراخواننده، مقدار بازگشتی از آخرین متدی که فراخوانی شده را دریافت می کند. این به این معنا نیست که متدهای دیگر صدا زده نمی شوند؛ آن ها فراخوانی می شوند، اما مقادیر بازگشتی شان نادیده گرفته می شود.
به جز موارد خاص، داشتن یک delegate چندپخشی با چندین متد که مقدار بازگشتی دارند، زیاد معنا ندارد؛ پس نگران این بخش نباشید، زیاد به آن نیاز پیدا نمی کنید.
بیایید یک مثال ببینیم:
class Program
{
delegate void Logger(string message);
class LogUtilities
{
public static void Log(Logger logger, string message)
{
logger(message);
}
}
public static void Main(string[] args)
{
Logger logger = ConsoleLogger;
logger += FileLogger;
LogUtilities.Log(logger, "My Log Message");
void ConsoleLogger(string message) => Console.WriteLine(message);
void FileLogger(string message) => System.IO.File.WriteAllText("Log.txt", message);
}
}
در خط 3 delegate خود را تعریف می کنیم (قرارداد یا قاعده) و در خطوط 14، 15 و 16 یک نمونه delegate چندپخشی می سازیم تا هر دو متد ConsoleLogger و FileLogger را فراخوانی کند. بعد از اجرای برنامه، پیام My Log Message را روی صفحه می بینید و یک فایل به نام Log.txt در ریشه برنامه ساخته می شود که محتوای آن My Log Message است.
انواع جنریک delegate
می خواهم این مبحث را جذاب تر کنم. بیایید با مثال Transformer ادامه دهیم. می خواهیم مربع و مکعب یک عدد را به شکل زیر محاسبه کنیم:
delegate int Transformer(int x);
public static void Main(string[] args)
{
Transformer transformer = Square;
Console.WriteLine(transformer(10)); // 100
transformer = Cube;
Console.WriteLine(transformer(2)); // 8
static int Square(int a) => a * a;
static int Cube(int a) => a * a * a;
}
بهتر نیست اگر با یک delegate بتوانیم هر نوع عددی را پوشش دهیم؟ منظورم این است که در حال حاضر، این delegate فقط با متدهایی سازگار است که int برمی گردانند و یک پارامتر از نوع int می گیرند، درست است؟ اگر متد Cube را با نوع بازگشتی double و پارامتر double بنویسید، باید یک delegate دیگر با نوع بازگشتی و پارامتر از نوع double بسازید، درست است؟
آیا جنریک ها در سی شارپ را به خاطر دارید که قبلاً درباره شان مطلبی منتشر کردم؟ اینجا هم می توانیم از همان راه حل استفاده کنیم.
نحوه نوشتن انواع جنریک delegate به شکل زیر است:
public delegate T MethodName<T>(T arg);
حالا بیایید delegate Transformer خود را به روز کنیم:
delegate T Transformer<T>(T x);
public static void Main(string[] args)
{
Transformer<int> intTransformer = Square;
Console.WriteLine(intTransformer(10)); // 100
Transformer<double> doubleTransformer = Cube;
Console.WriteLine(doubleTransformer(2.0)); // 8
static int Square(int a) => a * a;
static double Cube(double a) => a * a * a;
}
عالی است، نه؟
Func و Action
حالا که با delegateهای جنریک و نحوه ساخت آن ها آشنا شدید، باید بدانید که با استفاده از delegateهای جنریک می توانید delegateهایی با هر نوع ورودی و خروجی بسازید. خود سی شارپ دو delegate جنریک از پیش تعریف شده دارد: Func و Action.
Func
Func یک delegate از پیش تعریف شده است که نشان دهنده متدی با یک یا چند پارامتر (تا 16 پارامتر) است و یک مقدار برمی گرداند. نوع بازگشتی Func می تواند هر نوعی باشد.
Func<T1, T2, T3, ..., T16, TResult>
T1 تا T16 پارامترها و TResult مقدار بازگشتی است.
بیایید یک مثال ببینیم:
class Calculator
{
public int Add(int a, int b) => a + b;
public int Subtract(int a, int b) => a - b;
public int Multiply(int a, int b) => a * b;
public int Divide(int a, int b) => a / b;
}
class Program
{
static void Main(string[] args)
{
var FuncCalculator = new Calculator();
Func<int, int, int> add = FuncCalculator.Add;
Func<int, int, int> subtract = FuncCalculator.Subtract;
Func<int, int, int> multiply = FuncCalculator.Multiply;
Func<int, int, int> divide = FuncCalculator.Divide;
Console.WriteLine($"نتیجه جمع: {add(10, 5)}");
Console.WriteLine($"نتیجه تفریق: {subtract(10, 5)}");
Console.WriteLine($"نتیجه ضرب: {multiply(10, 5)}");
Console.WriteLine($"نتیجه تقسیم: {divide(10, 5)}");
}
}
// یا به شکل دیگر
Func<int, int, int> add = (a, b) => a + b;
Func<int, int, int> subtract = (a, b) => a - b;
Func<int, int, int> multiply = (a, b) => a * b;
Func<int, int, int> divide = (a, b) => a / b;
Console.WriteLine($"نتیجه جمع: {add(10, 5)}");
Console.WriteLine($"نتیجه تفریق: {subtract(10, 5)}");
Console.WriteLine($"نتیجه ضرب: {multiply(10, 5)}");
Console.WriteLine($"نتیجه تقسیم: {divide(10, 5)}");
ساده است، نه؟ این همان Func است:
یک delegate از پیش تعریف شده که یک متد با صفر یا چند (تا 16) پارامتر ورودی و یک نوع بازگشتی را نشان می دهد.
Action
در برخی موارد، نیازی به مقدار بازگشتی نداریم، یا به delegateای نیاز داریم که متدی با خروجی void را نشان دهد.
در این حالت، می توانیم از delegate از پیش تعریف شده دیگری به نام Action استفاده کنیم.
Action یک نوع delegate از پیش تعریف شده است که یک متد با صفر یا چند (تا 16) پارامتر را نشان می دهد که هیچ مقداری برنمی گرداند.
Action<int, int> ActionCalculator = (a, b) =>
{
Console.WriteLine($"نتیجه جمع: {a + b}");
Console.WriteLine($"نتیجه تفریق: {a - b}");
Console.WriteLine($"نتیجه ضرب: {a * b}");
Console.WriteLine($"نتیجه تقسیم: {a / b}");
};
ActionCalculator(10, 5);
همان طور که می بینید، یک delegate از نوع Action تعریف کردیم که دو پارامتر صحیح می گیرد، چند عمل حسابی انجام می دهد و هیچ چیزی برنمی گرداند.
نکات
موارد استفاده:
- وقتی متدی دارید که بر اساس پارامترهای ورودی نتیجه ای را محاسبه می کند، از
Funcاستفاده کنید. - وقتی متدی دارید که قرار است کاری انجام دهد بدون اینکه مقداری برگرداند، از
Actionاستفاده کنید.
- وقتی متدی دارید که بر اساس پارامترهای ورودی نتیجه ای را محاسبه می کند، از
عبارات لامبدا (Lambda Expressions):
- معمولاً از لامبدا همراه با
FuncوActionاستفاده می شود تا متدها را به صورت درون خطی (inline) تعریف کنیم بدون اینکه نیاز به نوشتن یک متد نام گذاری شده داشته باشیم.
- معمولاً از لامبدا همراه با
ترکیب delegateها:
- می توانید چندین delegate از نوع
FuncیاActionرا با استفاده از عملگر+=به هم زنجیر کنید تا چندین عملیات را به ترتیب انجام دهید.
- می توانید چندین delegate از نوع
مثال ترکیب delegateها
Action<string> greetAction = message => Console.WriteLine("درود: " + message);
Action<string> farewellAction = message => Console.WriteLine("بدرود: " + message);
Action<string> combinedAction = greetAction + farewellAction;
combinedAction("John"); // هم درود و هم بدرود را برای "John" چاپ می کند
عالی است، نه؟
موارد استفاده
Func:
ترکیب توابع (Function Composition): ترکیب چندین تابع برای ایجاد یک تابع جدید با زنجیر کردن delegateهای
Func.LINQ (Language Integrated Query): در LINQ برای عملیات projection یا تبدیل روی مجموعه ها استفاده می شود.
توابع بازخورد (Callback Functions): ارائه یک تابع بازخورد که پس از اتمام یک عملیات ناهمگام اجرا شود.
نگاشت (Mapping): تبدیل یک نوع شیء به نوع دیگر با استفاده از یک تابع نگاشت.
مدیریت خطا: تعریف توابعی برای مدیریت موارد خاص خطا یا استثناها.
ارزیابی تنبل (Lazy Evaluation): محاسبه مقادیر فقط در صورت نیاز، که به ویژه برای بهینه سازی کارایی مفید است.
پیکربندی (Configuration): ارائه توابعی برای پیکربندی مؤلفه ها یا رفتارهای یک سیستم.
Action:
مدیریت رویداد (Event Handling): تعریف اقداماتی که هنگام وقوع یک رویداد اجرا می شوند.
تعامل با رابط کاربری (UI Interaction): مدیریت رویدادهای رابط کاربری و تعریف اقدامات برای به روزرسانی عناصر UI.
ثبت وقایع (Logging): اقدامات یا توابع ثبت وقایع که رویدادها یا اطلاعات را برای اهداف اشکال زدایی یا بازرسی ثبت می کنند.
چندرشته ای (Multithreading): مشخص کردن اقداماتی که در رشته های جداگانه یا به صورت ناهمگام اجرا می شوند.
عملیات پاک سازی (Clean-Up Operations): تعریف اقدامات برای پاک سازی یا آزادسازی منابع پس از انجام یک کار خاص.
توابع بازخورد (Callback Functions): ارائه یک اقدام بازخورد که پس از اتمام یک عملیات ناهمگام اجرا شود.
تزریق وابستگی (Dependency Injection): مشخص کردن اقداماتی که پس از حل وابستگی ها در یک ظرف تزریق وابستگی انجام می شوند.
مشکلی که با delegate حل می شود، با یک interface هم قابل حل است.
اگر این را متوجه نمی شوید، پیشنهاد می کنم دوباره مقاله را بخوانید، مثال های بیشتری جستجو کنید و delegateها را با interfaceها مقایسه کنید. در پایان، سعی کنید مثال های قبلی را با یک interface پیاده سازی کنید.
طراحی مبتنی بر delegate ممکن است در شرایط زیر از طراحی با interface بهتر باشد:
- interface فقط یک متد را تعریف می کند.
- به قابلیت چندپخشی (multicast) نیاز دارید.
- مشترک (subscriber) نیاز دارد که interface را چندین بار پیاده سازی کند.
سازگاری delegateها
انواع delegateها با یکدیگر ناسازگار هستند، حتی اگر امضای یکسانی داشته باشند:
D1 d1 = Method1;
D2 d2 = d1; // خطای زمان کامپایل
void Method1() { }
delegate void D1() { }
delegate void D2() { }
اما مورد زیر مجاز است: D2 d2 = new D2(d1);
برابری delegateها
نمونه های delegate در صورتی برابر در نظر گرفته می شوند که متدهای هدف یکسانی داشته باشند:
D d1 = Method1;
D d2 = Method1;
Console.WriteLine( d1 == d2); // True
void Method1() { }
delegate void D() { }
delegateهای چندپخشی در صورتی برابر در نظر گرفته می شوند که به همان متدها و به همان ترتیب اشاره کنند.
بحث های دیگری درباره delegateها وجود دارد مانند سازگاری پارامترها، سازگاری نوع بازگشتی، هم واریانس (covariant) و ضدواریانس (contravariant) که من به آنها نمی پردازم و می توانید خودتان جستجو کنید یا از ChatGPT بخواهید با مثال برایتان توضیح دهد.
حالا آماده ایم تا مفهوم Events را درک کنیم. در پست بعدی، درباره رویدادها (Events) صحبت خواهم کرد و اینکه چطور آشنایی با delegateها به ما کمک می کند تا Events را سریع تر و راحت تر یاد بگیریم.
**لبخند یادت نره!
نظرات (0)
نظر خود را بنویسید
هنوز نظری ثبت نشده است