من رفتم سربازی اگر محتوای منو دوست داشتید و بدردتون خورد از من حمایت مالی کنید

آموزش سی شارپ – عملگرهای سربار

آموزش سی شارپ - عملگرهای سربار
آموزش سی شارپ - عملگرهای سربار

آموزش سی شارپ – عملگرهای سربار

یکی از موارد جالبی که در کمتر زبان‌های برنامه نویسی دیده می‌شود بحث overload عملگرها است. در سی شارپ به صورت کامل بحث شده است. این قسمت از دوره‌ی آموزشی سی شارپ در ارتباط با “عملگرهای سربار سی شارپ” با ما بخوانید.

 

حتما قبل از بررسی عملگرهای سربار در سی شارپ نوشته‌ی مطالب قبلی این دوره‌ی آموزشی را با هشتگ #دوره آموزشی_سی_شارپ در سایت ما را مطالعه کنید.

 

آموزش سی شارپ عملگرهای سربار

overload کردن عملگرهای رابطه‌ای

به‌طور معمول، یک عملگر رابطه‌ای overload شده، مقدار true یا false را return می‌کند. نکته‌ی مهم دیگر این‌جاست که بایستی relational operators را به‌طور جفتی overload کنید. به‌عنوان مثال اگر > را overload کردید، بایستی < را نیز overload کنید. این مورد برای operator‌های (<= و >=) و (== و !=) نیز صادق است.

class TwoD
{
   int X, Y;
   
   public TwoD()
   {
      X = Y = 0;
   }
   public TwoD(int a, int b)
   {
      X = a;
      Y = b;
   }
   public static bool operator <(TwoD op1, TwoD op2)
   {
      return ((op1.X < op2.X) && (op1.X < op2.Y));
   }
   public static bool operator >(TwoD op1, TwoD op2)
   {
      return ((op1.X > op2.X) && (op1.Y > op2.Y));
   }
}

// Using :
TwoD ob1 = new TwoD(1, 4);
TwoD ob2 = new TwoD(2, 3);
if (ob1 > ob2) Console.WriteLine("ob1 is greater than ob2");
if (ob1 < ob2) Console.WriteLine("ob1 is less than ob2");

نکته‌: اگر می‌خواهید operatorهای == و =! را overload کنید، بایستی متدهای ()Object.Equels و ()Object.GetHashCode را نیز override کنید.

 

overload کردن True و False

کلمات کلیدی true و false نیز می‌توانند به‌عنوان unary operators به‌ منظور overload کردن مورد استفاده قرار گیرند. هنگامی‌که true و false برای یک کلاس overload می‌شوند، می‌توانید از اشیای آن کلاس برای کنترل کردن if، for، while و do-while و هم‌چنین ? استفاده کنید. Operatorهای true و false باید باهم overload شوند. نمی‌توانید فقط یکی از آن‌ها را overload کنید. هر دوی آن‌ها unary operator هستند.

فرم کلی آن‌ها به‌صورت زیر است:

public static bool operator true(param-type operand)
{
    // return true or false
}
public static bool operator false(param-type operand)
{
    // return true or false
}

مثال: اگر یکی از فیلدها غیر صفر شود، شیء true است و اگر همه‌ی فیلدها صفر شوند، شیء false است.

class TwoD
{
   int X, Y;

   public TwoD()
   {
      X = Y = 0;
   }
   public TwoD(int a, int b)
   {
      X = a;
      Y = b;
   }
   public static bool operator true(TwoD op)
   {
      return (op.X != 0 || op.Y != 0);
   }
   public static bool operator false(TwoD op)
   {
      return (op.X == 0 && op.Y == 0);
   }
   public static TwoD operator --(TwoD op)
   {
      TwoD result = new TwoD();
      result.X = op.X - 1;
      result.Y = op.Y - 1;
      return result;
   }
   public static TwoD operator ++(TwoD op)
   {
      TwoD result = new TwoD();
      result.X = op.X + 1;
      result.Y = op.Y + 1;
      return result;
   }
   public void Show()
   {
      Console.WriteLine("{0}, {1}", X, Y);
   }
}

// Using :
TwoD ob = new TwoD(5, 5);
if (ob)
   Console.WriteLine("ob is true");
Console.WriteLine(); 
for (; ob; ob--)
   ob.Show();
Console.WriteLine();
ob = new TwoD(-3, -3);
while (ob)
{
   ob.Show();
   ob++;
}

 

overload کردن عملگرهای منطقی

فقط & و | و ! می‌توانند overload شوند، اما با دنبال کردن یک سری قوانین می‌توانید از مزیت‌های && و || بهره ببرید؛ اگر قصد استفاده از عملگرهای منطقی short-circuit را نداشته، می‌توانید بسیار ساده & و | را overload کنید. هر کدام از آن‌ها یک مقدار bool را return می‌کند. overload کردن ! نیز مقدار bool را return می‌کند.

class TwoD
{
   int X, Y;

   public TwoD()
   {
      X = Y = 0;
   }
   public TwoD(int a, int b)
   {
      X = a;
      Y = b;
   }
   public static bool operator &(TwoD op1, TwoD op2)
   {
      return ((op1.X != 0 && op1.Y != 0) && (op2.X != 0 && op2.Y != 0));
   }
   public static bool operator |(TwoD op1, TwoD op2)
   {
      return ((op1.X != 0 || op1.Y != 0) || (op2.X != 0 || op2.Y != 0));
   }
   public static bool operator !(TwoD op)
   {
      return (op.X == 0 & op.Y == 0);
   }
   public void Show()
   {
      Console.WriteLine("{0}, {1}", X, Y);
   }
}

// Using :
TwoD a = new TwoD(1, 2);
TwoD b = new TwoD(8, 8);
TwoD c = new TwoD(); 
Console.Write("Here is a: ");
a.Show();
Console.Write("Here is b: ");
b.Show();
Console.Write("Here is c: ");
c.Show();
Console.WriteLine();
if (!a) Console.WriteLine("a is false");

if (!b) Console.WriteLine("b is false");

if (!c) Console.WriteLine("c is false");  

if (a & b) Console.WriteLine("a & b is true");
else Console.WriteLine("a & b is false");

if (a & c) Console.WriteLine("a & c is true");
else Console.WriteLine("a & c is false");
Console.WriteLine();

if (a | b) Console.WriteLine("a | b is true");
else Console.WriteLine("a | b i false");

if (a | c) Console.WriteLine("a | c is true");
else Console.WriteLine("a | c is false");

 

معرفی Conversion Operators

Conversion operator‌ یک شیء از کلاس شما را به نوع دیگری که مد نظرتان است تبدیل می‌کند.

دو حالت از conversion operator موجود است:

implicit و explicit که فرم کلی آن‌ها به شکل زیر است:

public static operator implicit target-type(source-type v) 
{
   return value;
}
public static operator explicit target-type(source-type v) 
{
   return value;
}

target-type مشخص کننده‌ی نوعی است که قصد دارید source-type را به آن تبدیل کنید و value مقدار کلاس، بعد از تبدیل است. Conversion operator اطلاعات را مطابق با target-type باز می‌گرداند. اگر conversion operator به‌طور implicit مشخص شود، conversion به‌صورت اتوماتیک انجام خواهد شد. اگر conversion به‌صورت explicit تعریف شده باشد، cast مورد نیاز است. نمی‌توانید برای یک source-type و target-type هم implicit و explicit را تعریف کنید.

مثال:

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;
    }
}

// Using :
TwoD ob1 = new TwoD(2, 2);
TwoD ob2 = new T TwoD(1, 1);
int i = ob1 * 3;
Console.WriteLine(i);
i = ob1 - 3;
Console.WriteLine(i);
i = ob1 + ob2;
Console.WriteLine(i);
i = ob1;
Console.WriteLine(i);

نکته:

محدودیت‌هایی که در 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 تبدیل انجام دهید
  • علاوه‌بر این قوانین، برای انتخاب بین implicit یا explicit باید دقت کنید.
  • implicit conversion باید زمانی مورد استفاده قرار گیرد که تبدیل کاملاً عاری از خطا باشد.

برای کسب اطمینان در این مورد از این دو قانون پیروی کنید:

  • یک، هیچ فقدان اطلاعاتی (مثل کوتاه‌سازی، سرریز، تغییر علامت و…) نباید رخ دهد.
  • دو، تبدیل نباید باعث بروز exception یا خطا در برنامه شود.

اگر conversion نتواند این دو قانون را رعایت کند، باید از explicit conversion بهره ببرید.

 

معرفی Indexersها

index گذاری آرایه از طریق اپراتور [ ] انجام می‌شود. تعریف کردن اپراتور [ ] برای کلاس نیز امکان‌پذیر است اما برای این منظور از operator method‌ استفاده نکرده و در عوض از Indexer استفاده می‌کنیم. Indexer اجازه می‌دهد یک شیء مانند یک آرایه index گذاری شود. 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 است. پارامتر index در واقع index عنصری که می‌خواهید به آن دسترسی دارد را مشخص می‌کند. توجه کنید که نیازی نیست حتماْ جنس پارامتر int‌ شود اما استفاده از 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 مشخص اختصاص داده می‌شود.

مثال:

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;
                }
            }
        }


// Using :
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]);
}

برای اینکه این بحث به طور کامل واضح شود، در مثال زیر روی get و set کنترل بیش‌تری اعمال کرده‌ایم:

class IndexerDemo
{
   int[] arr;
   public int Length;
   public bool ErrFlag;
   
   public IndexerDemo(int size)
   {
      arr = new int[size];
      Length = size;
   }
&nbsp;  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;
         }
      }
&nbsp;     private bool Ok(int index)
      {
         if (index >= 0 && index < Length)
            return true;
         return false;
      }   
}

// Using :
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]);
}

یک کلاس این قابلیت را اضافه کنید تا به‌ شکل آرایه نیز بتوان از آن استفاده کرد.

به مثال زیر توجه کنید:

class IndexerDemo
{
   public int this[int index]
   {
      get
      {
         if (index >= 1 && index <= 10)
         {
             return index * 10;
         }
         else 
             return -1;
      }
   }
}

// Using :
IndexerDemo ob = new IndexerDemo();
Console.WriteLine(ob[1]);
Console.WriteLine(ob[2]);
Console.WriteLine(ob[3]);
Console.WriteLine(ob[11]);       
Console.WriteLine(ob[10]);

دو محدودیت دیگر برای Indexerها موجود است. یک، به‌دلیل این‌که indexerها در واقع storage location (محل ذخیره سازی) تعریف نمی‌کنند و به نوعی متد هستند. استفاده از آن‌ها به‌عنوان پارامتر ref و out غیرمجاز است. دو، indexer نمی‌تواند به‌صورت static تعریف شود.

 

Properties در سی شارپ

Property یکی دیگر از اعضای کلاس است. برای این‌که با اساس کار Properties آشنا شوید.

به مثال ساده‌ی زیر توجه کنید:

class MyClass
{
   private int ID;
   public void SetID(int id)
   {
      if (id >= 0 && id <= 10)
      {
         ID = id;
      }
   }
   public int GetID()
   {
       return ID;
   }
}

// Using :
MyClass ob = new MyClass();
ob.SetID(5);
Console.WriteLine("ID: " + ob.GetID()); 
ob.SetID(20);
Console.WriteLine("ID: " + ob.GetID());
ob.SetID(9);
Console.WriteLine("ID: " + ob.GetID());

کاری که Property انجام می‌دهد: کنترل دسترسی و مقداردهی به فیلد است. Property مانند Indexer از get accessor و set accessor استفاده می‌کند تا مقداری را در یک متغیر set و یا مقداری را از آن get کند.

فرم کلی یک property به‌شکل زیر است:

type name
{
   get
   {
   }
   set
   {
   }
}

مثال:

class MyClass
{
   private int id;
   public int ID
   {
      get
      {
         return id;
      }
      set
      {
         id = value;
      }
   }
}

 

معرفی Auto-Implemented Properties

می‌توان propertyهای خیلی ساده را تعریف کرد که دیگر نیازی به متغیر ندارند تا property روی آن‌ها مدیریت نشود. در عوض شما به کامپایلر اجازه می‌دهید که یک متغیر (underlying variable) برای این مورد به‌وجود آورد.

فرم کلی auto-implemented property به‌شکل زیر است:

type name { get; set; }

در این‌جا، type مشخص‌‌کننده‌ی نوع و name مشخص‌کننده‌‌ی نام property است. توجه کنید که get و set بدنه ندارند و مستقیماً بعد از آن‌ها semicolon قرار می‌گیرد. این syntax به کامپایلر می‌فهماند که باید یک storage location (که به آن backing field هم گفته می‌شود) برای نگه‌داری مقدار مورد نظر بسازد. این متغیر (backing field) دارای اسم نبوده و مستقیماً برای شما قابل دسترس نیست. تنها می‌توانید از طریق property به آن دسترسی پیدا کرد.

class Person
{
    public string Name { get; set; }
    public string Family { get; set; }
    public int Age { get; set; }
    public string Gender { get; set; }
    public Person(string name, string family)
    {
        Name = name;
        Family = family;
    }
}

// Using :
Person a = new Person("Ian", "Somerhalder");
Console.WriteLine("Name: " + a.Name);
Console.WriteLine("Family: " + a.Family);
 
a.Age = 26;
a.Gender = "Male";
 
Console.WriteLine("Age: " + a.Age);
Console.WriteLine("Gender: " + a.Gender);

همان‌طور که می‌بینید، به‌جای تعریف متغیر مستقیماً property تعریف کرده‌ایم. از آن‌جا که propertyهای تعریف شده public بوده و دارای getter و setter هستند، می‌توانید مقادیر را get و set کنید. برخلاف propertyهای معمولی، auto-implemented properties نمی‌توانند read-only یا write-only بشوند و همیشه get و set باید تعریف شوند. با این‌که auto- implemented properties روش جالب و راحتی است، تنها زمانی باید از آن استفاده کنید. نیازی به کنترل کردن backing field نیست. به‌طور پیش‌فرض، دسترسی به get و set بر اساس دسترسی خود properties (یا indexer) است. به‌عنوان مثال اگر property را به‌صورت public تعریف کنید، get و set نیز public هستند. با این حال می‌توانید برای get و set دسترسی جداگانه (مثلاً private) در نظر بگیرید.

به مثال زیر توجه کنید:

class Properties
{
   public int ID { get; private set; }
   public Properties()
   {
       ID = 180;
   }
}

// Using :
Properties ob = new Properties();
Console.WriteLine(ob.ID);
// ob.ID = 550; // Illegal!

در این مثال، ID در کلاس خودش هم می‌تواند get و هم می‌تواند set شود اما خارج از کلاس فقط قابل get شدن است. همان‌طور که ذکر شد auto-implemented property نمی‌تواند read-only یا write-only باشد (نمی‌تواند فقط get یا set شود). اما با در نظر گرفتن get یا set به‌صورت private می‌توانید دسترسی را محدود کنید.

 

مبحث Recursion

یک متد می‌تواند خودش را فراخوانی کند (درون خودش، خودش را صدا بزند)، به این پروسه recursion گفته می‌شود. متدی که خودش را صدا زده، recursive است. در مثال زیر محاسبه‌ی factorial را با روش recursive (بازگشتی) و nonrecursive (غیربازگشتی) می‌بینید:

class Factorial
{
   public int FactR(int n)
   {
      int result;
      if (n == 0) return 1;
      result = FactR(n - 1) * n;
      return result;
   }
}

// Using :
Factorial f = new Factorial();
Console.WriteLine("Factorials using recursive method.");
Console.WriteLine("Factorial of 3 is " + f.FactR(3));

خب این جلسه هم به پایان رسید باید به این نکته توجه کنید که مباحث “عملگرهای سربار سی شارپ” در برنامه نویسی تجاری اهمیت فراوانی دارد. برای استفاده از تمامی جلسات از هشتگ #دوره آموزشی_سی_شارپ در سایت ما استفاده کنید. منتظر جلسه‌ی بعدی دوره آموزشی سی شارپ بمانید.

برای امتیاز به این نوشته کلیک کنید!
[کل: 0 میانگین: 0]