فصل ۹: Joining، Ordering، Grouping، Set، Conversion و Aggregation Operators

فصل ۹: Joining، Ordering، Grouping، Set، Conversion و Aggregation Operators

فصل ۹: Joining، Ordering، Grouping، Set، Conversion و Aggregation Operators

راه‌حل درست برای افزودن فیلتر قیمت به left outer join این است که بند Where را با یک subquery پیش از DefaultIfEmpty قرار دهیم:

from c in dbContext.Customers
from p in c.Purchases.Where (p => p.Price > 1000).DefaultIfEmpty()
select new {
             c.Name,
             Descript = p == null ? null : p.Description,
             Price = p == null ? (decimal?) null : p.Price
           };

EF Core این الگو را به left outer join ترجمه می‌کند و این شیوه برای چنین پرس‌وجوهایی مؤثر است.

Joining

روششرحمعادل SQL
Joinبا یک راهبرد lookup عناصر دو collection را تطبیق می‌دهد و result set تخت منتشر می‌کند.INNER JOIN
GroupJoinشبیه Join است، اما result set سلسله‌مراتبی می‌دهد.INNER JOIN، LEFT OUTER JOIN
Zipدو دنباله را هم‌قدم، مثل زیپ، پیمایش می‌کند و تابعی را روی هر جفت عنصر اعمال می‌کند.Exception thrown

Join و GroupJoin

IEnumerable<TOuter>, IEnumerable<TInner> → IEnumerable<TResult>

آرگومان‌های Join
آرگوماننوع
Outer sequenceIEnumerable<TOuter>
Inner sequenceIEnumerable<TInner>
Outer key selectorTOuter => TKey
Inner key selectorTInner => TKey
Result selector(TOuter,TInner) => TResult
آرگومان‌های GroupJoin
آرگوماننوع
Outer sequenceIEnumerable<TOuter>
Inner sequenceIEnumerable<TInner>
Outer key selectorTOuter => TKey
Inner key selectorTInner => TKey
Result selector(TOuter,IEnumerable<TInner>) => TResult

Query syntax:

from outer-var in outer-enumerable
join inner-var in inner-enumerable on outer-key-expr equals inner-key-expr
 [ into identifier ]

مرور کلی

Join و GroupJoin دو دنبالهٔ ورودی را در یک دنبالهٔ خروجی درهم می‌آمیزند. Join خروجی تخت و GroupJoin خروجی سلسله‌مراتبی می‌دهد.

این دو اپراتور راهبردی جایگزین برای Select و SelectMany هستند. مزیتشان در collectionهای محلیِ داخل حافظه این است که ابتدا inner sequence را در یک keyed lookup بار می‌کنند و از پیمایش مکرر همهٔ inner elementها جلوگیری می‌کنند. عیبشان این است که فقط معادل inner join و left outer join را ارائه می‌کنند؛ cross join و non-equi join همچنان باید با Select/SelectMany انجام شوند. در queryهای EF Core، Join و GroupJoin مزیت واقعی نسبت به Select و SelectMany ندارند.

جدول ۹-۱ — راهبردهای Joining
راهبردشکل نتیجهکارایی query محلیInner joinLeft outer joinCross joinNon-equi join
Select + SelectManyتختضعیفبلهبلهبلهبله
Select + Selectتو‌در‌توضعیفبلهبلهبلهبله
Joinتختخوببله
GroupJoinتو‌در‌توخوببلهبله
GroupJoin + SelectManyتختخوببلهبله

Join

اپراتور Join یک inner join انجام می‌دهد و دنبالهٔ خروجی تخت منتشر می‌کند. پرس‌وجوی زیر بدون navigation property همهٔ مشتریان را کنار خریدهایشان فهرست می‌کند:

IQueryable<string> query =
  from c in dbContext.Customers
  join p in dbContext.Purchases on c.ID equals p.CustomerID
  select c.Name + " bought a " + p.Description;

نتایج با query مبتنی بر SelectMany برابر است:

Tom bought a Bike
Tom bought a Holiday
Dick bought a Phone
Harry bought a Car

برای دیدن مزیت Join نسبت به SelectMany باید آن را به query محلی تبدیل کنیم؛ ابتدا مشتریان و خریدها را در آرایه کپی می‌کنیم:

Customer[] customers = dbContext.Customers.ToArray();
Purchase[] purchases = dbContext.Purchases.ToArray();
var slowQuery = from c in customers
                from p in purchases where c.ID == p.CustomerID
                select c.Name + " bought a " + p.Description;
var fastQuery = from c in customers
                join p in purchases on c.ID equals p.CustomerID
                select c.Name + " bought a " + p.Description;

هر دو نتیجه یکسان‌اند، اما query با Join به‌مراتب سریع‌تر است، چون پیاده‌سازی Enumerable inner collection یعنی purchases را از قبل در keyed lookup بار می‌کند.

قالب کلی join در query syntax:

join inner-var in inner-sequence on outer-key-expr equals inner-key-expr

در LINQ میان outer و inner sequence تمایز وجود دارد: outer همان input sequence است (اینجا customers) و inner collection جدید معرفی‌شده است (اینجا purchases). Join inner join است؛ پس مشتری بدون خرید حذف می‌شود. در inner join می‌توانید جای inner/outer را عوض کنید و همان نتیجه را بگیرید:

from p in purchases
join c in customers on p.CustomerID equals c.ID
...

می‌توانید joinهای بیشتری به همان query اضافه کنید. اگر هر purchase یک یا چند purchase item داشته باشد:

from c in customers
join p in purchases on c.ID equals p.CustomerID
join pi in purchaseItems on p.ID equals pi.PurchaseID
...

purchases در join اول inner و در join دوم outer است. همان نتیجه را به‌شکل ناکارآمد با foreachهای تو‌در‌تو می‌توان نوشت:

foreach (Customer c in customers)
  foreach (Purchase p in purchases)
    if (c.ID == p.CustomerID)
      foreach (PurchaseItem pi in purchaseItems)
        if (p.ID == pi.PurchaseID)
          Console.WriteLine (c.Name + "," + p.Price + "," + pi.Detail);

در query syntax، متغیرهای joinهای قبلی همچنان در scope هستند؛ درست مانند queryهای مبتنی بر SelectMany. همچنین می‌توانید بین joinها where و let قرار دهید.

Joining روی چند کلید

با anonymous type می‌توان روی چند کلید join کرد:

from x in sequenceX
join y in sequenceY on new { K1 = x.Prop1, K2 = x.Prop2 }
                equals new { K1 = y.Prop3, K2 = y.Prop4 }
...

برای کارکرد صحیح، دو anonymous type باید ساختار کاملاً یکسان داشته باشند. کامپایلر هر دو را با یک نوع داخلی واحد پیاده‌سازی می‌کند و کلیدهای join سازگار می‌شوند.

Joining در fluent syntax

join زیر:

from c in customers
join p in purchases on c.ID equals p.CustomerID
select new { c.Name, p.Description, p.Price };

در fluent syntax چنین است:

customers.Join (
       purchases,
       c => c.ID,
       p => p.CustomerID,
       (c, p) => new { c.Name, p.Description, p.Price }
 );

result selector انتهایی هر عنصر دنبالهٔ خروجی را می‌سازد. اگر پیش از projection بندهای بیشتری مانند orderby داشته باشید:

from c in customers
join p in purchases on c.ID equals p.CustomerID
orderby p.Price
select c.Name + " bought a " + p.Description;

در fluent syntax باید در result selector یک anonymous type موقت بسازید تا هر دو c و p بعد از join در scope بمانند:

customers.Join (
      purchases,
      c => c.ID,
      p => p.CustomerID,
      (c, p) => new { c, p } )
  .OrderBy (x => x.p.Price)
  .Select  (x => x.c.Name + " bought a " + x.p.Description);

برای joinها معمولاً query syntax ترجیح‌پذیر است، چون دردسر کمتری دارد.

GroupJoin

GroupJoin همان کار Join را انجام می‌دهد، اما به‌جای نتیجهٔ تخت، خروجی سلسله‌مراتبی گروه‌بندی‌شده بر اساس هر outer element می‌دهد و left outer join را هم ممکن می‌کند. در متن کتاب، GroupJoin در EF Core پشتیبانی نمی‌شود.

query syntax آن مانند Join است، اما بعدش into می‌آید:

Customer[] customers = dbContext.Customers.ToArray();
Purchase[] purchases = dbContext.Purchases.ToArray();
IEnumerable<IEnumerable<Purchase>> query =
  from c in customers
  join p in purchases on c.ID equals p.CustomerID
  into custPurchases
  select custPurchases;

نتیجه دنباله‌ای از دنباله‌هاست:

foreach (IEnumerable<Purchase> purchaseSequence in query)
  foreach (Purchase p in purchaseSequence)
    Console.WriteLine (p.Description);

این خروجی به‌تنهایی چندان مفید نیست چون purchaseSequence ارجاعی به customer ندارد. معمولاً این‌طور می‌نویسید:

from c in customers
join p in purchases on c.ID equals p.CustomerID
into custPurchases
select new { CustName = c.Name, custPurchases };

این همان نتیجهٔ Select subquery زیر را می‌دهد، با این تفاوت که آن نسخه برای دادهٔ محلی ناکارآمد است:

from c in customers
select new
{
  CustName = c.Name,
  custPurchases = purchases.Where (p => c.ID == p.CustomerID)
};

به‌طور پیش‌فرض GroupJoin معادل left outer join است. برای inner join باید روی custPurchases فیلتر کنید:

from c in customers join p in purchases on c.ID equals p.CustomerID
into custPurchases
where custPurchases.Any()
select ...

بندهای پس از group-join into روی زیردنباله‌های child elementها کار می‌کنند، نه روی child element منفرد. برای فیلتر خریدهای منفرد، Where را پیش از joining اجرا کنید:

from c in customers
join p in purchases.Where (p2 => p2.Price > 1000)
  on c.ID equals p.CustomerID
into custPurchases ...

queryهای لامبدا با GroupJoin را مانند Join می‌توان ساخت.

Flat outer joins

اگر هم outer join و هم result set تخت بخواهید، GroupJoin outer join می‌دهد و Join خروجی تخت. راه‌حل: ابتدا GroupJoin، سپس DefaultIfEmpty روی هر child sequence و در آخر SelectMany:

from c in customers
join p in purchases on c.ID equals p.CustomerID into custPurchases
from cp in custPurchases.DefaultIfEmpty()
select new
{
  CustName = c.Name,
  Price = cp == null ? (decimal?) null : cp.Price
};

DefaultIfEmpty اگر زیرمجموعهٔ خریدها خالی باشد دنباله‌ای با یک null منتشر می‌کند. from دوم به SelectMany ترجمه می‌شود.

در این نقش، SelectMany همهٔ زیردنباله‌های purchase را گسترش و تخت کرده و در یک دنبالهٔ واحد از purchase elementها به هم متصل می‌کند.

Joining با lookupها

متدهای Join و GroupJoin در Enumerable دو مرحله دارند: ابتدا inner sequence را در lookup بار می‌کنند و سپس outer sequence را همراه lookup پرس‌وجو می‌کنند.

lookup دنباله‌ای از groupingهاست که با کلید مستقیماً قابل دسترسی است؛ می‌توان آن را «دیکشنریِ دنباله‌ها» یا multidictionary دانست، چون زیر هر کلید چند عنصر می‌پذیرد. lookup فقط‌خواندنی است:

public interface ILookup<TKey,TElement> :
   IEnumerable<IGrouping<TKey,TElement>>, IEnumerable
{
  int Count { get; }
  bool Contains (TKey key);
  IEnumerable<TElement> this [TKey key] { get; }
}

برای collectionهای محلی می‌توانید به‌جای join operatorها lookup را دستی بسازید و پرس‌وجو کنید. دو مزیت: می‌توان lookup را در چند query یا کد imperative عادی دوباره استفاده کرد؛ و پرس‌وجوی lookup راه خوبی برای فهم کار Join/GroupJoin است.

ToLookup یک lookup می‌سازد. همهٔ خریدها را با کلید CustomerID بار می‌کنیم:

ILookup<int?,Purchase> purchLookup =
  purchases.ToLookup (p => p.CustomerID, p => p);

آرگومان اول کلید و آرگومان دوم شیءهایی را که به‌عنوان value در lookup قرار می‌گیرند انتخاب می‌کند. indexer lookup برخلاف dictionary یک دنباله از موارد تطبیق‌یافته می‌دهد:

foreach (Purchase p in purchLookup [1])
  Console.WriteLine (p.Description);

با lookup می‌توانید queryهای SelectMany/Select را با کارایی Join/GroupJoin اجرا کنید. Join معادل SelectMany روی lookup است:

from c in customers
from p in purchLookup [c.ID]
select new { c.Name, p.Description, p.Price };
// Tom Bike 500
// Tom Holiday 2000
// Dick Bike 600
// Dick Phone 300
// ...

افزودن DefaultIfEmpty آن را outer join می‌کند:

from c in customers
from p in purchLookup [c.ID].DefaultIfEmpty()
select new {
             c.Name,
             Descript = p == null ? null : p.Description,
             Price = p == null ? (decimal?) null : p.Price
           };

GroupJoin معادل خواندن lookup داخل projection است:

from c in customers
select new {
             CustName = c.Name,
             CustPurchases = purchLookup [c.ID]
           };

پیاده‌سازی Enumerable.Join

public static IEnumerable <TResult> Join
                                    <TOuter,TInner,TKey,TResult> (
  this IEnumerable <TOuter>     outer,
  IEnumerable <TInner>          inner,
  Func <TOuter,TKey>            outerKeySelector,
  Func <TInner,TKey>            innerKeySelector,
  Func <TOuter,TInner,TResult>  resultSelector)
{
  ILookup <TKey, TInner> lookup = inner.ToLookup (innerKeySelector);
  return
    from outerItem in outer
    from innerItem in lookup [outerKeySelector (outerItem)]
    select resultSelector (outerItem, innerItem);
}

پیاده‌سازی GroupJoin شبیه Join ولی ساده‌تر است:

public static IEnumerable <TResult> GroupJoin
                                    <TOuter,TInner,TKey,TResult> (
  this IEnumerable <TOuter>     outer,
  IEnumerable <TInner>          inner,
  Func <TOuter,TKey>            outerKeySelector,
  Func <TInner,TKey>            innerKeySelector,
  Func <TOuter,IEnumerable<TInner>,TResult>  resultSelector)
{
  ILookup <TKey, TInner> lookup = inner.ToLookup (innerKeySelector);
  return
    from outerItem in outer
    select resultSelector
     (outerItem, lookup [outerKeySelector (outerItem)]);
}

اپراتور Zip

IEnumerable<TFirst>, IEnumerable<TSecond> → IEnumerable<TResult>. دو دنباله را هم‌قدم پیمایش می‌کند و تابعی را روی هر جفت عنصر اعمال می‌کند:

int[] numbers = { 3, 5, 7 };
string[] words = { "three", "five", "seven", "ignored" };
IEnumerable<string> zip = numbers.Zip (words, (n, w) => n + "=" + w);
// 3=three
// 5=five
// 7=seven

عناصر اضافه در هر یک از دنباله‌ها نادیده گرفته می‌شوند. Zip در EF Core پشتیبانی نمی‌شود.

Ordering

IEnumerable<TSource> → IOrderedEnumerable<TSource>

روششرحمعادل SQL
OrderBy, ThenByمرتب‌سازی صعودیORDER BY ...
OrderByDescending, ThenByDescendingمرتب‌سازی نزولیORDER BY ... DESC
Reverseبازگرداندن دنباله در ترتیب معکوسException thrown

اپراتورهای ordering همان عناصر را با ترتیب متفاوت برمی‌گردانند.

OrderBy، OrderByDescending، ThenBy، ThenByDescending

آرگومان OrderBy/Descendingنوع
Input sequenceIEnumerable<TSource>
Key selectorTSource => TKey
Return typeIOrderedEnumerable<TSource>
آرگومان ThenBy/Descendingنوع
Input sequenceIOrderedEnumerable<TSource>
Key selectorTSource => TKey
Query syntaxorderby expression1 [descending] [, expression2 [descending] ... ]

OrderBy با استفاده از keySelector نسخه‌ای مرتب از ورودی می‌دهد. مرتب‌سازی الفبایی:

IEnumerable<string> query = names.OrderBy (s => s);

مرتب‌سازی بر اساس طول:

IEnumerable<string> query = names.OrderBy (s => s.Length);
// { "Jay", "Tom", "Mary", "Dick", "Harry" }

ترتیب نسبی عناصری که کلید مرتب‌سازی یکسان دارند نامعین است، مگر ThenBy اضافه کنید:

IEnumerable<string> query = names.OrderBy (s => s.Length).ThenBy (s => s);
// { "Jay", "Tom", "Dick", "Mary", "Harry" }

ThenBy فقط عناصر دارای کلید برابر در مرتب‌سازی قبلی را مرتب می‌کند. می‌توان هر تعداد ThenBy زنجیره کرد:

names.OrderBy (s => s.Length).ThenBy (s => s[1]).ThenBy (s => s[0]);

معادل query syntax:

from s in names
orderby s.Length, s[1], s[0]
select s;
from s in names
orderby s.Length
orderby s[1]
...

OrderByDescending و ThenByDescending همان کار را در جهت معکوس انجام می‌دهند. query زیر خریدها را بر اساس قیمت نزولی و موارد هم‌قیمت را الفبایی می‌چیند:

dbContext.Purchases.OrderByDescending (p => p.Price)
                     .ThenBy (p => p.Description);

در query syntax:

from p in dbContext.Purchases
orderby p.Price descending, p.Description
select p;

Comparerها و Collationها

در query محلی، خود objectهای key selector الگوریتم ترتیب را از طریق پیاده‌سازی پیش‌فرض IComparable تعیین می‌کنند. با IComparer می‌توانید الگوریتم را جایگزین کنید:

names.OrderBy (n => n, StringComparer.CurrentCultureIgnoreCase);

دادن comparer در query syntax یا EF Core پشتیبانی نمی‌شود. در پایگاه داده، collation ستون الگوریتم مقایسه را تعیین می‌کند. اگر collation حساس به بزرگی/کوچکی حروف باشد، می‌توانید با ToUpper sort غیرحساس درخواست کنید:

from p in dbContext.Purchases
orderby p.Description.ToUpper()
select p;

IOrderedEnumerable و IOrderedQueryable

اپراتورهای ordering subtype ویژه‌ای از IEnumerable<T> برمی‌گردانند: در Enumerable نوع IOrderedEnumerable<TSource> و در Queryable نوع IOrderedQueryable<TSource>. این subtypeها باعث می‌شوند ThenBy ترتیب موجود را refine کند، نه جایگزین.

اعضای اضافی این subtypeها به‌صورت عمومی دیده نمی‌شوند، بنابراین مانند دنبالهٔ عادی به نظر می‌رسند، اما هنگام ساخت progressive query تفاوت نوع مهم می‌شود:

IOrderedEnumerable<string> query1 = names.OrderBy (s => s.Length);
IOrderedEnumerable<string> query2 = query1.ThenBy (s => s);

اگر query1 را IEnumerable<string> اعلام کنیم، خط دوم compile نمی‌شود چون ThenBy ورودی IOrderedEnumerable<string> می‌خواهد. با var می‌توان این نگرانی را کم کرد:

var query1 = names.OrderBy (s => s.Length);
var query2 = query1.ThenBy (s => s);

اما implicit typing مشکل دیگری می‌سازد:

var query = names.OrderBy (s => s.Length);
query = query.Where (n => n.Length > 3); // Compile-time error

کامپایلر query را IOrderedEnumerable<string> استنباط کرده، ولی Where یک IEnumerable<string> عادی برمی‌گرداند. با explicit typing یا AsEnumerable() پس از OrderBy حل می‌شود:

var query = names.OrderBy (s => s.Length).AsEnumerable();
query = query.Where (n => n.Length > 3); // OK

معادل در interpreted queryها AsQueryable است.

Grouping

روششرحمعادل SQL
GroupByدنباله را به زیردنباله‌ها گروه‌بندی می‌کند.GROUP BY
Chunkدنباله را به آرایه‌هایی با اندازهٔ ثابت گروه‌بندی می‌کند.

GroupBy

IEnumerable<TSource>→IEnumerable<IGrouping<TKey,TElement>>

آرگوماننوع
Input sequenceIEnumerable<TSource>
Key selectorTSource => TKey
Element selector (optional)TSource => TElement
Comparer (optional)IEqualityComparer<TKey>

query syntax: group element-expression by key-expression

GroupBy یک ورودی تخت را به دنباله‌ای از گروه‌ها سازمان می‌دهد. مثال: گروه‌بندی همهٔ فایل‌های Path.GetTempPath() بر اساس extension:

string[] files = Directory.GetFiles (Path.GetTempPath());
IEnumerable<IGrouping<string,string>> query =
  files.GroupBy (file => Path.GetExtension (file));

یا با implicit typing:

var query = files.GroupBy (file => Path.GetExtension (file));

پیمایش نتیجه:

foreach (IGrouping<string,string> grouping in query)
{
  Console.WriteLine ("Extension: " + grouping.Key);
  foreach (string filename in grouping)
    Console.WriteLine ("   - " + filename);
}
// Extension: .pdf
//   -- chapter03.pdf
//   -- chapter04.pdf
// Extension: .doc
//   -- todo.doc
//   -- menu.doc
//   -- Copy of menu.doc

Enumerable.GroupBy عناصر ورودی را در یک dictionary موقت از listها می‌خواند تا عناصر هم‌کلید در یک sublist قرار بگیرند و سپس دنباله‌ای از groupingها می‌دهد. grouping دنباله‌ای دارای property Key است:

public interface IGrouping <TKey,TElement> : IEnumerable<TElement>,
                                             IEnumerable
{
  TKey Key { get; }
}

به‌طور پیش‌فرض عناصر هر گروه همان input elementهای تغییرنیافته‌اند، مگر elementSelector بدهید. مثال، uppercase کردن هر عنصر:

files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper());

elementSelector مستقل از keySelector است، پس Key گروه در مثال همچنان case اصلی را حفظ می‌کند.

Extension: .pdf
  -- CHAPTER03.PDF
  -- CHAPTER04.PDF
Extension: .doc
  -- TODO.DOC

زیرمجموعه‌ها بر اساس کلید مرتب نمی‌شوند. GroupBy فقط گروه‌بندی می‌کند و ترتیب اصلی را حفظ می‌کند. برای sort باید OrderBy اضافه شود:

files.GroupBy (file => Path.GetExtension (file), file => file.ToUpper())
     .OrderBy (grouping => grouping.Key);

معادل مستقیم query syntax:

from file in files
group file.ToUpper() by Path.GetExtension (file);

همانند select، group query را «تمام» می‌کند مگر query continuation اضافه شود:

from file in files
group file.ToUpper() by Path.GetExtension (file) into grouping
orderby grouping.Key
select grouping;

query continuation در group by مفید است. مثال، حذف گروه‌هایی با کمتر از پنج فایل:

from file in files
group file.ToUpper() by Path.GetExtension (file) into grouping
where grouping.Count() >= 5
select grouping;

گاهی فقط نتیجهٔ aggregation روی هر grouping مهم است و می‌توان subsequenceها را کنار گذاشت:

string[] votes = { "Dogs", "Cats", "Cats", "Dogs", "Dogs" };
IEnumerable<string> query = from vote in votes
                            group vote by vote into g
                            orderby g.Count() descending
                            select g.Key;
string winner = query.First();    // Dogs

GroupBy در EF Core

Grouping هنگام پرس‌وجوی پایگاه داده نیز به همان شکل کار می‌کند. اگر navigation propertyها تنظیم شده باشند، نیاز به group نسبت به SQL سنتی کمتر می‌شود. برای مشتریانی با حداقل دو خرید، نیازی به group نیست:

from c in dbContext.Customers
where c.Purchases.Count >= 2
select c.Name + " has made " + c.Purchases.Count + " purchases";

نمونهٔ مناسب grouping، مجموع فروش به تفکیک سال است:

from p in dbContext.Purchases
group p.Price by p.Date.Year into salesByYear
select new {
             Year       = salesByYear.Key,
             TotalValue = salesByYear.Sum()
           };

Grouping در LINQ از GROUP BY SQL قدرتمندتر است، چون از نظر مفهومی می‌تواند همهٔ detail rowها را بدون aggregation برگرداند:

from p in dbContext.Purchases
group p by p.Date.Year

اما این حالت در EF Core کار نمی‌کند. workaround ساده این است که درست پیش از grouping، .AsEnumerable() را صدا بزنید تا grouping روی client انجام شود. اگر filtering را قبل از grouping انجام دهید، از نظر واکشی داده از server ناکارآمدتر نیست.

تفاوت دیگر با SQL سنتی: الزامی ندارید variableها یا expressionهایی را که برای grouping یا sorting استفاده شده‌اند در projection نهایی بیاورید.

Grouping با چند کلید

با anonymous type می‌توان composite key ساخت:

from n in names
group n by new { FirstLetter = n[0], Length = n.Length };

Custom equality comparer

در query محلی می‌توانید comparer سفارشی به GroupBy بدهید، هرچند معمولاً تغییر key selector کافی است. برای grouping غیرحساس به بزرگی حروف:

group n by n.ToUpper()

Chunk

IEnumerable<TSource>→IEnumerable<TElement[]>. آرگومان‌ها: input sequence از نوع IEnumerable<TSource> و size از نوع int. Chunk در .NET 6 معرفی شد و دنباله را به chunkهایی با اندازهٔ داده‌شده (یا کمتر، اگر عناصر کافی نمانده باشد) تقسیم می‌کند:

foreach (int[] chunk in new[] { 1, 2, 3, 4, 5, 6, 7, 8 }.Chunk (3))
  Console.WriteLine (string.Join (", ", chunk));
// 1, 2, 3
// 4, 5, 6
// 7, 8

Set Operators

IEnumerable<TSource>, IEnumerable<TSource>→IEnumerable<TSource>

روششرحمعادل SQL
Concatعناصر دو دنباله را پشت سر هم برمی‌گرداند.UNION ALL
Union, UnionByترکیب عناصر دو دنباله بدون تکراری‌ها.UNION
Intersect, IntersectByعناصر موجود در هر دو دنباله.WHERE ... IN (...)
Except, ExceptByعناصر موجود در دنبالهٔ اول و غایب در دومی.EXCEPT یا WHERE ... NOT IN (...)

Concat، Union، UnionBy

Concat همهٔ عناصر دنبالهٔ اول و سپس دومی را می‌دهد؛ Union همان کار را با حذف duplicate انجام می‌دهد:

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };
IEnumerable<int>
  concat = seq1.Concat (seq2),    // { 1, 2, 3, 3, 4, 5 }
  union  = seq1.Union  (seq2);    // { 1, 2, 3, 4, 5 }

اعلام صریح type argument وقتی دنباله‌ها type متفاوت ولی base type مشترک دارند مفید است؛ مثلاً API Reflection، MethodInfo و PropertyInfo را دارد که base class مشترک MemberInfo دارند.

MethodInfo[] methods = typeof (string).GetMethods();
PropertyInfo[] props = typeof (string).GetProperties();
IEnumerable<MemberInfo> both = methods.Concat<MemberInfo> (props);

می‌توان پیش از concatenation متدها را فیلتر کرد:

var methods = typeof (string).GetMethods().Where (m => !m.IsSpecialName);
var props = typeof (string).GetProperties();
var both = methods.Concat<MemberInfo> (props);

این مثال متکی به variance پارامتر نوع interface است: IEnumerable<MethodInfo> به‌طور covariant به IEnumerable<MemberInfo> تبدیل می‌شود و نشان می‌دهد variance چگونه رفتار طبیعی‌تر مورد انتظار را ممکن می‌کند.

UnionBy در .NET 6 معرفی شد و یک key selector می‌گیرد تا duplicate بودن را تعیین کند. union غیرحساس به case:

string[] seq1 = { "A", "b", "C" };
string[] seq2 = { "a", "B", "c" };
var union = seq1.UnionBy (seq2, x => x.ToUpperInvariant());
// { "A", "b", "C" }

همین کار با Union و equality comparer:

var union = seq1.Union (seq2, StringComparer.InvariantCultureIgnoreCase);

Intersect، IntersectBy، Except و ExceptBy

Intersect اشتراک دو دنباله و Except عناصر دنبالهٔ اول را که در دومی نیستند می‌دهد:

int[] seq1 = { 1, 2, 3 }, seq2 = { 3, 4, 5 };
IEnumerable<int>
  commonality = seq1.Intersect (seq2),    // { 3 }
  difference1 = seq1.Except    (seq2),    // { 1, 2 }
  difference2 = seq2.Except    (seq1);    // { 4, 5 }

Enumerable.Except درون خود ابتدا عناصر collection اول را در dictionary بار می‌کند و سپس عناصر موجود در دنبالهٔ دوم را از dictionary حذف می‌کند. معادل SQL یک NOT EXISTS یا NOT IN subquery است:

SELECT number FROM numbers1Table
WHERE number NOT IN (SELECT number FROM numbers2Table)

IntersectBy و ExceptBy (از .NET 6) اجازه می‌دهند پیش از equality comparison یک key selector اعمال شود.

Conversion Methods

LINQ عمدتاً با دنباله‌ها، یعنی collectionهای IEnumerable<T> کار می‌کند. روش‌های conversion به/از انواع دیگر collection تبدیل می‌کنند:

روششرح
OfTypeIEnumerable را به IEnumerable<T> تبدیل و عناصر با type نامناسب را حذف می‌کند.
Castتبدیل به IEnumerable<T> و exception در صورت عنصر با type نامناسب.
ToArrayIEnumerable<T> به T[]
ToListبه List<T>
ToDictionaryبه Dictionary<TKey,TValue>
ToLookupبه ILookup<TKey,TElement>
AsEnumerableupcast به IEnumerable<T>
AsQueryablecast/convert به IQueryable<T>

OfType و Cast

یک IEnumerable nongeneric را می‌گیرند و IEnumerable<T> generic تولید می‌کنند:

ArrayList classicList = new ArrayList();
classicList.AddRange ( new int[] { 3, 4, 5 } );
IEnumerable<int> sequence1 = classicList.Cast<int>();

تفاوت هنگام عنصر ناسازگار است: Cast exception می‌دهد، OfType عنصر ناسازگار را نادیده می‌گیرد:

DateTime offender = DateTime.Now;
classicList.Add (offender);
IEnumerable<int>
  sequence2 = classicList.OfType<int>(), // OK
  sequence3 = classicList.Cast<int>();   // Throws exception

قواعد سازگاری عنصر دقیقاً از اپراتور is در C# پیروی می‌کند؛ فقط reference conversion و unboxing conversion را در نظر می‌گیرد. بخشی از پیاده‌سازی OfType:

public static IEnumerable<TSource> OfType <TSource> (IEnumerable source)
{
  foreach (object element in source)
    if (element is TSource)
      yield return (TSource)element;
}

Cast پیاده‌سازی مشابه دارد، فقط تست type compatibility را حذف می‌کند:

public static IEnumerable<TSource> Cast <TSource> (IEnumerable source)
{
  foreach (object element in source)
    yield return (TSource)element;
}

نتیجهٔ این پیاده‌سازی‌ها آن است که از Cast برای numeric conversion یا custom conversion نمی‌توان استفاده کرد؛ برای آن‌ها باید Select به‌کار برد. Cast به‌اندازهٔ cast operator خود C# منعطف نیست:

int i = 3;
long l = i;         // Implicit numeric conversion int->long
int i2 = (int) l;   // Explicit numeric conversion long->int

تلاش برای تبدیل دنبالهٔ int به long:

int[] integers = { 1, 2, 3 };
IEnumerable<long> test1 = integers.OfType<long>();
IEnumerable<long> test2 = integers.Cast<long>();

در پیمایش، test1 صفر عنصر می‌دهد و test2 exception. در OfType عبارت (element is long) برای int false است چون رابطهٔ inheritance وجود ندارد.

راه‌حل استفاده از Select عادی است:

IEnumerable<long> castLong = integers.Select (s => (long) s);

OfType و Cast برای downcast عناصر در input sequence generic هم مفیدند؛ مثلاً اگر IEnumerable<Fruit> دارید، OfType<Apple> فقط سیب‌ها را می‌دهد؛ این قابلیت به‌ویژه در LINQ to XML کاربرد دارد.

Cast در query syntax پشتیبانی می‌شود: type را پیش از range variable بنویسید:

from TreeNode node in myTreeView.Nodes
...

ToArray، ToList، ToDictionary، ToHashSet، ToLookup

ToArray، ToList و ToHashSet نتیجه را به array، List<T> یا HashSet<T> می‌ریزند و هنگام اجرا input sequence را فوراً enumerate می‌کنند.

آرگومان‌های ToDictionary و ToLookup
آرگوماننوع
Input sequenceIEnumerable<TSource>
Key selectorTSource => TKey
Element selector (optional)TSource => TElement
Comparer (optional)IEqualityComparer<TKey>

ToDictionary نیز immediate execution را تحمیل می‌کند و نتیجه را در Dictionary می‌نویسد. key selector باید برای هر ورودی مقدار یکتا بدهد وگرنه exception. در مقابل، ToLookup چند عنصر با یک کلید را می‌پذیرد.

AsEnumerable و AsQueryable

AsEnumerable دنباله را به IEnumerable<T> upcast می‌کند و کامپایلر را مجبور می‌کند اپراتورهای بعدی را به Enumerable bind کند نه Queryable. AsQueryable اگر دنباله IQueryable<T> را پیاده‌سازی کند آن را downcast می‌کند، در غیر این صورت wrapper از IQueryable<T> روی query محلی می‌سازد.

Element Operators

IEnumerable<TSource>→TSource

روششرحمعادل SQL
First, FirstOrDefaultاولین عنصر، به‌صورت اختیاری مطابق predicate.SELECT TOP 1 ... ORDER BY ...
Last, LastOrDefaultآخرین عنصر، به‌صورت اختیاری مطابق predicate.SELECT TOP 1 ... ORDER BY ... DESC
Single, SingleOrDefaultمانند First/FirstOrDefault، اما اگر بیش از یک match باشد exception.
ElementAt, ElementAtOrDefaultعنصر در موقعیت مشخص.Exception thrown
MinBy, MaxByعنصر دارای کوچک‌ترین/بزرگ‌ترین مقدار.Exception thrown
DefaultIfEmptyاگر دنباله خالی باشد دنبالهٔ تک‌عنصری با default(TSource).OUTER JOIN

متدهای دارای پسوند OrDefault اگر ورودی خالی یا بدون match باشد به‌جای exception مقدار default(TSource) برمی‌گردانند. این مقدار برای reference type برابر null، برای bool برابر false و برای numeric type صفر است.

First، Last و Single

int[] numbers  = { 1, 2, 3, 4, 5 };
int first      = numbers.First();                      // 1
int last       = numbers.Last();                       // 5
int firstEven  = numbers.First  (n => n % 2 == 0);     // 2
int lastEven   = numbers.Last   (n => n % 2 == 0);     // 4
int firstBigError  = numbers.First          (n => n > 10); // Exception
int firstBigNumber = numbers.FirstOrDefault (n => n > 10); // 0

برای جلوگیری از exception، Single دقیقاً یک match و SingleOrDefault صفر یا یک match می‌خواهد:

int onlyDivBy3 = numbers.Single (n => n % 3 == 0);   // 3
int divBy2Err  = numbers.Single (n => n % 2 == 0);   // Error: 2 & 4 match
int singleError = numbers.Single          (n => n > 10);      // Error
int noMatches   = numbers.SingleOrDefault (n => n > 10);      // 0
int divBy2Error = numbers.SingleOrDefault (n => n % 2 == 0);  // Error

Single سخت‌گیرترین عضو این خانواده است؛ FirstOrDefault و LastOrDefault تحمل بیشتری دارند. در EF Core، Single اغلب برای گرفتن row بر اساس primary key استفاده می‌شود:

Customer cust = dataContext.Customers.Single (c => c.ID == 3);

ElementAt

عنصر nام را برمی‌گزیند:

int[] numbers  = { 1, 2, 3, 4, 5 };
int third      = numbers.ElementAt (2);            // 3
int tenthError = numbers.ElementAt (9);            // Exception
int tenth      = numbers.ElementAtOrDefault (9);   // 0

Enumerable.ElementAt اگر input sequence، IList<T> باشد از indexer آن استفاده می‌کند؛ وگرنه n بار enumerate کرده و عنصر بعدی را برمی‌گرداند. در EF Core پشتیبانی نمی‌شود.

MinBy و MaxBy

در .NET 6 معرفی شدند و با key selector، عنصر دارای کوچک‌ترین یا بزرگ‌ترین مقدار را می‌دهند:

string[] names = { "Tom", "Dick", "Harry", "Mary", "Jay" };
Console.WriteLine (names.MaxBy (n => n.Length));   // Harry
Console.WriteLine (names.Max   (n => n.Length));   // 5
Console.WriteLine (names.MinBy (n => n.Length));   // Tom

اگر چند عنصر min/max مشترک داشته باشند، اولین مورد برگردانده می‌شود.

اگر input sequence خالی باشد، MinBy و MaxBy برای element type nullable مقدار null می‌دهند و برای type غیرnullable exception می‌دهند.

DefaultIfEmpty

اگر دنبالهٔ ورودی خالی باشد، دنبالهٔ تک‌عنصری با default(TSource) برمی‌گرداند؛ وگرنه ورودی را بدون تغییر می‌دهد. کاربرد اصلی آن در flat outer joinهاست.

Aggregation Methods

IEnumerable<TSource>→scalar

روششرحمعادل SQL
Count, LongCountتعداد عناصر، با predicate اختیاری.COUNT (...)
Min, Maxکوچک‌ترین یا بزرگ‌ترین عنصر.MIN (...), MAX (...)
Sum, Averageمجموع یا میانگین عددی.SUM (...), AVG (...)
Aggregateaggregation سفارشی.Exception thrown

Count و LongCount

Count دنباله را پیمایش و تعداد را برمی‌گرداند:

int fullCount = new int[] { 5, 6, 7 }.Count();    // 3

پیاده‌سازی Enumerable.Count بررسی می‌کند آیا ورودی ICollection<T> است؛ اگر بله مستقیم Count آن را می‌خواند، وگرنه همهٔ عناصر را enumerate و شمارنده را افزایش می‌دهد. predicate اختیاری:

int digitCount = "pa55w0rd".Count (c => char.IsDigit (c));   // 3

LongCount همان کار را با عدد ۶۴ بیتی انجام می‌دهد و دنباله‌های بیش از دو میلیارد عنصر را پوشش می‌دهد.

Min و Max

کوچک‌ترین یا بزرگ‌ترین عنصر دنباله را می‌دهند:

int[] numbers = { 28, 32, 14 };
int smallest = numbers.Min();  // 14
int largest  = numbers.Max();  // 32

اگر selector بدهید، هر عنصر ابتدا project می‌شود:

int smallest = numbers.Max (n => n % 10);  // 8

اگر خود itemها ذاتاً قابل مقایسه نباشند و IComparable<T> را پیاده‌سازی نکنند، selector الزامی است:

Purchase runtimeError = dbContext.Purchases.Min ();             // Error
decimal? lowestPrice = dbContext.Purchases.Min (p => p.Price);  // OK

selector هم روش مقایسه و هم نوع نتیجه را تعیین می‌کند. برای گرفتن خودِ ارزان‌ترین purchase به subquery نیاز است:

Purchase cheapest = dbContext.Purchases
  .Where (p => p.Price == dbContext.Purchases.Min (p2 => p2.Price))
  .FirstOrDefault();

در این مورد می‌توان به‌جای aggregation از OrderBy سپس FirstOrDefault نیز استفاده کرد.

Sum و Average

مانند Min/Max استفاده می‌شوند:

decimal[] numbers  = { 3, 4, 8 };
decimal sumTotal   = numbers.Sum();               // 15
decimal average    = numbers.Average();           // 5
int combinedLength = names.Sum (s => s.Length);   // 19

Sum و Average از نظر type محدودترند؛ تعریفشان برای numeric typeهای int, long, float, double, decimal و nullableهای آن‌ها hard-wire شده است. Min/Max می‌توانند روی هر چیزی که IComparable<T> دارد، مانند string، کار کنند.

Average همیشه بر اساس selector type یکی از decimal، float یا double را می‌دهد:

Selector typeResult type
decimaldecimal
floatfloat
int, long, doubledouble

پس این compile نمی‌شود:

int avg = new int[] { 3, 4 }.Average();

ولی این می‌شود:

double avg = new int[] { 3, 4 }.Average();   // 3.5

Average ورودی را ضمنی upscale می‌کند تا precision از دست نرود؛ در مثال integerها میانگین 3.5 می‌دهند. در query پایگاه داده، Sum و Average به aggregationهای استاندارد SQL ترجمه می‌شوند. مشتریانی با میانگین خرید بالاتر از ۵۰۰ دلار:

from c in dbContext.Customers
where c.Purchases.Average (p => p.Price) > 500
select c.Name;

Aggregate

الگوریتم accumulation سفارشی برای aggregationهای غیرمعمول می‌دهد؛ در EF Core پشتیبانی نمی‌شود و کاربرد تخصصی دارد. انجام کار Sum:

int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate (0, (total, n) => total + n);   // 6

آرگومان اول seed و آرگومان دوم expression به‌روزرسانی accumulated value با عنصر جدید است. آرگومان سوم اختیاری می‌تواند نتیجهٔ نهایی را project کند.

Unseeded aggregations

می‌توانید seed را حذف کنید؛ در این صورت عنصر اول seed ضمنی می‌شود و aggregation از عنصر دوم ادامه می‌یابد:

int[] numbers = { 1, 2, 3 };
int sum = numbers.Aggregate ((total, n) => total + n);   // 6

نتیجه مثل قبل است، اما محاسبه متفاوت: قبلاً 0+1+2+3 و اکنون 1+2+3. تفاوت با ضرب روشن‌تر می‌شود:

int[] numbers = { 1, 2, 3 };
int x = numbers.Aggregate (0, (prod, n) => prod * n);   // 0
int y = numbers.Aggregate (   (prod, n) => prod * n);   // 6

aggregation بدون seed می‌تواند بدون overload ویژه parallel شود، اما trapهایی دارد.

Trapهای unseeded aggregation

متدهای بدون seed برای delegateهایی طراحی شده‌اند که commutative و associative باشند. در غیر این صورت نتیجه در query عادی غیرشهودی و در PLINQ nondeterministic می‌شود. تابع زیر هیچ‌کدام نیست:

(total, n) => total + n * n

برای مربع‌های ۲، ۳ و ۴:

int[] numbers = { 2, 3, 4 };
int sum = numbers.Aggregate ((total, n) => total + n * n); // 27
// انتظار ریاضی: 2*2 + 3*3 + 4*4 = 29
// محاسبه واقعی: 2 + 3*3 + 4*4 = 27

یک اصلاح ظاهری، افزودن 0 به ابتدای دنباله است:

int[] numbers = { 0, 2, 3, 4 };

این راه نه‌تنها زیبا نیست، در parallel هم غلط می‌شود چون PLINQ بر اساس فرض associativity چند عنصر را به‌عنوان seed انتخاب می‌کند. اگر تابع را f(total,n)=total+n*n بنامیم، LINQ to Objects محاسبه می‌کند:

f(f(f(0, 2),3),4)

اما PLINQ ممکن است:

f(f(0,2),f(3,4))
First partition:   a = 0 + 2*2  (= 4)
Second partition:  b = 3 + 4*4  (= 19)
Final result:          a + b*b  (= 365)
OR EVEN:               b + a*a  (= 35)

دو راه‌حل خوب وجود دارد: اول، seeded aggregation با seed صفر (در PLINQ برای parallel ماندن نیاز به overload ویژه دارد). دوم، بازساخت query به‌گونه‌ای که aggregation commutative و associative باشد:

int sum = numbers.Select (n => n * n).Aggregate ((total, n) => total + n);
Math.Sqrt (numbers.Average (n => n * n))

double mean = numbers.Average();
double sdev = Math.Sqrt (numbers.Average (n =>
              {
                double dif = n - mean;
                return dif * dif;
              }));

این محاسبات امن، کارآمد و کاملاً parallelizable هستند. کتاب برای یک custom aggregation واقعی‌تر به فصل ۲۲ ارجاع می‌دهد.

Quantifiers

IEnumerable<TSource>→bool

روششرحمعادل SQL
Containsآیا دنباله عنصر داده‌شده را دارد؟WHERE ... IN (...)
Anyآیا حداقل یک عنصر predicate را ارضا می‌کند؟WHERE ... IN (...)
Allآیا همهٔ عناصر predicate را ارضا می‌کنند؟WHERE (...)
SequenceEqualآیا دنبالهٔ دوم عناصری دقیقاً یکسان و با ترتیب یکسان دارد؟

Contains و Any

Contains آرگومان TSource و Any predicate اختیاری می‌گیرد:

bool hasAThree = new int[] { 2, 3, 4 }.Contains (3);       // true
bool hasAThree2 = new int[] { 2, 3, 4 }.Any (n => n == 3); // true
bool hasABigNumber = new int[] { 2, 3, 4 }.Any (n => n > 10); // false

Any بدون predicate وقتی دست‌کم یک عنصر باشد true می‌دهد:

bool hasABigNumber = new int[] { 2, 3, 4 }.Where (n => n > 10).Any();

Any در subqueryها بسیار مفید و در query پایگاه داده رایج است:

from c in dbContext.Customers
where c.Purchases.Any (p => p.Price > 1000)
select c

All و SequenceEqual

All وقتی همهٔ عناصر predicate را برآورده کنند true است. مشتریانی که همهٔ خریدهایشان کمتر از ۱۰۰ دلار است:

dbContext.Customers.Where (c => c.Purchases.All (p => p.Price < 100));

SequenceEqual دو دنباله را مقایسه می‌کند؛ برای true، عناصر و ترتیب باید یکسان باشند. equality comparer اختیاری است و پیش‌فرض EqualityComparer<T>.Default.

Generation Methods

void→IEnumerable<TResult>

روششرح
Emptyدنبالهٔ خالی می‌سازد.
Repeatدنباله‌ای از عنصر تکراری می‌سازد.
Rangeدنباله‌ای از integerها می‌سازد.

Empty، Repeat و Range متدهای static و nonextension هستند که دنباله‌های محلی ساده می‌سازند.

Empty

فقط type argument می‌خواهد:

foreach (string s in Enumerable.Empty<string>())
  Console.Write (s); // <nothing>

در کنار ??، Empty عکس DefaultIfEmpty عمل می‌کند. آرایهٔ jagged زیر یک null دارد و SelectMany ساده شکست می‌خورد:

int[][] numbers =
{
  new int[] { 1, 2, 3 },
  new int[] { 4, 5, 6 },
  null
};
IEnumerable<int> flat = numbers.SelectMany (innerArray => innerArray);

با Empty و ??:

IEnumerable<int> flat = numbers
  .SelectMany (innerArray => innerArray ?? Enumerable.Empty <int>());
foreach (int i in flat)
  Console.Write (i + " ");     // 1 2 3 4 5 6

Range و Repeat

Range starting index و count می‌گیرد:

foreach (int i in Enumerable.Range (5, 3))
  Console.Write (i + " ");                    // 5 6 7

Repeat یک عنصر و تعداد تکرار می‌گیرد:

foreach (bool x in Enumerable.Repeat (true, 3))
  Console.Write (x + " ");                    // True True True

پایان محتوای تخصیص‌یافته از فایل PDF برای این مقاله.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620