ارث بری یا Inheritance یکی از جنبه های مهم شی گرایی در سی شارپ است.
ارث بری در زبان ساده یعنی من یک کلاسی دارم که در آن یک سری خصوصیت و رفتار، پیاده سازی کردم. حالا میخوام از روی این کلاس، یک کلاس دیگر را Extend کنم. به نحوی که در اون کلاس Extend شده، یا بسط شده:
نکته ای که در ارث بری مهمه، اینه که در واقع ما میخواهیم از یک کدی که از پیش نوشته شده هست،، دوباره استفاده کنیم و اون کد رو به عنوان یک base یا پایه برای کد جدید، در نظر بگیریم و دیگه دوباره کاری نکنیم و از اول چرخ نسازیم.
و نکته مهم دیگه اینه که ارث بری ساختار سلسه مراتبی داره. یعنی چی؟ یعنی یک کلاس میتونه یک کلاس Extend شده ای داشته باشه، که خود اون کلاس Extend شده، باز کلاس دیگری ازش بسط یا Extend بشه و این روال تا بی نهایت هم بره!!
یک اشتباهی که در ارث بری به کار میره، اینه که ما از لفظ های Parent-Child یا پدر، فرزندی یا Master-Detail برای توصیف کلاس اول، و کلاس Extend شده استفاده میکنیم و این اشتباهه.
لفظ درست ارث بری که اصل L سالید یا همون قانون Liskov بهش اشاره داره، BaseType و SubType هست و سعی کنید که از این به بعد، با این لفظ به ارث بری ارجاع کنید.
البته غیر از این لفظ، از دوتایی SubClass-BaseClass یا DriveType-BaseType یا DriveClass-SuperClass هم استفاده می شود.
ارث بری یکی از ساده ترین کارهای ممکن در سی شارپه! اصلا سینتکس پیچیده ای هم نداره.
یک کلاس دارم به اسم A
public class A
{
}
یک کلاس دیگه دارم به اسم B
public class B
{
}
چطور بگم B از A ارث میبره؟ فقط کافیه یک : بگذارم، جلوی اعلان کلاس B و بعدش بنویسم A.
public class B : A
{
}
تمام! B از A ارث برد.
عامل دومش سازنده است!. کلاس ها، متد سازنده دارند. حتی اگر شما در کدتون صراحتا متد سازنده ای
نداشته باشید هم، پشت صحنه این سازنده وجود داره! و خود سی شارپ میسازدش. حالا فرض کنید من
شی ای از کلاس B میسازم. با کلمه کلیدی new. اونوقت این سازنده ها چطور صدا زده میشوند؟ سازنده
SuperType اول فراخوانی میشه. اگر من چند تا کلاس Super داشته باشم، اون وقت این ایجاد Conflict
ممکنه بکنه و اینکه نحوه فراخوانی این سازنده ها، برای برنامه گیج کننده باشه.
public class A
{
public virtual void Add()
{
}
}
و بعد در کلاس B، زمان تعریف متد Add از کلمه کلیدی Override استفاده کنید.
public class B : A
{
public override void Add()
{
}
}
public class B : A
{
public override void Add()
{
}
public void AddAndPrint()
{
base.Add();
}
}
پلی مورفیسم و وراثت، حکایت شون دقیقا حکایت ماست ها و قیمه هاست که نباید قاطی بشوند!
درسته که Override کردن یک متد Virtual، درون یک Subtype یک نمونه از کاربرد Polymorphism هست. ولی پلی مورفیسم یا چند ریختی، اصلا مترادف وراثت نیست و این دو در یک کفه با هم قرار نمیگیرند.
چند ریختی، یک جنبه دیگری از برنامه نویسی شی گراست. مثل وراثت. و مثل وراثت، به هدف استفاده مجدد کد ها به کار میره.
منتها وراثت، میگه اگر کلاسی از یک کلاس دیگه Extend شد، من همه رفتارها و خصیصه های غیر Private اش رو به اون کلاس دیگه هم میدم و میتونه اونم ازش استفاده کنه. ولی پلی مورفیسم، جزئی تر در مورد استفاده مجدد یک کد صحبت میکنه، نه لزوما در سطح کلاس.
چند ریختی به دو دسته کلی تقسیم میشه:
که خود همین به دو دسته Function overloading و Operator overloading تقسیم میشه.
حالا یعنی چی Compile Time و Run Time. این نامگذاری و تفکیک، از چی ناشی میشه؟
به این سوال جواب میدم ولی قبلش بیایم در مورد overriding و overloading صحبت کنیم.
مفهوم overloading
مفهوم Overloading ، یعنی من برای یک نام(Function) یا یک عملگر(operator)، شکل های رفتاری مختلفی ارائه بدم.
برای متد یا Function به سه طریق میشه این کار رو کرد:
و برای Overloading در سطح عملگر. فرض کنید من یک کلاس دارم که میتونم ازش instance یا شی تعریف کنم. حالا میخوام برابر بودن دو شی از این کلاس رو چک کنم.
بدترین کار، اینه که بشینم دونه دونه، value های پراپرتی های شی ها رو باهم چک کنم.
بهترین کار اینه که عملگر == رو برای اون کلاس Overload کنم. به نحوی که هرجا لازم بود، تساوی دو شی رو باهاش چک کنم.
public class A
{
public int a { get; set; }
public int b { get; set; }
public static bool operator ==(A obj1, A obj2)
{
return (obj1.a == obj2.a) && (obj1.b == obj2.b);
}
public static bool operator !=(A obj1, A obj2)
{
return (obj1.a != obj2.a) || (obj1.b != obj2.b);
}
}
چند نکته مهم درباره Operator overloading اینکه :
مفهوم overriding
در بالا اشاره کردیم که overriding در زمان ارث بری کاربرد داره و به این شکل هست که من میتونم با virtual کردن یک متد در یک کلاس، با استفاده کلمه override در کلاس SubType، بدنه دیگری رو جایگزین این متد بکنم بدون اینکه اسمش رو تغییر بدم.
تقسیم بندی CompileTime و RunTime
حالا برگردیم به اون سوال اول که تقسیم بندی CompileTime و RunTime از کجا میاد.
این تقسیم بندی به این اشاره داره که پلی مورفیسم کی قطعا اتفاق می افته. آیا زمانی که من سورسم رو کامپایل کنم و ازش یک اسمبلی بسازم. پلی مورفیسم رخ داده؟ یا نه.ممکنه در حین اجرا شدن برنامه، پلی مورفیسم اتفاق بیفته و ممکن هم هست اتفاق نیفتاده. همه چیز به زمان اجرا شدن بستگی داره؟!
بیاین ساده تر کنیم.
فرض کنید من یک متد Add نوشتم داخل یک کلاسی، که دو تا پارامتر int میگیره و جمع شون رو خروجی میده.
حالا توی همون کلاس، یک Add دیگه نوشتم که دو تا String میگیره و بهم دیگه join شون میکنه و خروجی میده.
من سورسم رو که کامپایل کنم و اسمبلی ازش بسازم. پلی مورفیسم من اتفاق می افته و دیگه هیچ ربطی به اتفاقات حین اجرا شدن اون برنامه نداره. این کلاس الان دیگه دارای دو تا Add شده!.تا به ابد. پلی مورفیسم رخ داده و اگر این سورس، به یک سورس دیگه Reference داشته باشه. هر بار من یک شی ایجاد کنم از این کلاس، دو تا Add براش میبینم.
ولی حالا فرض کنید که در همون مثال اولیه خودمون، یعنی کلاس B ، من اومدم متد Add رو توش override کردم و این شکلی از پلی مورفیسم بهره بردم. ولی توی بدنه اون متد override شده، بنویسم if کاربر لاگین کننده admin بود، آنگاه Base.Add وگرنه برو این کد دیگه رو اجرا کن. حالا من سورس رو کامپایل میکنم. آیا چند ریختی یا همون پلی مورفیسم اتفاق افتاده با کامپایل شدن؟ نه!
چون ممکنه کاربر لاگین کننده ادمین باشه و اینجوری اصلا من ریخت! دیگری از Add ارائه ندادم. چون گفتم Base.Add. که یعنی برو همون Add کلاس A رو صدا بزن. ولی اگر کارم ادمین نبود.الان پلی مورفیسم اتفاق می افته.
سالید، یک مجموعه از اصول در شی گرایی هست که متشکل شده از پنج اصل:
الان میخوایم اصل سوم که به اختصار LSP گفته میشود و در رابطه با ارث بری هست رو، به تفصیل اینجا بررسی کنیم. اصل لیسکو! که در بعضی منابع به اصل جایگزینی مشهوره. قرارم نیست تاریخچه بگیم که چی شده و کی بوده و چجور شده.
اصل Liskov، این پرسش رو مطرح میکنه که چه زمانی، یک کلاس میتواند از کلاس دیگری ارث ببرد؟ آیا تنها یک سینتکس، که من بنویسم
public class B : A
یعنی ارث بری نوشته شده، درست و صحیح است؟
نه!.
و برای صحیح بودنش، یک قاعده کلی رو مطرح میکنه و اون اینه که :
یک SubType باید از رفتار BaseType خودش پیروی کنه. منظورش چیه؟ منظورش اینه که هر جا شما در کد ات نوشتی :
A myObj = new B();
و Subtype رو به جای BaseType قرار دادی(جایگزین) کردی، رفتار شی myObj، مشابه تمام آبجکت هایی باشه که از نوع کلاس A هستند و فرقی نکنه.
و بابت توضیحش که منظورش چیه، یک مثال کلاسیک میزنه:
میگه فرض کنید یک کلاس دارید به اسم Rectangle یا مستطیل.
public class Rectangle
{
public int Hight {get; set;}
public int Width {get; set;}
public Rectangle(int H,int W)
{
Hight = H;
Width = W;
}
public int Area
{
return Hight * Width;
}
}
کلاس Rectangle یک سازنده داره، که میاد به پراپرتی های خودش مقدار میده. و یک تابع محسابه ی محیط داره.
حالا شما میخواین یک کلاس دیگه بسازین به اسم مربع و با خودتون میگین :
پس میشه که من کلاس مربعی که می سازم، از مستطیل ارث ببرم، و از اون کد ها دوباره استفاده کنم. با این تفاوت که شرط یکسان بودن طول و عرض رو هم بهش اعمال کنم:
public class Squre : Rectangle
{
public Squre(int H,int W) : base(H,W)
{
Hight = Width = H;
}
}
حالا در متد Main ، قاعده کلی Liskov رو مینویسیم .
یعنی دو تا شی مشابه ایجاد میکنیم از نوع BaseType یعنی مستطیل. منتها برای یکیشون، یعنی S، اومدیم SubType رو جایگزین کردیم. و انتظار داریم S ، رفتارش مشابه با R باشه. چون مربع هم یک جور مستطیله. اگر ما طول و عرض رو یکسان پاس ندهیم، انتظار داریم مربع تبدیل بشه به مستطیل و بتونه جایگزین BaseType خودش بشه.
static void Main(string[] args)
{
Rectangle R = new Rectangle(2, 3);
Rectangle S = new Squre(2, 3);
System.Console.WriteLine(R.Area());
System.Console.WriteLine(S.Area());
System.Console.ReadKey();
}
و هیچ خطای کامپایلی هم تولید نمیکنیم و سورسمون اجرا میشه.
اما خروجی اون چیزی که انتظار داریم نیست.
شی Rectangle S رفتارش در خروجی، به ازای شرایط پارامتری یکسانی که پاس داده شده، مشابه با Rectangle R نیست.
چرا میگیم یکسان نیست؟ چون خروجی متد محاسبه محیط برای شی R میشه 6، ولی برای S میشه 2.
چرا اینطوری شد؟ چون کلاس Squre با سازنده ای که درون خودش نوشته، قاعده مستطیل بودن baseType رو نابود کرده! و اینطوریه که با پارامتر های طول و عرض متفاوت، خروجی ایجاد کرده که جوریه که انگار یک مربع با طول و عرض 2 هست!
و قاعده جایگزینی، یا لیسکو، یا LSP میگه که این ارث بری، ناصحیح است.
حالا چطور بفهمیم که ارث بری مون صحیح هست :
لیسکو، 6 شرط رو اعلام میکنه و میگه اگر این 6 شرط بین BaseType و SubType برقرار بود. بدونین ارث بری صحیح بوده.
در خروجی متد ها یا Return Type ها مطرح میشه.
به این ترتیب که میگه در سلسله مراتب وراثت، ما یک Base کلاس داریم و یک Sub کلاس. حالا اگر متدی در Base هست که داره یک نوع خروجی برمیگردونه، SubType یا باید از همون نوع خروجی بده یا یک کمی محدود تر.
یعنی چی؟ یعنی اگر در کلاس base ما متدی داریم که double برمیگردونه. نباید Subtype بتونه در کد خودش، object برگردونه به جای double. باید یا double برگردونه یا محدودتر، مثل int.
این مشابه با بالایی هست.ولی در سطح پارامتر های ارسالی یا Method Arguments.
اگر متدی در Base هست که داره یک نوع ورودی، SubType یا باید از همون نوع ورودی یا یک کمی محدود تر.
یعنی اگر مثلا در کلاس Base ما متدی داریم که double میگیره. نباید Subtype بتونه در کد خودش، به این متد Object پاس بده. یا double پاس میده یا محدودتر میتونه پاس بده و int بده.
این شرط میگه که SubType باید از Exception هایی که BaseType میده پیروی کنه و Exception جدیدی از خودش نده. مگر اونکه Exception های خودش هم ارث برده شده از Exception های base باشه.
به هر شرطی که قبل از اجرای کامل یک متد یا کلی تر بگیم یک قطعه کد درون یک Scope، باید true یا صحیح باشه میگن PreCondition یا پیش شرط. مثلا متدی که به عنوان پارامتر ورودی عدد صحیح یا int میگیره و بعد هنوز اجرا نشده یک if گذاشته و میگه عدد پاس داده شده بزرگتر از 10 بود اونوقت بدنه رو اجرا کن، وگرنه برو بیرون از متد. خب این یک PreCondition هست.
اصل Liskov میگه که یک SubType حق این رو نداره که پیش شرط ها رو سخت گیرانه تر کنه.
یعنی چی؟ یعنی بیاد توی همون مثال متدی که پارامتر ورودی عدد صحیح میگیره،، شرط دیگه هم کنار بزرگتر از ده، قرار بده و بگه and عدد صحیح زوج!. این تغییر جریان در PreCondition نباید برای هیچ متدی از BaseType اتفاق بیفته.
بر خلاف، PreCondition ، که شرط های قبل از اجرا شدن هستند، PostCondition ها بعد از اجرا حتما صحیح یا true هستند. یعنی چی؟ یعنی مثلا همون متد قبلی فرض کنید که درون بدنه ما همون اول نوشتیم که if پارامتر نال بود، برو به جاش بزار 0. این شرط یک شرط postCondition هست. یعنی این کد اجرا بشه، پارامتر ما دیگه نال نیست. 0 ئه! چون ما داخل متد 0 رو به جاش گذاشتیم.
حالا Liskov میگه که یک SubType حق نداره که PostCondition ها رو ضعیف کنه. یعنی چی؟ یعنی نباید پارامتر رو نال رها کنه. و postCondition رو ضعیف کنه.
یعنی ثابت ها. اگر کلاس Base داره به طور ثابت یک سری مقدار دهی میکنه. یک سری عدد های ثابتی رو داره در کد اش قرار میده. Subtype هم باید از همون ثابت های BaseType پیروی کنه.