یکی از مهمترین ویژگیهای CLR «امنیت نوع» یا Type Safety هست. تو زمان اجرا CLR میدونه که نوع هر شی دقیقا چیه. با استفاده از متد GetType (که تو مطلب قبلی توضیح داده شده) میشه از نوع دقیق شی موردنظر باخبر شد، و ازاونجاکه این متد non-virtual تعریف شده، بنابراین انواع دیگه نمیتونن با override کردن اون، درباره نوعشون اطلاعات غلطی فراهم کنند. مثلا نوع Copper نمیتونه با override و تغییر پیاده سازی این متد نوع Gold رو برگردونه.
موارد زیادی پیش میاد که نیازه نوع داده یه متغیر رو به نوعی دیگه تبدیل کرد. CLR این اجازه رو میده تا یه شی رو به نوع خودش یا به تمام انواع پدرش تبدیل کرد. مثلا تو #C برای تبدیل نوع به نوع پدر نیازی به هیچ سینتکس خاصی نیست و کار تبدیل کاملا بصورت ضمنی (implicit) انجام میشه، چون تبدیل به نوع پدر همیشه امنه. اما تبدیل به انواع فرزند تو #C باید بصورت صریح (explicit) انجام بشه، چون این فرایند همیشه امن و درست نیست. سینتکس این تبدیلات تو مثال زیر نشون داده شده:
class Base { } class Derived : Base { } class Test { Test() { var d = new Derived(); Base b = d; // تبدیل ضمنی Derived d1 = b; // خطای زمان کامپایل Derived d2 = (Derived)b; // تبدیل صریح } }
تو کد بالا، چون نوع Derived از نوع Base مشتق شده، بنابراین امکان تبدیل متغیری از نوع Derived به نوع Base وجود داره. اما عکس این فرایند امکان نداره و همونجوری که تو کد بالا نشون داده شده یه خطای زمان کامپایل با پیغام زیر نمایش داده میشه:
برای حل این مشکل مثل خط بعدی که بصورت صریح تبدیل نوع رو نمایش داده باید عمل کرد.
همونطور که مشخصه، این قواعد حداقلهای موردنیاز تنها برای کامپایل موفق کد برنامست. اما در زمان اجرا CLR یه سری بررسی های دیگه هم انجام میده. در زمان اجرا CLR همیشه چک میکنه تا تو عملیاتهای تبدیل نوع، نوع نهایی که به متغیر اختصاص داده میشه برابر با نوع واقعی اون یا یکی از انواع پایه ای اون باشه. مثلا کد زیر رو درنظر بگیرین:
var obj = new object(); Derived d1 = obj; // خطای زمان کامپایل Derived d2 = (Derived)obj; // خطای زمان اجرا
ازاونجاکه نوع object رو نمیشه به یه نوع پایینتر تو سلسله مراتب ارثبری (Derived تو اینجا) بدون تبدیل صریح اختصاص داد، بنابراین خط دوم کد بالا یه خطای زمان کامپایل رو صادر میکنه. اما خط سوم به دلیل صریح بودن تبدیل، در زمان کامپایل خطایی نمیده. اما در زمان اجرا بدلیل چکی که CLR انجام میده یه خطای InvalidCastException با پیغام زیر صادر میشه:
اگر CLR این بررسی رو انجام نمیداد و خطای مربوطه رو صادر نمیکرد، امنیت نوع تو دات نت از بین میرفت و دیگه نمیشد به داده های ذخیره شده تو اشیای مختلف اعتماد کرد. این اختلال ممکنه باعث اتفاقات ناگواری مثل خراب شدن برنامه و داده های اون یا نفوذهای غیرمجاز امنیتی تو برنامه و سیستم بشه. اصولا این مشکل یکی از خطرات محیطهاییه که «چنین امنیت نوعی» رو تضمین نمیکنن.
یکی دیگه از روشهای تبدیل نوع تو زبان #C استفاده از اپراتور is هست. همونطور که از نام این اپراتور برمیاد، اپراتور is در زمان اجرا، نوع شی موردنظر رو با نوع فراهم شده بررسی میکنه و یه مقدار bool مشخص کننده صحت عملیات تبدیل برمیگردونه. درضمن این اپراتور هیچ وقت خطایی صادر نمیکنه. کد زیر چگونگی کار با اپراتور is رو نشون میده:
var b = new Base(); var d = new Derived(); var r1 = b is object; // true var r2 = b is Base; // true var r3 = b is Derived; // false ! var r4 = d is object; // true var r5 = d is Base; // true var r6 = d is Derived; // true
درضمن اگه مقدار شی موردنظر برابر null باشه، اپراتور is همیشه مقدار false برمیگردونه. برای استفاده تو محیط واقعی از این اپراتور معمولا از روش زیر استفاده میشه:
if (obj is Base) { var b = (Base)obj; // ادامه عملیات ... }
اما این کد یه مشکل کوچیک داره و اون اینه که تو این فرایند، CLR دو بار عملیات بررسی نوع رو انجام میده. یه بار برای اپراتور is که بررسی تطابق نوع داده انجام میشه و بار دیگه تو تبدیل نوع، و همونجور که قبلا هم اشاره شد اینکار در زمان اجرا همیشه توسط CLR انجام میشه. با اینکه اینکار برای افزایش امنیت انواع انجام میشه، اما تو مواردی این چنینی باعث کاهش هرچند کوچیک کارایی اجرای برنامه میشه. ازاونجاکه این روش بررسی نوع و سپس تبدیل نسبتا رایجه، زبان #C یه اپراتور دیگه به نام as برای راحتتر کردن این فرایند ارائه داده. این اپراتور درواقع عملیات بررسی و تبدیل رو همزمان انجام میده. اگه عملیات تبدیل موفق باشه تبدیل نوع درخواستی انجام میشه و شی موردنظر با نوع مناسب برگشت داده میشه، اما اگه امکان تبدیل وجود نداشته باشه مقدار null برگشت داده میشه. درضمن اپراتور as رفتاری مشابه یه عملیات تبدیل صریح انجام میده اما با این تفاوت که مثل اپراتور is هیچوقت خطایی صادر نمیکنه. بنابراین روشی که برای کار با این اپراتور مرسومه بصورت زیره:
var b = obj as Base; if (b != null) { // ادامه عملیات ... }
دقت کنین که برخلاف روشی که از اپراتور is استفاده میکنه، تو این روش CLR تنها یکبار عملیات بررسی نوع رو انجام میده.
برای بررسی بیشتر و مشخص کردن تفاوت این دو روش، با استفاده از ابزار ILDasm (که با خط فرمان ویژوال استودیو میشه اونو راحتتر اجرا کرد)، کد IL تولید شده نمایش داده میشه. برای اینکار دو قطعه کد بالا ابتدا درون متد Main یه برنامه کنسول بصورت زیر قرار داده شدن:
private static void Main(string[] args) { var obj = new object(); if (obj is Base) { var b1 = (Base)obj; Console.WriteLine(b1); } var b2 = obj as Base; if (b2 != null) { Console.WriteLine(b2); } }
و اما کد IL متد Main:
.method private hidebysig static void Main(string[] args) cil managed { .entrypoint // Code size 44 (0x2c) .maxstack 1 .locals init ([0] object obj, [1] class ConsoleApplication1.Base b1, [2] class ConsoleApplication1.Base b2) IL_0000: newobj instance void [mscorlib]System.Object::.ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: isinst ConsoleApplication1.Base <-- بررسی نوع IL_000c: brfalse.s IL_001b IL_000e: ldloc.0 IL_000f: castclass ConsoleApplication1.Base <-- تبدیل نوع IL_0014: stloc.1 IL_0015: ldloc.1 IL_0016: call void [mscorlib]System.Console::WriteLine(object) IL_001b: ldloc.0 IL_001c: isinst ConsoleApplication1.Base <-- بررسی و تبدیل نوع IL_0021: stloc.2 IL_0022: ldloc.2 IL_0023: brfalse.s IL_002b IL_0025: ldloc.2 IL_0026: call void [mscorlib]System.Console::WriteLine(object) IL_002b: ret } // end of method Program::Main
تو این قطعه کد IL، از خط شماره IL_006 تا IL_0016 مربوط به کد اپراتور is و از خط IL_001b تا خط IL_0026 مربوط به کد اپراتور as هست.
تو این کد IL معلومه که عملیات بررسی نوع چه با اپراتور is و چه با اپراتور as با استفاده از دستور isinst انجام میشه. اما نکته ای که این دستور داره اینه که علاوه بر بررسی نوع متغیر بارگذاری شده تو stack، نتیجه تبدیل رو هم تو stack ذخیره میکنه. این فرایند دقیقا همونطوریه که برای اپراتور as شرح داده شد. یعنی درصورت عدم امکان تبدیل، مقدار null به stack بارگذاری میشه.
بنابراین تو کد مربوط به اپراتور is به دلیل عدم استفاده بهینه از دستور isinst نیاز به استفاده از یه تبدیل صریح (که به دستور castclass کامپایل شده) وجود داره. همین قطعه کد IL نشون میده که بدلیل استفاده بهتر از منابع CLR، روش دوم به مراتب از روش اول بهتره.
درضمن تو روش دوم قسمت چک مقدار شی با null از بررسی نوع داده سریعتر انجام میشه. مخصوصا اگه نوع موردنظر تو یه سلسله مراتب ارثبری بزرگ قرار داشته باشه و CLR مجبور باشه عملیات بررسی نوع رو تا انتهای این سلسله انجام بده.
تو جدول زیر خلاصه مطالب تا اینجا آورده شده:
عبارت | نتیجه |
---|---|
Object o1 = new Object(); | درست |
Object o2 = new Base(); | درست |
Object o3 = new Derived(); | درست |
Object o4 = o3; | درست |
Base b1 = new Base(); | درست |
Base b2 = new Derived(); | درست |
Derived d1 = new Derived(); | درست |
Base b3 = new Object(); | خطای زمان کامپایل |
Derived d2 = new Object(); | خطای زمان کامپایل |
Base b4 = d1; | درست |
Derived d3 = b2; | خطای زمان کامپایل |
Derived d4 = (Derived)d1; | درست |
Derived d5 = (Derived)b2; | درست |
Derived d6 = (Derived)b1; | خطای زمان اجرا |
Base b5 = (Base)o1; | خطای زمان اجرا |
Base b6 = (Derived)b2; | درست |
.: . :.