مقدمه

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

اجزای سازنده‌ی LINQ

LINQ بر پایه‌ی چندین ویژگی زبان بنا شده است. درک نقش هر کدام از این ویژگی‌ها به شما کمک می‌کند تا نحوه‌ی کار LINQ در پشت صحنه را بهتر درک کنید.

متغیرهای محلی با نوع ضمنی (var)

کوئری‌های LINQ اغلب نتایجی را تولید می‌کنند که نوع آن‌ها پیچیده است یا حتی از یک نوع ناشناس (که در ادامه می‌بینیم) تشکیل شده است. نوشتن نام کامل این نوع‌ها می‌تواند طولانی و خسته‌کننده باشد. کلمه‌ی کلیدی var به ما اجازه می‌دهد تا تعریف متغیر را به کامپایلر بسپاریم. کامپایلر به طور خودکار نوع صحیح را استنباط می‌کند و کد ما بسیار تمیزتر و خواناتر باقی می‌ماند. تقریباً در تمام مثال‌های LINQ از var برای ذخیره‌ی نتایج کوئری استفاده می‌شود.

متدهای بسطی (Extension Methods)

این ویژگی، ستون فقرات سینتکس متدی LINQ است. تمام عملگرهای استاندارد کوئری مانند Where، Select، OrderBy و... در واقع متدهای بسطی هستند که برای اینترفیس IEnumerable<T> تعریف شده‌اند. این بدان معناست که هر کالکشنی که این اینترفیس را پیاده‌سازی کند (مانند List<T> و آرایه‌ها)، به طور خودکار به تمام این متدهای قدرتمند دسترسی پیدا می‌کند.

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

عبارات لامبدا "سوخت" لازم برای متدهای بسطی LINQ را فراهم می‌کنند. متدهایی مانند Where و Select یک نماینده (معمولاً از نوع Func) به عنوان پارامتر می‌پذیرند تا منطق فیلتر کردن یا پرتاب کردن را مشخص کنند. عبارات لامبدا مختصرترین و خواناترین راه برای ارائه‌ی این منطق به صورت درون‌خطی هستند.

Copy Icon Program.cs
int[] numbers = { 1, 2, 3, 4, 5 };
// 'Where' is an extension method that accepts a Func.
// 'n => n % 2 == 0' is a lambda expression providing the logic for that delegate.
var evenNumbers = numbers.Where(n => n % 2 == 0);

نوع‌های ناشناس (Anonymous Types)

اغلب اوقات، ما به تمام داده‌های یک شیء در نتیجه‌ی کوئری خود نیاز نداریم. انواع ناشناس به ما اجازه می‌دهند تا با استفاده از select new { ... } یک نوع داده‌ی سبک و موقتی بسازیم که فقط شامل پراپرتی‌های مورد نیاز ماست. این کار به خصوص در پرتاب (projection) داده‌ها بسیار مفید است و از ساختن کلاس‌های اضافی جلوگیری می‌کند.

ترکیب همه اجزا: یک کوئری LINQ

حالا بیایید ببینیم چگونه تمام این ویژگی‌ها در یک کوئری LINQ واقعی با هم ترکیب می‌شوند. فرض کنید لیستی از محصولات داریم و می‌خواهیم نام محصولاتی را که قیمتشان کمتر از 50 واحد است، پیدا کرده و بر اساس نام مرتب کنیم.

Copy Icon Program.cs
public record Product(string Name, decimal Price);

// A collection of objects
var products = new List<Product>
{
    new("Keyboard", 75m),
    new("Mouse", 25m),
    new("Monitor", 300m),
    new("Webcam", 45m)
};

// LINQ query combining all concepts
var cheapProductInfo = products
    .Where(p => p.Price < 50)      // Extension Method + Lambda Expression
    .OrderBy(p => p.Name)     // Extension Method + Lambda Expression
    .Select(p => new { p.Name }); // Anonymous Type in a projection

// 'var' is used to hold the result, which is IEnumerable of an anonymous type.
Console.WriteLine("Cheap products:");
foreach (var product in cheapProductInfo)
{
    Console.WriteLine(product.Name);
}

خروجی این کد به شکل زیر خواهد بود:

Cheap products:
Mouse
Webcam
                    

در این مثال واحد، ما از var برای نگهداری نتیجه، از متدهای بسطی Where، OrderBy و Select، از عبارات لامبدا برای تعریف منطق آن‌ها و از یک نوع ناشناس برای پرتاب نتیجه‌ی نهایی استفاده کرده‌ایم.

نگاهی به آینده: دو سینتکس برای LINQ

LINQ را می‌توان با دو سینتکس متفاوت نوشت: سینتکس متد (Method Syntax) که در مثال بالا دیدیم و از زنجیره‌ی متدهای بسطی استفاده می‌کند، و سینتکس کوئری (Query Syntax) که شبیه به SQL است.

// The same query in Query Syntax
var querySyntaxResult = from p in products
                            where p.Price < 50
                            orderby p.Name
                            select new { p.Name };

هر دو سینتکس در نهایت توسط کامپایلر به یک کد مشابه تبدیل می‌شوند و انتخاب بین آن‌ها اغلب به سلیقه و خوانایی بستگی دارد. در درس‌های آینده، هر دو سینتکس و عملگرهای مختلف LINQ را به تفصیل بررسی خواهیم کرد.