۱۱ اشتباه رایج در LINQ که حتی توسعهدهندگان باتجربه #C مرتکب میشوند (و راهحل آنها)
نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۳/۱۳ ۰۸:۳۳
آدرس: www.dntips.ir
| مطالب | ۳۶۹۴ |
| نویسندگان | ۲۷۶ |
| گروههای مطالب | ۱۰۲۴ |
| نقشههای راه | ۱۱۹ |
| دورهها | ۱۴ |
| اشتراکها | ۱۷۹۱۴ |
.ToList(): دام اجرایی فوری.ToList() یا .ToArray() پیش از اتمام عملیات فیلتر یا تبدیل دادهها است. این کار باعث اجرای فوری (Immediate Execution) کوئری میشود، به این معنی که تمام دادهها بلافاصله از منبع بارگذاری شده و در حافظه مادیسازی (Materialize) میشوند. مشکل زمانی آغاز میشود که پس از این مادیسازی، عملیات فیلترینگ یا پرسوجوی بیشتری روی مجموعه حاصل انجام شود، چرا که این امر منجر به تکرار (Iteration) دوباره روی مجموعه دادهها میشود.// ❌ BAD
var result = GetUsers().Where(u => u.IsActive).ToList();
if (result.Any()) { /* عملیات دیگر */ }result تکرار میشود. این عمل سربار اضافی ایجاد کرده و کارایی را کاهش میدهد.// ✅ GOOD
var activeUsers = GetUsers().Where(u => u.IsActive);
if (activeUsers.Any()) { /* عملیات دیگر */ }activeUsers تا زمانی که واقعاً نیاز به دادهها نباشد (مثلاً هنگام فراخوانی .Any() یا foreach) اجرا نمیشود. این کار تضمین میکند که دادهها فقط در صورت نیاز بارگذاری شده و از تکرار غیرضروری جلوگیری میشود..Count()به جای.Any(): سربار بیمورد شمارش.Count() به جای .Any() یک اشتباه رایج است. متد .Count() برای شمارش تمام عناصر یک مجموعه، کل آن را پیمایش میکند، در حالی که .Any() به محض یافتن اولین عنصر، متوقف میشود.// ❌ BAD
if (orders.Count() > 0) { /* عملیات */ }// ✅ GOOD
if (orders.Any()) { /* عملیات */ }.Any() هم از نظر عملکرد بهینه است و هم کد را خواناتر میکند، زیرا به وضوح نیت برنامهنویس را برای بررسی وجود عناصر بیان میکند.InvalidOperationExceptionforeach است که بر اساس یک کوئری LINQ روی همان مجموعه انجام میشود. این عمل معمولاً منجر به بروز استثنای InvalidOperationException با پیامی شبیه به "Collection was modified; enumeration operation may not execute." (مجموعه تغییر یافت؛ عملیات شمارش ممکن است اجرا نشود) میشود.// ❌ BAD
foreach (var user in users.Where(u => u.IsBlocked))
users.Remove(user); // تغییر منبع در حین پیمایش!// ✅ BETTER
var toRemove = users.Where(u => u.IsBlocked).ToList(); // مادیسازی عناصر برای حذف
foreach (var u in toRemove)
users.Remove(u);.Count() و سپس .First())، این عمل منجر به پیمایش دوگانه (Double Enumeration) و اجرای مکرر کوئری میشود که کارایی را کاهش میدهد.// ❌ BAD var query = GetItems().Where(x => x.IsReady); Log(query.Count()); // پیمایش اول return query.First(); // پیمایش دوم
.ToList() یا .ToArray()مادیسازی (Materialize) کنید.// ✅ GOOD var materializedItems = GetItems().Where(x => x.IsReady).ToList(); // مادیسازی تنها یک بار Log(materializedItems.Count); return materializedItems.First();
Where, Select, OrderBy) یک کوئری را بلافاصله اجرا نمیکنند، بلکه تنها تعریفی از کوئری را برمیگردانند. اجرای واقعی کوئری تنها زمانی اتفاق میافتد که نتایج آن پیمایش (Enumerated) شود (مثلاً در یک حلقه foreach، یا با فراخوانی متدهایی مانند .ToList(), .ToArray(), .First(), .Count()).// ❌ BAD: فکر میکنید این کوئری اجرا شده است var result = GetItems().Where(x => x.IsArchived); // در اینجا هیچ عملیاتی روی دادهها انجام نشده است!
// ✅ GOOD var materialized = result.ToList(); // این خط کوئری را مجبور به اجرا میکند // حالا میتوانید روی materialized کار کنید
// ❌ BAD
var result = users.Where(u => u.Orders.Any(o => o.Total > 100))
.Select(u => new {
u.Name,
Orders = u.Orders.Where(o => o.Total > 100)
});// ✅ GOOD (تقسیم به مراحل)
var usersWithHighValueOrders = users.Where(u => u.Orders.Any(o => o.Total > 100));
var finalResult = usersWithHighValueOrders.Select(u => new {
u.Name,
HighValueOrders = u.Orders.Where(o => o.Total > 100)
});
// ✅ GOOD (استفاده از Query Syntax)
var resultQuerySyntax = from u in users
where u.Orders.Any(o => o.Total > 100)
select new {
u.Name,
Orders = u.Orders.Where(o => o.Total > 100)
};IQueryableوIEnumerable: افت کاراییIQueryable و IEnumerable است. IEnumerable برای کار با مجموعهها در حافظه (LINQ to Objects) طراحی شده است و پرسوجوها روی دادههایی که قبلاً به حافظه بارگذاری شدهاند، اجرا میشوند. در مقابل، IQueryable (مانند آنچه در Entity Framework Core استفاده میشود) کوئریها را به عبارتهای قابل فهم برای منبع داده (مانند SQL برای پایگاه داده) ترجمه میکند و اجرای پرسوجو را به آن منبع واگذار (Delegate) میکند.// ❌ BAD: تمام کاربران از دیتابیس به حافظه کشیده میشوند، سپس فیلتر میشوند db.Users.ToList().Where(u => u.IsActive);
db.Users.ToList() تمام رکوردهای جدول کاربران را از پایگاه داده به حافظه برنامه میکشد و سپس فیلترینگ Where(u => u.IsActive) در حافظه انجام میشود. این عمل به شدت ناکارآمد است، بهویژه برای جداول بزرگ.// ✅ GOOD: فیلترینگ در دیتابیس انجام شده و تنها کاربران فعال به حافظه کشیده میشوند db.Users.Where(u => u.IsActive).ToList();
Where(u => u.IsActive) به SQL ترجمه شده و در پایگاه داده اجرا میشود، و تنها کاربران فعال به حافظه برنامه منتقل میشوند. این کار بهینگی بالایی دارد..First()بدون بررسی وجود عنصر: خطر استثنا.First() اولین عنصر یک مجموعه را برمیگرداند. با این حال، اگر مجموعه خالی باشد، این متد استثنای InvalidOperationException را پرتاب میکند. این اشتباه میتواند منجر به خرابی برنامه شود.// ❌ BAD var result = users.First(); // اگر users خالی باشد، خطا میدهد Console.WriteLine(result.Name);
// ✅ GOOD
var result = users.FirstOrDefault(); // اگر users خالی باشد، null برمیگرداند
if (result != null)
Console.WriteLine(result.Name);
else
Console.WriteLine("کاربری یافت نشد.");.FirstOrDefault() ایمنتر است، زیرا اگر عنصری یافت نشود، مقدار null (برای انواع ارجاعی) یا مقدار پیشفرض (برای انواع مقداری) را برمیگرداند. شما میتوانید سپس با بررسی مقدار برگشتی از بروز استثنا جلوگیری کنید..Where(...).First()به جای.First(...): سربار فیلترینگ اضافی.Where() با .First() یک الگوی رایج اما ناکارآمد است. متد .First() خود دارای یک سربار (Predicate) است که میتواند مستقیماً برای فیلترینگ استفاده شود.// ❌ BAD: ایجاد یک مجموعه میانی غیرضروری var active = users.Where(u => u.IsActive).First();
Where ایجاد میشود که میتواند کل مجموعه را پیمایش کند تا عناصر فعال را بیابد و سپس First روی آن مجموعه میانی اجرا میشود.// ✅ GOOD: فیلترینگ مستقیم و بهینه var active = users.First(u => u.IsActive);
.First() هم کد را خواناتر میکند و هم از ایجاد مجموعه میانی غیرضروری جلوگیری کرده و عملکرد را بهبود میبخشد.AsParallel()برای عملیاتهای پرهزینه: فرصت موازیسازی.AsParallel() میتواند به معنای از دست دادن یک فرصت بزرگ برای بهبود عملکرد باشد. PLINQ با توزیع عملیات بین هستههای پردازنده، میتواند زمان اجرا را به طرز چشمگیری کاهش دهد.// ❌ BAD var results = bigList.Select(DoExpensiveWork).ToList(); // عملیات سریالی
// ✅ GOOD var results = bigList.AsParallel().Select(DoExpensiveWork).ToList(); // عملیات موازی
.AsParallel()، باید به ایمنی رشته (Thread Safety) عملیات خود توجه ویژهای داشته باشید، زیرا عملیات به صورت موازی در چندین رشته اجرا میشوند.// ❌ BAD
var anon = users.Select(u => new { u.Id, u.Name }).ToList();
// بعداً نیاز به آبجکت کامل کاربر است که با این نوع بینام قابل دسترسی نیست.// ✅ GOOD
public class UserDto { public int Id { get; set; } public string Name { get; set; } }
var projected = users.Select(u => new UserDto { Id = u.Id, Name = u.Name }).ToList();orders در LINQ است؟IQueryable و IEnumerable) برای استفاده بهینه از آن ضروری است. اشتباهات رایج مورد بحث در این مقاله، گرچه ممکن است در ابتدا منجر به خطاهای فوری نشوند، اما میتوانند به "قاتلان خاموش" عملکرد تبدیل شده و منجر به کدی شوند که کندتر، باگدارتر و نگهداری آن دشوارتر است. با ممیزی کد خود و اعمال بهترین روشهای توضیح داده شده، میتوانید از پتانسیل کامل LINQ بهرهمند شوید و برنامههایی با کارایی و کیفیت بالاتر توسعه دهید.if (orders.Any())AsParallel به اطمینان از Thread Safe بودن اشاره کردید. آیا لزوما تعریف یک ConcurrentCollection برای نگهداری نتیجه نهایی حاصل از اجرای ToList میتونه این اطمینان رو بده که دادهها به درستی از بانک اطلاعاتی دریافت شده و برنامه با خطا مواجه نخواهد شد؟AsParallel() در PLINQ استفاده میشود، عملیات روی مجموعه دادهها بهصورت موازی بین چندین رشته تقسیم میشود. بنابراین، اگر عملیاتی که انجام میدهید (مثل DoExpensiveWork) به منابع مشترکی دسترسی داشته باشد یا حالت داخلی (State) را تغییر دهد، باید اطمینان حاصل کنید که این عملیات Thread-Safe است.DoExpensiveWork به یک لیست مشترک دسترسی دارید و میخواهید عناصری به آن اضافه کنید. بدون محافظت، افزودن همزمان از چندین رشته میتواند باعث خطا شود. با استفاده از قفل، میتوانید Thread Safety را تضمین کنید:private static readonly object _lock = new object();
private static List<int> sharedList = new List<int>();
var results = bigList.AsParallel().Select(item =>
{
lock (_lock)
{
sharedList.Add(DoExpensiveWork(item));
return item;
}
}).ToList();lock تضمین میکند که فقط یک رشته در هر لحظه به sharedList دسترسی دارد.System.Collections.Concurrent استفاده کنید. برای مثال:using System.Collections.Concurrent;
private static ConcurrentBag<int> concurrentBag = new ConcurrentBag<int>();
var results = bigList.AsParallel().Select(item =>
{
int result = DoExpensiveWork(item);
concurrentBag.Add(result); // ایمن برای استفاده در محیط چندرشتهای
return result;
}).ToList();ConcurrentBag بهگونهای طراحی شده که بدون نیاز به قفلهای دستی، عملیات افزودن یا خواندن را بهصورت Thread-Safe انجام میدهد.DoExpensiveWork به هیچ منبع مشترکی وابسته نباشد و فقط روی ورودی خودش کار کند، نیازی به قفل یا مجموعههای همزمان نیست:var results = bigList.AsParallel().Select(item => DoExpensiveWork(item)).ToList();
DoExpensiveWork باید بهگونهای باشد که هر فراخوانی آن مستقل از دیگران باشد (مثلاً بدون تغییر متغیرهای استاتیک یا اشیاء مشترک).Aggregate در PLINQ استفاده کنید که بهصورت Thread-Safe عمل میکند:var sum = bigList.AsParallel()
.Select(DoExpensiveWork)
.Aggregate((acc, curr) => acc + curr);Aggregate تضمین میکند که تجمیع نتایج بهصورت ایمن انجام شود.Interlocked استفاده کنید.AsParallel() بررسی کنید که آیا عملیات شما به منابع مشترک دسترسی دارد یا خیر، و در صورت نیاز، اقدامات لازم برای Thread Safety را انجام دهید.