فصل ۳: ساخت Typeها در C# — Classها و اعضای کلاس
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
۳. ساخت Typeها در C#
در این فصل، به بررسی عمیق Typeها و اعضای Type میپردازیم.
Classها
Class (کلاس) رایجترین نوعِ Reference Type (نوع ارجاعی) است. سادهترین اعلان ممکن برای یک Class به شکل زیر است:
class YourClassName
{
}
یک Class پیچیدهتر میتواند بهصورت اختیاری موارد زیر را داشته باشد:
- پیش از کلمهٔ کلیدی
class - Attributeها و modifierهای Class. modifierهای Class غیرتودرتو عبارتاند از
public، internal، abstract، sealed، static، unsafe و partial.
- پس از
YourClassName - پارامترها و constraintهای Generic Type، یک Base Class و Interfaceها.
- داخل آکولادها
- اعضای Class؛ یعنی methodها، propertyها، indexerها، eventها، fieldها، constructorها، operatorهای overloadشده، Typeهای تودرتو و finalizer.
این فصل همهٔ این ساختارها را، بهجز Attributeها، تابعهای operator و کلمهٔ کلیدی unsafe، پوشش میدهد؛ موارد استثنا در فصل ۴ بررسی میشوند. بخشهای بعدی تکتک اعضای Class را معرفی میکنند.
Fieldها
Field (فیلد) متغیری است که عضو یک Class یا Struct است؛ برای نمونه:
class Octopus
{
string name;
public int Age = 10;
}
Fieldها modifierهای زیر را میپذیرند:
- modifier ایستا (Static)
static
- modifierهای دسترسی (Access)
public internal private protected
- modifier وراثت (Inheritance)
new
- modifier کد ناامن (Unsafe code)
unsafe
- modifier فقطخواندنی (Read-only)
readonly
- modifier Threading
volatile
برای نامگذاری Fieldهای private دو قرارداد رایج وجود دارد: camelCase، مانند firstName، و camelCase همراه با underscore، مانند _firstName. قرارداد دوم باعث میشود Fieldهای private را فوراً از parameterها و local variableها تشخیص دهید.
modifierِ readonly
modifierِ readonly مانع تغییر Field پس از ساخت شیء میشود. یک Field فقطخواندنی فقط در اعلان خودش یا داخل constructor مربوط به Type دربرگیرنده قابل مقداردهی است.
مقداردهی اولیهٔ Field
مقداردهی اولیهٔ Field اختیاری است. Fieldی که مقداردهی اولیه نشده باشد، مقدار پیشفرض دارد (0، '\0'، null، false). مقداردهندههای Field پیش از constructorها اجرا میشوند:
public int Age = 10;
مقداردهندهٔ Field میتواند شامل expression باشد و method فراخوانی کند:
static readonly string TempFolder = System.IO.Path.GetTempPath();
اعلان چند Field با هم
برای راحتی میتوانید چند Field از یک Type یکسان را در فهرستی جداشده با کاما اعلان کنید. این روش باعث میشود همهٔ Fieldها Attributeها و modifierهای Field یکسانی داشته باشند:
static readonly int legs = 8,
eyes = 2;
Constantها
Constant (ثابت) در زمان Compile بهصورت ایستا ارزیابی میشود و Compiler در هر محل استفاده، مقدار آن را بهطور literal جایگزین میکند؛ تقریباً شبیه macro در C++. یک Constant میتواند از نوع bool، char، string، هر یک از Typeهای عددی built-in یا یک Enum Type باشد.
یک Constant با کلمهٔ کلیدی const اعلان میشود و باید با یک مقدار مقداردهی اولیه شود. برای مثال:
public class Test
{
public const string Message = "Hello World";
}
یک Constant میتواند نقشی شبیه یک Field از نوع static readonly داشته باشد، اما بسیار محدودکنندهتر است؛ هم از نظر Typeهایی که میتوانید استفاده کنید و هم از نظر قواعد مقداردهی اولیهٔ Field. Constant همچنین از این جهت با Field از نوع static readonly تفاوت دارد که ارزیابی Constant در زمان Compile انجام میشود؛ بنابراین:
public static double Circumference (double radius)
{
return 2 * System.Math.PI * radius;
}
به این کد Compile میشود:
public static double Circumference (double radius)
{
return 6.2831853071795862 * radius;
}
منطقی است که PI یک Constant باشد، زیرا مقدار آن در زمان Compile از پیش مشخص است. در مقابل، مقدار یک Field از نوع static readonly میتواند بالقوه در هر بار اجرای برنامه متفاوت باشد:
static readonly DateTime StartupTime = DateTime.Now;
Constantها را میتوان بهصورت local در یک method نیز اعلان کرد:
void Test()
{
const double twoPI = 2 * System.Math.PI;
...
}
Constantهای nonlocal modifierهای زیر را میپذیرند:
- modifierهای دسترسی
public internal private protected- modifier وراثت
new
Methodها
Method مجموعهای از statementها را برای انجام یک عمل اجرا میکند. Method میتواند با مشخصکردن parameterها، دادهٔ ورودی را از caller دریافت کند و با مشخصکردن return type، دادهٔ خروجی را به caller بازگرداند. Method میتواند return type از نوع void داشته باشد که نشان میدهد هیچ مقداری به caller برنمیگرداند. همچنین Method میتواند از طریق parameterهای ref/out داده را به caller برگرداند.
Signature یک Method باید درون Type یکتا باشد. Signature یک Method از نام آن و Typeهای parameterها بهترتیب تشکیل میشود؛ اما نام parameterها و return type جزو Signature نیستند.
Methodها modifierهای زیر را میپذیرند:
- modifier ایستا
static- modifierهای دسترسی
public internal private protected- modifierهای وراثت
new virtual abstract override sealed- modifierِ partial method
partial- modifierهای unmanaged code
unsafe extern- modifier کد asynchronous
async
Methodهای expression-bodied
Methodی که تنها از یک expression تشکیل شده باشد، مانند:
int Foo (int x) { return x * 2; }
را میتوان بهصورت کوتاهتر، بهشکل expression-bodied method نوشت. در این حالت fat arrow جای آکولادها و کلمهٔ کلیدی return را میگیرد:
int Foo (int x) => x * 2;
تابعهای expression-bodied میتوانند return type از نوع void نیز داشته باشند:
void Foo (int x) => Console.WriteLine (x);
Methodهای local
میتوانید یک Method را داخل Method دیگری تعریف کنید:
void WriteCubes()
{
Console.WriteLine (Cube (3));
Console.WriteLine (Cube (4));
Console.WriteLine (Cube (5));
int Cube (int value) => value * value * value;
}
Method محلی، که در این مثال Cube است، فقط برای Method دربرگیرنده، یعنی WriteCubes، قابل مشاهده است. این موضوع Type دربرگیرنده را سادهتر میکند و فوراً به هر کسی که کد را میبیند نشان میدهد Cube در جای دیگری استفاده نمیشود. مزیت دیگر Methodهای local این است که میتوانند به local variableها و parameterهای Method دربرگیرنده دسترسی داشته باشند. این موضوع پیامدهای متعددی دارد که در «Capturing Outer Variables» در صفحهٔ 190 با جزئیات توضیح میدهیم.
Methodهای local میتوانند داخل انواع دیگر Function نیز ظاهر شوند؛ مانند property accessorها، constructorها و غیره. حتی میتوانید Methodهای local را داخل Methodهای local دیگر و داخل lambda expressionهایی که از statement block استفاده میکنند قرار دهید (فصل ۴). Methodهای local میتوانند iterator (فصل ۴) یا asynchronous (فصل ۱۴) باشند.
Methodهای local ایستا
افزودن modifierِ static به یک Method محلی (از C# 8) مانع میشود که local variableها و parameterهای Method دربرگیرنده را ببیند. این کار coupling را کاهش میدهد و از اشارهٔ تصادفی Method محلی به متغیرهای Method دربرگیرنده جلوگیری میکند.
Methodهای local و top-level statementها
هر Methodی که در top-level statementها اعلان کنید، بهعنوان Method محلی در نظر گرفته میشود. این یعنی، مگر اینکه با static علامتگذاری شده باشد، میتواند به متغیرهای top-level statementها دسترسی داشته باشد:
int x = 3;
Foo();
void Foo() => Console.WriteLine (x);
Overload کردن Methodها
یک Type میتواند Methodها را overload کند، یعنی چند Method با نام یکسان تعریف کند، بهشرط آنکه Signatureهای آنها متفاوت باشند. برای مثال Methodهای زیر همگی میتوانند در یک Type در کنار هم وجود داشته باشند:
void Foo (int x) {...}
void Foo (double x) {...}
void Foo (int x, float y) {...}
void Foo (float x, int y) {...}
با این حال، جفت Methodهای زیر نمیتوانند در یک Type همزمان وجود داشته باشند، زیرا return type و modifierِ params بخشی از Signature یک Method نیستند:
void Foo (int x) {...}
float Foo (int x) {...} // Compile-time error
void Goo (int[] x) {...}
void Goo (params int[] x) {...} // Compile-time error
اینکه parameter بهصورت pass-by-value یا pass-by-reference ارسال شود نیز بخشی از Signature است. برای مثال، Foo(int) میتواند در کنار Foo(ref int) یا Foo(out int) وجود داشته باشد. با این حال، Foo(ref int) و Foo(out int) نمیتوانند در کنار هم وجود داشته باشند:
void Foo (int x) {...}
void Foo (ref int x) {...} // OK so far
void Foo (out int x) {...} // Compile-time error
Instance Constructorها
Constructorها کد مقداردهی اولیه را روی یک Class یا Struct اجرا میکنند. Constructor شبیه Method تعریف میشود، با این تفاوت که نام Method و return type به نام Type دربرگیرنده تقلیل مییابد:
Panda p = new Panda ("Petey"); // Call constructor
public class Panda
{
string name; // Define field
public Panda (string n) // Define constructor
{
name = n; // Initialization code (set up field)
}
}
Instance constructorها modifierهای زیر را میپذیرند:
- modifierهای دسترسی
public internal private protected- modifierهای unmanaged code
unsafe extern
Constructorهای تک-statement را نیز میتوان بهصورت expression-bodied member نوشت:
public Panda (string n) => name = n;
Overload کردن Constructorها
یک Class یا Struct میتواند Constructorهای خود را overload کند. برای جلوگیری از تکرار کد، یک Constructor میتواند با استفاده از کلمهٔ کلیدی this Constructor دیگری را فراخوانی کند:
public class Wine
{
public decimal Price;
public int Year;
public Wine (decimal price) => Price = price;
public Wine (decimal price, int year) : this (price) => Year = year;
}
وقتی یک Constructor، Constructor دیگری را فراخوانی میکند، Constructor فراخوانیشده ابتدا اجرا میشود.
میتوانید یک expression را به Constructor دیگری پاس دهید، به شکل زیر:
public Wine (decimal price, DateTime year) : this (price, year.Year) { }
این expression میتواند به اعضای static کلاس دسترسی داشته باشد، اما به اعضای instance دسترسی ندارد. دلیل اعمال این محدودیت آن است که در این مرحله، شیء هنوز توسط Constructor مقداردهی اولیه نشده است؛ بنابراین هر Methodی که روی آن فراخوانی کنید احتمالاً با شکست روبهرو میشود.
Constructorهای بدون parameter ضمنی
برای Classها، Compiler زبان C# یک Constructor عمومیِ بدون parameter را بهصورت خودکار تولید میکند، اگر و فقط اگر خودتان هیچ Constructorی تعریف نکرده باشید. با این حال، بهمحض اینکه دستکم یک Constructor تعریف کنید، Constructor بدون parameter دیگر بهطور خودکار تولید نمیشود.
ترتیب مقداردهی اولیهٔ Constructor و Field
پیشتر دیدیم که Fieldها را میتوان هنگام اعلان با مقدارهای پیشفرض مقداردهی اولیه کرد:
class Player
{
int shields = 50; // Initialized first
int health = 100; // Initialized second
}
مقداردهی اولیهٔ Fieldها پیش از اجرای Constructor و به همان ترتیبی انجام میشود که Fieldها اعلان شدهاند.
Constructorهای nonpublic
لازم نیست Constructorها حتماً public باشند. یکی از دلایل رایج برای داشتن Constructor غیرعمومی، کنترل ساخت instance از طریق فراخوانی یک Method ایستا است. Method ایستا میتواند بهجای ساخت شیء جدید، شیئی را از یک pool برگرداند یا بر اساس آرگومانهای ورودی، subclassهای مختلفی را بازگرداند:
public class Class1
{
Class1() {} // Private constructor
public static Class1 Create (...)
{
// Perform custom logic here to return an instance of Class1
...
}
}
Deconstructorها
Deconstructor ــ که deconstructing method نیز نامیده میشود ــ تقریباً در جهت معکوس Constructor عمل میکند: Constructor معمولاً مجموعهای از مقدارها را بهصورت parameter میگیرد و آنها را به Fieldها اختصاص میدهد، درحالیکه Deconstructor کار معکوس را انجام میدهد و Fieldها را دوباره به مجموعهای از متغیرها نسبت میدهد.
یک Method مربوط به deconstruction باید Deconstruct نام داشته باشد و یک یا چند parameter از نوع out داشته باشد، مانند Class زیر:
class Rectangle
{
public readonly float Width, Height;
public Rectangle (float width, float height)
{
Width = width;
Height = height;
}
public void Deconstruct (out float width, out float height)
{
width = Width;
height = Height;
}
}
Syntax ویژهٔ زیر Deconstructor را فراخوانی میکند:
var rect = new Rectangle (3, 4);
(float width, float height) = rect; // Deconstruction
Console.WriteLine (width + " " + height); // 3 4
خط دوم، فراخوانی deconstructing است. این خط دو local variable میسازد و سپس Methodِ Deconstruct را فراخوانی میکند. فراخوانی deconstructing ما معادل کد زیر است:
float width, height;
rect.Deconstruct (out width, out height);
یا:
rect.Deconstruct (out var width, out var height);
فراخوانیهای deconstructing از implicit typing پشتیبانی میکنند؛ بنابراین میتوانیم فراخوانی خود را به این شکل کوتاهتر کنیم:
(var width, var height) = rect;
یا حتی بهسادگی:
var (width, height) = rect;
اگر متغیرهایی که قرار است مقدارهای deconstruct در آنها قرار گیرد از قبل تعریف شدهاند، Typeها را کاملاً حذف کنید:
float width, height;
(width, height) = rect;
این کار deconstructing assignment نام دارد. میتوانید از deconstructing assignment برای سادهترکردن Constructor کلاس خود استفاده کنید:
public Rectangle (float width, float height) =>
(Width, Height) = (width, height);
با overload کردن Methodِ Deconstruct میتوانید مجموعهای از گزینههای مختلف deconstruction در اختیار caller قرار دهید.
از C# 10، هنگام deconstruct کردن میتوانید متغیرهای موجود و متغیرهای جدید را با هم ترکیب کنید:
double x1 = 0;
(x1, double y2) = rect;
Object Initializerها
برای سادهکردن مقداردهی اولیهٔ شیء، هر Field یا Property قابلدسترسیِ یک شیء را میتوان مستقیماً پس از ساخت، از طریق Object Initializer مقداردهی کرد. برای مثال Class زیر را در نظر بگیرید:
public class Bunny
{
public string Name;
public bool LikesCarrots, LikesHumans;
public Bunny () {}
public Bunny (string n) => Name = n;
}
با استفاده از Object Initializerها، میتوانید شیءهای Bunny را به شکل زیر instantiate کنید:
// Note parameterless constructors can omit empty parentheses
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };
کدی که b1 و b2 را میسازد دقیقاً معادل کد زیر است:
Bunny temp1 = new Bunny(); // temp1 is a compiler-generated name
temp1.Name = "Bo";
temp1.LikesCarrots = true;
temp1.LikesHumans = false;
Bunny b1 = temp1;
Bunny temp2 = new Bunny ("Bo");
temp2.LikesCarrots = true;
temp2.LikesHumans = false;
Bunny b2 = temp2;
متغیرهای موقت برای این هستند که اگر هنگام مقداردهی اولیه Exception رخ داد، در نهایت با یک شیء نیمهمقداردهیشده روبهرو نشوید.
Object Initializerها در برابر Optional Parameterها
بهجای تکیه بر Object Initializerها، میتوانستیم Constructorِ Bunny را به شکل زیر، با یک parameter اجباری و دو parameter اختیاری، بنویسیم:
public Bunny (string name,
bool likesCarrots = false,
bool likesHumans = false)
{
Name = name;
LikesCarrots = likesCarrots;
LikesHumans = likesHumans;
}
در این صورت میتوانستیم یک Bunny را به شکل زیر بسازیم:
Bunny b1 = new Bunny (name: "Bo",
likesCarrots: true);
از نظر تاریخی، تکیه بر Constructorها برای مقداردهی اولیهٔ شیء میتوانست مزیت داشته باشد، زیرا به ما اجازه میداد Fieldها ــ یا Propertyها که بهزودی توضیح میدهیم ــ را readonly کنیم. وقتی دلیل معتبری برای تغییر یک Field یا Property در طول عمر شیء وجود ندارد، readonly کردن آن practice خوبی است. بااینحال، همانطور که بهزودی در بحث Propertyها خواهیم دید، modifierِ init که در C# 9 معرفی شد، اجازه میدهد همین هدف را با Object Initializerها بهدست آوریم.
Optional Parameterها دو عیب دارند. نخست اینکه اگرچه استفاده از آنها در Constructorها امکان ساخت Typeهای readonly را فراهم میکند، اما بهآسانی امکان nondestructive mutation را نمیدهند. Nondestructive mutation و راهحل این مسئله را در «Records» در صفحهٔ 227 بررسی خواهیم کرد.
عیب دوم Optional Parameterها این است که وقتی در Libraryهای عمومی استفاده میشوند، backward compatibility را مختل میکنند. دلیلش این است که افزودن یک Optional Parameter در زمانی بعد، binary compatibility آن Assembly را با مصرفکنندگان موجود میشکند.
این موضوع بهویژه وقتی یک Library روی NuGet منتشر میشود اهمیت دارد: اگر مصرفکنندهای به Packageهای A و B reference داشته باشد و A و B هرکدام به نسخههای ناسازگار L وابسته باشند، مسئله عملاً حلناشدنی میشود.
مشکل این است که مقدار هر Optional Parameter داخل محل فراخوانی bake میشود. به بیان دیگر، C# فراخوانی Constructor ما را به شکل زیر ترجمه میکند:
Bunny b1 = new Bunny ("Bo", true, false);
اگر Classِ Bunny را از Assembly دیگری instantiate کنیم و بعداً Bunny را با افزودن Optional Parameter دیگری ــ مثلاً likesCats ــ تغییر دهیم، این وضعیت مشکلساز میشود. مگر اینکه Assembly ارجاعدهنده نیز دوباره Compile شود، همچنان Constructor سه-parameterیِ اکنون ناموجود را فراخوانی میکند و در Runtime شکست میخورد. مشکل ظریفتر این است که اگر مقدار یکی از Optional Parameterها را تغییر دهیم، callerهای Assemblyهای دیگر تا زمان Compile مجدد همچنان مقدار اختیاری قدیمی را استفاده خواهند کرد.
ملاحظهٔ نهایی، اثر Constructorها بر subclassing است که در «Inheritance» صفحهٔ 126 بررسی میکنیم. داشتن Constructorهای متعدد با فهرستهای طولانی parameter، subclassing را دشوار میکند؛ بنابراین مفید است تعداد و پیچیدگی Constructorها را حداقل نگه دارید و جزئیات را با Object Initializerها پر کنید.
Referenceِ this
Referenceِ this به خود instance اشاره میکند. در مثال زیر، Methodِ Marry از this برای تنظیم Fieldِ mate در partner استفاده میکند:
public class Panda
{
public Panda Mate;
public void Marry (Panda partner)
{
Mate = partner;
partner.Mate = this;
}
}
Referenceِ this همچنین ابهام میان local variable یا parameter و Field را برطرف میکند؛ برای مثال:
public class Test
{
string name;
public Test (string name) => this.name = name;
}
Referenceِ this فقط داخل اعضای nonstatic یک Class یا Struct معتبر است.
Propertyها
Propertyها از بیرون شبیه Fieldها به نظر میرسند، اما درونشان مانند Methodها منطق وجود دارد. برای مثال، تنها با نگاهکردن به کد زیر نمیتوانید تشخیص دهید CurrentPrice یک Field است یا Property:
Stock msft = new Stock();
msft.CurrentPrice = 30;
msft.CurrentPrice -= 3;
Console.WriteLine (msft.CurrentPrice);
Property مانند Field اعلان میشود، اما یک block شامل get/set به آن افزوده میشود. پیادهسازی CurrentPrice بهصورت Property چنین است:
public class Stock
{
decimal currentPrice; // The private "backing" field
public decimal CurrentPrice // The public property
{
get { return currentPrice; }
set { currentPrice = value; }
}
}
get و set بیانگر Property Accessorها هستند. Accessorِ get هنگام خواندن Property اجرا میشود و باید مقداری از Type همان Property برگرداند. Accessorِ set هنگام مقداردهی Property اجرا میشود. این Accessor یک parameter ضمنی با نام value و Type همان Property دارد که معمولاً آن را به یک Field private ــ در اینجا currentPrice ــ نسبت میدهید.
با وجود اینکه Propertyها مانند Fieldها access میشوند، تفاوتشان این است که به پیادهساز کنترل کامل بر گرفتن و تنظیم مقدار میدهند. این کنترل اجازه میدهد پیادهساز هر نمایش داخلی لازم را انتخاب کند، بدون اینکه جزئیات داخلی را به استفادهکنندهٔ Property آشکار کند. در این مثال، Methodِ set میتوانست در صورتی که value خارج از دامنهٔ معتبر باشد Exception پرتاب کند.
Propertyها modifierهای زیر را میپذیرند:
- Static modifier
static- Access modifiers
public internal private protected- Inheritance modifiers
new virtual abstract override sealed- Unmanaged code modifiers
unsafe extern
Propertyهای read-only و محاسبهشده
اگر Property فقط Accessorِ get داشته باشد read-only است و اگر فقط Accessorِ set داشته باشد write-only است. Propertyهای write-only بهندرت استفاده میشوند.
Property معمولاً یک backing field اختصاصی برای ذخیرهٔ دادهٔ زیربنایی دارد. بااینحال، Property میتواند از دادههای دیگر نیز محاسبه شود:
decimal currentPrice, sharesOwned;
public decimal Worth
{
get { return currentPrice * sharesOwned; }
}
Propertyهای expression-bodied
میتوانید یک Property read-only مانند مثال قبلی را بهصورت کوتاهتر و expression-bodied اعلان کنید. Fat arrow جای همهٔ آکولادها و کلمههای کلیدی get و return را میگیرد:
public decimal Worth => currentPrice * sharesOwned;
با کمی Syntax اضافه، Accessorهای set نیز میتوانند expression-bodied باشند:
public decimal Worth
{
get => currentPrice * sharesOwned;
set => sharesOwned = value / currentPrice;
}
Propertyهای خودکار
رایجترین پیادهسازی Property یک getter و/یا setter است که بهسادگی یک Field private همType با Property را میخواند و مینویسد. اعلان automatic property به Compiler دستور میدهد این پیادهسازی را فراهم کند. میتوانیم مثال نخست این بخش را با اعلان CurrentPrice بهصورت automatic property بهتر کنیم:
public class Stock
{
...
public decimal CurrentPrice { get; set; }
}
Compiler بهطور خودکار یک backing field private با نامی تولیدشده توسط Compiler ایجاد میکند که نمیتوان به آن اشاره کرد. اگر بخواهید Property را برای Typeهای دیگر read-only نشان دهید، Accessorِ set میتواند private یا protected علامتگذاری شود.
Automatic Propertyها در C# 3.0 معرفی شدند.
Property Initializerها
همانند Fieldها، میتوانید به automatic propertyها Property Initializer اضافه کنید:
public decimal CurrentPrice { get; set; } = 123;
این کار مقدار اولیهٔ 123 را به CurrentPrice میدهد. Property دارای Initializer میتواند read-only باشد:
public int Maximum { get; } = 999;
همانند Fieldهای read-only، automatic propertyهای read-only را نیز میتوان در Constructorِ Type مقداردهی کرد. این ویژگی برای ساخت Typeهای immutable ــ فقطخواندنی ــ مفید است.
سطح دسترسی get و set
Accessorهای get و set میتوانند سطح دسترسی متفاوتی داشته باشند. مورد استفادهٔ معمول، داشتن یک Property عمومی با access modifier از نوع internal یا private روی setter است:
public class Foo
{
private decimal x;
public decimal X
{
get { return x; }
private set { x = Math.Round (value, 2); }
}
}
توجه کنید که خود Property را با سطح دسترسی permissiveتر ــ در اینجا public ــ اعلان میکنید و modifier را به Accessorی میافزایید که میخواهید دسترسی محدودتری داشته باشد.
Setterهای init-only
از C# 9 میتوانید Property Accessor را با init بهجای set اعلان کنید:
public class Note
{
public int Pitch { get; init; } = 20; // “Init-only” property
public int Duration { get; init; } = 100; // “Init-only” property
}
این Propertyهای init-only مانند Propertyهای read-only رفتار میکنند، با این تفاوت که از طریق Object Initializer نیز قابل تنظیماند:
var note = new Note { Pitch = 50 };
پس از آن Property دیگر قابل تغییر نیست:
note.Pitch = 200; // Error – init-only setter!
Propertyهای init-only حتی از داخل Class خودشان نیز قابل تنظیم نیستند، مگر از طریق Property Initializer خودشان، Constructor یا Accessorِ init-only دیگری.
جایگزین Propertyهای init-only این است که Propertyهای read-only داشته باشید و آنها را از طریق Constructor مقداردهی کنید:
public class Note
{
public int Pitch { get; }
public int Duration { get; }
public Note (int pitch = 20, int duration = 100)
{
Pitch = pitch; Duration = duration;
}
}
اگر Class بخشی از یک Library عمومی باشد، این رویکرد versioning را دشوار میکند، زیرا افزودن یک Optional Parameter به Constructor در زمانی بعد، binary compatibility با مصرفکنندگان را میشکند؛ درحالیکه افزودن یک Property جدید init-only چیزی را نمیشکند.
همانند Accessorهای معمولی set، Accessorهای init-only نیز میتوانند implementation داشته باشند:
public class Note
{
readonly int _pitch;
public int Pitch { get => _pitch; init => _pitch = value; }
...
}
توجه کنید Fieldِ _pitch readonly است: setterهای init-only اجازه دارند Fieldهای readonly را در Class خودشان تغییر دهند. بدون این قابلیت، _pitch باید writable میبود و Class از نظر internal immutable بودن شکست میخورد.
پیادهسازی Property در CLR
Accessorهای Property در C# درونساختار به Methodهایی با نام get_XXX و set_XXX Compile میشوند:
public decimal get_CurrentPrice {...}
public void set_CurrentPrice (decimal value) {...}
Accessorِ init مانند Accessorِ set پردازش میشود، اما یک Flag اضافی در metadataِ modreq مربوط به Accessorِ set encode میشود؛ به «Init-only properties» در صفحهٔ 818 مراجعه کنید.
Accessorهای ساده و nonvirtual Property توسط Compilerِ Just-In-Time (JIT) inline میشوند و هرگونه تفاوت performance میان دسترسی به Property و Field را از بین میبرند. Inlining یک optimization است که در آن فراخوانی Method با body همان Method جایگزین میشود.
Indexerها
Indexerها Syntax طبیعی برای دسترسی به elementهای یک Class یا Struct که فهرست یا Dictionaryای از مقدارها را encapsulate میکند فراهم میکنند. Indexerها شبیه Propertyها هستند، اما بهجای نام Property، از طریق یک index argument access میشوند. Classِ string یک Indexer دارد که اجازه میدهد هر مقدار char را با index از نوع int access کنید:
string s = "hello";
Console.WriteLine (s[0]); // 'h'
Console.WriteLine (s[3]); // 'l'
Syntax استفاده از Indexer شبیه استفاده از Array است، با این تفاوت که argument یا argumentهای index میتوانند از هر Typeی باشند.
Indexerها همان modifierهای Propertyها را دارند ــ به «Properties» در صفحهٔ 114 مراجعه کنید ــ و میتوان آنها را بهصورت null-conditional با قراردادن علامت سؤال پیش از براکت مربع فراخوانی کرد؛ به «Null Operators» در صفحهٔ 82 مراجعه کنید:
string s = null;
Console.WriteLine (s?[0]); // Writes nothing; no error.
پیادهسازی Indexer
برای نوشتن یک Indexer، Propertyای با نام this تعریف کنید و argumentها را داخل براکت مربع مشخص کنید:
class Sentence
{
string[] words = "The quick brown fox".Split();
public string this [int wordNum] // indexer
{
get { return words [wordNum]; }
set { words [wordNum] = value; }
}
}
میتوانیم از این Indexer به شکل زیر استفاده کنیم:
Sentence s = new Sentence();
Console.WriteLine (s[3]); // fox
s[3] = "kangaroo";
Console.WriteLine (s[3]); // kangaroo
یک Type میتواند چند Indexer با parameterهای دارای Typeهای متفاوت اعلان کند. Indexer همچنین میتواند بیش از یک parameter بگیرد:
public string this [int arg1, string arg2]
{
get { ... } set { ... }
}
اگر Accessorِ set را حذف کنید، Indexer read-only میشود و میتوانید با Syntaxِ expression-bodied تعریف آن را کوتاه کنید:
public string this [int wordNum] => words [wordNum];
پیادهسازی Indexer در CLR
Indexerها درونساختار به Methodهایی با نام get_Item و set_Item Compile میشوند:
public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}
استفاده از Index و Range با Indexerها
با تعریف Indexer دارای parameter از Typeِ Index یا Range میتوانید در Classهای خود از Index و Range پشتیبانی کنید؛ به «Indices and Ranges» در صفحهٔ 63 مراجعه کنید. میتوانیم مثال قبلی را با افزودن Indexerهای زیر به Classِ Sentence گسترش دهیم:
public string this [Index index] => words [index];
public string[] this [Range range] => words [range];
در نتیجه کد زیر ممکن میشود:
Sentence s = new Sentence();
Console.WriteLine (s [^1]); // fox
string[] firstTwoWords = s [..2]; // (The, quick)
Primary Constructorها (C# 12)
از C# 12 میتوانید یک فهرست parameter را مستقیماً بعد از اعلان Class ــ یا Struct ــ قرار دهید:
class Person (string firstName, string lastName)
{
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
این اعلان به Compiler دستور میدهد با استفاده از parameterهای Primary Constructor یعنی firstName و lastName بهصورت خودکار Primary Constructor بسازد؛ در نتیجه میتوانیم Class را اینگونه instantiate کنیم:
Person p = new Person ("Alice", "Jones");
p.Print(); // Alice Jones
Primary Constructorها برای prototyping و سناریوهای سادهٔ دیگر مفیدند. جایگزین آن این است که Fieldها را تعریف و Constructor را صریحاً بنویسید:
class Person // (without primary constructors)
{
string firstName, lastName; // Field declarations
public Person (string firstName, string lastName) // Constructor
{
this.firstName = firstName; // Assign field
this.lastName = lastName; // Assign field
}
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
Constructorی که C# میسازد «primary» نامیده میشود، زیرا هر Constructor اضافی که خودتان بهصورت صریح بنویسید باید آن را فراخوانی کند:
class Person (string firstName, string lastName)
{
public Person (string firstName, string lastName, int age)
: this (firstName, lastName) // Must call the primary constructor
{
...
}
}
این الزام تضمین میکند parameterهای Primary Constructor همیشه مقدار دریافت کنند.
Primary Constructorها بهسبب محدودیتهای زیر بیش از همه برای سناریوهای ساده مناسباند:
- نمیتوانید کد مقداردهی اولیهٔ اضافی به Primary Constructor اضافه کنید.
- اگرچه expose کردن parameterِ Primary Constructor بهصورت Property عمومی آسان است، مگر اینکه Property read-only باشد نمیتوانید بهآسانی منطق validation را در آن ادغام کنید.
Primary Constructorها Constructor پیشفرضِ بدون parameter را که C# در غیر این صورت تولید میکرد کنار میزنند.
Semanticsِ Primary Constructor
برای درک نحوهٔ کار Primary Constructorها، رفتار یک Constructor معمولی را در نظر بگیرید:
class Person
{
public Person (string firstName, string lastName)
{
... do something with firstName, lastName
}
}
وقتی اجرای کد داخل این Constructor تمام میشود، parameterهای firstName و lastName از scope خارج میشوند و دیگر قابل access نیستند. در مقابل، parameterهای Primary Constructor از scope خارج نمیشوند و در تمام طول عمر شیء از هر نقطهٔ داخل Class قابل access باقی میمانند.
Primary Constructorها و Field/Property Initializerها
قابلیت دسترسی parameterهای Primary Constructor به Field و Property Initializerها نیز گسترش مییابد. در مثال زیر، از Field و Property Initializer استفاده میکنیم تا firstName را به یک Field عمومی و lastName را به یک Property عمومی نسبت دهیم:
class Person (string firstName, string lastName)
{
public readonly string FirstName = firstName; // Field
public string LastName { get; } = lastName; // Property
}
Mask کردن parameterهای Primary Constructor
Fieldها یا Propertyها میتوانند نام parameterهای Primary Constructor را دوباره استفاده کنند:
class Person (string firstName, string lastName)
{
readonly string firstName = firstName;
readonly string lastName = lastName;
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
در این سناریو، Field یا Property اولویت پیدا میکند و در نتیجه parameterِ Primary Constructor را mask میکند؛ بهجز در سمت راست Field و Property Initializerها.
Validationِ parameterهای Primary Constructor
گاهی انجام محاسبه در Field Initializerها مفید است:
new Person ("Alice", "Jones").Print(); // Alice Jones
class Person (string firstName, string lastName)
{
public readonly string FullName = firstName + " " + lastName;
public void Print() => Console.WriteLine (FullName);
}
در مثال بعدی، نسخهٔ uppercaseِ lastName را در Fieldی با همان نام ذخیره میکنیم و مقدار اصلی را mask میکنیم:
new Person ("Alice", "Jones").Print(); // Alice JONES
class Person (string firstName, string lastName)
{
readonly string lastName = lastName.ToUpper();
public void Print() => Console.WriteLine (firstName + " " + lastName);
}
در «throw expressions» در صفحهٔ 200 توضیح میدهیم چگونه هنگام برخورد با سناریوهایی مانند دادهٔ نامعتبر Exception پرتاب کنید. در اینجا یک preview میآوریم تا نشان دهیم چگونه میتوان از آن همراه Primary Constructorها برای validate کردن lastName هنگام ساخت استفاده کرد و مطمئن شد مقدار آن null نیست:
new Person ("Alice", null); // throws ArgumentNullException
class Person (string firstName, string lastName)
{
readonly string lastName = (lastName == null)
? throw new ArgumentNullException ("lastName")
: lastName;
}
به یاد داشته باشید کد داخل Field یا Property Initializer هنگام ساخت شیء اجرا میشود، نه هنگام access به Field یا Property. در مثال بعد، parameterِ Primary Constructor را بهصورت Property قابل خواندن/نوشتن expose میکنیم:
class Person (string firstName, string lastName)
{
public string LastName { get; set; } = lastName;
}
افزودن validation به این مثال ساده نیست، زیرا باید در دو محل validate کنید: در Accessorِ set Property که دستی پیادهسازی شده و در Property Initializer. اگر Property بهشکل init-only تعریف شود نیز همین مشکل وجود دارد. در این نقطه، کنارگذاشتن میانبُر Primary Constructorها و تعریف صریح Constructor و backing fieldها آسانتر است.
Static Constructorها
Static Constructor بهجای یک بار برای هر instance، یک بار برای هر Type اجرا میشود. یک Type فقط میتواند یک Static Constructor تعریف کند و آن Constructor باید بدون parameter و همنام Type باشد:
class Test
{
static Test() { Console.WriteLine ("Type Initialized"); }
}
Runtime بهصورت خودکار درست پیش از استفاده از Type، Static Constructor را فراخوانی میکند. دو چیز این کار را trigger میکنند:
- Instantiate کردن Type
- Access کردن یک عضو
static در Type
تنها modifierهایی که Static Constructorها اجازه میدهند unsafe و extern هستند.
Static Constructorها و ترتیب مقداردهی اولیهٔ Field
Static Field Initializerها درست پیش از فراخوانی Static Constructor اجرا میشوند. اگر Type، Static Constructor نداشته باشد، Static Field Initializerها درست پیش از استفاده از Type ــ یا هر زمان زودتری که Runtime صلاح بداند ــ اجرا میشوند.
Static Field Initializerها به ترتیب اعلان Fieldها اجرا میشوند. مثال زیر این موضوع را نشان میدهد. X با 0 و Y با 3 مقداردهی اولیه میشود:
class Foo
{
public static int X = Y; // 0
public static int Y = 3; // 3
}
اگر جای دو Field Initializer را عوض کنیم، هر دو Field با 3 مقداردهی اولیه میشوند. مثال بعد ابتدا 0 و سپس 3 را چاپ میکند، زیرا Field Initializerی که یک Foo instantiate میکند پیش از مقداردهی X با 3 اجرا میشود:
Console.WriteLine (Foo.X); // 3
class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() => Console.WriteLine (X); // 0
}
اگر جای دو خط برجسته را عوض کنیم، مثال ابتدا 3 و سپس 3 را چاپ میکند.
Static Classها
Classی که با static علامتگذاری شده باشد قابل instantiate یا subclass شدن نیست و باید فقط از اعضای static تشکیل شده باشد. Classهای System.Console و System.Math نمونههای خوبی از Static Class هستند.
Finalizerها
Finalizerها Methodهایی مخصوص Class هستند که پیش از آنکه Garbage Collector حافظهٔ یک شیء بدون reference را reclaim کند اجرا میشوند. Syntax یک Finalizer نام Class است که نماد ~ پیش از آن قرار گرفته است:
class Class1
{
~Class1()
{
...
}
}
این در واقع Syntax زبان C# برای override کردن Methodِ Finalize از Object است و Compiler آن را به اعلان Method زیر گسترش میدهد:
protected override void Finalize()
{
...
base.Finalize();
}
Garbage Collection و Finalizerها را بهطور کامل در فصل ۱۲ بررسی میکنیم.
میتوانید Finalizerهای تک-statement را با Syntaxِ expression-bodied بنویسید:
~Class1() => Console.WriteLine ("Finalizing");
Partial Typeها و Methodها
Partial Typeها اجازه میدهند تعریف یک Type ــ معمولاً در چند فایل ــ تقسیم شود. سناریوی رایج این است که یک Partial Class از منبع دیگری ــ مانند template یا designer ویژوال استودیو ــ بهطور خودکار تولید شود و آن Class با Methodهای اضافیِ نوشتهشده بهصورت دستی گسترش یابد:
// PaymentFormGen.cs - auto-generated
partial class PaymentForm { ... }
// PaymentForm.cs - hand-authored
partial class PaymentForm { ... }
هر بخش مشارکتکننده باید اعلان partial داشته باشد؛ کد زیر غیرمجاز است:
partial class PaymentForm {}
class PaymentForm {}
بخشهای مشارکتکننده نمیتوانند اعضای متعارض داشته باشند. برای مثال، Constructorی با همان parameterها نمیتواند تکرار شود. Partial Typeها بهطور کامل توسط Compiler resolve میشوند؛ یعنی هر بخش باید در زمان Compile در دسترس باشد و در همان Assembly قرار داشته باشد.
میتوانید Base Class را روی یک یا چند اعلان Partial Class مشخص کنید، بهشرط آنکه Base Classِ مشخصشده در همه جا یکسان باشد. افزون بر این، هر بخش میتواند بهطور مستقل Interfaceهایی را که باید implement شوند مشخص کند. Base Classها و Interfaceها را در «Inheritance» صفحهٔ 126 و «Interfaces» صفحهٔ 147 بررسی میکنیم.
Compiler هیچ تضمینی دربارهٔ ترتیب مقداردهی اولیهٔ Fieldها میان اعلانهای مختلف Partial Type ارائه نمیکند.
Partial Methodها
یک Partial Type میتواند Partial Method داشته باشد. این Methodها به Partial Type تولیدشده بهطور خودکار اجازه میدهند hookهای قابل سفارشیسازی برای کدنویسی دستی فراهم کند؛ برای مثال:
partial class PaymentForm // In auto-generated file
{
...
partial void ValidatePayment (decimal amount);
}
partial class PaymentForm // In hand-authored file
{
...
partial void ValidatePayment (decimal amount)
{
if (amount > 100)
...
}
}
Partial Method از دو بخش تشکیل میشود: definition و implementation. Definition معمولاً توسط Code Generator نوشته میشود و implementation معمولاً بهصورت دستی نوشته میشود. اگر implementation فراهم نشود، definition مربوط به Partial Method در Compile حذف میشود، همانطور که کدی که آن را فراخوانی میکند نیز حذف میشود. این ویژگی به کد تولیدشده اجازه میدهد بدون نگرانی از bloat، hookهای زیادی فراهم کند.
Partial Methodها باید void باشند و بهصورت ضمنی private هستند. آنها نمیتوانند parameter از نوع out داشته باشند.
Extended Partial Methodها
Extended Partial Methodها ــ از C# 9 ــ برای سناریوی معکوس Code Generation طراحی شدهاند؛ جایی که programmer hookهایی تعریف میکند که Code Generator آنها را implement میکند. نمونهای از این کاربرد Source Generatorها هستند؛ قابلیتی در Roslyn که به شما اجازه میدهد Assemblyای به Compiler بدهید تا بخشهایی از کد شما را بهطور خودکار تولید کند.