مقدمه

زبان C# به طور پیش‌فرض یک زبان با کد مدیریت‌شده (Managed Code) است. این به این معناست که محیط اجرای .NET (CLR) مسئولیت مدیریت حافظه، امنیت نوع و دیگر عملیات سطح پایین را بر عهده می‌گیرد. این یکی از بزرگ‌ترین نقاط قوت C# است، زیرا برنامه‌نویس را از درگیری با جزئیات پیچیده و خطاپذیر مدیریت حافظه آزاد می‌کند. با این حال، سناریوهای بسیار خاصی وجود دارد (مانند تعامل با کتابخانه‌های C/C++ یا نیاز به حداکثر کارایی ممکن در پردازش تصویر) که در آن‌ها نیاز به دسترسی مستقیم به حافظه داریم. برای این موارد، C# یک "راه گریز" به نام کد ناامن (Unsafe Code) و قابلیت استفاده از اشاره‌گرها (Pointers) را فراهم کرده است.

هشدار جدی: این یک ویژگی بسیار پیشرفته است. برای ۹۹ درصد از کاربردهای برنامه‌نویسی با C# (مانند توسعه وب، دسکتاپ و موبایل) شما هرگز به کد ناامن و اشاره‌گرها نیاز نخواهید داشت. استفاده‌ی نادرست از اشاره‌گرها می‌تواند منجر به خطاهای حافظه، مشکلات امنیتی و ناپایداری شدید برنامه شود. از این ویژگی فقط زمانی استفاده کنید که دقیقاً می‌دانید چه می‌کنید و دلیل بسیار قانع‌کننده‌ای برای آن دارید.

کد ناامن (Unsafe Code)

برای استفاده از اشاره‌گرها، ابتدا باید به کامپایلر اعلام کنید که قصد دارید وارد یک محدوده‌ی "ناامن" شوید. این کار با استفاده از کلمه‌ی کلیدی unsafe انجام می‌شود. شما می‌توانید یک متد کامل یا فقط یک بلوک خاص از کد را به عنوان ناامن علامت‌گذاری کنید. با این کار، شما به کامپایلر می‌گویید: "من مسئولیت مدیریت حافظه را بر عهده می‌گیرم و برخی از بررسی‌های امنیتی تو را غیرفعال می‌کنم."

علاوه بر این، باید به پروژه‌ی خود اجازه دهید تا کد ناامن را کامپایل کند. برای این کار، باید فایل پروژه (.csproj) را ویرایش کرده و تگ زیر را به بخش PropertyGroup اضافه کنید:

<AllowUnsafeBlocks>true</AllowUnsafeBlocks>

مبانی اشاره‌گرها

یک اشاره‌گر، متغیری است که به جای یک مقدار، آدرس یک خانه‌ی حافظه را در خود ذخیره می‌کند. این با یک متغیر از نوع ارجاعی متفاوت است؛ یک ارجاع یک مفهوم سطح بالا و مدیریت‌شده است، اما یک اشاره‌گر آدرس خام و مستقیم حافظه است.

سینتکس اشاره‌گرها

  • تعریف: برای تعریف یک اشاره‌گر به یک نوع، از علامت `*` بعد از نام نوع استفاده می‌کنیم. برای مثال: int* p;.
  • عملگر آدرس (`&`): برای گرفتن آدرس حافظه‌ی یک متغیر استفاده می‌شود. برای مثال: p = &myVariable;.
  • عملگر ارجاع‌زدایی (`*`): برای دسترسی به مقداری که در آن آدرس حافظه قرار دارد، استفاده می‌شود. برای مثال: int value = *p;.
Copy Icon Program.cs
unsafe void PointerDemo()
{
    int number = 10;
    // Declare a pointer and assign the address of 'number' to it.
    int* p = &number;

    // Dereference the pointer to get the value.
    Console.WriteLine($"Value via pointer: {*p}");

    // Change the value at the memory location.
    *p = 20;
    Console.WriteLine($"Original variable is now: {number}"); // Output: 20
}

عبارت fixed و اشیاء مدیریت‌شده

یک مشکل بزرگ در کار با اشاره‌گرها و اشیاء .NET وجود دارد: زباله‌روب (GC) برای بهینه‌سازی حافظه، ممکن است اشیاء مدیریت‌شده را در حافظه جابجا کند. اگر شما یک اشاره‌گر به یک شیء داشته باشید و GC آن را جابجا کند، اشاره‌گر شما دیگر به مکان درستی اشاره نخواهد کرد و این فاجعه‌بار است.

برای حل این مشکل، از عبارت fixed استفاده می‌کنیم. این عبارت یک شیء مدیریت‌شده را در حافظه "پین" یا "میخکوب" می‌کند و به GC می‌گوید که تا پایان بلوک fixed، حق جابجا کردن این شیء را ندارد. این به ما اجازه می‌دهد تا با خیال راحت آدرس آن را بگیریم و با آن کار کنیم.

Copy Icon Program.cs
public unsafe static void ProcessArray(int[] array)
{
    // Pin the array in memory so the GC cannot move it.
    fixed (int* pArray = array)
    {
        for (int i = 0; i < array.Length; i++)
        {
            // Use pointer arithmetic to access elements.
            Console.WriteLine($"Value at address {i}: {*(pArray + i)}");
        }
    } // The array is "unpinned" here.
}

در این مثال، ما با استفاده از fixed، آرایه را در حافظه ثابت کرده و سپس با استفاده از محاسبات اشاره‌گر (pArray + i) در آن حرکت می‌کنیم. این روش می‌تواند در پردازش‌های سنگین روی آرایه‌های بزرگ، سریع‌تر از دسترسی با اندیس معمولی باشد، زیرا بررسی مرزهای آرایه را دور می‌زند.