حسین احمدی
بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

آموزش pointer ها و کد unsafe در سی شارپ

کدی که شما به عنوان برنامه نویس در زبان سی شارپ می نویسید کد مدیریت شده یا Managed Code است، یکی از مزیت های کد مدیریت شده، قابلیت مدیریت خودکار حافظه توسط سرویس Garbage Collector دات نت می باشد. اما در سی شارپ می توان کد مدیریت نشده نیز نوشت. در این مطلب می خواهیم در مورد ویژگی Pointer ها در سی شارپ بنویسیم.این قابلیت به شما این اجازه را می دهد تا متغیرهایی تعریف کنید که به صورت مستقیم با خانه های حافظه در ارتباط هستند، یعنی به جای ذخیره کردن مقدار در داخل خود، آدرس یک حافظه را در خورد ذخیره می کنند.

دوره های شبکه، برنامه نویسی، مجازی سازی، امنیت، نفوذ و ... با برترین های ایران
سرفصل های این مطلب
  1. تعریف struct ها به صورت unsafe

به این کدها اصطلاحاً کد unsafe یا نا امن گفته می شود. یکی از مزیت های استفاده از Pointer ها در زبان سی شارپ، بالا بودن سرعت اجرای آن ها نسبت به متغیرهای عادی است، زیرا شما به صورت مستقیم در حال کار با خانه های حافظه هستید و این موضوع باعث بالا رفتن سرعت برنامه های شما می شود. البته این نکته را نیز بگویم که Pointer ها خیلی به ندرت استفاده می شوند و در اکثر برنامه های شما کاربرد ندارند.قبل از هر چیز باید این موضوع را مد نظر داشته باشید که استفاده از Pointer ها به صورت پیش فرض در برنامه های سی شارپ امکان پذیر نیست و می بایست این قابلیت را فعال کنید. برای فعال سازی قابلیت unsafe در برنامه های سی شارپ، وارد قسمت تنظیمات پروژه شده و از قسمت Build گزینه Allow unsafe code را تیک بزنید:


img1

این قابلیت باعث می شود تا زمان فراخوانی کامپایلر سی شارپ، یعنی دستور csc.exe، یک آرگومان با نام unsafe به آن اضافه شود:

csc.exe /unsafe *.cs

برای شروع باید با کلمه کلیدی unsafe در زبان سی شارپ آشنا شویم. انتخاب گزینه Allow unsafe code در پنجره تنظیمات پروژه به تنهایی برای نوشتن کد unsafe کفایت نمی کند و می بایست در کد، برای بخش هایی که قرار است در آن ها کد unsafe نوشته شود بوسیله کلمه کلیدی unsafe یک بلاک برای اینکار تعریف کنیم. کلمه unsafe را می توان به صورت های زیر استفاده کرد:

1. تعریف یک متد به صورت unsafe:

public static unsafe void MyMethod()
{
}

2. تعریف یک struct به صورت unsafe:

public unsafe struct Point
{
}

3. ایجاد یک بلاک unsafe در کد:

public static void Main(string args[])
{
    unsafe
    {
    }
}

برای استفاده از Pointer ها در برنامه ها، باید از دو علامت ** و & استفاده کنیم، علامت ** به دو صورت استفاده می شود:

  1. یک حالت برای تعریف متغیرهایی از نوع Pointer
  2. حالت دیگر برای دریافت مقدار متغیری از نوع Pointer که آدرس خانه ای از حافظه در آن قرار گرفته

علامت & نیز برای گرفتن آدرس یک متغیر استفاده می شود. در مثال زیر، ما متغیری از نوع int تعریف کرده، آدرس آن را داخل یک Pointer قرار می دهیم و سپس مقدار آن را در خروجی چاپ می کنیم:

static void Main(string[] args)
{
    unsafe
    {
        int number = 1444;
        int* pointer = &number;
        Console.WriteLine(*pointer);
    }
}

همانطور که مشاهده می کنید، ابتدا متغیری از نوع int تعریف شده، سپس یک Pointer برای نوع داده int تعریف کرده و آدرس حافظه number را داخل آن قرار دادیم. سپس مقدار داخل pointer را بوسیله ** و با دستور Console.WriteLine در خروجی چاپ کردیم. این مثال بیشتر برای آشنایی اولیه با Pointer ها بود، مثال بعدی را کاربردی تر خواهیم زد، در این مثال متدی تعریف می کنیم که دو پارامتر را به عنوان ورودی می گیرد و سپس مقادیر آن ها را با هم عوض می کند، در صورتی که از Pointer ها استفاده نکنیم، می توانیم به وسیله کلمه کلیدی ref پارامتر ها را تعریف کرده و در بدنه متد به صورت زیر مقادیر آن ها را تغییر دهیم:

public static void Swap(ref int a, ref int b)
{
    var temp = a;
    a = b;
    b = temp;
}

همانور که می دانید، کلمه کلیدی ref باعث می شود که به جای ارسال مقادیر به پارامترهای متد، آدرس متغیرها به متد پاس داده شده و در صورت تغییر پارامترها در متد، مقادیری که در خارج متد به آن پاس داده شده اند نیز تغییر کنند. در ادامه می خواهیم متد بالا را با کمک Pointer ها بازنویسی کنیم. متد Swap را به صورت زیر تغییر می دهیم:

public static unsafe void Swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

در خط اول متد، مقدار a را داخل متغیر temp ریخته و در خط های بعدی مقادیر a و b را جابجا می کنیم، نحوه فراخوانی این متد به صورت زیر خواهد بود:

unsafe
{
    int n1 = 12;
    int n2 = 20;
    Swap(&n1, &n2);
}

همانطور که در کد بالا مشاهده می کنید، زمان فراخوانی متد Swap بوسیله علامت & آدرس متغیرها به عنوان پارامتر به متد پاس داده شده اند.همانطور که گفتیم علامت & آدرس حافظه را بر می گرداند، در صورتی که بخواهیم آدرس حافظه ای که Pointer به آن اشاره می کند را در خروجی چاپ کنیم، کافیست متغیر Pointer ار به نوع int تبدیل کنیم:

int number = 12;
int* pointer = &number;

Console.WriteLine((int) pointer);

با اجرای دستور بالا به جای مقدار number یعنی عدد 12، آدرس خانه ای از حافظه که pointer به آن اشاره می کند در خروجی چاپ خواهد شد.یکی از قابلیت هایی که در Pointer ها می توان از آن استفاده کرد، عملگرها ++ و -- است، فرض کنید متغیری داریم از نوع int که متغیری دیگری از نوع Pointer برای آن تعریف می کنیم:

int number = 12;
int* pointer = &number;
pointer++;

Console.WriteLine(*pointer);

همانطور که مشاهده می کنید بعد از تعریف pointer، عملگر ++ بر روی آن اعمال شده. این عملگر باعث می شود که خانه ای از حافظه که متغیر pointer به آن اشاره می کند، به اندازه 4 بایت به جلو حرکت کند. دلیل این موضوع اندازه نوع داده int است که 4 بایت است. برای مثال، اگر متغیر number و pointer را از نوع long در نظر می گرفتیم، با عملگر ++ این اشاره گر 8 بایت به سمت جلو حرکت می کرد که معادل اندازه نوع داده long است. با اعمال این عملگر و نمایش مقدار pointer در خروجی عددی غیر از مقدار 12 که در number ذخیره شده نمایش داده می شود که دلیل آن تغییر خانه حافظه می باشد. همچنین عملگر -- به اندازه سایز نوع داده اشاره گر به سمت عقب حرکت خواهد کرد.

تعریف struct ها به صورت unsafe

علاوه بر تعریف اشاره گر برای نوع های داده اولیه، می توان struct را نیز به صورت pointer استفاده کرد. دقت کنید که این موضوع تنها در باره struct ها صدق کرده و نمی توان class ها را به صورت unsafe تعریف کرد. در کد زیر ما یک struct ساده با نام Node تعریف می کنیم:

public unsafe struct Node
{
    public int Value { get; set; }
    public Node* LeftNode { get; set; }
    public Node* RightNode { get; set; }
}

دقت کنید که خصوصیت های LeftNode و RightNode به صورت Pointer تعریف شده اند. حال می توانیم به صورت زیر از این Struct استفاده کنیم:

unsafe
{
    Node baseNode = new Node() {Value = 12};

    Node leftNode = new Node() {Value = 20};
    Node rightNode = new Node() { Value = 18 };

    baseNode.LeftNode = &leftNode;
    baseNode.RightNode = &rightNode;                
}

همانطور که مشاهده می کنید، برای مقدار دهی LeftNode و RightNode از & استفاده شده که آدرس leftNode و rightNode را در این خصوصیت ها ذخیره می کند. اما موضوعی که وجود دارد، نحوه دسترسی به خصوصیت Value در خصوصیت های LeftNode و RightNode در متغیر baseNode است. برای دسترسی به خصوصیت pointer هایی که از نوع struct هستند، به جای استفاده از کاراکتر . باید از <- استفاده کرد. برای مثال، در صورتی که بخواهیم مقدار Value برای LeftNode و RightNode در baseNode را در خروجی چاپ کنیم، باید به صورت زیر کد را بنویسیم:

Console.WriteLine(baseNode.LeftNode->Value);
Console.WriteLine(baseNode.RightNode->Value);

این موضوع برای کلیه Pointer های که برای struct ها ایجاد می شوند صدق می کند. در این مطلب با مقدمات مربوط به Pointer ها آشنا شدیم. امیدوارم که این مطلب مورد توجه دوستان قرار گرفته باشد.  TOSINSO باشید


حسین احمدی
حسین احمدی

بنیانگذار توسینسو و برنامه نویس و توسعه دهنده ارشد وب

حسین احمدی ، بنیانگذار TOSINSO ، توسعه دهنده وب و برنامه نویس ، بیش از 12 سال سابقه فعالیت حرفه ای در سطح کلان ، مشاور ، مدیر پروژه و مدرس نهادهای مالی و اعتباری ، تخصص در پلتفرم دات نت و زبان سی شارپ ، طراحی و توسعه وب ، امنیت نرم افزار ، تحلیل سیستم های اطلاعاتی و داده کاوی ...

نظرات