שיטות הרחבה הן חלק בסיסי ב-C# ובתכנות מונחה עצמים (OOP). שיטות הרחבה ב-C# מאפשרות לך "להרחיב" סוגים קיימים, כולל מחלקות, ממשקים או מבנים, מבלי לשנות את הקוד המקורי שלהם.

זה במיוחד שימושי כאשר אתה רוצה להוסיף פונקציות חדשות לסוג שאינו בבעלותך או שאינך יכול לשנות, כמו סוגים מספריות צד שלישי או סוגים מובנים של .NET כמו string, List<T>, וכן הלאה.

במאמר זה, תלמד כיצד להוסיף שיטות הרחבה למחלקות שלך, כמו גם למחלקות צד שלישי ומערכות.

תוכן עניינים

כיצד ליצור שיטות תוספת עבור DateTime

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

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

שיטות תוספת צריכות להיות במחלקה סטטית מכיוון ש:

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

  2. קוד מאורגן: שמירת שיטות הרחבה במחלקה סטטית שומרת על סדר. זה מאפשר לך לקבץ שיטות קשורות, ואתה יכול בקלות לכלול אותן בקוד שלך על ידי שימוש בשם המרחב המתאים.

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

ראשית, בואו ניצור מחלקה סטטית בשם DateTimeExtensions.

public static class DateTimeExtensions {

}

זה יכלול את כל ההרחבות של DateTime שאנחנו רוצים ליצור.

public static bool IsWeekend(this DateTime date)
{
    return date.DayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday;
}

הסבר:

public static bool IsWeekend: זה מגדיר שזהו שיטה סטטית בשם IsWeekend שתשיב ערך bool (נכון/שקר).

תאריך DateTime זה: המילת מפתח this כארגומנט של שיטה מציינת כי השיטה היא שיטת הרחבה. זה אומר כי השיטה תהיה הרחבה של מחלקת DateTime.

איך לרכז שיטות הרחבה מאותו סוג

כדי ששיטת ההרחבה תוכל להיות רצופה עם שאר שיטות, היא צריכה להחזיר בדרך כלל את אותו סוג כזה שהיא מרחיבה (או סוג תואם). זה מאפשר לשיטה נוספת להתקשר לתוצאת השיטה הקודמת.

using System.Globalization;

public static string ToTitleCase(this string str)
{
    return CultureInfo.CurrentCulture.TextInfo.ToTitleCase(str.ToLower());
}

public static string TrimAndAppend(this string str, string toAppend)
{
    return str.Trim() + toAppend;
}

בדוגמה לעיל, שתי השיטות ToTitleCase ו- TrimAndAppend מחזירות ערך מסוג מחרוזת, כלומר ניתן לרכז את שיטות ההרחבה כך, שתמיד ימירו את המחרוזת לאותיות ראשונות גדולות, יחלצו את כל הרווחים ויצרפו את המחרוזת שסופקה.

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

var title = "hello world   "
    .ToTitleCase()
    .TrimAndAppend("!!");

//פלט:
// שלום עולם!!

אם שיטת ההרחבה מחזירה סוג שונה (לא המקורי או סוג תואם), אינך יכול לרכז אותה. לדוגמה:

var date = new DateTime();
date.IsWeekend().AddDays(1);

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

כאן, יש לנו תאריך בשם IsWeekend() שמחזיר בוליאני. ניסינו לקרוא ל־AddDays(1) על ערך בוליאני שאינו קיים, מכיוון שמדובר בתוספת של DateTime. המהדר יכשל בבניית הקוד, ויעלה שגיאה המיידעת אותך על כך.

כיצד להחזיר את המופע לשימור

בכמה מתודות הרחבה, במיוחד אלה להגדרה (כמו זרימת תלות), מחזירים את אותו המופע כדי לאפשר שימוש בשרשור של הפעולות. זה מאפשר לך להמשיך לעבוד עם האובייקט המקורי או במצבו השונה לאורך שיחות מרובות, מאפשר פנימה.

נקח לדוגמה רשימת מכוניות.

public static List<T> RemoveDuplicates<T>(this List<T> list)
{
    // השתמש ב־Distinct כדי להסיר כפילויות ולעדכן את הרשימה
    list = list.Distinct().ToList();

    // החזר את הרשימה ששונתה כדי לאפשר שרשור מתודות
    return list;
}

public static List<T> AddRangeOfItems<T>(this List<T> list, IEnumerable<T> items)
{
    // הוסף טווח של פריטים לרשימה
    list.AddRange(items);

    // החזר את הרשימה ששונתה כדי לאפשר שרשור מתודות
    return list;  
}

עכשיו שהחזרנו את הרשימה מהמתודות האלו של הרחבה, נוכל לשרשר מתודות נוספות על אותה רשימה. לדוגמה, לאחר הסרת כפילויות עם RemoveDuplicates(), נוכל מיד לקרוא ל־AddRangeOfItems() על אותה רשימה.

לכן נוכל לעשות משהו כזה:

var existingStock = new List<string> { "Ford", "Jaguar", "Ferrari", "Ford", "Renault" };

var availableBrands = existingStock
    .RemoveDuplicates()
    .AddRangeOfItems(new[] { "Lamborghini" }); // מלאי חדש זמין

Console.WriteLine("Brands Available Now: " + string.Join(", ", availableBrands));

// פלט: מותגים זמינים כעת: Ford, Jaguar, Ferrari, Renault, Lamborghini

הסרנו כפילויות מרשימה של מותגי רכב והוספנו מלאי חדש לאותה רשימה. זה עובד מכיוון ש-RemoveDuplicates מחזירה את הרשימה, מה שמאפשר לנו לשרשר אותה עם AddRangeOfItems.

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

נאלץ גם לעדכן את RemoveDuplicates כדי לעדכן את ארגумент הרשימה המועבר, מכיוון ש-Distinct() מחזירה רשימה חדשה שאינה מוחזרת כפי שמוצג למטה, שאני חושב שתסכים שזה הרבה יותר מפורט.

public static void RemoveDuplicates<T>(this List<T> list)
{
    // קבל את האלמנטים הייחודיים ונקה את הרשימה המקורית
    var distinctItems = list.Distinct().ToList();
    list.Clear(); 

    // הוסף את הפריטים הייחודיים חזרה לרשימה המקורית
    list.AddRange(distinctItems);
}

למה אני לא יכול פשוט להוסיף את השיטות הללו למחלקה שלי?

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

הפרדת דאגות: שימוש בשיטות הרחבה שומר על הקוד שלך נקי, ועוזר להפחית את המורכבות. זה עוזר להימנע מהכבדה של המחלקה בשיטות שאולי לא בשימוש תכוף.

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

יש לנחש שאתה משתמש במחלקת FileInfo מתוך המרחב שמות System.IO כדי לעבוד עם קבצים. ייתכן שתרצה להוסיף שיטה כדי לבדוק בקלות האם קובץ הוא גדול מדי (לדוגמה, יותר מ-1 ג'יגה-בייט), אך אינך יכול לשנות את מחלקת FileInfo ישירות מכיוון שהיא שייכת למרחב שמות System.IO (כלומר, זו חלק מ-.Net).

בלי הרחבה:

var fileInfo = new FileInfo("myFile.txt");

if (fileInfo.Length > 1024 * 1024 * 1024) // גודל הקובץ גדול מ-1GB
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

עם שיטת ההרחבה:

אפשר לעשות זאת יותר ניתן לשימוש על ידי הוספת שיטת הרחבה שבודקת האם הקובץ גדול מ-1GB.

public static class FileInfoExtensions
{
    // שיטת ההרחבה, עם גודל ברירת מחדל של 1GB (ניתן לשנות)
    public static bool IsFileTooLarge(this FileInfo fileInfo, long sizeInBytes = 1024 * 1024 * 1024)
    {
        return fileInfo.Length > sizeInBytes;
    }
}

כעת ניתן להשתמש בשיטת IsFileTooLarge ישירות על אובייקטים של FileInfo, על מנת לשפר את הקוד שלך:

csharpCopy codevar fileInfo = new FileInfo("myFile.txt");

if (fileInfo.IsFileTooLarge())
{
    Console.WriteLine("The file is too large.");
}
else
{
    Console.WriteLine("The file size is acceptable.");
}

להרחבת ספריות צד שלישי וחבילות יכולה לשפר באופן משמעותי את התאימות של הקוד שלך.

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

מתי להשתמש בהרחבות

  • לשיטות שימושיות: אם יש לך שיטות שימושיות שמועילות לסוג מסוים אך אינן שייכות ישירות לסוג עצמו (לדוגמה, עיצוב, תיקון).

  • לשיפור סוגים מובנים: אם ברצונך להוסיף פונקציונליות לסוגים מובנים (כמו string או DateTime) ללא לשנות אותם.

  • כאשר ברצונך לשמור על פעולות אופציונליות: אם ברצונך לספק שיטות נוספות שמשתמשים יכולים לבחור להשתמש בהן מבלי להכריחם לכלול אותם בעיצוב המחלקה הראשית.

דוגמה לתרחיש

דמיין שיש לך מחלקת Person, וברצונך להוסיף שיטה לעיצוב שמו של האדם בצורה נאה:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

// שיטת הרחבה במחלקה סטטית
public static class PersonExtensions
{
    public static string GetFullName(this Person person)
    {
        return $"{person.FirstName} {person.LastName}";
    }
}

על ידי שימוש בשיטת הרחבה עבור GetFullName, ניתן לשמור על פשטות המחלקה Person ולהתמקד באחריות היסודית שלה, תוך כדי הצגת פונקציונליות שימושית.

מתי לא להשתמש בשיטות הרחבה

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

  • לצורך חיבור הדוק: אם שיטת ההרחבה דורשת ידע אינטימי על המצב הפרטי של המחלקה או זקוקה לגישה קבועה ללוגיקה הפנימית שלה.

  • לצורך APIs ציבוריים: כאשר מעצבים ספרייה או API שמיועדים לציבור, לעיתים עדיף לכלול שיטות נחוצות ישירות במחלקה מאשר להכריח את המשתמשים למצוא או ליצור את שיטות ההרחבה שלהם.

דברים שיש לקחת בחשבון כאשר מעצבים הרחבות

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

התנהגות מוסתרת/בלבול

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

  • מפתחים צריכים לדעת שישנם שיטות הרחבה אלו, או שהם עלולים לפספס את השימוש בהן אלא אם כן הם עובדים בסביבת פיתוח (IDE) עם תכונות כמו IntelliSense (למשל, Visual Studio, JetBrains Rider). סביבת הפיתוח הזו יכולה להציע שיטות הרחבה מקבצים או מרחבי שם אחרים כאשר היא מזהה את הסוג המתאים. ללא IDE עשירה בתכונות, המפתח יצטרך להיות מודע לשיטות ההרחבה או למצוא את התיקייה שבה הן מאוחסנות.

לא ניתן לגשת לחברים פרטיים

  • שיטות הרחבה יכולות לגשת רק לחברים (שיטות, מאפיינים, שדות) שהם ציבוריים או פנימיים.

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

דוגמה:

public class Car
{
    private string engineNumber = "12345"; // שדה פרטי

    public string Brand { get; set; } = "Ford"; // מאפיין ציבורי

    private void StartEngine() // שיטה פרטית
    {
        Console.WriteLine("Engine started");
    }
}
public static class CarExtensions
{
    public static void DisplayBrand(this Car car)
    {
        Console.WriteLine($"Brand: {car.Brand}"); // גישה למאפיין הציבורי 'Brand'
    }

    public static void TryAccessPrivateField(this Car car)
    {
        // לא ניתן לגשת למספר המנוע הפרטי
        // זה יוביל לשגיאת זמן קומפילציה.
        Console.WriteLine(car.engineNumber);
    }
}

שכפול קוד ושימוש יתר

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

    כדי להימנע מכך, ארגן את הקוד שלך בצורה יעילה. אני ממליץ לשמור את כל ההרחבות בתיקיית הרחבות או בפרויקט, קרוב למקור (בהתאם לדפוסי העיצוב המשמשים באפליקציה שלך).

  • ניצול של הרחבות: אם משתמשים בהן בצורה מופרזת, הן יכולות להעמיס על המרחב הגלובלי עם מתודות שעשויות לא להיות נדרשות כגלובליות. זה יכול לגרום לזיהום של ה-API של הסוג, מה שהופך את זה לקשה יותר להבין מהו הליבה של הקלאסה ומה נוסף דרך הרחבות.

במקרים מסוימים, עדיף לאפיין פונקציונליות במחלקות עזר נפרדות או שירותים ולא להוסיף אותה דרך מתודות הרחבה.

מסקנה

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

כפי שהודגש לאורך המאמר, יש להן שימושים רבים והן בהחלט תכונה מאוד נחמדה של מסגרת ה-Dotnet כאשר משתמשים בהן בצורה יעילה. יש להשתמש בהן כאשר זה מתאים, אך לא כתחליף לפונקציונליות ששייכת לקלאסה עצמה.