Menu

Extension Methods ב-C#: תחביר, IEnumerable, LINQ וכללים

איך עובדות מתודות הרחבה ב-C#: מתודה static במחלקה static עם this על הפרמטר הראשון, קריאה אליה כמו למתודת מופע, הרחבה של string, enum ו-IEnumerable<T>, איך LINQ בנוי מהן, למה מתודות מופע גוברות, מקבל null, והנחיית ה-using שהן צריכות.

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

מתודת הרחבה (extension method) מוסיפה מתודה לטיפוס קיים בלי לשנות אותו, בלי לרשת ממנו ובלי לעטוף אותו. כותבים מתודה סטטית רגילה, מסמנים את הפרמטר הראשון שלה ב-this, ומי שקורא לה משתמש בתחביר הנקודה כאילו היא הייתה לטיפוס מאז ומעולם:

פלט:

Extension methods let yo...
12
short
Extension...

title.Truncate(24) מתקמפל בדיוק ל-StringExtensions.Truncate(title, 24). תחביר הנקודה הוא רק נוחות: המתודה עדיין סטטית, עדיין יושבת ב-StringExtensions, ויכולה להשתמש רק באיברים הציבוריים של string.

הכללים להצהרה

  • המתודה חייבת להיות static ולשבת במחלקה שהיא static, לא גנרית ולא מקוננת בתוך מחלקה אחרת. מחלקה לא סטטית או גנרית נותנת CS1106, Extension method must be defined in a non-generic static class; מחלקה מקוננת נותנת CS1109, Extension methods must be defined in a top level static class.
  • this נכתב רק על הפרמטר הראשון, והטיפוס של הפרמטר הזה הוא הטיפוס שמרחיבים. שאר הפרמטרים רגילים.
  • המתודה עצמה יכולה להיות גנרית (this IEnumerable<T> source), גם אם המחלקה לא יכולה.
  • היא רואה רק את מה שכל קוד חיצוני רואה: איברים ציבוריים (ובתוך אותו assembly גם internal). שדות פרטיים נשארים פרטיים.

המוסכמה היא לקרוא למחלקה על שם מה שהיא מרחיבה (StringExtensions, EnumerableExtensions) ולקבץ אותן ב-namespace כמו MyApp.Extensions.

הרחבה של ממשקים ושל IEnumerable<T>

הרחבה של ממשק מוסיפה את המתודה לכל טיפוס שמממש אותו. לכן הרחבה של IEnumerable<T> נותנת את המתודה בבת אחת למערכים, לרשימות, לקבוצות, לאוספי המפתחות של מילונים ולתוצאות של שאילתות LINQ:

פלט:

9.5
50
65
f0 f3 f6

EveryNth גנרית: T מוסק מהמקבל, ולכן היא עובדת כאן על מחרוזות ועל כל טיפוס איבר אחר. מכיוון שהיא משתמשת ב-yield return, היא עצלה כמו האופרטורים המובנים של LINQ ואפשר לשרשר אותה איתם.

LINQ בנוי על extension methods

Where, Select, OrderBy, Sum, First ושאר LINQ הן מתודות הרחבה על IEnumerable<T>, שמוגדרות במחלקה הסטטית System.Linq.Enumerable. זו הסיבה ש-using System.Linq; חייב להופיע בראש הקובץ לפני ש-list.Where(...) מתקמפל, וזו הסיבה ש-LINQ עובד על כל סוג אוסף בלי שאף אחד מהם מממש מתודת Where:

פלט:

72, 88, 95
72, 88, 95

שתי השורות הן אותו קוד. תחביר ההרחבה הופך קריאות מקוננות, שנקראות מבפנים החוצה, לשרשרת שנקראת בסדר שבו השלבים קורים, וזו הסיבה העיקרית שמתודות הרחבה קיימות: הן נוספו ב-C# 3 יחד עם LINQ. העמוד על LINQ מכסה את האופרטורים.

חייבים לייבא את ה-namespace

מתודת הרחבה נמצאת בטווח רק כשה-namespace של המחלקה הסטטית שלה מיובא עם using. בלעדיו הקריאה נכשלת כאילו המתודה לא קיימת:

error CS1061: 'string' does not contain a definition for 'Truncate' and no accessible extension method 'Truncate' accepting a first argument of type 'string' could be found (are you missing a using directive or an assembly reference?)

הוסיפו using ל-namespace של ההרחבות (או, מ-C# 10, global using פעם אחת לכל הפרויקט):

פלט:

EUR 49.90

לצד השני יש יתרון: מתודת הרחבה שלא רוצים בכל מקום יכולה לשבת ב-namespace שרק חלק מהקבצים מייבאים.

מתודות מופע גוברות

כשלטיפוס כבר יש מתודת מופע מתאימה, הקומפיילר משתמש בה ולא שוקל בכלל את ההרחבה. הרחבה עם אותה חתימה היא קוד מת בכל מקום שקורא לה בתחביר הנקודה:

פלט:

Invoice (instance method): 120
Invoice (extension): 120 USD
extension: only reachable as a static call

ההרחבה עם הארגומנט האחד הפסידה למתודת המופע; זו עם שני הארגומנטים נבחרה כי אין מתודת מופע שמקבלת string. המשמעות היא גם שעדכון של ספרייה שמוסיף מתודת מופע בשם של ההרחבה שלכם מעביר בשקט את הקריאות שלכם לגרסה של הספרייה. בחרו שמות להרחבות שלא סביר שיתנגשו.

קריאה על null

מכיוון שהקריאה היא בעצם קריאה סטטית, מקבל שהוא null לא זורק חריגה במקום הקריאה. המתודה מקבלת null בתור הארגומנט הראשון ומחליטה מה זה אומר:

פלט:

True
AL
ArgumentNullException: fullName

זה נוח ש-missing.IsBlank() מחזיר true, אבל זה יכול להפתיע קוראים שמצפים שקריאה לאיבר על null תזרוק חריגה. קבלו null רק במתודות שהשם שלהן מבהיר את זה (IsBlank, OrEmpty); בכל מקום אחר זרקו ArgumentNullException כמו ש-LINQ עושה.

הרחבה של enum

ל-enum אין מתודות משלו, ולכן הוא מטרה טבעית:

enum OrderStatus { Pending, Paid, Shipped, Delivered, Cancelled }

static class OrderStatusExtensions
{
    public static bool IsFinal(this OrderStatus s) =>
        s == OrderStatus.Delivered || s == OrderStatus.Cancelled;
}

// usage: if (order.Status.IsFinal()) { ... }

Extension members ב-C# 14

עד C# 14 רק מתודות יכלו להיות הרחבות. C# 14 הוסיפה בלוקי extension, שמקבצים איברים עבור מקבל אחד ומאפשרים גם מאפייני הרחבה ואיברים סטטיים:

// C# 14 and later
public static class StringExtensions
{
    extension(string s)
    {
        public bool IsBlank => string.IsNullOrWhiteSpace(s);   // extension property
        public string Truncate(int max) => s.Length <= max ? s : s[..max] + "...";
    }
}

מתודות הרחבה קלאסיות עם פרמטר this נשארות תקפות, והן מה שכמעט כל הקוד והספריות הקיימים משתמשים בו.

מתי לכתוב אחת

מתודות הרחבה מתאימות כשהטיפוס לא שלכם (string, DateTime, ממשקים של ה-framework), כשרוצים את אותה פונקציית עזר על כל מימוש של ממשק, או כששרשרת נקראת טוב יותר מקריאות מקוננות. כשהמחלקה שלכם, הוסיפו במקום זאת מתודת מופע אמיתית: היא יכולה להשתמש במצב פרטי ומופיעה במקום שבו קוראים מחפשים אותה. והימנעו מהרחבה של object, שמכניסה את המתודה שלכם לרשימת ההשלמה של כל ערך בתוכנית.

שאלות נפוצות

מה זה extension method ב-C#?

מתודה סטטית שאפשר לקרוא לה כאילו היא מתודת מופע של טיפוס אחר. כותבים אותה במחלקה static ומוסיפים this לפני הפרמטר הראשון שלה: public static bool IsBlank(this string s) => string.IsNullOrWhiteSpace(s);. אחרי זה name.IsBlank() עובד על כל string, בלי לשנות את string ובלי לרשת ממנו.

למה ה-extension method שלי לא נמצאת?

מתודות הרחבה גלויות רק כשה-namespace שלהן מיובא. אם המחלקה הסטטית נמצאת ב-MyApp.Extensions, הוסיפו using MyApp.Extensions; לקובץ שקורא לה; אחרת הקומפיילר מדווח CS1061 ואומר שלטיפוס אין הגדרה ואין מתודת הרחבה נגישה בשם הזה. בדקו גם שהמחלקה היא static, לא גנרית ולא מקוננת.

האם extension method יכולה לדרוס מתודת מופע?

לא. הקומפיילר מחפש קודם מתודת מופע מתאימה, ושוקל מתודות הרחבה רק כשאין כזו. מתודת הרחבה עם אותו שם ואותם פרמטרים כמו מתודת מופע פשוט מתעלמים ממנה, בלי שום אזהרה, בכל מקום שקורא לה בתחביר של מופע.

אפשר לקרוא ל-extension method על null ב-C#?

כן. הקריאה מתקמפלת לקריאה למתודה סטטית עם null בתור הארגומנט הראשון, ולכן לא נזרק NullReferenceException בקריאה עצמה. המתודה מחליטה מה לעשות: לקבל null כקלט תקין (כמו פונקציית עזר בסגנון IsNullOrEmpty) או לזרוק ArgumentNullException, כמו שעושות המתודות של LINQ.

יש extension properties ב-C#?

לא לפני C# 14. עד אז רק מתודות יכולות להיות הרחבות, ולכן מה שהיה אמור להיות מאפיין נכתב כמתודה (GetFullName()). C# 14 הוסיפה בלוקי extension, שיכולים להצהיר על מאפייני הרחבה ועל איברים סטטיים, בנוסף למתודות.

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

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

להתחיל