021-88881776

آموزش برنامه‌نویسی پیشرفته در C# با .NET

C# یکی از قدرتمندترین زبان‌های برنامه‌نویسی مدرن است که همراه با فریم‌ورک .NET امکانات بی‌نظیری برای توسعه نرم‌افزارهای پیشرفته ارائه می‌دهد.آموزش .NET از این جهت اهمیت دارد که این فریم‌ورک به شما اجازه می‌دهد برنامه‌هایی مقیاس‌پذیر، امن و کارآمد بسازید. C# به‌عنوان زبان اصلی این فریم‌ورک، با ویژگی‌هایی مانند جنریک‌ها، دلیگیت‌ها، عبارات لامبدا و ابزارهایی مثل LINQ و برنامه‌نویسی ناهمزمان، امکانات گسترده‌ای در اختیار برنامه‌نویسان قرار می‌دهد. اگر تازه‌کار هستید، نگران نباشید؛ ما هر مفهوم را با مثال‌های عملی و توضیحات گام‌به‌گام پوشش خواهیم داد تا بتوانید به‌راحتی برنامه‌نویسی پیشرفته در C# با .NET را یاد بگیرید.

جنریک‌ها (Generics)

جنریک‌ها یکی از ستون‌های اصلی برنامه‌نویسی پیشرفته در C# با .NET محسوب می‌شوند و ابزاری قدرتمند برای نوشتن کدهایی هستند که هم انعطاف‌پذیر باشند، هم قابل استفاده مجدد و هم از نظر عملکرد بهینه. این ویژگی که در نسخه 2.0 فریم‌ورک .NET معرفی شد، به برنامه‌نویسان اجازه می‌دهد تا کلاس‌ها، متدها، رابط‌ها و حتی ساختارهایی (struct) را بدون وابستگی به یک نوع داده خاص تعریف کنند. به زبان ساده‌تر، جنریک‌ها مثل یک “الگوی هوشمند” عمل می‌کنند که نوع داده موردنظر را در زمان اجرا یا استفاده مشخص می‌کند.

جنریک‌ها دقیقاً چه کاری انجام می‌دهند؟

در برنامه‌نویسی، گاهی نیاز داریم کدی بنویسیم که با انواع مختلفی از داده‌ها (مثل اعداد، رشته‌ها یا اشیاء پیچیده‌تر) کار کند. بدون جنریک‌ها، دو راه پیش رو داریم: یا برای هر نوع داده یک کد جداگانه بنویسیم (که تکراری و وقت‌گیر است)، یا از نوع عمومی object استفاده کنیم (که امنیت نوع داده را کاهش می‌دهد و نیاز به تبدیل نوع یا “casting” دارد). جنریک‌ها این مشکل را حل می‌کنند و به ما اجازه می‌دهند یک کد واحد بنویسیم که با هر نوع داده‌ای که بخواهیم کار کند، بدون از دست دادن ایمنی یا کارایی.

مزایای استفاده از جنریک‌ها

ایمنی نوع (Type Safety): وقتی از جنریک‌ها استفاده می‌کنید، کامپایلر در زمان کامپایل مطمئن می‌شود که فقط نوع داده‌ای که مشخص کرده‌اید استفاده شود. این یعنی خطاهایی مثل وارد کردن یک رشته به جای عدد در زمان اجرا رخ نمی‌دهند.
کاهش تکرار کد: به جای نوشتن چند کلاس یا متد مشابه برای انواع مختلف داده، یک نسخه جنریک کافی است.
بهبود عملکرد: برخلاف استفاده از object که نیاز به “boxing” و “unboxing” دارد (عملیاتی که در آن نوع‌های ساده مثل int به object تبدیل می‌شوند و برعکس)، جنریک‌ها مستقیماً با نوع داده کار می‌کنند و این عملیات اضافی را حذف می‌کنند.

مثال ساده و کاربردی

بیایید با یک مثال عملی شروع کنیم که قبلاً هم دیدیم، اما آن را گسترش می‌دهیم تا جنبه‌های بیشتری از جنریک‌ها را نشان دهد:

public class MyList<T>
{
    private T[] items;
    private int count = 0;

    public MyList(int capacity)
    {
        items = new T[capacity]; // آرایه‌ای با ظرفیت مشخص
    }

    public void Add(T item)
    {
        if (count < items.Length)
        {
            items[count] = item;
            count++;
        }
        else
        {
            Console.WriteLine("ظرفیت لیست پر شده است!");
        }
    }

    public T Get(int index)
    {
        if (index >= 0 && index < count)
            return items[index];
        throw new IndexOutOfRangeException("شاخص خارج از محدوده است!");
    }

    public int Count => count; // تعداد آیتم‌های فعلی
}

class Program
{
    static void Main()
    {
        // استفاده از جنریک‌ها با نوع int
        MyList<int> numbers = new MyList<int>(3);
        numbers.Add(10);
        numbers.Add(20);
        numbers.Add(30);
        Console.WriteLine($"تعداد اعداد: {numbers.Count}"); // خروجی: 3
        Console.WriteLine(numbers.Get(1)); // خروجی: 20

        // استفاده از جنریک‌ها با نوع string
        MyList<string> names = new MyList<string>(2);
        names.Add("علی");
        names.Add("مریم");
        Console.WriteLine($"تعداد نام‌ها: {names.Count}"); // خروجی: 2
        Console.WriteLine(names.Get(0)); // خروجی: علی
    }
}

در این کد، کلاس MyList<T> یک لیست ساده را پیاده‌سازی می‌کند که می‌تواند هر نوع داده‌ای را بپذیرد. T یک متغیر نوع (Type Parameter) است که در زمان استفاده از کلاس مشخص می‌شود. مثلاً وقتی می‌نویسیم MyList<int>، همه Tها به int تبدیل می‌شوند.

محدودیت‌ها در جنریک‌ها (Constraints)

گاهی نیاز داریم جنریک‌ها را محدود کنیم تا فقط با نوع خاصی از داده‌ها کار کنند. برای این کار از کلمه کلیدی where استفاده می‌کنیم. مثلاً اگر بخواهیم T حتماً یک کلاس باشد یا یک رابط خاص را پیاده‌سازی کند:

public class MyContainer<T> where T : class
{
    public T Item { get; set; }
}

class Program
{
    static void Main()
    {
        MyContainer<string> container = new MyContainer<string>(); // کار می‌کند چون string یک کلاس است
        container.Item = "سلام";
        Console.WriteLine(container.Item); // خروجی: سلام

        // MyContainer<int> error = new MyContainer<int>(); // خطا! چون int یک کلاس نیست
    }
}

در اینجا، where T : class تضمین می‌کند که T فقط می‌تواند یک نوع مرجع (مثل string یا کلاس‌های دیگر) باشد، نه یک نوع مقدار (مثل int).

جنریک‌ها در متدها

علاوه بر کلاس‌ها، می‌توانید متدهای جنریک هم بنویسید. مثلاً:

public class Helper
{
    public void Swap<T>(ref T a, ref T b)
    {
        T temp = a;
        a = b;
        b = temp;
    }
}

class Program
{
    static void Main()
    {
        int x = 5, y = 10;
        Helper helper = new Helper();
        helper.Swap(ref x, ref y);
        Console.WriteLine($"x: {x}, y: {y}"); // خروجی: x: 10, y: 5

        string s1 = "سلام", s2 = "خداحافظ";
        helper.Swap(ref s1, ref s2);
        Console.WriteLine($"s1: {s1}, s2: {s2}"); // خروجی: s1: خداحافظ, s2: سلام
    }
}

این متد جنریک می‌تواند دو مقدار از هر نوع را جابه‌جا کند.

جنریک‌ها در عمل: مجموعه‌های .NET

در برنامه‌نویسی پیشرفته در C# با .NET، جنریک‌ها به‌صورت گسترده در مجموعه‌هایی مثل List<T>، Dictionary<TKey, TValue> و Queue<T> استفاده می‌شوند. مثلاً:

List<int> numbers = new List<int>();
numbers.Add(42);
Console.WriteLine(numbers[0]); // خروجی: 42

این مجموعه‌ها از قبل توسط .NET پیاده‌سازی شده‌اند و نیازی به نوشتن دوباره آن‌ها ندارید، اما درک جنریک‌ها به شما کمک می‌کند تا از آن‌ها بهتر استفاده کنید یا حتی در صورت نیاز نسخه‌های سفارشی خودتان را بسازید.جنریک‌ها نه‌تنها کدنویسی را ساده‌تر می‌کنند، بلکه به شما اجازه می‌دهند پروژه‌های بزرگ‌تر و پیچیده‌تری را با اطمینان بیشتری مدیریت کنید. با تمرین و استفاده از آن‌ها در پروژه‌های واقعی، به‌سرعت متوجه قدرتشان در برنامه‌نویسی پیشرفته در C# با .NET خواهید شد!

دلیگیت‌ها و رویدادها (Delegates and Events)

دلیگیت‌ها و رویدادها از ابزارهای کلیدی در برنامه‌نویسی پیشرفته در C# با .NET هستند که به شما اجازه می‌دهند برنامه‌های پویا، انعطاف‌پذیر و تعاملی بسازید. این دو مفهوم به‌ویژه در سناریوهایی مثل مدیریت رفتارهای کاربر، ارتباطات بین اشیاء، یا اجرای عملیات به‌صورت غیرمستقیم بسیار مفیدند. بیایید این مفاهیم را با جزئیات بیشتری بررسی کنیم و ببینیم چگونه می‌توانند به بهبود طراحی کد شما کمک کنند.

دلیگیت چیست؟

دلیگیت (Delegate) در واقع یک “اشاره‌گر امن به متد” است که به شما امکان می‌دهد متدها را به‌عنوان اشیاء در نظر بگیرید و آن‌ها را در کد منتقل یا فراخوانی کنید. این ویژگی از نظر مفهومی شبیه به اشاره‌گرهای تابع (Function Pointers) در زبان‌هایی مثل C یا C++ است، اما با تفاوت‌های مهمی: دلیگیت‌ها در C# شیءگرا، نوع-ایمن (Type-Safe) و کاملاً در اکوسیستم .NET ادغام شده‌اند.

به زبان ساده، دلیگیت مثل یک قرارداد است که مشخص می‌کند متدی که به آن اشاره می‌کند باید چه شکل و شمایلی (تعداد پارامترها، نوع بازگشتی و غیره) داشته باشد. وقتی یک دلیگیت تعریف می‌کنید، می‌توانید هر متدی که با این قرارداد سازگار باشد را به آن متصل کنید و بعداً آن را فراخوانی کنید.

تعریف و استفاده از دلیگیت

برای تعریف یک دلیگیت، از کلمه کلیدی delegate استفاده می‌کنیم. بیایید یک مثال ساده‌تر و سپس یک مثال پیشرفته‌تر ببینیم:

public delegate void SayHello(string name);

class Program
{
    static void Main()
    {
        SayHello hello = Greet; // اتصال متد به دلیگیت (بدون new هم کار می‌کند)
        hello("مریم"); // فراخوانی دلیگیت
        // خروجی: سلام مریم!
    }

    static void Greet(string name)
    {
        Console.WriteLine($"سلام {name}!");
    }
}

در اینجا، SayHello یک دلیگیت است که به متدهایی اشاره می‌کند که یک پارامتر string می‌گیرند و چیزی برنمی‌گردانند (void). متد Greet با این امضا سازگار است، پس می‌توانیم آن را به دلیگیت متصل کنیم.

دلیگیت‌های چندپخشی (Multicast Delegates)

یکی از ویژگی‌های جذاب دلیگیت‌ها این است که می‌توانند به چندین متد همزمان اشاره کنند. این یعنی با یک فراخوانی، همه متدهای متصل‌شده اجرا می‌شوند:

public delegate void Notify(string message);

class Program
{
    static void Main()
    {
        Notify notifier = ShowMessage;
        notifier += LogMessage; // اضافه کردن متد دوم
        notifier("هشدار جدید"); // هر دو متد اجرا می‌شوند
        // خروجی:
        // پیام: هشدار جدید
        // لاگ: هشدار جدید
    }

    static void ShowMessage(string msg)
    {
        Console.WriteLine($"پیام: {msg}");
    }

    static void LogMessage(string msg)
    {
        Console.WriteLine($"لاگ: {msg}");
    }
}

در این مثال، علامت += یک متد دیگر را به دلیگیت اضافه می‌کند و وقتی notifier فراخوانی می‌شود، هر دو متد به ترتیب اجرا می‌شوند. این ویژگی به‌خصوص در سناریوهایی مثل اطلاع‌رسانی یا مدیریت چندین عملیات مفید است.

دلیگیت‌های آماده در .NET

.NET خودش دلیگیت‌های آماده‌ای مثل Action و Func ارائه می‌دهد که نیازی به تعریف دستی ندارند:

Action: برای متدهای بدون مقدار بازگشتی (مثلاً Action<string> مثل مثال بالا).
Func: برای متدهایی که مقدار برمی‌گردانند (مثلاً Func<int, int, int> برای متدی که دو عدد می‌گیرد و یک عدد برمی‌گرداند).
مثال با Func:

Func<int, int, int> add = (a, b) => a + b;
Console.WriteLine(add(3, 5)); // خروجی: 8

رویدادها چیست؟

رویدادها (Events) در واقع یک لایه بالاتر از دلیگیت‌ها هستند و برای ایجاد یک مکانیزم “ناشر-مشترک” (Publisher-Subscriber) استفاده می‌شوند. به زبان ساده، رویدادها به یک شیء اجازه می‌دهند به اشیاء دیگر بگوید: “چیزی اتفاق افتاد، اگر علاقه دارید واکنش نشان دهید!” این مفهوم به‌ویژه در برنامه‌نویسی رابط کاربری (مثل دکمه‌ها در فرم‌ها) یا سیستم‌های تعاملی بسیار پرکاربرد است.

رویدادها بر پایه دلیگیت‌ها ساخته می‌شوند و از کلمه کلیدی event برای تعریف آن‌ها استفاده می‌کنیم. این کلمه کلیدی دسترسی به دلیگیت را محدود می‌کند تا فقط شیء ناشر بتواند آن را فراخوانی کند.

مثال ساده رویداد

بیایید یک دکمه ساده را شبیه‌سازی کنیم:

public class Button
{
    public event Action OnClick; // تعریف رویداد با استفاده از Action

    public void Click()
    {
        if (OnClick != null) // بررسی اینکه مشترکی وجود دارد یا نه
            OnClick(); // فراخوانی رویداد
    }
}

class Program
{
    static void Main()
    {
        Button btn = new Button();
        btn.OnClick += () => Console.WriteLine("دکمه کلیک شد!"); // اشتراک در رویداد
        btn.Click(); // شبیه‌سازی کلیک
        // خروجی: دکمه کلیک شد!
    }
}

در اینجا:

OnClick یک رویداد از نوع Action است (یعنی متدی بدون پارامتر و بدون مقدار بازگشتی را قبول می‌کند).
با += یک متد (در اینجا یک عبارت لامبدا) به رویداد اضافه می‌شود.
متد Click رویداد را فعال می‌کند و تمام متدهای متصل‌شده اجرا می‌شوند.

رویداد با پارامتر

معمولاً رویدادها اطلاعاتی درباره آنچه اتفاق افتاده همراه خود دارند. برای این کار از کلاس EventArgs یا یک کلاس سفارشی استفاده می‌کنیم:

public class ButtonEventArgs : EventArgs
{
    public string ClickTime { get; }

    public ButtonEventArgs(string time)
    {
        ClickTime = time;
    }
}

public class Button
{
    public event EventHandler<ButtonEventArgs> OnClick;

    public void Click()
    {
        OnClick?.Invoke(this, new ButtonEventArgs(DateTime.Now.ToString()));
    }
}

class Program
{
    static void Main()
    {
        Button btn = new Button();
        btn.OnClick += (sender, args) => 
            Console.WriteLine($"کلیک در: {args.ClickTime}");
        btn.Click();
        // خروجی مثال: کلیک در: 2/25/2025 12:00:00 PM
    }
}

در این مثال:

EventHandler<T> یک دلیگیت آماده در .NET است که دو پارامتر می‌گیرد: sender (شیء فرستنده) و args (اطلاعات رویداد).
?.Invoke یک روش مدرن و ایمن برای فراخوانی رویداد است که جایگزین بررسی null می‌شود.

چرا از رویدادها استفاده کنیم؟

رویدادها به شما کمک می‌کنند کدتان را “جداگانه” (Decoupled) نگه دارید. مثلاً در یک برنامه واقعی، شیء Button نیازی به دانستن اینکه چه کسی یا چه چیزی به کلیک آن واکنش نشان می‌دهد ندارد؛ فقط اعلام می‌کند که کلیک شده و بقیه کار را به مشترکین می‌سپارد.

تفاوت دلیگیت و رویداد

دلیگیت: یک نوع پایه‌ای است که به متدها اشاره می‌کند و می‌تواند مستقیماً فراخوانی شود.
رویداد: یک دلیگیت محدودشده است که فقط توسط شیء تعریف‌کننده‌اش فراخوانی می‌شود و برای اشتراک و لغو اشتراک (+= و -=) طراحی شده.

مثال تفاوت:

public delegate void MyDelegate();
public class Test
{
    public MyDelegate MyDel = () => Console.WriteLine("دلیگیت");
    public event MyDelegate MyEvent = () => Console.WriteLine("رویداد");
}

class Program
{
    static void Main()
    {
        Test test = new Test();
        test.MyDel(); // مستقیماً فراخوانی می‌شود
        // test.MyEvent(); // خطا! رویداد را نمی‌توان مستقیماً فراخوانی کرد
        test.MyEvent?.Invoke(); // فقط از داخل کلاس Test ممکن است
    }
}

کاربردها در دنیای واقعی

دلیگیت‌ها و رویدادها در برنامه‌نویسی پیشرفته در C# با .NET در جاهای زیادی استفاده می‌شوند:

رابط کاربری: مثل کلیک دکمه‌ها در WPF یا WinForms.
برنامه‌نویسی ناهمزمان: برای Callbackها قبل از معرفی async/await.
الگوهای طراحی: مثل Observer Pattern که رویدادها ستون اصلی آن هستند.

دلیگیت‌ها و رویدادها ابزارهایی هستند که انعطاف‌پذیری و قدرت زیادی به کد شما اضافه می‌کنند. با تسلط بر آن‌ها، می‌توانید رفتار برنامه را به‌صورت پویا مدیریت کنید و کدهایی بنویسید که هم خوانا باشند و هم برای پروژه‌های بزرگ مقیاس‌پذیر. در ادامه مسیر یادگیری برنامه‌نویسی پیشرفته در C# با .NET، تمرین این مفاهیم با پروژه‌های واقعی به شما کمک زیادی خواهد کرد!

عبارات لامبدا (Lambda Expressions)

عبارات لامبدا (Lambda Expressions) یکی از ویژگی‌های جذاب و قدرتمند در برنامه‌نویسی پیشرفته در C# با .NET هستند که به شما اجازه می‌دهند کدهای کوتاه، خوانا و کاربردی بنویسید. این عبارات که در نسخه 3.0 از C# معرفی شدند، به‌ویژه در ترکیب با دلیگیت‌ها، LINQ و برنامه‌نویسی تابعی (Functional Programming) بسیار مفیدند. اگر تازه‌کار هستید، ممکن است در نگاه اول عجیب به نظر برسند، اما با کمی تمرین، به یکی از ابزارهای موردعلاقه‌تان در کدنویسی تبدیل می‌شوند.

عبارات لامبدا چیست؟

به زبان ساده، عبارت لامبدا یک روش فشرده برای تعریف متدهای ناشناس (Anonymous Methods) است. به جای اینکه یک متد کامل با نام و ساختار رسمی بنویسید، می‌توانید منطق آن را مستقیماً در جایی که نیاز دارید تعریف کنید. این کار هم کد را کوتاه‌تر می‌کند و هم خوانایی آن را (در صورت استفاده درست) افزایش می‌دهد.

ساختار اصلی یک عبارت لامبدا به این شکل است:

(پارامترها) => عملیات یا خروجی

(پارامترها): ورودی‌هایی که متد شما نیاز دارد.
=>: علامت لامبدا که پارامترها را از بدنه جدا می‌کند (به آن “می‌رود به” هم می‌گویند).
عملیات یا خروجی: کدی که اجرا می‌شود یا مقداری که برگردانده می‌شود.

چرا از عبارات لامبدا استفاده کنیم؟

قبل از معرفی عبارات لامبدا، برای کارهایی مثل فیلتر کردن یک لیست یا تعریف یک Callback، باید یک متد جداگانه بنویسید یا از متدهای ناشناس با ساختار پیچیده‌تر استفاده می‌کردید. لامبدا این فرایند را ساده‌تر می‌کند. بیایید این را با یک مقایسه ببینیم:

بدون لامبدا:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };

bool IsEven(int x)
{
    return x % 2 == 0;
}

var evenNumbers = numbers.FindAll(IsEven);
foreach (var num in evenNumbers)
{
    Console.WriteLine(num); // خروجی: 2, 4
}

با لامبدا:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
var evenNumbers = numbers.FindAll(x => x % 2 == 0);
foreach (var num in evenNumbers)
{
    Console.WriteLine(num); // خروجی: 2, 4
}

در نسخه لامبدا، نیازی به تعریف متد جداگانه نیست و منطق مستقیماً در همان خط نوشته شده. این کار هم زمان شما را صرفه‌جویی می‌کند و هم کد را متمرکزتر نگه می‌دارد.

انواع عبارات لامبدا

عبارات لامبدا می‌توانند به دو شکل اصلی باشند:

عبارت تک‌خطی (Expression Lambda): وقتی فقط یک عبارت ساده دارید که نتیجه را برمی‌گرداند.
مثال: x => x * 2 (عدد را دو برابر می‌کند).
بلوک دستورات (Statement Lambda): وقتی نیاز به چند خط کد دارید و از {} استفاده می‌کنید.
مثال: (x) => { Console.WriteLine(x); return x * 2; }.
مثال بلوک دستورات:

List<string> names = new List<string> { "علی", "مریم", "رضا" };
names.ForEach(name =>
{
    Console.WriteLine($"سلام {name}!");
    Console.WriteLine("خوش آمدی!");
});
// خروجی:
// سلام علی!
// خوش آمدی!
// سلام مریم!
// خوش آمدی!
// سلام رضا!
// خوش آمدی!

ترکیب با دلیگیت‌ها

عبارات لامبدا معمولاً با دلیگیت‌ها استفاده می‌شوند، چون دلیگیت‌ها به متدهایی اشاره می‌کنند و لامبدا راهی سریع برای تعریف این متدها ارائه می‌دهد. مثلاً:

public delegate int Calculate(int a, int b);

class Program
{
    static void Main()
    {
        Calculate calc = (a, b) => a + b; // لامبدا به‌عنوان دلیگیت
        Console.WriteLine(calc(3, 7)); // خروجی: 10
    }
}

اینجا، به جای تعریف یک متد جداگانه برای جمع، از لامبدا استفاده کردیم. همین سادگی باعث شده که لامبدا در برنامه‌نویسی پیشرفته در C# با .NET بسیار محبوب شود.

استفاده از Action و Func

.NET دو نوع دلیگیت آماده به نام‌های Action و Func دارد که با لامبدا بسیار خوب کار می‌کنند:

Action: برای متدهای بدون خروجی (تا 16 پارامتر).
Func: برای متدهایی که خروجی دارند (تا 16 پارامتر ورودی و یک خروجی).
مثال با Action:

Action<string> greet = message => Console.WriteLine($"پیام: {message}");
greet("سلام دنیا"); // خروجی: پیام: سلام دنیا

مثال با Func:

Func<int, int, string> formatSum = (x, y) => $"جمع: {x + y}";
Console.WriteLine(formatSum(5, 3)); // خروجی: جمع: 8

لامبدا و LINQ

یکی از مهم‌ترین کاربردهای عبارات لامبدا در برنامه‌نویسی پیشرفته در C# با .NET، استفاده در LINQ است. LINQ از لامبدا برای فیلتر کردن، مرتب‌سازی و تبدیل داده‌ها استفاده می‌کند. بیایید یک مثال پیشرفته‌تر ببینیم:

List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6 };
var result = numbers
    .Where(x => x > 3)        // فقط اعداد بزرگ‌تر از 3
    .Select(x => x * x)       // مربع اعداد
    .OrderBy(x => x);         // مرتب‌سازی صعودی
foreach (var num in result)
{
    Console.WriteLine(num); // خروجی: 16, 25, 36
}

در این کد:

x => x > 3 شرط فیلتر را مشخص می‌کند.
x => x * x هر عدد را به مربعش تبدیل می‌کند.
x => x برای مرتب‌سازی استفاده شده.
این ترکیب قدرت لامبدا و LINQ را نشان می‌دهد و چطور می‌توانید با چند خط کد، عملیات پیچیده روی داده‌ها انجام دهید.

متغیرهای خارجی (Closure)

یکی از ویژگی‌های جالب لامبدا این است که می‌تواند به متغیرهای خارج از خودش دسترسی داشته باشد. به این رفتار “Closure” می‌گویند:

int factor = 5;
Func<int, int> multiply = x => x * factor;
Console.WriteLine(multiply(3)); // خروجی: 15

factor = 10;
Console.WriteLine(multiply(3)); // خروجی: 30

در این مثال، لامبدا به factor دسترسی دارد و با تغییر آن، نتیجه هم تغییر می‌کند. این ویژگی در سناریوهای پویا خیلی کاربردی است، ولی باید مراقب باشید که تغییرات ناخواسته ایجاد نشود.

نکات مهم در استفاده از لامبدا

خوانایی: لامبدا عالی است، اما اگر بیش از حد پیچیده شود (مثلاً چند خط کد با منطق سنگین)، بهتر است از متد معمولی استفاده کنید.
عملکرد: لامبدا معمولاً همان عملکرد متدهای معمولی را دارد، چون در نهایت به کد IL (Intermediate Language) کامپایل می‌شود.
محدودیت: لامبدا نمی‌تواند جایگزین هر متدی شود (مثلاً متدهایی که نیاز به ref یا out دارند کمی پیچیده‌ترند).

مثال دنیای واقعی

فرض کنید یک برنامه دارید که باید لیستی از محصولات را بر اساس قیمت فیلتر کند:

class Product
{
    public string Name { get; set; }
    public double Price { get; set; }
}

class Program
{
    static void Main()
    {
        List<Product> products = new List<Product>
        {
            new Product { Name = "کتاب", Price = 50 },
            new Product { Name = "مداد", Price = 10 },
            new Product { Name = "لپ‌تاپ", Price = 2000 }
        };

        var expensive = products.Where(p => p.Price > 100);
        foreach (var p in expensive)
        {
            Console.WriteLine($"{p.Name}: {p.Price}"); // خروجی: لپ‌تاپ: 2000
        }
    }
}

اینجا لامبدا به ما کمک کرد بدون نوشتن متد جداگانه، محصولات گران‌تر از 100 را پیدا کنیم.
عبارات لامبدا در برنامه‌نویسی پیشرفته در C# با .NET مانند یک چاقوی سوئیسی عمل می‌کنند: کوچک، ساده و در عین حال قدرتمند. چه در کار با دلیگیت‌ها، چه در LINQ یا حتی در تعریف منطق سریع، لامبدا به شما کمک می‌کند کدی تمیزتر و کارآمدتر بنویسید. با تمرین و استفاده از آن‌ها در پروژه‌های واقعی، به‌سرعت به ابزاری ضروری در جعبه‌ابزار برنامه‌نویسی‌تان تبدیل می‌شوند!

LINQ (Language Integrated Query)

LINQ که مخفف “Language Integrated Query” است، یکی از قدرتمندترین و جذاب‌ترین ابزارها در برنامه‌نویسی پیشرفته در C# با .NET به شمار می‌رود. این ویژگی که در نسخه 3.0 از C# معرفی شد، به شما اجازه می‌دهد به‌راحتی و با زبانی نزدیک به SQL، پرس‌وجوهایی (Query) روی داده‌های مختلف انجام دهید، بدون اینکه نیازی به ترک محیط کدنویسی C# داشته باشید. چه با لیست‌های ساده در حافظه کار کنید، چه با پایگاه داده‌ها یا فایل‌های XML، LINQ راهی یکپارچه و خوانا برای مدیریت داده‌ها ارائه می‌دهد.

LINQ چیست؟

LINQ ابزاری است که قابلیت پرس‌وجو را مستقیماً به زبان C# اضافه می‌کند. به زبان ساده، LINQ به شما اجازه می‌دهد داده‌ها را فیلتر کنید، مرتب کنید، گروه‌بندی کنید یا به شکل‌های مختلف تبدیل کنید، درست مثل کاری که در یک پایگاه داده با دستورات SQL انجام می‌دهید. اما تفاوت بزرگ اینجاست که LINQ این کار را با синтакس (سینتکس) آشنا و طبیعی در C# انجام می‌دهد و به لطف یکپارچگی با این زبان، از مزایایی مثل بررسی نوع (Type Checking) در زمان کامپایل بهره‌مند می‌شوید.

LINQ می‌تواند با منابع مختلفی کار کند:

حافظه (In-Memory): مثل لیست‌ها و آرایه‌ها (LINQ to Objects).
پایگاه داده: مثل SQL Server (LINQ to SQL یا Entity Framework).
XML: برای کار با داده‌های ساختاریافته (LINQ to XML).
منابع دیگر: مثل JSON یا فایل‌ها با افزونه‌های خاص.

دو سبک نوشتاری LINQ

LINQ دو روش اصلی برای نوشتن پرس‌وجوها ارائه می‌دهد:

سینتکس پرس‌وجو (Query Syntax): شبیه به SQL و خواناتر برای مبتدیان.
سینتکس متد (Method Syntax): مبتنی بر متدهای زنجیره‌ای و عبارات لامبدا، که حرفه‌ای‌تر و انعطاف‌پذیرتر است.

مثال با هر دو سبک

فرض کنید لیستی از نام‌ها داریم و می‌خواهیم نام‌هایی که طولشان کمتر یا مساوی 4 کاراکتر است را پیدا کنیم:

سبک پرس‌وجو:

List<string> names = new List<string> { "علی", "مریم", "رضا", "سارا" };
var shortNames = from name in names
                 where name.Length <= 4
                 select name;

foreach (var name in shortNames)
{
    Console.WriteLine(name); // خروجی: علی, رضا, سارا
}

سبک متد:

List<string> names = new List<string> { "علی", "مریم", "رضا", "سارا" };
var shortNames = names.Where(n => n.Length <= 4);

foreach (var name in shortNames)
{
    Console.WriteLine(name); // خروجی: علی, رضا, سارا
}

هر دو روش نتیجه یکسانی دارند، اما سبک متد معمولاً در پروژه‌های پیشرفته‌تر رایج‌تر است چون با عبارات لامبدا ترکیب بهتری دارد و برای عملیات پیچیده‌تر مناسب‌تر است.

اجزای اصلی LINQ

برای درک بهتر LINQ، بیایید اجزای کلیدی آن را در پرس‌وجوی بالا بشکنیم:

from … in: منبع داده را مشخص می‌کند (مثل names).
where: شرط فیلتر کردن داده‌ها را تعیین می‌کند (مثل Length <= 4).
select: مشخص می‌کند چه چیزی از داده‌ها برگردانده شود (در اینجا خود name).
این ساختار به شما اجازه می‌دهد پرس‌وجوهای پیچیده‌تری هم بسازید، مثل مرتب‌سازی یا گروه‌بندی.

متدهای اصلی LINQ

LINQ مجموعه‌ای از متدهای آماده دارد که در سبک متد استفاده می‌شوند. برخی از پرکاربردترین‌ها عبارتند از:

Where: برای فیلتر کردن داده‌ها.

var numbers = new List<int> { 1, 2, 3, 4, 5 };
var bigNumbers = numbers.Where(n => n > 3); // خروجی: 4, 5

Select: برای تبدیل یا انتخاب داده‌ها.

var squares = numbers.Select(n => n * n); // خروجی: 1, 4, 9, 16, 25

OrderBy: برای مرتب‌سازی.

var sortedNames = names.OrderBy(n => n.Length); // خروجی: علی, رضا, سارا, مریم

GroupBy: برای گروه‌بندی داده‌ها.

var groupedByLength = names.GroupBy(n => n.Length);
foreach (var group in groupedByLength)
{
    Console.WriteLine($"طول {group.Key}:");
    foreach (var name in group)
        Console.WriteLine(name);
}
// خروجی:
// طول 3:
// علی
// رضا
// سارا
// طول 4:
// مریم

First, Last, Single: برای گرفتن یک عنصر خاص

var firstBig = numbers.First(n => n > 3); // خروجی: 4

مثال پیشرفته‌تر

فرض کنید یک لیست از اشیاء پیچیده داریم و می‌خواهیم عملیات ترکیبی انجام دهیم:

class Student
{
    public string Name { get; set; }
    public int Score { get; set; }
}

class Program
{
    static void Main()
    {
        List<Student> students = new List<Student>
        {
            new Student { Name = "علی", Score = 85 },
            new Student { Name = "مریم", Score = 92 },
            new Student { Name = "رضا", Score = 78 }
        };

        var topStudents = from s in students
                         where s.Score > 80
                         orderby s.Score descending
                         select new { s.Name, Grade = s.Score * 1.1 };

        foreach (var student in topStudents)
        {
            Console.WriteLine($"{student.Name}: {student.Grade}");
        }
        // خروجی:
        // مریم: 101.2
        // علی: 93.5
    }
}

در این مثال:

داده‌ها را بر اساس نمره فیلتر کردیم (where s.Score > 80).
آن‌ها را نزولی مرتب کردیم (orderby s.Score descending).
یک شیء جدید ساختیم که نمره را 10٪ افزایش داده (select new).

مزایای LINQ

خوانایی: کد شما شبیه به زبان طبیعی یا SQL می‌شود که درک آن را آسان‌تر می‌کند.
انعطاف‌پذیری: با هر نوع مجموعه داده‌ای (حتی Lazy-Loaded) کار می‌کند.
ایمنی نوع: چون در C# نوشته می‌شود، کامپایلر خطاها را قبل از اجرا پیدا می‌کند.
عملکرد بهینه: LINQ از اجرای تاخیری (Deferred Execution) استفاده می‌کند، یعنی پرس‌وجو تا زمانی که واقعاً به داده نیاز نداشته باشید اجرا نمی‌شود.

اجرای تاخیری چیست؟

مثال:

var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1); // هنوز اجرا نشده
numbers.Add(4); // تغییر لیست
foreach (var n in query)
{
    Console.WriteLine(n); // خروجی: 2, 3, 4
}

پرس‌وجو فقط وقتی foreach اجرا می‌شود، انجام می‌شود و تغییرات لیست را هم در نظر می‌گیرد.

LINQ و پایگاه داده

اگر با Entity Framework کار کنید، LINQ به شما اجازه می‌دهد پرس‌وجوهای پایگاه داده را مستقیماً در کد C# بنویسید:

using (var context = new MyDbContext())
{
    var recentOrders = from order in context.Orders
                       where order.Date > DateTime.Now.AddDays(-7)
                       select order;
    foreach (var order in recentOrders)
    {
        Console.WriteLine(order.Id);
    }
}

اینجا LINQ به SQL ترجمه می‌شود و مستقیماً روی دیتابیس اجرا می‌شود.

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

پیچیدگی بیش از حد: پرس‌وجوهای خیلی پیچیده ممکن است خواندن کد را سخت کنند؛ در این موارد، منطق را به چند بخش تقسیم کنید.
عملکرد: برای داده‌های کوچک، LINQ عالی است، اما در مجموعه‌های خیلی بزرگ، باید مراقب مصرف منابع باشید.
یادگیری اولیه: اگر با SQL یا برنامه‌نویسی تابعی آشنا نباشید، ممکن است کمی زمان ببرد تا به آن عادت کنید.

LINQ در برنامه‌نویسی پیشرفته در C# با .NET مانند یک جادوی کوچک عمل می‌کند: با چند خط کد، می‌توانید عملیات پیچیده روی داده‌ها انجام دهید، بدون اینکه درگیر حلقه‌های دستی یا منطق‌های طولانی شوید. چه بخواهید یک لیست را فیلتر کنید، چه داده‌های یک دیتابیس را تحلیل کنید، LINQ ابزاری است که هم کارتان را ساده می‌کند و هم حرفه‌ای‌تر نشان می‌دهد. با تمرین و استفاده از آن در پروژه‌های واقعی، به‌سرعت به قدرت و انعطاف‌پذیری‌اش پی خواهید برد!

برنامه‌نویسی ناهمزمان با async و await

برنامه‌نویسی ناهمزمان با async و await یکی از مهم‌ترین و کاربردی‌ترین قابلیت‌ها در برنامه‌نویسی پیشرفته در C# با .NET است که به شما امکان می‌دهد برنامه‌هایی پاسخ‌گو، مقیاس‌پذیر و کارآمد بسازید. این ویژگی که در نسخه 5.0 از C# معرفی شد، به‌ویژه برای کارهایی مثل دسترسی به شبکه، عملیات ورودی/خروجی (I/O) یا پردازش‌های زمان‌بر بسیار مناسب است. اگر تا به حال با برنامه‌هایی روبه‌رو شده‌اید که هنگام انجام یک کار سنگین “فریز” می‌شوند، ناهمزمانی راه‌حل شماست!

برنامه‌نویسی ناهمزمان چیست؟

در برنامه‌نویسی معمولی (همزمان یا Synchronous)، کدها به ترتیب اجرا می‌شوند و هر عملیات باید منتظر اتمام عملیات قبلی بماند. مثلاً اگر بخواهید یک فایل را از اینترنت دانلود کنید، تا وقتی دانلود تمام نشود، بقیه برنامه متوقف می‌ماند. اما در برنامه‌نویسی ناهمزمان (Asynchronous)، می‌توانید به برنامه بگویید که عملیات زمان‌بر را در پس‌زمینه انجام دهد و همزمان کارهای دیگری را ادامه دهید. async و await این فرایند را به شکلی ساده و قابل فهم پیاده‌سازی می‌کنند.

چرا ناهمزمانی مهم است؟

بیایید یک سناریوی واقعی را تصور کنیم: شما در حال ساخت یک برنامه دسکتاپ هستید و کاربر روی دکمه “دانلود” کلیک می‌کند. بدون ناهمزمانی:

رابط کاربری (UI) تا پایان دانلود قفل می‌شود.
کاربر نمی‌تواند هیچ کار دیگری انجام دهد و تجربه بدی خواهد داشت.

با ناهمزمانی:

دانلود در پس‌زمینه شروع می‌شود.
رابط کاربری همچنان پاسخ‌گو می‌ماند و کاربر می‌تواند کارهای دیگری مثل کلیک روی دکمه‌های دیگر انجام دهد.
این تفاوت در برنامه‌های وب، موبایل یا حتی سرورها هم صدق می‌کند، چون ناهمزمانی منابع را بهینه‌تر استفاده می‌کند و عملکرد کلی را بهبود می‌بخشد.

مفاهیم اصلی: async و await

async: این کلمه کلیدی به یک متد می‌گوید که می‌تواند عملیات ناهمزمان انجام دهد و معمولاً باید یک Task یا Task<T> برگرداند.
await: این کلمه کلیدی درون متدهای async استفاده می‌شود و به برنامه می‌گوید که منتظر اتمام یک عملیات ناهمزمان بماند، بدون اینکه رشته (Thread) اصلی را بلاک کند.

ساختار کلی:

public async Task DoSomethingAsync()
{
    Console.WriteLine("شروع...");
    await Task.Delay(1000); // شبیه‌سازی عملیات زمان‌بر
    Console.WriteLine("پایان!");
}

مثال ساده و توضیح خط‌به‌خط

بیایید مثال اولیه را گسترش دهیم و جزئیاتش را بررسی کنیم:

public class Program
{
    public async Task DownloadFileAsync()
    {
        Console.WriteLine("دانلود شروع شد...");
        await Task.Delay(2000); // شبیه‌سازی دانلود (2 ثانیه تاخیر)
        Console.WriteLine("دانلود تمام شد!");
    }

    static async Task Main()
    {
        Program p = new Program();
        Console.WriteLine("برنامه شروع شد");
        await p.DownloadFileAsync();
        Console.WriteLine("کار بعدی...");
    }
}
// خروجی:
// برنامه شروع شد
// دانلود شروع شد...
// (2 ثانیه صبر)
// دانلود تمام شد!
// کار بعدی...

توضیح:

async Task: متد DownloadFileAsync با async مشخص شده و یک Task برمی‌گرداند، یعنی یک عملیات ناهمزمان است.
await Task.Delay(2000): اینجا برنامه 2 ثانیه صبر می‌کند، اما رشته اصلی (Main Thread) بلاک نمی‌شود. در یک برنامه واقعی، به جای Task.Delay ممکن است از متدی مثل HttpClient.GetAsync برای دانلود استفاده کنید.
جریان اجرا: وقتی await اجرا می‌شود، کنترل به متد فراخواننده (یعنی Main) برمی‌گردد تا وقتی عملیات تمام شود. بعد از اتمام، ادامه کد اجرا می‌شود.

تفاوت با و بدون ناهمزمانی

بدون ناهمزمانی:

public void DownloadFileSync()
{
    Console.WriteLine("دانلود شروع شد...");
    Thread.Sleep(2000); // شبیه‌سازی با بلاک کردن رشته
    Console.WriteLine("دانلود تمام شد!");
}

static void Main()
{
    Console.WriteLine("برنامه شروع شد");
    new Program().DownloadFileSync();
    Console.WriteLine("کار بعدی...");
}
// خروجی:
// برنامه شروع شد
// دانلود شروع شد...
// (2 ثانیه صبر - برنامه متوقف است)
// دانلود تمام شد!
// کار بعدی...

در اینجا Thread.Sleep رشته اصلی را بلاک می‌کند و تا 2 ثانیه هیچ کاری نمی‌توانید انجام دهید. اما با async/await، برنامه پاسخ‌گو باقی می‌ماند.

مثال واقعی: دانلود از وب

بیایید یک مثال کاربردی‌تر با HttpClient ببینیم:

using System.Net.Http;

class Program
{
    static readonly HttpClient client = new HttpClient();

    public async Task<string> DownloadWebPageAsync(string url)
    {
        Console.WriteLine("در حال دانلود...");
        string content = await client.GetStringAsync(url);
        Console.WriteLine("دانلود完成了!");
        return content;
    }

    static async Task Main()
    {
        Program p = new Program();
        Console.WriteLine("شروع برنامه");
        string result = await p.DownloadWebPageAsync("https://example.com");
        Console.WriteLine($"طول محتوا: {result.Length} کاراکتر");
    }
}
// خروجی نمونه:
// شروع برنامه
// در حال دانلود...
// (زمان واقعی دانلود)
// دانلود完成了!
// طول محتوا: 1256 کاراکتر (بسته به صفحه)

در این کد:

GetStringAsync یک متد ناهمزمان است که محتوای یک صفحه وب را دانلود می‌کند.
await منتظر اتمام دانلود می‌ماند، اما برنامه در این مدت آزاد است.

مدیریت چندین عملیات ناهمزمان

اگر چند کار ناهمزمان دارید، می‌توانید آن‌ها را به‌صورت موازی اجرا کنید:

public async Task<int> Task1Async()
{
    await Task.Delay(1000);
    return 1;
}

public async Task<int> Task2Async()
{
    await Task.Delay(2000);
    return 2;
}

static async Task Main()
{
    Console.WriteLine("شروع");
    Task<int> t1 = new Program().Task1Async();
    Task<int> t2 = new Program().Task2Async();
    int[] results = await Task.WhenAll(t1, t2); // صبر برای همه
    Console.WriteLine($"نتایج: {results[0]}, {results[1]}");
    // خروجی:
    // شروع
    // (2 ثانیه صبر - چون Task2 طولانی‌تر است)
    // نتایج: 1, 2
}

Task.WhenAll همه وظایف را موازی اجرا می‌کند و وقتی همه تمام شدند، نتایج را برمی‌گرداند. این برای سناریوهایی مثل دانلود چندین فایل یا فراخوانی APIها عالی است.

نکات پیشرفته

لغو عملیات (Cancellation): می‌توانید با CancellationToken عملیات ناهمزمان را لغو کنید:

public async Task DoWorkAsync(CancellationToken token)
{
    Console.WriteLine("کار شروع شد");
    await Task.Delay(5000, token); // اگر لغو شود، خطا می‌دهد
    Console.WriteLine("کار تمام شد");
}

static async Task Main()
{
    var cts = new CancellationTokenSource(2000); // بعد از 2 ثانیه لغو
    try
    {
        await new Program().DoWorkAsync(cts.Token);
    }
    catch (TaskCanceledException)
    {
        Console.WriteLine("لغو شد!");
    }
}

خطایابی: همیشه از try-catch برای مدیریت خطاها در متدهای ناهمزمان استفاده کنید، چون خطاها در Taskها پیچیده‌تر هستند.
بازگشت مقدار: اگر متد ناهمزمان نتیجه‌ای دارد، از Task<T> استفاده کنید (مثل Task<string> در مثال دانلود).

مزایا و معایب

مزایا:
پاسخ‌گویی: رابط کاربری یا سرور شما متوقف نمی‌شود.
مقیاس‌پذیری: در برنامه‌های سرور، می‌توانید درخواست‌های بیشتری را با منابع کمتر مدیریت کنید.
سادگی: نسبت به روش‌های قدیمی مثل Thread یا Begin/End بسیار خواناتر است.
معایب:
پیچیدگی: برای مبتدیان ممکن است درک جریان اجرا کمی سخت باشد.
دیباگ کردن: ردیابی خطاها در کد ناهمزمان گاهی دشوارتر است.

برنامه‌نویسی ناهمزمان با async و await در برنامه‌نویسی پیشرفته در C# با .NET مانند یک ابرقدرت است که به شما اجازه می‌دهد برنامه‌هایی سریع‌تر، پاسخ‌گوتر و کاربرپسندتر بسازید. از دانلود فایل گرفته تا کار با APIها و حتی مدیریت وظایف سنگین، این ابزار به شما کمک می‌کند تجربه بهتری برای کاربرانتان فراهم کنید.

نتیجه‌گیری

برنامه‌نویسی پیشرفته در C# با .NET مجموعه‌ای از ابزارها و تکنیک‌های قدرتمند را در اختیار شما قرار می‌دهد که می‌تواند کدنویسی را از یک کار ساده به یک هنر حرفه‌ای تبدیل کند. جنریک‌ها به شما امکان می‌دهند کدهایی انعطاف‌پذیر و قابل استفاده مجدد بنویسید، دلیگیت‌ها و رویدادها رفتار برنامه را به‌صورت پویا مدیریت می‌کنند، عبارات لامبدا کدنویسی را کوتاه و خوانا نگه می‌دارند، LINQ کار با داده‌ها را به شکلی ساده و طبیعی ممکن می‌سازد و برنامه‌نویسی ناهمزمان با async و await برنامه‌های شما را پاسخ‌گو و کارآمد می‌کند. با تسلط بر این مفاهیم، می‌توانید پروژه‌هایی مقیاس‌پذیر و باکیفیت بسازید که هم نیازهای کاربران را برآورده کند و هم استانداردهای مدرن توسعه نرم‌افزار را رعایت کند. یادگیری و تمرین این ابزارها، کلید موفقیت شما در دنیای برنامه‌نویسی پیشرفته در C# با .NET است!

آموزش برنامه‌نویسی پیشرفته در C# با .NET

دیدگاه های شما

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *