فصل ۳: Inheritance، object، Struct و Access Modifierها در C#
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
اعلان Partial Method اگر با یک accessibility modifier آغاز شود، extended محسوب میشود:
public partial class Test
{
public partial void M1(); // Extended partial method
private partial void M2(); // Extended partial method
}
وجود accessibility modifier فقط روی accessibility اثر نمیگذارد؛ بلکه به Compiler میگوید اعلان را بهشکل متفاوتی پردازش کند.
Extended Partial Methodها باید implementation داشته باشند؛ اگر implement نشوند، مانند Partial Methodهای معمولی ناپدید نمیشوند. در این مثال، هم M1 و هم M2 باید implementation داشته باشند، زیرا هرکدام accessibility modifier ــ بهترتیب public و private ــ مشخص کردهاند.
از آنجا که Extended Partial Methodها نمیتوانند ناپدید شوند، میتوانند هر Typeی را return کنند و parameter از نوع out داشته باشند:
public partial class Test
{
public partial bool IsValid (string identifier);
internal partial bool TryParse (string number, out int result);
}
Operatorِ nameof
Operatorِ nameof نام هر symbol ــ Type، member، variable و غیره ــ را بهصورت String برمیگرداند:
int count = 123;
string name = nameof (count); // name is "count"
مزیت آن نسبت به مشخصکردن یک String ساده، static type checking است. ابزارهایی مانند Visual Studio میتوانند reference به symbol را درک کنند؛ بنابراین اگر symbol موردنظر را rename کنید، همهٔ referenceهای آن نیز rename میشوند.
برای مشخصکردن نام یک عضو Type، مانند Field یا Property، خود Type را نیز بیاورید. این روش هم با عضوهای static و هم instance کار میکند:
string name = nameof (StringBuilder.Length);
این expression به Length ارزیابی میشود. برای برگرداندن StringBuilder.Length چنین مینویسید:
nameof (StringBuilder) + "." + nameof (StringBuilder.Length);
Inheritance
یک Class میتواند از Class دیگری inherit کند تا Class اصلی را گسترش دهد یا customize کند. Inherit کردن از Class اجازه میدهد بهجای ساخت دوبارهٔ functionality از صفر، functionality آن Class را reuse کنید. یک Class فقط میتواند از یک Class واحد inherit کند، اما خودش میتواند توسط Classهای زیادی inherit شود و در نتیجه Class Hierarchy شکل گیرد. در این مثال، با تعریف Classی به نام Asset آغاز میکنیم:
public class Asset
{
public string Name;
}
سپس Classهایی به نام Stock و House تعریف میکنیم که از Asset inherit میکنند. Stock و House هرآنچه یک Asset دارد، بهعلاوهٔ هر عضو اضافهای که خودشان تعریف میکنند، در اختیار میگیرند:
public class Stock : Asset // inherits from Asset
{
public long SharesOwned;
}
public class House : Asset // inherits from Asset
{
public decimal Mortgage;
}
به این شکل میتوانیم از این Classها استفاده کنیم:
Stock msft = new Stock { Name="MSFT",
SharesOwned=1000 };
Console.WriteLine (msft.Name); // MSFT
Console.WriteLine (msft.SharesOwned); // 1000
House mansion = new House { Name="Mansion",
Mortgage=250000 };
Console.WriteLine (mansion.Name); // Mansion
Console.WriteLine (mansion.Mortgage); // 250000
Classهای derived یعنی Stock و House، Fieldِ Name را از Base Class یعنی Asset به ارث میبرند.
Derived Class را subclass نیز مینامند.
Base Class را superclass نیز مینامند.
Polymorphism
Referenceها polymorphic هستند. یعنی یک متغیر از Typeِ x میتواند به شیئی reference دهد که subclassِ x است. برای نمونه Method زیر را در نظر بگیرید:
public static void Display (Asset asset)
{
System.Console.WriteLine (asset.Name);
}
این Method میتواند هم یک Stock و هم یک House را نمایش دهد، زیرا هر دو Asset هستند:
Stock msft = new Stock ... ;
House mansion = new House ... ;
Display (msft);
Display (mansion);
Polymorphism بر این مبنا کار میکند که subclassها ــ Stock و House ــ همهٔ ویژگیهای Base Class خود یعنی Asset را دارند. اما عکس آن درست نیست. اگر Display طوری تغییر کند که House بپذیرد، نمیتوانید یک Asset به آن پاس دهید:
Display (new Asset()); // Compile-time error
public static void Display (House house) // Will not accept Asset
{
System.Console.WriteLine (house.Mortgage);
}
Casting و Reference Conversionها
یک Object Reference میتواند:
- بهصورت ضمنی به Reference یک Base Class، upcast شود.
- بهصورت صریح به Reference یک subclass، downcast شود.
Upcast و downcast میان Reference Typeهای سازگار، Reference Conversion انجام میدهد: یک Reference جدید ــ از نظر منطقی ــ ایجاد میشود که به همان شیء اشاره میکند. Upcast همیشه موفق است؛ downcast فقط وقتی موفق است که شیء Type مناسبی داشته باشد.
Upcasting
عملیات upcast یک Reference به Base Class را از Reference یک subclass میسازد:
Stock msft = new Stock();
Asset a = msft; // Upcast
پس از upcast، متغیر a همچنان به همان شیء Stock اشاره میکند که متغیر msft به آن اشاره دارد. خود شیء مورد reference تغییر یا convert نمیشود:
Console.WriteLine (a == msft); // True
با اینکه a و msft به شیء یکسانی اشاره میکنند، a دید محدودتری نسبت به آن شیء دارد:
Console.WriteLine (a.Name); // OK
Console.WriteLine (a.SharesOwned); // Compile-time error
خط آخر Compile-time error ایجاد میکند، زیرا متغیر a از Typeِ Asset است، حتی اگر به شیئی از Typeِ Stock اشاره کند. برای دسترسی به Fieldِ SharesOwned باید Asset را به Stock downcast کنید.
Downcasting
عملیات downcast یک Reference به subclass را از Reference یک Base Class ایجاد میکند:
Stock msft = new Stock();
Asset a = msft; // Upcast
Stock s = (Stock)a; // Downcast
Console.WriteLine (s.SharesOwned); // <No error>
Console.WriteLine (s == a); // True
Console.WriteLine (s == msft); // True
همانند upcast، فقط Referenceها تحت تأثیر قرار میگیرند، نه شیء زیربنایی. Downcast به cast صریح نیاز دارد، زیرا ممکن است در Runtime شکست بخورد:
House h = new House();
Asset a = h; // Upcast always succeeds
Stock s = (Stock)a; // Downcast fails: a is not a Stock
اگر downcast شکست بخورد، InvalidCastException پرتاب میشود. این نمونهای از runtime type checking است؛ این مفهوم را در «Static and Runtime Type Checking» صفحهٔ 140 بیشتر توضیح میدهیم.
Operatorِ as
Operatorِ as یک downcast انجام میدهد که در صورت شکست downcast، بهجای پرتاب Exception مقدار null تولید میکند:
Asset a = new Asset();
Stock s = a as Stock; // s is null; no exception thrown
این روش وقتی مفید است که بعداً بررسی کنید نتیجه null هست یا نه:
if (s != null) Console.WriteLine (s.SharesOwned);
Operatorِ as نمیتواند Custom Conversion انجام دهد ــ به «Operator Overloading» در صفحهٔ 256 مراجعه کنید ــ و Numeric Conversion نیز انجام نمیدهد:
long x = 3 as long; // Compile-time error
Operatorِ is
Operatorِ is بررسی میکند که آیا یک متغیر با یک Pattern مطابقت دارد یا نه. C# چند نوع Pattern را پشتیبانی میکند که مهمترینشان Type Pattern است؛ در این Pattern نام Type پس از کلمهٔ کلیدی is میآید.
در این context، Operatorِ is بررسی میکند آیا یک Reference Conversion موفق خواهد شد؛ به بیان دیگر، آیا شیء از Class مشخصشده derive شده است یا Interface مشخصشده را implement میکند. معمولاً پیش از downcast برای آزمایش استفاده میشود:
if (a is Stock)
Console.WriteLine (((Stock)a).SharesOwned);
Operatorِ is همچنین اگر unboxing conversion موفق باشد به true ارزیابی میشود؛ به «The object Type» در صفحهٔ 138 مراجعه کنید. بااینحال Custom یا Numeric Conversionها را در نظر نمیگیرد.
معرفی یک Pattern Variable
هنگام استفاده از Operatorِ is میتوانید متغیری را نیز معرفی کنید:
if (a is Stock s)
Console.WriteLine (s.SharesOwned);
این معادل کد زیر است:
Stock s;
if (a is Stock)
{
s = (Stock) a;
Console.WriteLine (s.SharesOwned);
}
متغیری که معرفی میکنید برای مصرف «فوری» در دسترس است، بنابراین کد زیر مجاز است:
if (a is Stock s && s.SharesOwned > 100000)
Console.WriteLine ("Wealthy");
و این متغیر بیرون از expressionِ is نیز در scope باقی میماند و کد زیر را ممکن میکند:
if (a is Stock s && s.SharesOwned > 100000)
Console.WriteLine ("Wealthy");
else
s = new Stock(); // s is in scope
Console.WriteLine (s.SharesOwned); // Still in scope
Virtual Function Memberها
Functionی که با virtual علامتگذاری شده باشد میتواند توسط subclassهایی که میخواهند implementation تخصصی ارائه کنند override شود. Methodها، Propertyها، Indexerها و Eventها همگی میتوانند virtual اعلان شوند:
public class Asset
{
public string Name;
public virtual decimal Liability => 0; // Expression-bodied property
}
Liability => 0 میانبُری برای { get { return 0; } } است. برای جزئیات بیشتر این Syntax، «Expression-bodied properties» در صفحهٔ 115 را ببینید.
یک subclass با اعمال modifierِ override یک Virtual Method را override میکند:
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage;
}
بهصورت پیشفرض Liability یک Asset برابر 0 است. Stock نیازی ندارد این رفتار را specialize کند؛ اما House Propertyِ Liability را specialize میکند تا مقدار Mortgage را برگرداند:
House mansion = new House { Name="McMansion", Mortgage=250000 };
Asset a = mansion;
Console.WriteLine (mansion.Liability); // 250000
Console.WriteLine (a.Liability); // 250000
Signatureها، return typeها و accessibilityِ Virtual Method و Method overrideشده باید یکسان باشند. Method overrideشده میتواند implementationِ Base Class را از طریق کلمهٔ کلیدی base فراخوانی کند؛ این موضوع را در «The base Keyword» صفحهٔ 134 پوشش میدهیم.
Covariant Return Typeها
از C# 9 میتوانید Method ــ یا Property get accessor ــ را طوری override کنید که Type مشتقشدهتری را برگرداند. برای مثال:
public class Asset
{
public string Name;
public virtual Asset Clone() => new Asset { Name = Name };
}
public class House : Asset
{
public decimal Mortgage;
public override House Clone() => new House
{ Name = Name, Mortgage = Mortgage };
}
این کار مجاز است، زیرا Contractِ اینکه Clone باید یک Asset برگرداند شکسته نمیشود: خروجی یک House است که خودش یک Asset ــ و چیزی تخصصیتر ــ است.
پیش از C# 9، باید Methodها را با return type کاملاً یکسان override میکردید:
public override Asset Clone() => new House { ... }
این روش همچنان کار را انجام میدهد، زیرا Methodِ Clone overrideشده بهجای Asset یک House instantiate میکند. بااینحال برای رفتار با شیء برگشتی بهعنوان House، باید بعداً downcast انجام دهید:
House mansion1 = new House { Name="McMansion", Mortgage=250000 };
House mansion2 = (House) mansion1.Clone();
Abstract Classها و Abstract Memberها
Classی که abstract اعلان شده باشد هیچگاه قابل instantiate نیست. در عوض فقط subclassهای concrete آن قابل instantiate هستند.
Abstract Classها میتوانند Abstract Member تعریف کنند. Abstract Memberها شبیه Virtual Member هستند، با این تفاوت که default implementation ارائه نمیکنند. آن implementation باید توسط subclass فراهم شود، مگر اینکه خود آن subclass نیز abstract اعلان شده باشد:
public abstract class Asset
{
// Note empty implementation
public abstract decimal NetValue { get; }
}
public class Stock : Asset
{
public long SharesOwned;
public decimal CurrentPrice;
// Override like a virtual method.
public override decimal NetValue => CurrentPrice * SharesOwned;
}
پنهانکردن اعضای Inherited
یک Base Class و subclass میتوانند اعضای یکسانی تعریف کنند. برای مثال:
public class A { public int Counter = 1; }
public class B : A { public int Counter = 2; }
گفته میشود Fieldِ Counter در Classِ B، Fieldِ Counter در Classِ A را hide میکند. معمولاً این اتفاق تصادفی رخ میدهد؛ وقتی پس از افزودن عضو یکسانی به subtype، عضوی با همان نام به Base Type اضافه میشود. به همین دلیل Compiler warning تولید میکند و سپس ابهام را به شکل زیر resolve میکند:
- Referenceهای
A ــ در زمان Compile ــ به A.Counter bind میشوند. - Referenceهای
B ــ در زمان Compile ــ به B.Counter bind میشوند.
گاهی عمداً میخواهید عضوی را hide کنید؛ در این حالت میتوانید modifierِ new را روی عضو در subclass اعمال کنید. Modifierِ new کاری جز suppress کردن Compiler warningی که در غیر این صورت ایجاد میشد انجام نمیدهد:
public class A { public int Counter = 1; }
public class B : A { public new int Counter = 2; }
Modifierِ new نیت شما را به Compiler و programmerهای دیگر منتقل میکند که عضو تکراری تصادفی نیست.
new در برابر override
Class Hierarchy زیر را در نظر بگیرید:
public class BaseClass
{
public virtual void Foo() { Console.WriteLine ("BaseClass.Foo"); }
}
public class Overrider : BaseClass
{
public override void Foo() { Console.WriteLine ("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine ("Hider.Foo"); }
}
تفاوت رفتار Overrider و Hider در کد زیر نشان داده میشود:
Overrider over = new Overrider();
BaseClass b1 = over;
over.Foo(); // Overrider.Foo
b1.Foo(); // Overrider.Foo
Hider h = new Hider();
BaseClass b2 = h;
h.Foo(); // Hider.Foo
b2.Foo(); // BaseClass.Foo
Sealing کردن Functionها و Classها
یک Function Member overrideشده میتواند implementation خود را با کلمهٔ کلیدی sealed seal کند تا subclassهای بیشتر نتوانند آن را override کنند. در مثال قبلی Virtual Function Member، میتوانستیم implementationِ Liability در House را seal کنیم و مانع override شدن Liability توسط Classی شویم که از House derive میشود:
public sealed override decimal Liability { get { return Mortgage; } }
همچنین میتوانید modifierِ sealed را روی خود Class اعمال کنید تا از subclassing جلوگیری شود. Sealing یک Class رایجتر از sealing یک Function Member است.
اگرچه میتوانید Function Member را در برابر override شدن seal کنید، نمیتوانید یک Member را در برابر hidden شدن seal کنید.
کلمهٔ کلیدی base
کلمهٔ کلیدی base شبیه this است و دو هدف اساسی دارد:
- Access کردن Function Member overrideشده از داخل subclass.
- فراخوانی Constructorِ Base Class؛ بخش بعد را ببینید.
در مثال زیر، House از base برای access کردن implementationِ Liability در Asset استفاده میکند:
public class House : Asset
{
...
public override decimal Liability => base.Liability + Mortgage;
}
با کلمهٔ کلیدی base، Propertyِ Liability از Asset را بهصورت nonvirtual access میکنیم. یعنی همیشه نسخهٔ این Property در Asset access خواهد شد، صرفنظر از Type واقعی instance در Runtime.
همین رویکرد اگر Liability بهجای override شدن hide شده باشد نیز کار میکند. همچنین میتوانید پیش از فراخوانی Function، با cast به Base Class به اعضای hidden access کنید.
Constructorها و Inheritance
یک subclass باید Constructorهای خودش را اعلان کند. Constructorهای Base Class برای Derived Class قابلدسترسیاند، اما هرگز بهطور خودکار inherit نمیشوند. برای مثال، اگر Baseclass و Subclass را چنین تعریف کنیم:
public class Baseclass
{
public int X;
public Baseclass () { }
public Baseclass (int x) => X = x;
}
public class Subclass : Baseclass { }
کد زیر غیرمجاز است:
Subclass s = new Subclass (123);
بنابراین Subclass باید هر Constructorی را که میخواهد expose کند «دوباره تعریف» کند. بااینحال در این فرایند میتواند هر یک از Constructorهای Base Class را از طریق base فراخوانی کند:
public class Subclass : Baseclass
{
public Subclass (int x) : base (x) { }
}
کلمهٔ کلیدی base تقریباً مانند this کار میکند، با این تفاوت که Constructor موجود در Base Class را فراخوانی میکند.
Constructorهای Base Class همیشه ابتدا اجرا میشوند؛ این کار تضمین میکند مقداردهی اولیهٔ Base پیش از مقداردهی اولیهٔ تخصصی انجام شود.
فراخوانی ضمنی Constructor بدون parameter در Base Class
اگر Constructor در subclass کلمهٔ کلیدی base را حذف کند، Constructor بدون parameterِ Base Type بهصورت ضمنی فراخوانی میشود:
public class Baseclass
{
public int X;
public Baseclass() { X = 1; }
}
public class Subclass : Baseclass
{
public Subclass() { Console.WriteLine (X); } // 1
}
اگر Base Class هیچ Constructor بدون parameter قابلدسترسی نداشته باشد، subclassها مجبور میشوند در Constructorهای خود از base استفاده کنند. یعنی Base Classی که فقط Constructor چند-parameterی دارد، subclassها را ملزم میکند آن را فراخوانی کنند:
class Baseclass
{
public Baseclass (int x, int y, int z, string s, DateTime d) { ... }
}
public class Subclass : Baseclass
{
public Subclass (int x, int y, int z, string s, DateTime d)
: base (x, y, z, s, d) { ... }
}
Required Memberها (C# 11)
الزام subclassها به فراخوانی Constructor موجود در Base Class، در Class Hierarchyهای بزرگ که Constructorهای متعدد با parameterهای زیاد دارند میتواند سنگین باشد. گاهی بهترین راهحل این است که Constructorها را بهکلی کنار بگذارید و فقط به Object Initializerها برای تنظیم Fieldها یا Propertyها هنگام ساخت تکیه کنید. برای کمک به این رویکرد، از C# 11 میتوانید Field یا Property را required علامتگذاری کنید:
public class Asset
{
public required string Name;
}
هنگام ساخت، Required Member باید از طریق Object Initializer مقداردهی شود:
Asset a1 = new Asset { Name="House" }; // OK
Asset a2 = new Asset(); // Error: will not compile!
اگر بخواهید Constructor نیز بنویسید، میتوانید Attributeِ [SetsRequiredMembers] را اعمال کنید تا محدودیت Required Member برای آن Constructor bypass شود:
public class Asset
{
public required string Name;
public Asset() { }
[System.Diagnostics.CodeAnalysis.SetsRequiredMembers]
public Asset (string n) => Name = n;
}
مصرفکنندگان اکنون میتوانند بدون trade-off از راحتی آن Constructor بهرهمند شوند:
Asset a1 = new Asset { Name = "House" }; // OK
Asset a2 = new Asset ("House"); // OK
Asset a3 = new Asset(); // Error!
توجه کنید Constructor بدون parameter نیز تعریف کردیم ــ برای استفاده با Object Initializer. وجود آن همچنین تضمین میکند subclassها مجبور به بازتولید هیچ Constructorی نباشند. در مثال زیر، Classِ House انتخاب میکند Convenience Constructor پیادهسازی نکند:
public class House : Asset { } // No constructor, no worries!
House h1 = new House { Name = "House" }; // OK
House h2 = new House(); // Error!
ترتیب مقداردهی اولیهٔ Constructor و Field
وقتی یک شیء instantiate میشود، مقداردهی اولیه به ترتیب زیر انجام میشود:
- از subclass به Base Class:
- Fieldها مقداردهی اولیه میشوند.
- Argumentهای فراخوانی Constructorِ Base Class ارزیابی میشوند.
- از Base Class به subclass:
- Bodyِ Constructorها اجرا میشود.
برای مثال:
public class B
{
int x = 1; // Executes 3rd
public B (int x)
{
... // Executes 4th
}
}
public class D : B
{
int y = 1; // Executes 1st
public D (int x)
: base (x + 1) // Executes 2nd
{
... // Executes 5th
}
}
Inheritance با Primary Constructorها
Classهای دارای Primary Constructor میتوانند با Syntax زیر subclass شوند:
public class Baseclass (int x) { ... }
public class Subclass (int x, int y) : Baseclass (x) { ... }
فراخوانی Baseclass(x) معادل فراخوانی base(x) در مثال زیر است:
public class Subclass : Baseclass
{
public Subclass (int x, int y) : base (x) { ... }
}
Overloading و Resolution
Inheritance اثر جالبی بر Method Overloading دارد. دو overload زیر را در نظر بگیرید:
static void Foo (Asset a) { }
static void Foo (House h) { }
وقتی overload فراخوانی میشود، Type تخصصیتر اولویت دارد:
House h = new House (...);
Foo(h); // Calls Foo(House)
اینکه کدام overload فراخوانی شود بهصورت static ــ در زمان Compile ــ تعیین میشود، نه در Runtime. کد زیر Foo(Asset) را فراخوانی میکند، حتی با اینکه Runtime Typeِ a برابر House است:
Asset a = new House (...);
Foo(a); // Calls Foo(Asset)
Typeِ object
object یا System.Object Base Class نهایی برای همهٔ Typeهاست. هر Typeی را میتوان به object upcast کرد.
برای نشاندادن کاربرد این قابلیت، یک Stack عمومی را در نظر بگیرید. Stack ساختار دادهای مبتنی بر اصل LIFO ــ «آخرین ورودی، اولین خروجی» ــ است. Stack دو عملیات دارد: push کردن یک شیء روی Stack و pop کردن یک شیء از Stack. این پیادهسازی ساده میتواند تا 10 شیء نگه دارد:
public class Stack
{
int position;
object[] data = new object[10];
public void Push (object obj) { data[position++] = obj; }
public object Pop() { return data[--position]; }
}
چون Stack با Typeِ object کار میکند، میتوانیم instanceهای هر Typeی را به آن Push و از آن Pop کنیم:
Stack stack = new Stack();
stack.Push ("sausage");
string s = (string) stack.Pop(); // Downcast, so explicit cast is needed
Console.WriteLine (s); // sausage
object بهدلیل Class بودن Reference Type است. با وجود این، Value Typeهایی مانند int نیز میتوانند به object و از object cast شوند و بنابراین به Stack افزوده شوند. این قابلیت C# «Type Unification» نام دارد:
stack.Push (3);
int three = (int) stack.Pop();
وقتی میان یک Value Type و object cast میکنید، CLR برای پرکردن فاصلهٔ semantics میان Value Type و Reference Type باید کار ویژهای انجام دهد. این فرایند Boxing و Unboxing نام دارد.
Boxing و Unboxing
Boxing عمل تبدیل یک instance از Value Type به instance از Reference Type است. Reference Type میتواند Classِ object یا یک Interface باشد که بعداً در همین فصل میبینیم. در مثال زیر یک int را داخل object box میکنیم:
int x = 9;
object obj = x; // Box the int
Unboxing عملیات را معکوس میکند و با cast کردن object به Value Type اصلی انجام میشود:
int y = (int)obj; // Unbox the int
Unboxing به cast صریح نیاز دارد. Runtime بررسی میکند Value Type اعلامشده با Object Type واقعی مطابقت دارد و اگر بررسی شکست بخورد InvalidCastException پرتاب میکند. برای نمونه، کد زیر Exception پرتاب میکند زیرا long دقیقاً با int مطابقت ندارد:
object obj = 9; // 9 is inferred to be of type int
long x = (long) obj; // InvalidCastException
اما کد زیر موفق میشود:
object obj = 9;
long x = (int) obj;
همچنین این کد:
object obj = 3.5; // 3.5 is inferred to be of type double
int x = (int) (double) obj; // x is now 3
در مثال آخر، (double) Unboxing انجام میدهد و سپس (int) Numeric Conversion انجام میدهد.
Copying Semantics در Boxing و Unboxing
Boxing instanceِ Value Type را داخل Object جدید copy میکند و Unboxing محتوای Object را به instanceِ Value Type copy میکند. در مثال زیر، تغییر مقدار i نسخهای را که قبلاً box شده تغییر نمیدهد:
int i = 3;
object boxed = i;
i = 5;
Console.WriteLine (boxed); // 3
Static و Runtime Type Checking
Programهای C# هم بهصورت static ــ هنگام Compile ــ و هم در Runtime ــ توسط CLR ــ Type-check میشوند.
Static Type Checking به Compiler اجازه میدهد بدون اجرای Program صحت آن را verify کند. کد زیر شکست میخورد زیرا Compiler Static Typing را enforce میکند:
int x = "5";
Runtime Type Checking توسط CLR زمانی انجام میشود که از طریق Reference Conversion یا Unboxing downcast میکنید:
object y = "5";
int z = (int) y; // Runtime error, downcast failed
Runtime Type Checking ممکن است زیرا هر Object روی Heap درون خود یک Type Token کوچک ذخیره میکند. با فراخوانی Methodِ GetType از object میتوانید این Token را بازیابی کنید.
Methodِ GetType و Operatorِ typeof
همهٔ Typeهای C# در Runtime با instanceای از System.Type نمایش داده میشوند. دو راه پایه برای بهدستآوردن Object از نوع System.Type وجود دارد:
- فراخوانی
GetType روی instance. - استفاده از Operatorِ
typeof روی نام Type.
GetType در Runtime ارزیابی میشود؛ typeof بهصورت static در Compile time ارزیابی میشود ــ وقتی Generic Type Parameter درگیر باشد، توسط JIT Compiler resolve میشود.
System.Type Propertyهایی برای مواردی مانند نام Type، Assembly، Base Type و غیره دارد:
Point p = new Point();
Console.WriteLine (p.GetType().Name); // Point
Console.WriteLine (typeof (Point).Name); // Point
Console.WriteLine (p.GetType() == typeof(Point)); // True
Console.WriteLine (p.X.GetType().Name); // Int32
Console.WriteLine (p.Y.GetType().FullName); // System.Int32
public class Point { public int X, Y; }
System.Type همچنین Methodهایی دارد که مانند Gateway به Reflection Modelِ Runtime عمل میکنند؛ این موضوع در فصل ۱۸ توضیح داده شده است.
Methodِ ToString
Methodِ ToString نمایش متنی پیشفرض یک Type Instance را برمیگرداند. همهٔ Typeهای built-in این Method را override میکنند. مثال زیر از Methodِ ToString در Typeِ int استفاده میکند:
int x = 1;
string s = x.ToString(); // s is "1"
میتوانید Methodِ ToString را در Custom Typeها اینگونه override کنید:
Panda p = new Panda { Name = "Petey" };
Console.WriteLine (p); // Petey
public class Panda
{
public string Name;
public override string ToString() => Name;
}
اگر ToString را override نکنید، Method نام Type را برمیگرداند.
فهرست Memberهای Object
همهٔ Memberهای object عبارتاند از:
public class Object
{
public Object();
public extern Type GetType();
public virtual bool Equals (object obj);
public static bool Equals (object objA, object objB);
public static bool ReferenceEquals (object objA, object objB);
public virtual int GetHashCode();
public virtual string ToString();
protected virtual void Finalize();
protected extern object MemberwiseClone();
}
Methodهای Equals، ReferenceEquals و GetHashCode را در «Equality Comparison» صفحهٔ 344 توضیح میدهیم.
Structها
Struct شبیه Class است، با تفاوتهای اصلی زیر:
- Struct یک Value Type است، درحالیکه Class یک Reference Type است.
- Struct از Inheritance پشتیبانی نمیکند؛ جز اینکه بهصورت ضمنی از
object، یا دقیقتر System.ValueType، derive میشود.
Struct میتواند همهٔ Memberهایی را که Class دارد، بهجز Finalizer، داشته باشد. و چون قابل subclass شدن نیست، Memberهای آن نمیتوانند virtual، abstract یا protected علامتگذاری شوند.
Struct وقتی مناسب است که Value-Type Semantics مطلوب باشد. Typeهای عددی نمونههای خوبی هستند، زیرا طبیعیتر است assignment یک مقدار را copy کند تا یک Reference را. چون Struct یک Value Type است، هر instance نیازمند instantiate شدن Object روی Heap نیست؛ هنگام ساخت تعداد زیادی instance، این موضوع صرفهجویی مفیدی ایجاد میکند. برای مثال ساخت Arrayی از elementهای Value Type فقط به یک Heap Allocation نیاز دارد.
چون Structها Value Type هستند، instance نمیتواند null باشد. مقدار پیشفرض Struct یک instance خالی است که همهٔ Fieldهایش خالیاند ــ روی default value خود تنظیم شدهاند.
Struct Construction Semantics
Constructor پیشفرض
علاوه بر هر Constructorی که خودتان تعریف میکنید، Struct همیشه یک Constructor ضمنیِ بدون parameter دارد که Fieldها را bitwise-zero میکند و آنها را روی default value قرار میدهد:
Point p = new Point(); // p.x and p.y will be 0
struct Point { int x, y; }
حتی وقتی خودتان Constructor بدون parameter تعریف میکنید، Constructor ضمنی بدون parameter همچنان وجود دارد و از طریق کلمهٔ کلیدی default قابل access است:
Point p1 = new Point(); // p1.x and p1.y will be 1
Point p2 = default; // p2.x and p2.y will be 0
struct Point
{
int x = 1;
int y;
public Point() => y = 1;
}
در این مثال، x را با Field Initializer برابر 1 و y را با Constructor بدون parameter برابر 1 مقداردهی کردیم. بااینحال با default هنوز توانستیم Pointای بسازیم که هر دو مقداردهی را bypass کند. Constructor پیشفرض از روشهای دیگری نیز قابل access است:
var points = new Point[10]; // Each point in the array will be (0,0)
var test = new Test(); // test.p will be (0,0)
class Test { Point p; }
استراتژی خوب برای Structها این است که آنها را طوری طراحی کنید که default valueشان یک state معتبر باشد و در نتیجه initialization اضافی غیرضروری شود. برای مثال، بهجای مقداردهی Property به شکل زیر:
public string Protocol { get; set; } = "https";
کد زیر را در نظر بگیرید:
struct WebOptions
{
string protocol;
public string Protocol { get => protocol ?? "https";
set => protocol = value; }
}
Read-Only Structها و Functionها
میتوانید modifierِ readonly را روی Struct اعمال کنید تا همهٔ Fieldها الزاماً readonly شوند؛ این کار هم نیت طراحی را بیان میکند و هم آزادی بیشتری برای optimization در اختیار Compiler میگذارد:
readonly struct Point
{
public readonly int X, Y; // X and Y must be readonly
}
اگر نیاز دارید readonly را در سطح granularتری اعمال کنید، میتوانید ــ از C# 8 ــ modifierِ readonly را روی Functionهای Struct بگذارید. این کار تضمین میکند اگر Function تلاش کند Fieldی را تغییر دهد، Compile-time error تولید شود:
struct Point
{
public int X, Y;
public readonly void ResetX() => X = 0; // Error!
}
اگر Functionِ readonly یک Functionِ non-readonly را فراخوانی کند، Compiler warning تولید میکند و برای جلوگیری از احتمال mutation، بهشکل defensive از Struct copy میگیرد.
Ref Structها
برخلاف Reference Typeها که instanceهایشان همیشه روی Heap زندگی میکنند، Value Typeها in-place ــ هرجا متغیر اعلان شده است ــ زندگی میکنند. اگر Value Type بهصورت parameter یا local variable ظاهر شود روی Stack قرار میگیرد:
void SomeMethod()
{
Point p; // p will reside on the stack
}
struct Point { public int X, Y; }
اما اگر Value Type بهصورت Field در یک Class ظاهر شود، روی Heap قرار میگیرد:
class MyClass
{
Point p; // Lives on heap, because MyClass instances live on the heap
}
بههمین ترتیب، Arrayهای Struct روی Heap قرار میگیرند و Boxing یک Struct نیز آن را به Heap میفرستد.
افزودن modifierِ ref به اعلان Struct تضمین میکند آن Struct فقط روی Stack قرار بگیرد. تلاش برای استفاده از Ref Struct به شکلی که ممکن است روی Heap قرار گیرد Compile-time error ایجاد میکند:
var points = new Point [100]; // Error: will not compile!
ref struct Point { public int X, Y; }
class MyClass { Point P; } // Error: will not compile!
Ref Structها عمدتاً برای Span<T> و ReadOnlySpan<T> معرفی شدند. چون instanceهای آنها فقط روی Stack میتوانند وجود داشته باشند، میتوانند با ایمنی Memory اختصاصیافته روی Stack را wrap کنند.
Ref Structها نمیتوانند در هیچ قابلیت C# شرکت کنند که مستقیم یا غیرمستقیم امکان قرارگرفتن روی Heap را ایجاد میکند. این شامل تعدادی از قابلیتهای پیشرفتهٔ C# است که در فصل ۴ توضیح میدهیم: Lambda Expressionها، Iteratorها و Asynchronous Functionها؛ زیرا این قابلیتها پشت صحنه Classهای مخفی دارای Field ایجاد میکنند. همچنین Ref Structها نمیتوانند داخل Structهای non-ref ظاهر شوند و نمیتوانند Interface implement کنند، چون این امر میتواند به Boxing منجر شود.
Access Modifierها
برای تقویت encapsulation، یک Type یا Type Member میتواند با افزودن Access Modifier به اعلان، accessibility خود را برای Typeها و Assemblyهای دیگر محدود کند:
public- کاملاً قابل access. این accessibility ضمنی برای Memberهای Enum یا Interface است.
internal- فقط داخل Assembly دربرگیرنده یا Friend Assemblyها قابل access. این accessibility پیشفرض Typeهای non-nested است.
private- فقط داخل Type دربرگیرنده قابل access. این accessibility پیشفرض Memberهای Class یا Struct است.
protected- فقط داخل Type دربرگیرنده یا subclassها قابل access.
protected internal- اجتماع accessibilityِ
protected و internal. Member از نوع protected internal از دو مسیر قابل access است.
private protected- اشتراک accessibilityِ
protected و internal. Member از نوع private protected فقط داخل Type دربرگیرنده یا از subclassهایی که در همان Assembly هستند قابل access است؛ بنابراین از protected یا internal بهتنهایی محدودتر است.
file (از C# 11)- فقط از داخل همان فایل قابل access. برای استفاده توسط Source Generatorها طراحی شده است؛ به «Extended partial methods» در صفحهٔ 125 مراجعه کنید. این modifier فقط روی Type Declarationها قابل اعمال است.
مثالها
Class2 از بیرون Assembly خود قابل access است؛ Class1 نیست:
class Class1 {} // Class1 is internal (default)
public class Class2 {}
ClassB Fieldِ x را برای Typeهای دیگر همان Assembly expose میکند؛ ClassA این کار را نمیکند:
class ClassA { int x; } // x is private (default)
class ClassB { internal int x; }
Functionهای داخل Subclass میتوانند Bar را فراخوانی کنند اما Foo را نه:
class BaseClass
{
void Foo() {} // Foo is private (default)
protected void Bar() {}
}
class Subclass : BaseClass
{
void Test1() { Foo(); } // Error - cannot access Foo
void Test2() { Bar(); } // OK
}
Friend Assemblyها
میتوانید با افزودن Assembly Attributeِ System.Runtime.CompilerServices.InternalsVisibleTo، Memberهای internal را برای Friend Assemblyهای دیگر expose کنید و نام Friend Assembly را مشخص کنید:
[assembly: InternalsVisibleTo ("Friend")]
اگر Friend Assembly Strong Name داشته باشد ــ فصل ۱۷ ــ باید Public Key کامل 160-byte آن را مشخص کنید:
[assembly: InternalsVisibleTo ("StrongFriend, PublicKey=0024f000048c...")]
با Query زیر در LINQ ــ LINQ را در فصل ۸ بهتفصیل توضیح میدهیم ــ میتوانید Public Key کامل را از Strongly Named Assembly استخراج کنید:
string key = string.Join ("",
Assembly.GetExecutingAssembly().GetName().GetPublicKey()
.Select (b => b.ToString ("x2")));
Accessibility Capping
یک Type سقف accessibilityِ Memberهایی را که اعلان میکند تعیین میکند. رایجترین نمونهٔ capping زمانی است که Typeِ internal با Memberهای public دارید. برای مثال:
class C { public void Foo() {} }
Accessibility پیشفرض و internalِ C، accessibilityِ Foo را cap میکند و در عمل Foo را internal میسازد. یکی از دلایل رایج public علامتزدن Foo این است که اگر بعداً C public شد refactoring آسانتر باشد.
محدودیتهای Access Modifierها
هنگام override کردن Function در Base Class، accessibility باید روی Functionِ overrideشده یکسان باشد؛ برای مثال:
class BaseClass { protected virtual void Foo() {} }
class Subclass1 : BaseClass { protected override void Foo() {} } // OK
class Subclass2 : BaseClass { public override void Foo() {} } // Error
یک استثنا هنگام override کردن Method از نوع protected internal در Assembly دیگری است؛ در این حالت override باید فقط protected باشد.
Compiler از هر استفادهٔ ناسازگار از Access Modifierها جلوگیری میکند. برای مثال، خود subclass میتواند accessibility کمتری از Base Class داشته باشد، اما نمیتواند accessibility بیشتری داشته باشد:
internal class A {}
public class B : A {} // Error
Interfaceها
Interface شبیه Class است، اما فقط behavior را مشخص میکند و state ــ داده ــ نگه نمیدارد. در نتیجه:
- Interface فقط میتواند Function تعریف کند و Field ندارد.
- Memberهای Interface بهصورت ضمنی abstract هستند. استثناهایی وجود دارد که در «Default Interface Members» صفحهٔ 151 و «Static Interface Members» صفحهٔ 152 توضیح میدهیم.
- Class یا Struct میتواند چند Interface را implement کند. در مقابل، Class فقط از یک Class واحد میتواند inherit کند و Struct اصلاً نمیتواند inherit کند؛ جز derive شدن از
System.ValueType.
اعلان Interface شبیه اعلان Class است، اما معمولاً implementation برای Memberهایش فراهم نمیکند، زیرا Memberها بهصورت ضمنی abstract هستند. این Memberها توسط Classها و Structهایی که Interface را implement میکنند پیادهسازی خواهند شد. Interface فقط میتواند Function داشته باشد؛ یعنی Method، Property، Event و Indexer ــ که تصادفاً دقیقاً همان Memberهای Class هستند که میتوانند abstract باشند.
تعریف Interfaceِ IEnumerator در System.Collections چنین است:
public interface IEnumerator
{
bool MoveNext();
object Current { get; }
void Reset();
}
Memberهای Interface همیشه بهصورت ضمنی public هستند و نمیتوانند Access Modifier اعلان کنند. Implement کردن Interface یعنی فراهمکردن implementation عمومی برای همهٔ Memberهای آن: