الواجهة (interface) عقد: قائمة مسمّاة من الأعضاء يعد نوع بتقديمها. تقول ما يستطيع النوع فعله ولا شيء عن الكيفية. تنفّذ الأصناف والبنى الواجهات، والشيفرة المكتوبة مقابل الواجهة تعمل مع كل تنفيذ، بما في ذلك تنفيذات تُكتب لاحقًا.
التصريح بواجهة وتنفيذها
تصرّح الواجهة بأعضاء بلا أجسام. وعُرفًا يبدأ اسمها بـ 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))
حاويات حقن الاعتماديات في 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
العضو المنفّذ صراحة لا يمكن الوصول إليه إلا عبر الواجهة. يستخدم الإطار ذلك لإبعاد الضجيج عن الأنواع الشائعة: تنفّذ المصفوفات IList.Add صراحة (ترمي NotSupportedException، لأن المصفوفات ثابتة الحجم)، فلا تظهر الدالة حين تكتب array..
واجهات الإطار التي ستنفّذها
تنفيذ واجهة قياسية يوصل نوعك بشيفرة الإطار الموجودة. ثلاث منها تظهر باستمرار.
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 تعلّم نوعًا يمسك شيئًا يجب تحريره (مقبض ملف، اتصال، مؤقّت). دالتها الوحيدة، 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. وعلى هذا بُنيت الرياضيات العامة في .NET 7 (INumber<T> وwhere T : INumber<T>).
واجهة أم صنف مجرّد؟
استخدم واجهة لقدرة يمكن لأنواع غير مترابطة تشاركها، أو حين يجب أن تشارك البنى، أو حين يحتاج صنف إلى عدة أدوار من هذا النوع. واستخدم صنفًا مجرّدًا حين تكون التنفيذات تنويعات على شيء واحد وتتشارك حالة أو خوارزمية ثابتة. في صفحة الأصناف المجرّدة جدول مقارنة. وتصاميم كثيرة تستخدم الاثنين: واجهة للمستهلكين، وصنفًا أساسًا مجرّدًا ينفّذ الأجزاء المملة للمنفّذين.
أخطاء شائعة
- تنفيذ مفقود أو غير عام. كل عضو واجهة يحتاج إلى عضو عام بتوقيع مطابق، أو إلى تنفيذ صريح. وإلا CS0535 (أو CS0737 حين تكون الدالة موجودة لكنها غير عامة).
- واجهات بتنفيذ واحد "احتياطًا". تستحق الواجهة مكانها حين توجد عدة تنفيذات أو نسخة وهمية للاختبار. وإلا فهي ملف آخر للتنقل بينه.
- الواجهات المتضخمة. واجهة من عشرين عضوًا تجبر كل منفّذ على كتابة عشرين عضوًا. قسّمها إلى أدوار أصغر (
IReaderوIWriter) تجمعها الأصناف. - تحويل واجهة إلى صنف. يلغي
((StripeGateway)gateway).Refund()الفصل. إن احتاج المستدعون إلىRefundفمكانها الواجهة. - توقّع استدعاء دالة واجهة افتراضية على الصنف. يُوصل إليها عبر نوع الواجهة.
الأسئلة الشائعة
ما هي الواجهة في C#؟
الواجهة مجموعة مسمّاة من الأعضاء (دوال وخصائص وأحداث ومفهرسات) يعد نوع بتقديمها، بلا حالة مثيل. تقول interface IDiscount { decimal Apply(decimal price); } إن "أي شيء هو IDiscount يستطيع تطبيق نفسه على سعر". تنفّذها الأصناف والبنى بالنقطتين، ثم تستطيع الشيفرة العمل مع أي منها عبر نوع الواجهة.
كيف أنفّذ واجهة في C#؟
اذكرها بعد نقطتين في تصريح الصنف (بعد الصنف الأساس إن وُجد) وقدّم عضوًا عامًا لكل عضو في الواجهة: class HolidayDiscount : IDiscount { public decimal Apply(decimal price) => price * 0.9m; }. العضو المفقود هو الخطأ CS0535. ويستطيع Visual Studio وRider توليد الهياكل بإصلاح سريع.
هل يمكن لصنف في C# تنفيذ عدة واجهات؟
نعم، بقدر ما تحتاج، مفصولة بفواصل: class Invoice : IPrintable, IComparable<Invoice>, IDisposable. هذا جواب C# على الوراثة المتعددة: للصنف صنف أساس واحد لكنه يستطيع أداء أدوار كثيرة. إن صرّحت واجهتان بالعضو نفسه، يمكن لدالة عامة واحدة أن ترضي الاثنتين، أو يمكنك استخدام التنفيذ الصريح لتمنح كلًّا منهما عضوها الخاص.
ما هو التنفيذ الصريح للواجهة في C#؟
تنفيذ عضو مع اسم الواجهة قبله ودون معدّل وصول: string IExportable.Format() { ... }. لا يمكن عندها استدعاء العضو إلا عبر متغيّر من نوع الواجهة، لا عبر الصنف. يُستخدم لحل تصادم الأسماء بين واجهتين ولإبقاء أعضاء الواجهة نادرة الاستخدام خارج الواجهة العامة للصنف.
ما هي دوال الواجهة الافتراضية في C#؟
منذ C# 8 يمكن أن يكون لعضو الواجهة جسم: void Error(string m) => Write("ERROR: " + m);. تحصل الأصناف المنفّذة عليه مجانًا ويمكنها تقديم نسختها الخاصة. وُجدت الميزة كي يستطيع مؤلفو المكتبات إضافة أعضاء إلى واجهة منشورة دون كسر كل منفّذ. ويُوصل إلى الدالة الافتراضية عبر نوع الواجهة، لا عبر الصنف.
لماذا تبدأ أسماء الواجهات في C# بالحرف I؟
هذا عُرف التسمية في .NET (IEnumerable وIDisposable وIComparable<T>): حرف I كبير يليه اسم بنمط PascalCase، وغالبًا صفة تنتهي بـ "-able". لا يتطلبه المترجم، لكن اتباعه يجعل أنواع الواجهات معروفة من النظرة الأولى، خاصة في تصريح صنف يتشارك فيه الصنف الأساس والواجهات قائمة واحدة.