LINQ (Language Integrated Query) היא ספרייה של מתודות לסינון, שינוי, מיון, קיבוץ וסיכום של רצפים. הוסיפו using System.Linq; וכל מערך, List<T>, Dictionary וכל IEnumerable<T> אחר מקבלים מתודות כמו Where, Select ו-OrderBy, שכל אחת מהן מקבלת lambda שאומרת מה לעשות עם איבר אחד.
שאילתה ראשונה: תחביר מתודות ותחביר שאילתות
ל-LINQ יש שני סימונים. תחביר המתודות משרשר קריאות; תחביר השאילתות נקרא כמו SQL. הקומפיילר הופך את תחביר השאילתות לאותן קריאות למתודות, ולכן הם מפיקים תוצאות זהות.
פלט:
95, 91, 88
95, 91, 88
תחביר המתודות נפוץ יותר בקוד מודרני, וחלק מהאופרטורים (Take, Distinct, Count, First, ToList) קיימים רק כמתודות. תחביר השאילתות בולט ב-joins ובשאילתות שמכניסות משתני ביניים עם let. שאר העמוד משתמש בתחביר המתודות ומראה את תחביר השאילתות כשהוא מוסיף משהו.
Where ו-Select: סינון והטלה
Where שומר את האיברים שמתאימים לתנאי. Select הופך כל איבר למשהו אחר. יחד הם מכסים את רוב השאילתות היומיומיות.
פלט:
Mug, Kettle
MUG $8.50 | LAMP $32.00 | KETTLE $27.00 | RUG $120.00
Lamp: 38.40
Rug: 144.00
1. Mug 2. Lamp 3. Kettle 4. Rug
Where אף פעם לא משנה איברים, ו-Select אף פעם לא משנה את הכמות שלהם. new { p.Name, WithTax = ... } יוצר טיפוס אנונימי, מחלקה שהקומפיילר כותב בשבילכם, שאת המאפיינים שלה קוראים בתחביר נקודה רגיל. אין לו שם שאפשר לכתוב, ולכן משתנה שמחזיק אחד כזה חייב להיות מוצהר עם var, והוא מתאים לתוצאות שנשארות בתוך מתודה אחת.
OrderBy, ThenBy ומיון לפי כמה מפתחות
פלט:
Ana 60
Ana 15
Ben 90
Ben 40
Chloe 25
Ben, Ana, Ben, Chloe, Ana
OrderBy יציב: איברים עם מפתחות שווים נשארים בסדר המקורי שלהם. השתמשו ב-ThenBy או ב-ThenByDescending למפתח שני. OrderBy שני היה ממיין מחדש הכול לפי המפתח שלו, ומשאיר את המפתח הראשון רק כמכריע בשוויון: עדיפות הפוכה. בתחביר השאילתות, המפתחות מופרדים בפסיקים: orderby o.Customer, o.Total descending.
בניגוד ל-List<T>.Sort, OrderBy לא נוגע במקור ומחזיר רצף חדש.
First, Single, Last והגרסאות שלהם עם OrDefault
אלה מחזירים איבר אחד במקום רצף. הגרסאות הרגילות זורקות חריגה כשאין מה להחזיר; גרסאות ה-OrDefault מחזירות את ערך ברירת המחדל של הטיפוס.
פלט:
Ana
Ben
Bea
True
First: InvalidOperationException
Single: InvalidOperationException
0
החריגה מ-First על רצף ריק היא InvalidOperationException עם ההודעה "Sequence contains no elements" (או "Sequence contains no matching element" כשפרדיקט לא התאים לכלום), אחת השגיאות הכי מחופשות ב-C#. השתמשו ב-FirstOrDefault כש"אין כלום" היא תוצאה רגילה, ובדקו אם התקבל null.
השורה האחרונה מראה את המלכודת של טיפוסי ערך: ברצף של int, FirstOrDefault מחזיר 0 כששום דבר לא מתאים, ואי אפשר להבחין בינו לבין 0 אמיתי. סננו עם Where ובדקו קודם Any(), או ב-.NET 6 ואילך העבירו ברירת מחדל משלכם: scores.FirstOrDefault(s => s > 100, -1).
Single מיועד ל"בדיוק אחד": הוא זורק חריגה אם אין אף אחד או שיש יותר מאחד, מה שהופך אותו לטענה (assertion) לא פחות משאילתה.
Any, All, Count ו-Contains
פלט:
True
True
True
3
True
True
Any() עוצר בהתאמה הראשונה, ולכן items.Any() היא הדרך הנכונה לשאול "יש משהו?". items.Count() > 0 קורא את המאפיין Count כשהמקור הוא אוסף כמו רשימה או מערך, אבל על שאילתה או רצף עצל אחר הוא עובר קודם על כל האיברים. All על רצף ריק מחזיר true, כי אין אף איבר שנכשל בתנאי.
Sum, Average, Min ו-Max
פלט:
Total: 28.25
Items: 7
Priciest: 7.25
Average price: 4.92
0
Average: InvalidOperationException
Sum של רצף ריק הוא 0, אבל Average, Min ו-Max של רצף ריק של מספרים זורקים InvalidOperationException: אין תשובה הגיונית. הגנו עם Any(), או השתמשו קודם ב-DefaultIfEmpty(): empty.DefaultIfEmpty(0).Average().
Distinct, Take ו-Skip
פלט:
home, shop, cart
Page 1: 1 2 3 4 5 6 7 8 9 10
Page 2: 11 12 13 14 15 16 17 18 19 20
Page 3: 21 22 23
12, 15
Distinct שומר את המופע הראשון של כל ערך לפי הסדר. Skip(n).Take(m) היא התבנית הסטנדרטית לחלוקה לעמודים, ו-TakeWhile/SkipWhile עוצרים או מתחילים באיבר הראשון שנכשל בתנאי. Enumerable.Range(start, count) מייצר רצף של מספרים שלמים בלי מערך.
GroupBy
GroupBy ממיין איברים לדליים לפי מפתח. לכל קבוצה יש Key והיא עצמה רצף, ולכן אפשר לבצע עליה צבירה.
פלט:
North: 3 sales, total 600
South: 2 sales, total 400
East: 1 sales, total 350
Top rep: Ana 400
North -> Ana/Chloe/Ana; South -> Ben/Eli; East -> Dev
הקבוצות יוצאות בסדר שבו המפתחות שלהן מופיעים לראשונה במקור (North, South, East), לא ממוינות; הוסיפו OrderBy(g => g.Key) אם צריך מיון. בתחביר השאילתות, group X by K בוחר מה נכנס לכל קבוצה (כאן רק השם של הנציג) ו-into g נותן שם לקבוצה כדי שהשאילתה תוכל להמשיך.
ToList, ToArray ו-ToDictionary
המתודות To... מריצות את השאילתה ושומרות את התוצאות.
פלט:
ben@x.com
2
ToDictionary: ArgumentException
ToDictionary זורק ArgumentException כששני איברים מפיקים את אותו מפתח, בדיוק כמו Dictionary.Add. אם צפויות כפילויות, קבצו קודם (כמו ש-wordCounts עושה) או השתמשו ב-ToLookup, שממפה כל מפתח לרצף של ערכים.
ביצוע נדחה
Where, Select, OrderBy, GroupBy ורוב האופרטורים האחרים מחזירים אובייקט שאילתה ולא עושים שום עבודה עד שמשהו עובר עליו. יש לזה שתי השלכות ששווה לראות:
פלט:
25, 40, 99
converting 10
converting 25
converting 40
converting 99
Count: 4
converting 10
converting 25
converting 40
converting 99
Max: 9900
converting 10
converting 25
converting 40
converting 99
Cached: 4, 9900
השאילתה הראשונה ראתה את 99 למרות שהוא נוסף אחרי שהשאילתה נכתבה: הסינון רץ כש-string.Join עבר עליה. השאילתה השנייה הדפיסה את ההמרות שלה פעמיים כי Count() ו-Max() עברו עליה כל אחד מההתחלה. עם lambda זולה זו רק עבודה מבוזבזת; עם שאילתה למסד נתונים, קריאה מקובץ או מספר אקראי בתוך ה-lambda, שני המעברים יכולים להחזיר נתונים שונים. כשתשתמשו בתוצאה יותר מפעם אחת, ממשו אותה עם ToList() או ToArray().
אופרטורים שמחזירים ערך יחיד (Count, Sum, First, Any, Max) והמתודות To... רצים מיד. ראו IEnumerable ו-yield כדי להבין איך העצלות הזו בנויה.
טבלת עזר מהירה
| אופרטור | עושה | מחזיר |
|---|---|---|
Where(x => cond) | סינון | רצף (נדחה) |
Select(x => expr) | שינוי של כל איבר | רצף (נדחה) |
SelectMany(x => seq) | שיטוח רצפים מקוננים | רצף (נדחה) |
OrderBy, ThenBy (+Descending) | מיון, יציב | רצף (נדחה) |
GroupBy(x => key) | חלוקה לדליים לפי מפתח | רצף של קבוצות (נדחה) |
Distinct, Take, Skip | הסרת כפילויות, עמודים | רצף (נדחה) |
First, Single, Last | איבר אחד, זורק אם אין | איבר |
FirstOrDefault וכו' | איבר אחד או ברירת מחדל | איבר |
Any, All, Contains | בדיקה | bool |
Count, Sum, Min, Max, Average | צבירה | מספר |
ToList, ToArray, ToDictionary | הרצה ושמירה | אוסף |
טעויות נפוצות
First()על רצף שיכול להיות ריק. זורק "Sequence contains no elements"; השתמשו ב-FirstOrDefaultובדקו.- שתי קריאות ל-
OrderBy. המפתח השני הופך לראשי; השתמשו ב-ThenBy. - מעבר על שאילתה כמה פעמים. העבודה חוזרת על עצמה; קראו ל-
ToList()פעם אחת. - ציפייה ששאילתה תשמור תמונת מצב של המקור. היא קוראת את המקור כשעוברים עליה, לא כשכותבים אותה.
Average,Min,Maxעל רצף ריק. הם זורקים חריגה; הגנו עםAny()אוDefaultIfEmpty.Count() > 0כדי לבדוק אם יש איברים.Any()עוצר באיבר הראשון.
שאלות נפוצות
מה זה LINQ ב-C#?
LINQ (Language Integrated Query) הוא אוסף של מתודות הרחבה ב-System.Linq, כמו Where, Select, OrderBy ו-GroupBy, שעובדות על כל IEnumerable<T>: מערכים, רשימות, מילונים ותוצאות של שאילתות אחרות. ב-C# יש גם תחביר שאילתות (from x in items where ... select ...) שהקומפיילר מתרגם לאותן קריאות למתודות.
מה ההבדל בין Select ל-Where ב-LINQ?
Where מסנן: הוא שומר את האיברים שעבורם ה-lambda מחזירה true ומשאיר אותם כמו שהם. Select מטיל: הוא הופך כל איבר למשהו אחר, כמו מאפיין או אובייקט חדש, ושומר על אותה כמות. לעיתים קרובות משרשרים אותם: people.Where(p => p.Age >= 18).Select(p => p.Name).
מה ההבדל בין First ל-FirstOrDefault?
First() מחזיר את האיבר הראשון (או את ההתאמה הראשונה לפרדיקט) וזורק InvalidOperationException ("Sequence contains no elements" או "no matching element") כשאין כזה. FirstOrDefault() מחזיר במקום זאת את ערך ברירת המחדל של הטיפוס: null למחלקות ו-0 ל-int, ולכן בדקו את התוצאה לפני שמשתמשים בה.
איך ממיינים לפי שני שדות עם LINQ?
שרשרו ThenBy אחרי OrderBy: orders.OrderBy(o => o.Customer).ThenByDescending(o => o.Total). קריאה ל-OrderBy פעמיים לא עובדת: הקריאה השנייה ממיינת מחדש את כל הרצף, ולכן המפתח שלה הופך לראשי והמפתח הראשון רק מכריע בשוויון, ההפך ממה שהתכוונו.
איך GroupBy עובד ב-LINQ?
GroupBy(x => key) מחזיר קבוצה אחת לכל מפתח שונה. כל קבוצה היא IGrouping<TKey, TElement>: יש לה מאפיין Key והיא עצמה רצף של האיברים עם המפתח הזה, ולכן אפשר לקרוא עליה ל-Count(), ל-Sum() או ל-Select. הקבוצות יוצאות בסדר שבו המפתחות שלהן מופיעים לראשונה.
מה זה ביצוע נדחה (deferred execution) ב-LINQ?
רוב האופרטורים של LINQ לא רצים כשקוראים להם; הם מחזירים שאילתה שרצה בכל פעם שעוברים עליה (עם foreach, ToList(), Count() וכן הלאה). לכן שאילתה רואה שינויים שנעשו במקור אחרי שהיא הוגדרה, ומעבר כפול עליה עושה את העבודה פעמיים. קראו ל-ToList() או ל-ToArray() כדי להריץ אותה פעם אחת ולשמור את התוצאות.