عنوان:

‫ساده‌سازی اتصالات LINQ بدون انتخاب‌گر نتیجه (Result Selector) در NET 11.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۵/۰۵ ۰۸:۲۰
آدرس: www.dntips.ir
چکیده
زبان پرس‌وجوی یکپارچه (LINQ) همواره یکی از ارکان اصلی افزایش بهره‌وری و خوانایی کد در اکوسیستم دات‌نت بوده است. یکی از متداول‌ترین عملیات‌ها در LINQ، اتصال داده‌ها (Join) بر اساس کلیدهای مشترک است. تا پیش از معرفی پیش‌نمایش .NET 11، متد Join توسعه‌دهندگان را ملزم می‌کرد که همواره پارامتر چهارم یا همان انتخاب‌گر نتیجه (resultSelector) را برای نگاشت خروجی مشخص کنند؛ امری که در اکثر موارد منجر به نوشتن کدهای تکراری و عبارات چندگانه (Tuples) می‌شد. در این مقاله، به بررسی بهبود جدید ارائه شده در .NET 11 برای ساده‌سازی متد Join، نحوه حذف تشریفات اضافی کد (Boilerplate)، بررسی دقیق رفتار کامپایلر و اثرات آن بر تمیزی کد می‌پردازیم.


مقدمه
توسعه‌دهندگان #C و دات‌نت سال‌هاست که از LINQ برای اجرای پرس‌وجوهای صریح، خوانا و ایمن از نظر نوع (Type-Safe) استفاده می‌کنند. عملیات Join یکی از قابلیت‌های اصلی LINQ است که امکان پیوند دادن دو مجموعه داده مجزا را بر اساس کلید یکسان فراهم می‌سازد.
با وجود کاربرد گسترده، نحو (Syntax) متد Enumerable.Join، شامل یک الزام تاریخی بود: ارائه صریح تابع انتخاب‌گر خروجی. در سناریوهایی که هدف صرفاً دریافت جفت‌های منطبق از دو مجموعه بدون تغییر ساختار اولیه بود، توسعه‌دهندگان مجبور به تعریف دستی یک Tuple واسط بودند. .NET 11 با معرفی Overloadهای جدید و هوشمندتر، این تشریفات کدنویسی را برطرف کرده است.

بررسی فنی و مقایسه نحو پیاده‌سازی

تعریف مدل‌های داده و مجموعه‌ها
برای درک بهتر تغییرات، دو کلاس ساده Product (کالا) و Order (سفارش) را در نظر بگیرید:
public class Product
{
    public required int ID { get; init; }
    public required string Name { get; init; }
}

public class Order
{
    public required int OrderID { get; init; }
    public required int ProductID { get; init; }
    public required int Quantity { get; init; }
}
سپس نمونه‌هایی از این داده‌ها را مقداردهی می‌کنیم:
Product[] products = [
    new() { ID = 1, Name = "Mango" },
    new() { ID = 2, Name = "Banana" },
    new() { ID = 3, Name = "Potato" },
    new() { ID = 4, Name = "Cabbage" }
];

Order[] orders = [
    new() { OrderID = 1, ProductID = 1, Quantity = 10 },
    new() { OrderID = 1, ProductID = 2, Quantity = 13 },
    new() { OrderID = 2, ProductID = 3, Quantity = 5 },
    new() { OrderID = 2, ProductID = 4, Quantity = 8 }
];

۱. روش سنتی (تا قبل از .NET 11)
در نسخه‌های قبلی .NET، برای اتصال این دو مجموعه بر اساس کد محصول و دریافت شیء محصول و سفارش متناظر، باید یک عبارت لامبدا اضافی به عنوان resultSelector ارسال می‌شد:
var result = products.Join(
    orders,
    product => product.ID,
    order => order.ProductID,
    (product, order) => (product, order) // انتخاب‌گر نتیجه اجباری (ValueTuple)
);
عبارت (product, order) => (product, order) هیچ کار ساختاری یا محاسباتی خاصی انجام نمی‌داد و تنها برای تبدیل خروجی به یک ValueTuple نوشته می‌شد.

۲. روش جدید و ساده‌شده در .NET 11
در .NET 11 با ارائه Overload پیش‌فرض جدید برای Join، نیازی به نگارش پارامتر چهارم نیست:
var newResult = products.Join(
    orders,
    product => product.ID,
    order => order.ProductID
);
در هر دو حالت، خروجی حاصل یک مجموعه از جفت‌های (Product product, Order order) خواهد بود و پیمایش آن دقیقاً یکسان است:
foreach (var (product, order) in newResult)
{
    Console.WriteLine($"Product: {product.Name}, Quantity: {order.Quantity}");
}
خروجی اجرا:
Product: Mango, Quantity: 10
Product: Banana, Quantity: 13
Product: Potato, Quantity: 5
Product: Cabbage, Quantity: 8

نکات تکمیلی
الف) مقایسه نحوی Query Syntax در برابر Method Syntax
در نحو پرس‌وجویی (Query Syntax)، LINQ از گذشته رفتار هوشمندانه‌ای داشت و اگر کلمه کلیدی join استفاده می‌شد، می‌توانستید خروجی دلخواه را انتخاب کنید. اما در Method Syntax الزام وجود پارامتر چهارم همواره حس می‌شد. بهبود .NET 11 باعث شده است تا Method Syntax از نظر سادگی به همان میزان از تمیزی دست یابد.

ب) خروجی کامپایلر و عملکرد (Performance)
از نظر کارایی و حافظه، متد جدید دقیقاً همان ساختار داده خروجی (یک ValueTuple) را تولید می‌کند که متد قبلی با انتخاب‌گر دستی تولید می‌کرد. بنابراین این تغییر هیچ افت عملکردی ایجاد نمی‌کند و با حذف نیاز به ارزیابی یک Delegate اضافه، کدهای CIL تمیزتری تولید می‌شود.

ج) مقایسه با سایر متدهای LINQ
این ساده‌سازی بخشی از تلاش‌های مستمر تیم دات‌نت برای کاهش کد تشریفاتی (Boilerplate Reduction) است که پیش از این نیز در متدهایی نظیر Zip مشاهده شده بود.
ویژگیتا پیش از .NET 11در .NET 11 Preview
تعداد پارامترهای اجباری Join۴ پارامتر۳ پارامتر
نیاز به تعریف Manual Tupleبله (a, b) => (a, b)خیر (تولید خودکار ValueTuple)
خوانایی کد (Readability)همراه با شلوغی بصریتمیز و مستقیم
نوع خروجی (Type Signature)IEnumerable<(T1, T2)>IEnumerable<(T1, T2)>
نکته: اگر پس از انجام Join قصد داشته باشید مستقیماً یک DTO جدید بسازید یا فیلد خاصی را استخراج کنید، همچنان می‌توانید از Overload چهارپارامتری کلاسیک استفاده کنید. متد جدید صرفاً برای سناریوی شایع Projection کامل دو عنصر ارائه شده است.

نتیجه‌گیری
تغییرات معرفی‌شده در پیش‌نمایش .NET 11 برای متد LINQ Join گامی کوچک اما ارزشمند در جهت تمیزتر شدن کدهای C# و کاهش تشریفات غیرضروری است. با حذف نیاز اجباری به resultSelector هنگام بازگرداندن ساده جفت‌های منطبق، توسعه‌دهندگان می‌توانند کدهایی خواناتر، کوتاه‌تر و گویاتر بنویسند.