هنگامی که .NET Framework 3.5 در سال 2007 منتشر شد، دارای ویژگی جدیدی به نام Language Integrated Query یا به اختصار LINQ بود. LINQ به توسعه دهندگان دات نت اجازه می دهد تا با استفاده از arrow function ها، کدهای سی شارپ کارآمدی را بنویسند تا لیست ها یا حتی پایگاه داده ها را با استفاده از کتابخانه هایی مانند Entity Framework Core جستجو کنند.
مانند همه فیچرهای دات نت، LINQ در طول زمان به تکامل خود ادامه می دهد. نسخه آتی NET 6 تعدادی ویژگی واقعا جالب را به همراه دارد، از جمله مجموعه ای از قابلیت های جدید LINQ.
در این مقاله نگاهی اجمالی به پیشرفتهای اصلی LINQ خواهیم داشت که به زودی برای توسعهدهندگان .NET با NET 6 ارائه میشوند.
برای استفاده از این API های جدید LINQ ، باید در پیش نمایش 6 یا بالاتر باشید. انتشار کامل دات نت 6 در اواخر سال 2021 اتفاق می افتد، اما اگر می خواهید قبل از این با این ویژگی ها بیشتر آشنا شوید، می توانید پیش نمایش رسمی را از مایکروسافت دانلود کنید.
افزونه Chunking احتمالاً بزرگترین افزونه به LINQ در NET 6 است. اگر شما هم قبلاً مجبور شدهاید با مجموعههای بزرگی از اشیاء کار کنید و نیاز داشتید که آن مجموعه را تکه تکه کنید ، مجبور بودید که از حلقهها و منطق شرطی استفاده میکردید:
List<List<Movie>> pages = new List<List<Movie>>(); const int PAGE_SIZE = 5; List<Movie> currentPage = null; int spaceRemaining = 0; foreach (Movie movie in movies) { // Check to see if we're at the start of a new page if (spaceRemaining <= 0) { // Move to a new page and add it to our list currentPage = new List<Movie>(); pages.Add(currentPage); spaceRemaining = PAGE_SIZE; } // Add items to the current page and decrease the count allowable in this page currentPage.Add(movie); spaceRemaining--; }
مسلماً این کد برای مفهوم ساده تقسیم یک مجموعه بزرگ به چند تکه کوچک بسیار زیاد است. علاوه بر تعداد خطوط زیاد، کد قطعه کننده نیاز به آزمایش خاصی داشت تا اطمینان حاصل شود که همه موارد در یک قطعه گنجانده شدهاند، حتی اگر آخرین تکه بهطور کامل پر نشده باشد.
خوشبختانه، در NET 6 LINQ اکنون همه اینها را در یک خط کد #C از طریق روش Chunk خود پشتیبانی می کند.
const int PAGE_SIZE = 5; IEnumerable<Movie[]> chunks = movies.Chunk(PAGE_SIZE);
ویژگی بعدی ساده تر، اما بسیار کاربردی است. قبلاً، اگر میخواستید از انتهای یک مجموعه چیزی را به دست آورید، باید طول مجموعه را محاسبه میکردید و سپس آن مورد را با آن شاخص خارج میکردید و در صورت نیاز از آن کم میکردید به صورت زیر:
Movie lastMovie = movies.ElementAt(movies.Count() - 1);
اما به کمک ویژگی جدید دات نت 6 میتوانیم کد بالا را به صورت زیر بهینه کنیم:
Movie lastMovie = movies.ElementAt(^1);
برای مثال برای پیاده سازی pagination به کمک EFCore و Linq کدی که قبلا می نوشتیم به این صورت بود:
var movies = DatabaseContext.Movies.Skip(6).Take(10).ToList();
اما به کمک این افزونه جدید ما میتوانیم تابع Skip رو از کد بالا حذف کنیم و به این شکل کد را بهینه کنیم:
var movies = DatabaseContext.Movies.Take(6..10).ToList();
این کد بسیار ساده تر است کالکشن ما تا عنصر ششم رد می شود و سپس 4 مقدار را برمیگرداند، این افزونه به طور قابل توجهی توانایی شما را برای گرفتن عناصر از مجموعه ها توسط شاخص های خاص افزایش می دهد.
پیش از این ،اگر نیاز داشتیم تا بین دو مجموعه به طور موازی iterate کنیم از تابع Zip استفاده میکردیم به صورت زیر:
string[] titles = { "A Tale of Two Variables", "The Heisenbug", "Pride, prejudice, and semicolons" }; string[] genres = { "Drama", "Horror", "Romance" }; foreach ((string title, string genre) in titles.Zip(genres)) { Console.WriteLine($"{title} is a {genre} film"); }
این روش بسیار خوبی بود و باعث میشد که نیاز به ساخت آبجکت های بیشتری برای این منظور نداشته باشیم.
با این حال ، مواردی وجود دارد که ممکن است بخواهیم بر روی سه مجموعه iterate کنیم.
برای رفع این نیاز ، LINQ یک overload جدیدی برای تابع Zip پیاده سازی کرده که می توانیم به استفاده از آن به هدف خود برسیم:
string[] titles = { "A Tale of Two Variables", "The Heisenbug", "Pride, prejudice, and semicolons" }; string[] genres = { "Drama", "Horror", "Romance" }; float[] ratings = { 5f, 3.5f, 4.5f }; foreach ((string title, string genre, float rating) in titles.Zip(genres, ratings)) { Console.WriteLine($"{title} is a {genre} film that got a rating of {rating}"); }
متدهای LINQ همیشه و در حال حاضر FirstOrDefault ، SingleOrDefault و LastOrDefault یک پایه اصلی در توسعه LINQ در #C هستند.
به بیان ساده ، این روش ها به مجموعه ای نگاه می کنند و در صورت برآورده شدن شرط ، مقدار را برمی گردانند. اگر شرطی برآورده نشده باشد ، مقدار پیش فرض برای آن نوع استفاده می شود. برای انواع ریفرنس تایپ ها که null خواهد بود ، برای انواع عددی 0 و برای boolean مقدار false برمیگردد.
به عنوان مثال ، فرض کنید ما سعی کردیم اولین فیلمی را که این نویسنده را در گروه بازیگرانش داشت ، پیدا کنیم:
Movie movie = movies.FirstOrDefault(m => m.Cast.Includes("Matt Eland"));
از آنجایی که من هرگز در فیلمی حضور نداشتم ، FirstOrDefault با مقدار پیش فرض خود کار می کرد و مقدار فیلم بر روی null ست میشد.
با این حال ، در NET 6 ، LINQ اکنون به شما امکان می دهد مقدار پیشفرض خود را تعیین کنید تا در صورت عدم تطابق با شرایط مورد استفاده قرار گیرد. با این کار از مقابله با مقادیر null جلوگیری می شود و در عوض جایگزین ایمن آن را را مشخص می کند :
Movie defaultValue = movies.First(); Movie movie = movies.FirstOrDefault(m => m.Cast.Includes("Matt Eland"), defaultValue); var numbers = new List<int>() { 7, 1, 9, 41, 8, 22 }; var matchingNumber = numbers.FirstOrDefault(x => x > 50, -1); //-1 var matchingNumber = numbers.SingleOrDefault(x => X > 30, -1); //41
بدین شکل اگر مقداری در شرط وجود نداشت به جای null مقدار defaultValue ای که ست کردیم برگشت داده میشود.
مشابه این دستور برای توابع SingleOrDefault ، LastOrDefault نیز ارائه شده است.
هنگام کار با LINQ ، همیشه با لیست ها و یا مجموعه هایی سر و کار نداریم که بتوانیم به سادگی طول آنها را بدست آوریم. در حقیقت ، هنگام کار با انواع خاصی از مجموعه ها ، مانند مجموعه هایی از جنس IQueryable ، حتی یک عملیات ساده مانند فراخوانی تابع Count ممکن است باعث شود کل کوئری مورد شمارش مجدد قرار گیرد.
برای حل این مشکل ، دات نت 6 متد بسیار تخصصی TryGetNonEnumeratedCount را ارائه داده است. این متد بررسی می کند که آیا تعیین تعداد موارد موجود در مجموعه باعث شمارش مجموعه می شود یا خیر. در غیر این صورت ، مقدار شمارش شده در یک پارامتر out تولید و ذخیره می شود و مقدار true از متد بر می گردد. اگر متد شمارش، باعث شمارش مجموعه شود ، متد به جای آن false ، و پارامتر out مقدار 0 را برمیگرداند.
if (movies.TryGetNonEnumeratedCount(out int count)) { Console.WriteLine($"The count is {count}"); } else { Console.WriteLine("Could not get a count of movies without enumerating the collection"); }
اگر این کد کمی گیج کننده بنظر میرسد ، این یک الگوی بسیار شبیه به نحوه کارکرد int.TryParse و دیگر API های TryX است که در حال حاضر در کتابخانه کلاس پایه .NET کار می کنند. تنها تفاوت این است که TryGetNonEnumeratedCount بر اجتناب از عملیات به طور بالقوه آهسته به دلیل شمارش مجموعه ای بسیار بزرگ یا کوئری زدن مجدد از پایگاه داده متمرکز شده است.
در نهایت ، .NET 6 به توسعه دهندگان .NET اکستنشن متدهای MinBy و MaxBy را ارائه می دهد. این دو متد به شما امکان می دهد مجموعه خود را بررسی کرده و بر اساس شرطی که به آن می دهید ، بزرگترین یا کوچکترین چیزی را پیدا کند.
قبل از .NET 6 برای بدست آوردن کوچکترین و یا بزرگترین مقدار یک کالکشن، باید از Max یا Min برای پیدا کردن مقدار استفاده کنید ، سپس مجدداً برای دریافت آبجکت موردنظر کوئری دیگری با استفاده از مقدار بدست آمده اجرا میکردیم:
int numBattles = movies.Max(m => m.NumSpaceBattles); Movie mostAction = movies.First(m => m.NumSpaceBattles == numBattles);
روش فوق نیاز ما را برطرف میکرد اما بهینه نبود و برای یک کار، ما دوبار به دیتابیس درخواست ارسال میکردیم
در دات نت 6 دوخط دستور بالا به یک خط زیر خلاصه شده و برای دریافت آبجکت موردنظرمان فقط یک درخواست به سمت دیتابیس ارسال میشود.
Movie mostAction = movies.MaxBy(m => m.NumSpaceBattles);