فصل ۳: Interfaceها، Enumها و Nested Typeها در C#
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
internal class Countdown : IEnumerator
{
int count = 11;
public bool MoveNext() => count-- > 0;
public object Current => count;
public void Reset() { throw new NotSupportedException(); }
}
میتوانید یک Object را بهصورت implicit به هر Interfaceای که implement میکند cast کنید:
IEnumerator e = new Countdown();
while (e.MoveNext())
Console.Write (e.Current); // 109876543210
گسترش یک Interface
Interfaceها میتوانند از Interfaceهای دیگر derive شوند؛ برای مثال:
public interface IUndoable { void Undo(); }
public interface IRedoable : IUndoable { void Redo(); }
IRedoable همهٔ Memberهای IUndoable را «به ارث میبرد». به بیان دیگر، Typeهایی که IRedoable را implement میکنند باید Memberهای IUndoable را نیز implement کنند.
Explicit Interface Implementation
Implement کردن چند Interface گاهی باعث collision میان Member Signatureها میشود. میتوانید چنین collisionهایی را با implement کردن صریح یک Interface Member حل کنید. مثال زیر را در نظر بگیرید:
interface I1 { void Foo(); }
interface I2 { int Foo(); }
public class Widget : I1, I2
{
public void Foo()
{
Console.WriteLine ("Widget's implementation of I1.Foo");
}
int I2.Foo()
{
Console.WriteLine ("Widget's implementation of I2.Foo");
return 42;
}
}
چون I1 و I2 Signatureهای متعارض برای Foo دارند، Widget Methodِ Foo متعلق به I2 را صریحاً implement میکند. این کار اجازه میدهد دو Method در یک Class همزمان وجود داشته باشند. تنها راه فراخوانی Memberی که صریحاً implement شده، cast کردن به Interface مربوط است:
Widget w = new Widget();
w.Foo(); // Widget's implementation of I1.Foo
((I1)w).Foo(); // Widget's implementation of I1.Foo
((I2)w).Foo(); // Widget's implementation of I2.Foo
دلیل دیگر برای Explicit Interface Implementation، پنهانکردن Memberهایی است که بسیار تخصصیاند و در use case عادی Type مزاحمت بصری ایجاد میکنند. برای نمونه، Typeی که ISerializable را implement میکند معمولاً نمیخواهد Memberهای ISerializable را آشکارا نشان دهد مگر اینکه صریحاً به آن Interface cast شود.
Implement کردن Virtualِ Interface Memberها
Interface Memberی که بهصورت implicit implement شده است بهطور پیشفرض sealed است. برای اینکه در subclass override شود باید در Base Class با virtual یا abstract علامتگذاری شود:
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
public virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
public override void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
فراخوانی Interface Member چه از طریق Base Class و چه از طریق Interface، implementationِ subclass را فراخوانی میکند:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // RichTextBox.Undo
Interface Memberی که صریحاً implement شده باشد نمیتواند virtual علامتگذاری شود و به شیوهٔ معمول نیز قابل override نیست. بااینحال میتوان آن را reimplement کرد.
Reimplement کردن Interface در subclass
یک subclass میتواند هر Interface Memberی را که قبلاً توسط Base Class implement شده، دوباره implement کند. Reimplementation، implementation یک Member را ــ وقتی از طریق Interface فراخوانی میشود ــ hijack میکند و چه Member در Base Class virtual باشد یا نباشد کار میکند.
همچنین فرقی نمیکند Member بهصورت implicit یا explicit implement شده باشد، هرچند همانطور که نشان میدهیم در حالت explicit بهتر عمل میکند.
در مثال زیر، TextBox، IUndoable.Undo را صریحاً implement میکند و بنابراین نمیتواند virtual علامتگذاری شود. برای «override» کردن آن، RichTextBox باید Methodِ Undo متعلق به IUndoable را reimplement کند:
public interface IUndoable { void Undo(); }
public class TextBox : IUndoable
{
void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox, IUndoable
{
public void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
فراخوانی Member reimplementشده از طریق Interface، implementationِ subclass را فراخوانی میکند:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
با همان تعریف RichTextBox، فرض کنید TextBox، Undo را بهصورت implicit implement کند:
public class TextBox : IUndoable
{
public void Undo() => Console.WriteLine ("TextBox.Undo");
}
این کار راه دیگری برای فراخوانی Undo به ما میدهد که سیستم را «میشکند»، همانطور که Case 3 نشان میدهد:
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo Case 1
((IUndoable)r).Undo(); // RichTextBox.Undo Case 2
((TextBox)r).Undo(); // TextBox.Undo Case 3
Case 3 نشان میدهد hijacking در Reimplementation فقط وقتی مؤثر است که Member از طریق Interface فراخوانی شود، نه از طریق Base Class. این معمولاً نامطلوب است زیرا میتواند Semanticهای ناسازگار ایجاد کند. بنابراین Reimplementation بیشتر بهعنوان استراتژی override کردن Interface Memberهای Explicit مناسب است.
جایگزینهای Interface Reimplementation
حتی با Explicit Member Implementation، Interface Reimplementation به چند دلیل مشکلساز است:
- subclass راهی برای فراخوانی Methodِ Base Class ندارد.
- نویسندهٔ Base Class ممکن است پیشبینی نکرده باشد که Method دوباره implement شود و پیامدهای احتمالی آن را در نظر نگرفته باشد.
وقتی subclassing از ابتدا پیشبینی نشده باشد، Reimplementation میتواند راهحل آخر مناسبی باشد. اما گزینهٔ بهتر این است که Base Class را طوری طراحی کنیم که Reimplementation هیچگاه لازم نشود. دو راه برای این کار وجود دارد:
- هنگام implement کردن implicit یک Member، در صورت مناسببودن آن را
virtual علامتگذاری کنید. - هنگام implement کردن explicit یک Member، اگر پیشبینی میکنید subclassها لازم باشد بخشی از logic را override کنند، از Pattern زیر استفاده کنید:
public class TextBox : IUndoable
{
void IUndoable.Undo() => Undo(); // Calls method below
protected virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
اگر هیچ subclassingای پیشبینی نمیکنید، میتوانید Class را sealed کنید تا Interface Reimplementation را از پیش منتفی کنید.
Interfaceها و Boxing
Convert کردن Struct به Interface باعث Boxing میشود. فراخوانی Memberی که بهصورت implicit روی Struct implement شده، باعث Boxing نمیشود:
interface I { void Foo(); }
struct S : I { public void Foo() {} }
...
S s = new S();
s.Foo(); // No boxing.
I i = s; // Box occurs when casting to interface.
i.Foo();
Default Interface Memberها
از C# 8 میتوانید Default Implementation به Interface Member اضافه کنید و implement کردن آن را اختیاری کنید:
interface ILogger
{
void Log (string text) => Console.WriteLine (text);
}
اگر بخواهید Memberی به Interface تعریفشده در یک Library محبوب اضافه کنید، این قابلیت مزیت دارد زیرا implementationهای موجود ــ بالقوه هزاران مورد ــ را نمیشکند.
Default Implementationها همیشه explicit هستند؛ بنابراین اگر Classی که ILogger را implement میکند نتواند Methodِ Log را تعریف کند، تنها راه فراخوانی آن از طریق Interface است:
class Logger : ILogger { }
...
((ILogger)new Logger()).Log ("message");
این ویژگی از مشکل Multiple Implementation Inheritance جلوگیری میکند: اگر Default Member یکسانی به دو Interface که یک Class implement میکند اضافه شود، هیچگاه دربارهٔ اینکه کدام Member فراخوانی شود ambiguity وجود ندارد.
Static Interface Memberها
Interface میتواند Memberهای static نیز اعلان کند. دو نوع Static Interface Member وجود دارد:
- Static nonvirtual Interface Memberها
- Static virtual/abstract Interface Memberها
Static nonvirtual Interface Memberها
Static nonvirtual Interface Memberها عمدتاً برای کمک به نوشتن Default Interface Memberها وجود دارند. آنها توسط Class یا Struct implement نمیشوند؛ بلکه مستقیماً مصرف میشوند. علاوه بر Method، Property، Event و Indexer، Static nonvirtual Memberها اجازهٔ Field نیز میدهند؛ این Fieldها معمولاً از کد داخل Default Member Implementationها access میشوند:
interface ILogger
{
void Log (string text) =>
Console.WriteLine (Prefix + text);
static string Prefix = "";
}
Static nonvirtual Interface Memberها بهطور پیشفرض public هستند، بنابراین از بیرون قابل access هستند:
ILogger.Prefix = "File log: ";
با افزودن Access Modifier مانند private، protected یا internal میتوانید این دسترسی را محدود کنید.
Instance Fieldها همچنان ممنوعاند. این موضوع با اصل Interfaceها همراستاست که تعریف behavior است، نه state.
Static virtual/abstract Interface Memberها
Static virtual/abstract Interface Memberها ــ از C# 11 ــ Static Polymorphism را ممکن میکنند؛ قابلیتی پیشرفته که در فصل ۴ دربارهٔ آن بحث میکنیم. Static Virtual Interface Memberها با static abstract یا static virtual علامتگذاری میشوند:
interface ITypeDescribable
{
static abstract string Description { get; }
static virtual string Category => null;
}
Class یا Struct پیادهساز باید Static Abstract Memberها را implement کند و میتواند Static Virtual Memberها را بهصورت اختیاری implement کند:
class CustomerTest : ITypeDescribable
{
public static string Description => "Customer tests"; // Mandatory
public static string Category => "Unit testing"; // Optional
}
علاوه بر Method، Property و Event، Operatorها و Conversionها نیز targetهای مجاز برای Static Virtual Interface Memberها هستند؛ به «Operator Overloading» صفحهٔ 256 مراجعه کنید. Static Virtual Interface Memberها از طریق Type Parameter دارای Constraint فراخوانی میشوند؛ پس از پوشش Generics در ادامهٔ همین فصل، این موضوع را در «Static Polymorphism» صفحهٔ 260 و «Generic Math» صفحهٔ 262 نشان خواهیم داد.
نوشتن Class در برابر Interface
بهعنوان راهنما:
- برای Typeهایی که بهطور طبیعی implementation مشترک دارند از Class و subclass استفاده کنید.
- برای Typeهایی که implementationهای مستقل دارند از Interface استفاده کنید.
Classهای زیر را در نظر بگیرید:
abstract class Animal {}
abstract class Bird : Animal {}
abstract class Insect : Animal {}
abstract class FlyingCreature : Animal {}
abstract class Carnivore : Animal {}
// Concrete classes:
class Ostrich : Bird {}
class Eagle : Bird, FlyingCreature, Carnivore {} // Illegal
class Bee : Insect, FlyingCreature {} // Illegal
class Flea : Insect, Carnivore {} // Illegal
Classهای Eagle، Bee و Flea Compile نمیشوند، زیرا Inherit کردن از چند Class ممنوع است. برای حل این مسئله باید بعضی Typeها را به Interface تبدیل کنیم. سؤال این است که کدام Typeها؟ با پیروی از قاعدهٔ کلی، میتوانیم بگوییم Insectها implementation مشترکی دارند و Birdها نیز implementation مشترکی دارند، پس Class باقی میمانند. در مقابل، Flying Creatureها mechanismهای مستقل برای پرواز و Carnivoreها strategyهای مستقل برای خوردن حیوانات دارند؛ بنابراین FlyingCreature و Carnivore را به Interface تبدیل میکنیم:
interface IFlyingCreature {}
interface ICarnivore {}
در یک سناریوی معمولی، Bird و Insect ممکن است متناظر با Windows Control و Web Control باشند؛ FlyingCreature و Carnivore ممکن است متناظر با IPrintable و IUndoable باشند.
Enumها
Enum یک Value Type ویژه است که به شما اجازه میدهد گروهی از Named Numeric Constantها را مشخص کنید. برای مثال:
public enum BorderSide { Left, Right, Top, Bottom }
میتوانیم از این Enum Type به شکل زیر استفاده کنیم:
BorderSide topSide = BorderSide.Top;
bool isTop = (topSide == BorderSide.Top); // true
هر Enum Member یک مقدار Integral زیربنایی دارد. بهطور پیشفرض:
- Underlying Valueها از Typeِ
int هستند. - Constantهای
0, 1, 2... بهصورت خودکار و بهترتیب اعلان Enum Memberها تخصیص داده میشوند.
میتوانید Integral Type دیگری مشخص کنید:
public enum BorderSide : byte { Left, Right, Top, Bottom }
همچنین میتوانید Underlying Value هر Enum Member را صریحاً مشخص کنید:
public enum BorderSide : byte { Left=1, Right=2, Top=10, Bottom=11 }
Enum Conversionها
با cast صریح میتوانید Enum Instance را به Underlying Integral Value آن و برعکس تبدیل کنید:
int i = (int) BorderSide.Left;
BorderSide side = (BorderSide) i;
bool leftOrRight = (int) side <= 2;
همچنین میتوانید یک Enum Type را صریحاً به Enum Type دیگری cast کنید. فرض کنید HorizontalAlignment چنین تعریف شده باشد:
public enum HorizontalAlignment
{
Left = BorderSide.Left,
Right = BorderSide.Right,
Center
}
ترجمهٔ میان Enum Typeها از Underlying Integral Valueها استفاده میکند:
HorizontalAlignment h = (HorizontalAlignment) BorderSide.Right;
// same as:
HorizontalAlignment h = (HorizontalAlignment) (int) BorderSide.Right;
Numeric Literalِ 0 در Enum Expression توسط Compiler بهشکل ویژه رفتار میشود و به cast صریح نیاز ندارد:
BorderSide b = 0; // No cast required
if (b == 0) ...
دو دلیل برای رفتار ویژه با 0 وجود دارد:
- اولین Member یک Enum اغلب بهعنوان مقدار «default» استفاده میشود.
- برای Combined Enum Typeها،
0 به معنی «بدون Flag» است.
Flags Enumها
میتوانید Enum Memberها را با هم ترکیب کنید. برای جلوگیری از ambiguity، Memberهای Enum قابل ترکیب به مقدارهای صریح نیاز دارند که معمولاً توانهای دو هستند:
[Flags]
enum BorderSides { None=0, Left=1, Right=2, Top=4, Bottom=8 }
یا:
enum BorderSides { None=0, Left=1, Right=1<<1, Top=1<<2, Bottom=1<<3 }
برای کار با Combined Enum Valueها از Bitwise Operatorهایی مانند | و & استفاده میکنید. این Operatorها روی Underlying Integral Valueها عمل میکنند:
BorderSides leftRight = BorderSides.Left | BorderSides.Right;
if ((leftRight & BorderSides.Left) != 0)
Console.WriteLine ("Includes Left"); // Includes Left
string formatted = leftRight.ToString(); // "Left, Right"
BorderSides s = BorderSides.Left;
s |= BorderSides.Right;
Console.WriteLine (s == leftRight); // True
s ^= BorderSides.Right; // Toggles BorderSides.Right
Console.WriteLine (s); // Left
طبق convention، هرگاه Memberهای Enum قابل ترکیب باشند باید Attributeِ Flags روی Enum Type اعمال شود. اگر چنین Enumی را بدون Attributeِ Flags اعلان کنید، همچنان میتوانید Memberها را ترکیب کنید، اما فراخوانی ToString روی Enum Instance بهجای مجموعهای از نامها یک عدد تولید میکند.
طبق convention، نام Enum Type قابل ترکیب بهصورت جمع انتخاب میشود، نه مفرد.
برای راحتی میتوانید Combination Memberها را داخل خود Enum Declaration قرار دهید:
[Flags]
enum BorderSides
{
None=0,
Left=1, Right=1<<1, Top=1<<2, Bottom=1<<3,
LeftRight = Left | Right,
TopBottom = Top | Bottom,
All = LeftRight | TopBottom
}
Enum Operatorها
Operatorهایی که با Enumها کار میکنند عبارتاند از:
= == != < > <= >= + - ^ & | ˜
+= -= ++ -- sizeof
Bitwise، Arithmetic و Comparison Operatorها نتیجهٔ پردازش Underlying Integral Valueها را برمیگردانند. Addition میان یک Enum و Integral Type مجاز است، اما میان دو Enum مجاز نیست.
مسائل Type Safety
Enum زیر را در نظر بگیرید:
public enum BorderSide { Left, Right, Top, Bottom }
چون Enum میتواند به Underlying Integral Type خود و از آن cast شود، مقدار واقعی آن ممکن است خارج از محدودهٔ Enum Memberهای قانونی باشد:
BorderSide b = (BorderSide) 12345;
Console.WriteLine (b); // 12345
Bitwise و Arithmetic Operatorها نیز میتوانند مقدارهای نامعتبر مشابهی تولید کنند:
BorderSide b = BorderSide.Bottom;
b++; // No errors
یک BorderSide نامعتبر کد زیر را میشکند:
void Draw (BorderSide side)
{
if (side == BorderSide.Left) {...}
else if (side == BorderSide.Right) {...}
else if (side == BorderSide.Top) {...}
else {...} // Assume BorderSide.Bottom
}
یک راهحل، افزودن Clauseِ else دیگر است:
...
else if (side == BorderSide.Bottom) ...
else throw new ArgumentException ("Invalid BorderSide: " + side, "side");
راهحل دیگر، بررسی صریح اعتبار Enum Value است. Method ایستای Enum.IsDefined این کار را انجام میدهد:
BorderSide side = (BorderSide) 12345;
Console.WriteLine (Enum.IsDefined (typeof (BorderSide), side)); // False
متأسفانه Enum.IsDefined برای Flagged Enumها کار نمیکند. بااینحال Helper Method زیر ــ ترفندی وابسته به رفتار Enum.ToString() ــ اگر Flagged Enum دادهشده معتبر باشد true برمیگرداند:
for (int i = 0; i <= 16; i++)
{
BorderSides side = (BorderSides)i;
Console.WriteLine (IsFlagDefined (side) + " " + side);
}
bool IsFlagDefined (Enum e)
{
decimal d;
return !decimal.TryParse(e.ToString(), out d);
}
[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
Nested Typeها
Nested Type داخل scope یک Type دیگر اعلان میشود:
public class TopLevel
{
public class Nested { } // Nested class
public enum Color { Red, Blue, Tan } // Nested enum
}
Nested Type ویژگیهای زیر را دارد:
- میتواند به Memberهای privateِ Type دربرگیرنده و هر چیز دیگری که Type دربرگیرنده به آن access دارد، دسترسی داشته باشد.
- میتوانید آن را با دامنهٔ کامل Access Modifierها اعلان کنید، نه فقط
public و internal. - Accessibility پیشفرض Nested Type،
private است، نه internal. - برای access کردن Nested Type از بیرون Type دربرگیرنده، باید نام Type دربرگیرنده را qualifier قرار دهید؛ همانند access کردن Static Memberها.
برای مثال، برای access کردن Color.Red از بیرون Classِ TopLevel باید چنین بنویسیم:
TopLevel.Color color = TopLevel.Color.Red;
همهٔ Typeها ــ Class، Struct، Interface، Delegate و Enum ــ میتوانند داخل Class یا Struct nested شوند.
مثال access کردن Member privateِ یک Type از Nested Type:
public class TopLevel
{
static int x;
class Nested
{
static void Foo() { Console.WriteLine (TopLevel.x); }
}
}
مثال اعمال Access Modifierِ protected روی Nested Type:
public class TopLevel
{
protected class Nested { }
}
public class SubTopLevel : TopLevel
{
static void Foo() { new TopLevel.Nested(); }
}
مثال reference دادن به Nested Type از بیرون Type دربرگیرنده:
public class TopLevel
{
public class Nested { }
}
class Test
{
TopLevel.Nested n;
}