Conversion Operators
در قسمت قبل اندکي با conversion operators آشنا شديد و همينطور چگونگي استفاده از implicit conversion را فرا گرفتيد. براي تبديل implicit بهصورت زير عمل ميکرديم:
using System;
class TwoD
{
int X, Y;
public TwoD()
{
X = Y = 0;
}
public TwoD(int a, int b)
{
X = a;
Y = b;
}
public static implicit operator int(TwoD op)
{
return op.X * op.Y;
}
}
class OpOvDemo
{
static void Main()
{
TwoD ob1 = new TwoD(2, 2);
int i = ob1;
Console.WriteLine(i);
}
}
در اين حالت، تبديل بهطور اتوماتيک انجام ميشود و مقدار ? در i قرار ميگيريد. اگر conversion را بهطور explicit تعريف کنيد، تبديل بهصورت اتوماتيک انجام نميشود و cast مورد نياز است. در زير برنامهي بالا را بازنويسي کردهايم اما اينبار بهجاي implicit از explicit استفاده شده است:
using System;
class TwoD
{
int X, Y;
public TwoD()
{
X = Y = 0;
}
public TwoD(int a, int b)
{
X = a;
Y = b;
}
public static explicit operator int(TwoD op)
{
return op.X * op.Y;
}
}
class OpOvDemo
{
static void Main()
{
TwoD ob1 = new TwoD(2, 2);
int i = (int)ob1;
Console.WriteLine(i);
}
}
همانطور که ميبينيد، مقدار شيء ob1 درون i قرار نميگيرد مگر اينکه ابتدا cast انجام شود:
1
int i = (int)ob1;
اگر cast را حذف کنيد برنامه کامپايل نخواهد شد.
محدوديتهايي که در conversion operators وجود دارد:
Target-type يا source-type در conversion بايستي از جنس همان کلاسي طراحی وبسایت باشد که conversion در آن تعريف شده است. براي مثال نميتوانيد تبديل double به int را از نو تعريف کنيد.
نميتوانيد class type را به نوع دادهي object تبديل کنيد.
نميتوانيد براي يک source-type و target-type هم تبديل implicit و هم تبديل explicit تعريف کنيد.
نميتوانيد از يک base class به يک derived class تبديل انجام دهيد (با مبحث ارثبري بعداً آشنا خواهيد شد).
نميتوانيد براي يک class-type به/از interface تبديل انجام دهيد (با مبحث interface بعداً آشنا خواهيد شد).
علاوهبر اين قوانين، براي انتخاب بين implicit يا explicit بايد دقت کنيد. implicit conversion بايد زماني مورد استفاده قرار گيرد که تبديل کاملاً عاري از خطا باشد. براي کسب اطمينان در اين مورد از اين دو قانون پيروي کنيد: يک، هيچ فقدان اطلاعاتي (مثل کوتاهسازي، سرريز، تغيير علامت و…) نبايد رخ دهد. دو، تبديل نبايد باعث بروز exception يا خطا در برنامه شود. اگر conversion نتواند اين دو قانون را رعايت کند، بايد از explicit conversion بهره ببريد.
هيچ نياز و اجباري نيست که عملکرد اپراتور overload شده با عمکرد اصلي آن operator ارتباط داشته باشد. با اين حال، بهدليل اينکه ساختار و خوانايي کد حفظ شود، بهتر است اپراتور overload شده بازتابي از رفتار اصلي آن operator باشد. براي مثال، + مربوط به کلاس TwoD از نظر مفهومي، مشابه + در نوع integer است و اينکه اين operator رفتاري مشابه / داشته باشد چندان جالب نيست.
توجه کنيد که الويت operator ها قابل تغيير نيست و نميتوانيد اين الويت را عوض کنيد همچنين تعدادي از operator ها قابل overload شدن نيستند. در جدول زير operator هايي که قابل overload شدن نيستند مشخص شدهاند:
با operator هاي ناآشنا در جدول بالا، در مقالات آينده آشنا خواهيد شد. قابل ذکر است که operator هاي انتسابي نيز overload نميشوند و همينطور operator هايي بهشکل += نيز قابل overload شدن نيستند. البته اگر يک operator را overload کنيد که بهطور کلي حالت ترکيبي مثل =+ را هم دارا باشد، اين حالت ترکيبي براي شيء شما نيز بهصورت اتوماتيک اعمال ميشود. بهعنوان مثال اگر + را overload کنيد، =+ نيز براي استفاده فعال است:
TwoD a = new TwoD(2, 2);
TwoD b = new TwoD(3, 4);
a += b;
نکتهي ديگر اينکه، اگرچه نميتوانيد اپراتور [ ] که مربوط به index آرايه است را overload کنيد، اما ميتوانيد از indexer استفاده کنيد که در ادامه به آن ميپردازيم.
Indexers
همانطور که ميدانيد، index گذاري آرايه از طريق اپراتور [ ] انجام ميشود. تعريف کردن اپراتور [ ] براي کلاس نيز امکانپذير است اما براي اين منظور از operator method استفاده نکرده و در عوض از Indexer استفاده ميکنيد. Indexer اجازه ميدهد يک شيء مانند يک آرايه index گذاري شود. Indexer ها ميتوانند يک يا بيشتر از يک بعد داشته باشند و ما در اينجا با Indexer يک بعدي شروع ميکنيم.
فرم کلي Indexer يک بعدي بهشکل زير است:
element-type this[int index] {
// The get accessor
get {
// return the value specified by index
}
// The set accessor
set {
// set the value specified by index
}
}
در اينجا، element-type مشخص کنندهي نوع عنصر indexer است. از اينرو، هر عنصري که توسط indexer قابل دسترسي باشد، از نوع element-type است. اين نوع با نوع يک آرايه (که براي indexer در نظر ميگيريد و اصطلاحاْ به آن backing store ميگويند) يکسان است. پارامتر index در واقع index عنصري که ميخواهيد به آن دسترسي داشته باشيد را مشخص ميکند. توجه کنيد که نيازي نيست حتماْ جنس پارامتر int باشد اما از آنجا که indexer ها مشابه با index آرايه مورد استفاده قرار ميگيرند، استفاده از int در اين مورد رايج است.
درون بدنهي indexer کلمههاي get و set را مشاهده ميکنيد که به هر کدام از آنها accessor گفته ميشود. يک accessor مشابه يک متد است با اين تفاوت که return-type و parameter ندارد. هنگاميکه از indexer استفاده ميکنيد اين accessor ها بهطور اتوماتيک فراخواني ميشوند و هر دوي accessor ها index را بهعنوان پارامتر دريافت ميکنند. اگر indexer در طرف چپ تساوي قرار گرفته باشد، بنابراين set accessor فراخواني و يک مقدار به عنصري که توسط index مشخص شده است، اختصاص داده ميشود. در غير اينصورت get accessor فراخواني شده و عنصر مشخص شده توسط index، return ميشود. Set method همچنين يک پارامتر به اسم value دارد که شامل مقداري است که به يک index مشخص اختصاص داده ميشود.
يکي ديگر از مزيتهاي indexer اين است که ميتوانيد دسترسي به آرايه را دقيقاً تحت کنترل داشته باشيد و از دسترسيهاي نامناسب جلوگيري کنيد.
به مثال سادهي زير توجه کنيد:
using System;
class IndexerDemo
{
int[] arr; // reference to underlying array (backing store)
public int Lenght;
public IndexerDemo(int size)
{
arr = new int[size];
Lenght = size;
}
// Indexer
public int this[int index]
{
// get accessor
get
{
return arr[index];
}
// set accessor
set
{
arr[index] = value;
}
}
}
class idx
{
static void Main()
{
IndexerDemo ob = new IndexerDemo(4);
ob[0] = 10;
ob[1] = 20;
ob[2] = 30;
ob[3] = 40;
for (int i = 0; i < ob.Lenght; i++)
{
Console.WriteLine(ob[i]);
}
}
}
همانطور که ميبينيد، يک indexer تعريف کردهايم که با عناصري از نوع int سروکار دارد. Indexer را بهصورت public تعريف کردهايم تا خارج از کلاس نيز قابل دسترس باشد. در قسمت get accessor مقدار [arr[index را return کردهايم و همينطور در قسمت set accessor نيز value به index عنصر مربوطه اختصاص داده ميشود. Value يک پارامتر بوده و شامل مقداري است که به آرايه اختصاص داده ميشود. نيازي نيست که يک indexer هم get و set را داشته باشد بلکه ميتوانيد يک indexer داشته باشيد که تنها get يا set را دارد و read-only يا write-only است. البته در مثال بالا براي سادگي بيشتر، هيچ کنترلي روي مقاديري که قرار است get يا set شوند اعمال نکردهايم.
در مثال زير روي get و set کنترل بيشتري اعمال کردهايم:
using System;
class IndexerDemo
{
int[] arr;
public int Length;
public bool ErrFlag;
public IndexerDemo(int size)
{
arr = new int[size];
Length = size;
}
public int this[int index]
{
get
{
if (Ok(index))
{
ErrFlag = false;
return arr[index];
}
else
{
ErrFlag = true;
return 0;
}
}
set
{
if (Ok(index))
{
ErrFlag = false;
arr[index] = value;
}
else
ErrFlag = true;
}
}
private bool Ok(int index)
{
if (index >= 0 && index < Length)
return true;
return false;
}
}
class Idx
{
static void Main()
{
IndexerDemo ob = new IndexerDemo(5);
for (int i = 0; i < 10; i++)
{
ob[i] = i * 10;
if (ob.ErrFlag)
Console.WriteLine("ob[{0}] is out of bound!", i);
else
Console.WriteLine("ob[{0}]: {1}", i, ob[i]);
}
}
}
خروجي:
همانطور که ميبينيد، پيش از آنکه get يا set کنيم، ابتدا توسط متد ()Ok صحيح بودن index را بررسي کردهايم تا در محدودهي درست بوده و out of bound نباشد. همچنين هنگاميکه index نامناسبي در حال get يا set شدن است، متغيري به اسم ErrFalg مقداردهي ميشود که نشاندهندهي بروز خطا است. البته براي خطايابي در مقالات آينده با روش مناسبتري آشنا خواهيد شد اما در حال حاضر همين روش مناسب است.
يک indexer ميتواند overload شود. در مثال زير علاوهبر indexer هاي int ميتوانيد indexer هايي از نوع double نيز داشته باشيد. در اين مثال double indexer به نزديکترين index گرد (round) ميشود:
public int this[double index]
{
get
{
int idx = (int)Math.Round(index);
if (Ok(idx))
{
ErrFlag = false;
return arr[idx];
}
else
{
ErrFlag = true;
return 0;
}
}
set
{
int idx = (int)Math.Round(index);
if (Ok(idx))
{
ErrFlag = false;
arr[idx] = value;
}
else
{
ErrFlag = true;
}
}
}
private bool Ok(int index)
{
if (index >= 0 && index < Length)
return true;
return false;
}
}
class Idx
{
static void Main()
{
IndexerDemo ob = new IndexerDemo(5);
for (int i = 0; i < 10; i++)
{
ob[i] = i * 10;
if (ob.ErrFlag)
Console.WriteLine("ob[{0}] is out of bound!", i);
else
Console.WriteLine("ob[{0}]: {1}", i, ob[i]);
}
Console.WriteLine();
ob[1] = 4;
ob[2] = 8;
Console.WriteLine("ob[1]: {0}", ob[1]);
Console.WriteLine("ob[2]: {0}", ob[2]);
Console.WriteLine("ob[1.3]: {0}", ob[1.3]);
Console.WriteLine("ob[1.7]: {0}", ob[1.7]);
}
}
خروجي:
همانطور که در خروجي ميبينيد، index هاي double توسط متد ()Math.Round به نزديکترين عدد صحيح، گرد شدهاند. ??? به ? و ??? به ? گرد شده است.
قابل ذکر است که نيازي نيست حتماً يک آرايه براي indexer داشته باشيد. مهم اين است که به يک کلاس اين قابليت را اضافه کنيد تا بهشکل آرايه نيز بتوان از آن استفاده کرد.
به مثال زير توجه کنيد:
using System;
class IndexerDemo
{
public int this[int index]
{
get
{
if (index >= 1 && index <= 10)
{
return index * 10;
}
else return -1;
}
}
}
class Idx
{
static void Main()
{
IndexerDemo ob = new IndexerDemo();
Console.WriteLine(ob[1]);
Console.WriteLine(ob[2]);
Console.WriteLine(ob[3]);
Console.WriteLine(ob[11]);
Console.WriteLine(ob[10]);
}
}
يعني استفاده بهشکل زير، نادرست است:
1
ob[2] = 5;
اما بهصورت زير، کاملاً صحيح است:
1
int i = ob[2];
دو محدوديت ديگر براي Indexer ها موجود است. يک، بهدليل اينکه indexer ها درواقع storage location (محل ذخيره سازي) تعريف نميکنند و به نوعي متد هستند، استفاده از آنها بهعنوان پارامتر ref و out غيرمجاز است. دو، indexer نميتواند بهصورت static تعريف شود.
Indexer هاي چند بعدي
شما ميتوانيد براي آرايههاي چند بعدي نيز، indexer بسازيد.
به مثال زير توجه کنيد:
// A two-dimensional fail-soft array.
using System;
class FailSoftArray2D
{
int[,] a; // reference to underlying 2D array
int rows, cols; // dimensions
public int Length; // Length is public
public bool ErrFlag; // indicates outcome of last operation
// Construct array given its dimensions.
public FailSoftArray2D(int r, int c)
{
rows = r;
cols = c;
a = new int[rows, cols];
Length = rows * cols;
}
// This is the indexer for FailSoftArray2D.
public int this[int index1, int index2]
{
// This is the get accessor.
get
{
if (ok(index1, index2))
{
ErrFlag = false;
return a[index1, index2];
}
else
{
ErrFlag = true;
return 0;
}
}
// This is the set accessor.
set
{
if (ok(index1, index2))
{
a[index1, index2] = value;
ErrFlag = false;
}
else ErrFlag = true;
}
}
// Return true if indexes are within bounds.
private bool ok(int index1, int index2)
{
if (index1 >= 0 & index1 < rows &
index2 >= 0 & index2 < cols)
return true;
return false;
}
}
// Demonstrate a 2D indexer.
class TwoDIndexerDemo
{
static void Main()
{
FailSoftArray2D fs = new FailSoftArray2D(3, 5);
int x;
// Show quiet failures.
Console.WriteLine("Fail quietly.");
for (int i = 0; i < 6; i++)
fs[i, i] = i * 10;
for (int i = 0; i < 6; i++)
{
x = fs[i, i];
if (x != -1) Console.Write(x + " ");
}
Console.WriteLine();
// Now, display failures.
Console.WriteLine("\nFail with error reports.");
for (int i = 0; i < 6; i++)
{
fs[i, i] = i * 10;
if (fs.ErrFlag)
Console.WriteLine("fs[" + i + ", " + i + "] out-of-bounds");
}
Console.WriteLine();
for (int i = 0; i < 6; i++)
{
x = fs[i, i];
if (!fs.ErrFlag) Console.Write(x + " ");
else
Console.Write("\nfs[" + i + ", " + i + "] out-of-bounds");
}
Console.WriteLine();
}
}