بررسی قابلیت FullJoin در NET 11.؛ قطعه گمشده پازل LINQ
نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۷ ۱۴:۰۰
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
Join (معادل Inner Join) دسترسی داریم و پیادهسازی LeftJoin نیز در نسخههای اخیر سادهتر شده است؛ اما زمانیکه نوبت به یک اتصال کامل دوطرفه یا همان Full Outer Join میرسد، LINQ پیادهسازی بومی (Built-in) روانی در اختیار ما قرار نمیدهد. توسعهدهندگان معمولاً برای حل این چالش به ترکیبی از GroupJoin، SelectMany و DefaultIfEmpty یا ترکیب نتایج یک Left Join و Right Join متوسل میشوند که علاوه بر کاهش خوانایی کد، بر کارایی (Performance) برنامه نیز تأثیر منفی میگذارد.FullJoin برطرف شود. در این مقاله به بررسی مفهوم Full Outer Join، ساختار این API پیشنهادی و نحوه عملکرد آن در قالب کد میپردازیم.سلب مسئولیت (Disclaimer): هرچند کلیات این API مورد تأیید (Approved) قرار گرفته است، اما تا زمان نگارش این مقاله هنوز Pull Request (PR) رسمی برای ادغام آن در SDK داتنت یا Entity Framework Core باز نشدهاست. بنابراین، ساختار نهایی و جزئیات پیادهسازی آن ممکن است تا زمان عرضه نسخه نهایی .NET 11 دستخوش تغییرات شود.
null در نظر گرفته میشود.null در نظر گرفته میشود.customers) و سفارشها (orders) داریم:var customers = new[]
{
new Customer(1, "Alice"),
new Customer(2, "Bob"),
new Customer(3, "Charlie")
};
var orders = new[]
{
new Order(1, 100), // سفارش برای Alice
new Order(1, 101), // سفارش دوم برای Alice
new Order(4, 102) // سفارشی بدون مشتری ثبت شده در این آرایه
};FullJoin روی این دو مجموعه، خروجی مفهومی ما شامل موارد زیر خواهد بود:FullJoin به صورت متد توسعهدهنده (Extension Method) روی IEnumerable شکل گرفته است:public static IEnumerable<TResult> FullJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer,
IEnumerable<TInner> inner,
Func<TOuter, TKey> outerKeySelector,
Func<TInner, TKey> innerKeySelector,
Func<TOuter?, TInner?, TResult> resultSelector);resultSelector دقت کنید، متوجه یک تفاوت بنیادی با متد Join معمولی میشوید:Func<TOuter?, TInner?, TResult> resultSelector
TOuter و TInner به صورت Nullable (?) تعریف شدهاند. این طراحی کاملاً منطقی و هوشمندانه است؛ چرا که در فرآیند Full Join، در زمان عدم تطابق، قطعاً یکی از طرفین null خواهد بود. این ساختار توسعهدهنده را ملزم میکند تا در بدنه resultSelector مدیریت مقادیر نول (Null-safety) را به کمک عملگرهای پیشفرض داتنت انجام دهد.IEqualityComparer را برای مقایسه سفارشی کلیدها میپذیرد.IQueryable (جهت استفاده در ORMهایی مثل EF Core) و IAsyncEnumerable (برای پردازشهای ناهمگام جریان دادهها).FullJoin بسیار ساده، تمیز و خوانا خواهد بود:var results = customers.FullJoin(
orders,
c => c.CustomerId,
o => o.CustomerId,
(customer, order) => new
{
CustomerName = customer?.Name ?? "(no customer)",
OrderId = order?.Id ?? 0
});?. (Null-conditional) و ?? (Null-coalescing) استفاده شده است تا در صورت عدم وجود مشتری یا سفارش، مقادیر پیشفرض جایگزین شوند و از بروز خطای NullReferenceException جلوگیری به عمل آید.// راهکار سنتی و پیچیده فعلی (شبیهسازی Full Join)
var leftJoin = from customer in customers
join order in orders on customer.CustomerId equals order.CustomerId into joinedOrders
from order in joinedOrders.DefaultIfEmpty()
select new { customer, order };
var rightJoin = from order in orders
join customer in customers on order.CustomerId equals customer.CustomerId into joinedCustomers
from customer in joinedCustomers.DefaultIfEmpty()
select new { customer, order };
var fullJoinAlternative = leftJoin.Union(rightJoin);Union برای حذف تکراریها) نیاز به تخصیص حافظه (Memory Allocation) بیشتر و زمان پردازش مضاعف دارد.FullJoin در .NET 11 این فرآیند را در سطح ساختار داده (احتمالاً با استفاده از مکانیزمهای بهینهشده هَشتیبل داخلی) تنها با یک بار پیمایش بهینه انجام خواهد داد.FullJoin در .NET 11 (در صورت نهایی شدن پروپوزالهای #124787 و #120596 رانتایم داتنت) یکی از کاربردیترین افزونهها به مجموعه ابزار LINQ در سالهای اخیر خواهد بود. این ویژگی نه تنها کد توسعهدهندگان را تمیزتر و منسجمتر میکند، بلکه بار فکری مدیریت اتصالات پیچیده دادهای را کاهش داده و کارایی بالاتری را به ارمغان میآورد. #127236 و #121998 در داتنت 11، Preview 5):FullJoin اشاره میکند. در توسعه نرمافزار، ماژولهای زیادی وجود دارند که نیاز به تطبیق دو مجموعه داده (Reconciliation) دارند. با متد جدید FullJoin دیگر نیازی به نوشتن کوئریهای پیچیده دیتابیسی یا پردازشهای سنگین در حافظه برای سناریوهای زیر نیست:using System.Linq;
Product[] catalog =
[
new("A100", "Keyboard"),
new("B200", "Mouse"),
];
Sale[] sales =
[
new("B200", 4),
new("C300", 2),
];
var rows = catalog.FullJoin(
sales,
product => product.Sku,
sale => sale.Sku,
(product, sale) => new
{
Sku = product?.Sku ?? sale!.Sku,
ProductName = product?.Name ?? "(not in catalog)",
QuantitySold = sale?.Quantity ?? 0,
});
foreach (var row in rows)
{
Console.WriteLine($"{row.Sku}: {row.ProductName}, sold {row.QuantitySold}");
}
public sealed record Product(string Sku, string Name);
public sealed record Sale(string Sku, int Quantity);resultSelector با این خط مواجه میشوید:Sku = product?.Sku ?? sale!.Sku,
FullJoin احتمال عدم تطابق در هر دو سمت وجود دارد، هر دو شیء product و sale میتوانند null باشند. اما ما مطمئن هستیم که حداقل یکی از آنها در هر ردیف مقدار دارد (چون اگر هر دو نال باشند، اصلاً ردیفی تولید نمیشود). بنابراین با استفاده از ترکیب عملگر هوشمند ?. و عملگر ناوردا (Null-forgiving operator) یعنی !. به کامپایلر اطمینان میدهیم که کلید Sku تحت هر شرایطی مقدار غیر نال خواهد داشت.Join، LeftJoin، RightJoin، GroupJoin و خود FullJoin) اضافه شده است.resultSelector بنویسید تا دو شیء را به یک شیء جدید (معمولاً Anonymous Type) نگاشت کند. اما اکنون اگر هدف شما صرفاً دریافت جفتهای متناظر باشد، میتوانید اصلاً resultSelector را ننویسید!// روش قدیمی (نیاز به تعریف صریح خروجی):
var oldWay = catalog.Join(sales, p => p.Sku, s => s.Sku, (p, s) => new { p, s });
// روش جدید در .NET 11 (خروجی مستقیم به صورت جفت/Tuple):
var newWay = catalog.Join(sales, p => p.Sku, s => s.Sku);
// خروجی این متد به طور خودکار IEnumerable<(Product, Sale)> استforeach (var (product, sale) in catalog.Join(sales, p => p.Sku, s => s.Sku))
{
// دسترسی مستقیم و تمیز به اشیاء بدون نیاز به پراپرتیهای واسط
}LeftJoin، RightJoin و FullJoin، المانهای درون این Tupleها به طور خودکار متناظر با ماهیت آن Join، قابلیت Nullable بودن (?) را خواهند داشت.