Menu

Interface ב-C#: הצהרה, מימוש, מתודות ברירת מחדל ודוגמאות

איך עובדים ממשקים ב-C#: הצהרה על ממשק, מימוש שלו במחלקות וב-structs, שימוש בממשק כטיפוס, מימוש של כמה ממשקים בבת אחת, מימוש מפורש, ממשקי ה-framework שתממשו הכי הרבה (IComparable<T>, IEnumerable<T>, IDisposable), ומתודות ברירת מחדל בממשקים מ-C# 8.

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

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

הצהרה על ממשק ומימוש שלו

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

פלט:

15% off: 68
10 off: 70
Best for 80: 68
Best for 40: 30

BestPrice לא יודעת כלום על PercentOff או על FixedOff. היא תלויה רק ב-IDiscount, ולכן הוספה של מחלקה BuyOneGetOne בהמשך לא דורשת בה שום שינוי. הניתוק הזה הוא כל הסיבה שממשקים קיימים.

כמה כללים לגבי ההצהרה:

  • איברי ממשק הם ציבוריים כברירת מחדל, וגם האיברים המממשים חייבים להיות ציבוריים (אלא אם הם ממומשים במפורש, בהמשך).
  • ממשק יכול להצהיר על מתודות, מאפיינים, אינדקסרים ואירועים. הוא לא יכול להצהיר על שדות מופע או על בנאים.
  • מחלקה שמשמיטה איבר לא מתקמפלת: שגיאה CS0535, "'FixedOff' does not implement interface member 'IDiscount.Apply(decimal)'".
  • אי אפשר ליצור ממשק עם new. יוצרים מחלקה מממשת ואפשר להחזיק אותה במשתנה מטיפוס הממשק: IDiscount d = new FixedOff(5);.

הממשק כטיפוס

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

public class Checkout
{
    private readonly IPaymentGateway gateway;          // not StripeGateway, not PayPalGateway
    public Checkout(IPaymentGateway gateway) { this.gateway = gateway; }

    public bool Pay(decimal amount) => gateway.Charge(amount);
}

// Production: new Checkout(new StripeGateway(apiKey))
// Unit test:  new Checkout(new FakeGateway(alwaysSucceeds: true))

קונטיינרים של dependency injection ב-ASP.NET Core בנויים על זה: שירותים נרשמים ומתבקשים לפי ממשק.

מימוש של כמה ממשקים

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

פלט:

INVOICE Studio rent March: 950.00
True
True

ממשקים יכולים גם לרשת מממשקים אחרים: interface IRepository<T> : IReadRepository<T> מוסיף איברים לאלה שהוא יורש, ומחלקה שמממשת את IRepository<T> חייבת לספק את שתי הקבוצות.

מימוש מפורש של ממשק

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

פלט:

Desk lamp,34.90
Desk lamp for 34.90

אפשר להגיע לאיבר שמומש במפורש רק דרך הממשק. ה-framework משתמש בזה כדי להרחיק רעש מטיפוסים נפוצים: מערכים מממשים את IList.Add במפורש (הוא זורק NotSupportedException, כי למערכים יש גודל קבוע), ולכן המתודה לא מופיעה כשמקלידים array..

ממשקים מה-framework שתממשו

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

IComparable<T> נותן לטיפוס סדר טבעי, שבו משתמשים List<T>.Sort(), Array.Sort ו-Max(). CompareTo מחזירה מספר שלילי, אפס או מספר חיובי:

פלט:

1.4, 2.9, 2.10, 10.0

מיון של אותן גרסאות כמחרוזות היה נותן 1.4, 10.0, 2.10, 2.9. בלי IComparable<T>, Sort() זורקת InvalidOperationException כי אין לה דרך להשוות שני אובייקטים של Version.

IEnumerable<T> הופך טיפוס לשמיש ב-foreach ועם LINQ. הדרך הקלה לממש את GetEnumerator היא עם yield return, שמוסבר בעמוד על IEnumerable ו-yield.

IDisposable מסמן טיפוס שמחזיק משהו שחייב להשתחרר (handle של קובץ, חיבור, טיימר). המתודה היחידה שלו, Dispose, היא מה שפקודת using קוראת לו כשהבלוק מסתיים, גם אם נזרקת חריגה:

פלט:

open sales.txt
  write to sales.txt: March total: 12400
close sales.txt
after using

מתודות ברירת מחדל בממשקים (C# 8)

הוספת איבר לממשק שכבר פורסם הייתה שוברת כל מחלקה שמימשה אותו. מאז C# 8, לאיבר של ממשק יכול להיות גוף, שמחלקות מממשות יורשות אלא אם הן מספקות מימוש משלהן:

public interface ILogger
{
    void Write(string message);

    // New in version 2 of the library. Existing implementers keep compiling.
    void Error(string message) => Write("ERROR: " + message);
}

public class ConsoleLogger : ILogger
{
    public void Write(string message) => Console.WriteLine(message);
}

ILogger log = new ConsoleLogger();
log.Error("disk full");            // ERROR: disk full

var direct = new ConsoleLogger();
// direct.Error("x");              // does not compile: the default method belongs to the interface

השורה האחרונה היא החלק שמפתיע אנשים: מתודת ברירת מחדל לא עוברת בירושה לאיברים של המחלקה עצמה, ולכן אפשר לקרוא לה רק דרך טיפוס הממשק. מתודות ברירת מחדל גם לא יכולות להשתמש בשדות מופע (לממשקים אין כאלה); הן עובדות דרך האיברים האחרים של הממשק. הן דורשות .NET Core 3.0 ואילך ואינן זמינות ב-.NET Framework.

C# 8 גם אפשרה איברים סטטיים בממשקים, ו-C# 11 הוסיפה איברי static abstract, שמאפשרים לקוד גנרי לקרוא למתודה סטטית או לאופרטור על T. על זה בנוי ה-generic math ב-.NET 7 (INumber<T>, where T : INumber<T>).

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

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

טעויות נפוצות

  • מימוש חסר או לא ציבורי. כל איבר של ממשק צריך איבר ציבורי עם חתימה תואמת, או מימוש מפורש. אחרת CS0535 (או CS0737 כשהמתודה קיימת אבל לא ציבורית).
  • ממשקים עם מימוש אחד "ליתר ביטחון". ממשק מצדיק את קיומו כשיש כמה מימושים או מימוש מזויף לבדיקות. אחרת הוא עוד קובץ לנווט אליו.
  • ממשקים שמנים. ממשק עם עשרים איברים מכריח כל מי שמממש לכתוב עשרים איברים. פצלו אותו לתפקידים קטנים יותר (IReader, IWriter) שמחלקות משלבות.
  • המרה של ממשק בחזרה למחלקה. ((StripeGateway)gateway).Refund() מבטל את הניתוק. אם מי שקורא צריך את Refund, היא שייכת לממשק.
  • ציפייה לקרוא למתודת ברירת מחדל של ממשק דרך המחלקה. מגיעים אליה דרך טיפוס הממשק.

שאלות נפוצות

מה זה interface ב-C#?

ממשק הוא קבוצה עם שם של איברים (מתודות, מאפיינים, אירועים, אינדקסרים) שטיפוס מתחייב לספק, בלי מצב של מופע. interface IDiscount { decimal Apply(decimal price); } אומר "כל דבר שהוא IDiscount יודע להחיל את עצמו על מחיר". מחלקות ו-structs מממשים אותו עם נקודתיים, ואז קוד יכול לעבוד עם כל אחד מהם דרך טיפוס הממשק.

איך מממשים interface ב-C#?

כתבו אותו אחרי נקודתיים בהצהרת המחלקה (אחרי מחלקת הבסיס, אם יש כזו) וספקו איבר ציבורי לכל איבר של הממשק: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. איבר חסר הוא שגיאה CS0535. Visual Studio ו-Rider יכולים לייצר את השלדים עם תיקון מהיר.

האם מחלקה ב-C# יכולה לממש כמה ממשקים?

כן, כמה שצריך, מופרדים בפסיקים: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. זו התשובה של C# לירושה מרובה: למחלקה יש מחלקת בסיס אחת, אבל היא יכולה למלא תפקידים רבים. אם שני ממשקים מצהירים על אותו איבר, מתודה ציבורית אחת יכולה לספק את שניהם, או שאפשר להשתמש במימוש מפורש כדי לתת לכל אחד מימוש משלו.

מה זה explicit interface implementation ב-C#?

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

מה הן default interface methods ב-C#?

מאז C# 8, לאיבר של ממשק יכול להיות גוף: void Error(string m) => Write("ERROR: " + m);. מחלקות מממשות מקבלות אותו בחינם ורשאיות לספק מימוש משלהן. היכולת קיימת כדי שכותבי ספריות יוכלו להוסיף איברים לממשק שכבר פורסם בלי לשבור כל מי שמממש אותו. מגיעים למתודת ברירת מחדל דרך טיפוס הממשק, לא דרך המחלקה.

למה שמות של ממשקים ב-C# מתחילים ב-I?

זו מוסכמת השמות של .NET (IEnumerable, IDisposable, IComparable<T>): I גדולה ואחריה שם ב-PascalCase, לעיתים קרובות תואר שמסתיים ב-"-able". הקומפיילר לא מחייב את זה, אבל ההקפדה עליה הופכת טיפוסי ממשק לקלים לזיהוי במבט אחד, במיוחד בהצהרה של מחלקה שבה מחלקת הבסיס והממשקים חולקים רשימה אחת.

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

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

להתחיל