Menu

LINQ ב-C#: Where, Select, OrderBy, GroupBy ועוד

LINQ מוסיף אופרטורי שאילתה לכל אוסף ב-C#. למדו את תחביר המתודות ואת תחביר השאילתות, סינון עם Where, הטלה עם Select, מיון עם OrderBy ו-ThenBy, קיבוץ עם GroupBy, המלכודת של First מול FirstOrDefault, פונקציות צבירה, ואיך ביצוע נדחה משנה את הרגע שבו השאילתה רצה.

בדף הזה יש עורכים שאפשר להריץ - לערוך, להריץ ולראות את הפלט מיד.

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() כדי להריץ אותה פעם אחת ולשמור את התוצאות.

איור של שפות התכנות ב-Coddy

ללמוד תכנות עם Coddy

להתחיל