عنوان:

‫پارادایم جدید امنیت حافظه در C# 16: بازطراحی مکانیسم Unsafe با الهام از Rust


نویسنده: وحید نصیری
تاریخ: ۱۴۰۵/۰۳/۱۵ ۰۹:۰۰
آدرس: www.dntips.ir
زبان برنامه‌نویسی سی‌شارپ از بدو پیدایش، به عنوان یک زبان مدیریت‌شده (Managed) و امن از نظر حافظه (Memory-Safe) شناخته شده‌است. با این حال، برای سناریوهایی که نیاز به عملکرد فوق‌العاده بالا (Performance Optimization) یا تعامل با کدهای بومی سیستم‌عامل (Native Interoperability/FFI) دارند، قابلیتی به نام محیط‌های ناامن (unsafe) در نظر گرفته شده‌است. در مدل سنتی سی‌شارپ (از نسخه ۱.۰ تا کنون)، کلیدواژه unsafe صرفاً به عنوان یک مجوز سینتکسی برای استفاده از اشاره‌گرها (Pointers) عمل می‌کرد؛ رویکردی که در آن مرز میان کد امن و ناامن برای کامپایلر و ابزارهای تحلیل کدهای هوش مصنوعی شفاف نبود.
با رشد روزافزون تولید کد توسط ابزارهای هوش مصنوعی (AI-assisted code generation) و سرعت بالای توسعه، بازبینی سیستماتیک این مرزها اهمیت دوچندانی یافته است. مایکروسافت در نسخه C# 16 (که پیش‌نمایش آن در .NET 11 و نسخه نهایی آن در .NET 12 عرضه می‌شود)، معماری کلیدواژه unsafe را به طور کامل بازطراحی کرده‌است. هدف این تغییر بزرگ، تبدیل unsafe از یک «مجوز سینتکسی ساده» به یک «قرارداد صریح میان فراخواننده و فراخوانده» (Caller-Facing Contract) است؛ رویکردی که شباهت بسیار زیادی به مدل پیشرفته و اثبات‌شده‌ی زبان راست (Rust) دارد.

۱. تبیین مسئله: چرا مدل سنتی نیاز به تغییر داشت؟
در مدل‌های قبلی سی‌شارپ، وقتی یک متد با اصلاح‌گر unsafe تعریف می‌شد، این کلیدواژه بیشتر شبیه به یک جعبه سیاه‌ رفتار می‌کرد. کامپایلر متوجه تعهدات و الزامات خاصی که برنامه‌نویس باید در زمان فراخوانی آن متد رعایت کند (مانند معتبر بودن آدرس حافظه یا خارج نشدن از محدوده آرایه) نمی‌شد.
علاوه بر این، حضور نوع‌های اشاره‌گری (مثل byte*) در امضای متد به طور خودکار ناامنی را به کدهای بالاتر سرایت می‌داد، بدون اینکه مرز دقیق جایی که عملیات خطرناکِ آزادسازی یا دی‌رفرنس (Dereferencing) اتفاق می‌افتد، مشخص باشد. با تکامل اکوسیستم دات‌نت، نیاز بود که این تعهدات به صورت کاملاً مستند، ساختاریافته و قابل‌ارزیابی توسط کامپایلر درآیند.

۲. ارکان مدل جدید امنیت حافظه در C# 16
مدل جدید بر سه اصل استوار است: شفافیت مرزها (Explicit Scoping)، تعهدات مستندسازی‌شده (Documented Obligations) و کنترل سرایت ناامنی (Propagation Control).
الف) تغییر نقشunsafeبه یک قرارداد صریح
در C# 16، قرار دادن کلیدواژه unsafe بر روی یک متد، به این معناست که فراخواننده (Caller) باید وظایف خاصی را برای حفظ پایداری برنامه انجام دهد. کامپایلر از این پس به شما اجازه نخواهد داد که یک متد ناامن را بدون قرار دادن صریح آن در یک بلوک unsafe {} در سمت فراخواننده، صدا بزنید.
اجازه دهید این مفهوم را با یک مثال ساده مقایسه کنیم. در مدل قدیمی، کد زیر بدون ساختار بلاک داخلی کامپایل می‌شد:
// مدل قدیمی (C# 1.0 - 15)
public unsafe void ProcessData(byte* buffer, int length)
{
    // کار با اشاره‌گر
}

// فراخوانی در متد دیگر بدون نیاز به بلاک صریح در صورت unsafe بودن متد بیرونی
اما در مدل جدید C# 16، حتی اگر متد بیرونی خود ناامن باشد، هرگونه فراخوانی اکشن‌های ناامن باید در مرز مشخص خود محصور شوند:
// مدل جدید C# 16
/// <summary>
/// پردازش بافر خام حافظه.
/// </summary>
/// <safety>
/// فراخواننده باید تضمین کند که اشاره‌گر buffer به یک حافظه معتبر اشاره می‌کند
/// و طول آرایه حداقل به اندازه پارامتر length است.
/// </safety>
public unsafe void ProcessData(byte* buffer, int length)
{
    // پیاده‌سازی متد
}

public void Execute()
{
    byte[] data = new byte[10];

    fixed (byte* p = data)
    {
        // کامپایلر الزام می‌کند که محل دقیق فراخوانی متد unsafe مشخص باشد
        unsafe 
        {
            ProcessData(p, 10);
        }
    }
}

ب) معرفی تگ مستندسازی
مایکروسافت یک استاندارد مستندسازی جدید به نام /// معرفی کرده است. از این پس، هر متدی که از اصلاح‌گر unsafe استفاده می‌کند، وظیفه دارد تعهدات فراخواننده (Caller Obligations) را در این تگ مستند کند. ابزارهای تحلیل ایستا (Static Analysis) و کامپایلر می‌توانند از این تگ‌ها برای بازبینی کد (Auditing) استفاده کنند.

ج) عدم سرایت ناامنی از طریق انواع اشاره‌گر (Pointer Types)
یک تغییر مهم در سیستم تایپ این است که وجود اشاره‌گرها (مانند void* یا int*) در امضای متد، دیگر به خودی خود باعث ناامنی متد نمی‌شود. تنها عملیات دی‌رفرنس کردن اشاره‌گر (Dereferencing) ناامن محسوب می‌شود. بنابراین، جابجایی اشاره‌گرها امن است، اما دسترسی به مقدار پشت آن‌ها نیازمند مرز صریح unsafe است.
همچنین پیشنهاد شده است که برای پوینترهای مبهم، به جای IntPtr قدیمی، از پوینترهای تایپ‌شده مثل byte* یا void* استفاده شود و برای هندل‌های بومی از SafeHandle بهره گرفته شود.

۳. قوانین جدید کامپایلر و محدودیت‌ها
برای محکم‌کاری هرچه بیشتر، کامپایلر قوانین سخت‌گیرانه‌ای را اعمال می‌کند:
  • خطا در اصلاح‌گر نوع (Type Modifier Error): اعمال کلیدواژه unsafe روی تعریف خود class یا struct ممنوع شده و منجر به خطای کامپایل می‌شود. ناامنی باید دقیقاً روی اعضا (Members) مشخص شود.
  • ممنوعیت در سازنده‌های استاتیک و نهایی‌سازها: متدهای static constructor و finalizer دیگر نمی‌توانند لایه بیرونی ناامن داشته باشند؛ چرا که زمان‌بندی اجرای آن‌ها توسط رانتایم مدیریت می‌شود و فراخواننده‌ی مستقیمی برای پذیرش تعهدات وجود ندارد.
  • محدودیت در قیدهای ژنریک (new() Constraint): اگر یک کلاس دارای سازنده بدون پارامتر ناامن (unsafe parameterless constructor) باشد، دیگر نمی‌تواند قید ژنریک new() را برآورده کند؛ زیرا ساخت نمونه از آن نیازمند رعایت الزامات امنیتی است.

۴. مهاجرت به مدل جدید و سازگاری عقب‌رو (Backward Compatibility)
مایکروسافت برای جلوگیری از شکستن کدهای قدیمی (Breaking Changes)، این قابلیت را به صورت یک انتخاب پروژه‌ای (Project-level opt-in) طراحی کرده است. شما می‌توانید در فایل پروژه (.csproj) رفتار پروژه خود را مدیریت کنید:
<PropertyGroup>
  <MemorySafetyRules>2</MemorySafetyRules>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
اگر مقدار MemorySafetyRules برابر با 1 باشد، رفتار کلاسیک کامپایلر حفظ می‌شود و با تنظیم آن روی 2 (که در .NET 12 به صورت پیش‌فرض خواهد بود)، قوانین سخت‌گیرانه جدید فعال می‌گردند. برای تسهیل این انتقال، مایکروسافت ابزارهای اصلاح خودکار کد نظیر dotnet format را به‌روزرسانی کرده است تا کدهای قدیمی را به طور خودکار با افزودن بلاک‌های صریح unsafe {} به ساختار جدید پورت کنند.

نتیجه‌گیری
تغییرات اعمال‌شده در مدل امنیت حافظه C# 16، گامی بلند در جهت مدرن‌سازی مدیریت حافظه در دات‌نت به شمار می‌رود. سی‌شارپ با الهام گرفتن از موفقیت‌های زبان Rust در تفکیک مرزهای کدهای امن و ناامن، توانسته است بدون از دست دادن سادگی و کارایی محیط مدیریت‌شده (Managed Runtime)، امنیت کدهای سطح پایین خود را به شدت ارتقا دهد.
این رویکرد قراردادمحور، نه تنها کار را برای توسعه‌دهندگان در پروژه‌های بزرگ شفاف‌تر می‌کند، بلکه بستری امن و قابل ممیزی برای ابزارهای هوش مصنوعی فراهم می‌سازد تا کدهای بهینه‌تر و بدون آسیب‌پذیری‌های امنیتی حافظه تولید کنند. صراحت در ساختار جدید، تضمین‌کننده‌ی این است که در آینده، خطاهای مربوط به دسترسی‌های غیرمجاز به حافظه، پیش از اجرا و در زمان کامپایل کشف و برطرف شوند.

نظرات

  • وحید نصیری در ۱۴۰۵/۰۳/۱۷ ۰۹:۰۵
    اشاره‌گرها در #C و ایمنی حافظه: Span در برابر unsafe در C# 16

    C# برای بیشتر عمر خود، توسعه‌دهندگان را از کار مستقیم با حافظه خام دور نگه داشته‌است. جمع‌آوری زباله (Garbage Collection)، بررسی مرز آرایه‌ها و سیستم نوع قدرتمند، از خواندن بیش از حد بافرها جلوگیری می‌کنند. این ویژگی‌ها نه محدودیت، بلکه نقطه قوت زبان هستند. اما گاهی هزینه این «تور ایمنی» بیش از حد می‌شود و نیاز به دسترسی سریع‌تر یا انعطاف‌پذیرتر پیش می‌آید. در #C، درِ پشتی این دسترسی، کلمه کلیدی unsafe و اشاره‌گرها (pointers) هستند.
    با این حال، در سال‌های اخیر دلایل استفاده از این درِ پشتی به شدت کاهش یافته است. نوع Span که در سال ۲۰۱۸ معرفی شد، بیشتر نیازهای کد unsafe را پوشش داد و slicing حافظه را با سرعتی نزدیک به اشاره‌گرها ممکن کرد. حالا C# 16 گام بیشتری برداشته و مفهوم unsafe را دقیق‌تر، شفاف‌تر و ایمن‌تر بازتعریف کرده است. این مطلب به بررسی هر دو سر طیف می‌پردازد: از اشاره‌گرهای کلاسیک تا رویکردهای مدرن و ایمن.

    چرا اشاره‌گرها در ابتدا وجود داشتند؟
    اضافه کردن اشاره‌گرها به #C تصادفی نبود. طراحان زبان آن‌ها را برای سه هدف اصلی اضافه کردند:
    • تعامل با کد native که آدرس خام و طول انتظار دارد.
    • کسب آخرین ذره عملکرد در حلقه‌های تنگ با حذف بررسی مرز آرایه.
    • خواندن و بازتفسیر مستقیم بایت‌ها، مثلاً هنگام پارس کردن فرمت‌های باینری.
    برای استفاده از آن‌ها باید از زمینه unsafe و سوئیچ AllowUnsafeBlocks استفاده کنید. این کار انواع اشاره‌گر، عملگر آدرس (&)، حسابگری اشاره‌گر، fixed و stackalloc را فعال می‌کند.

    مثال کلاسیک: کپی بایت‌ها بین دو آرایه:
    static unsafe void Copy(byte[] source, int sourceOffset,
                            byte[] target, int targetOffset, int count)
    {
        if (source == null || target == null)
            throw new ArgumentException("source or target is null");
        if (sourceOffset < 0 || targetOffset < 0 || count < 0)
            throw new ArgumentException("offset or count is negative");
        if (source.Length - sourceOffset < count ||
            target.Length - targetOffset < count)
            throw new ArgumentException("not enough room to copy");
    
        // پین کردن آرایه‌ها تا GC آن‌ها را جابه‌جا نکند
        fixed (byte* pSource = source, pTarget = target)
        {
            for (int i = 0; i < count; i++)
                pTarget[targetOffset + i] = pSource[sourceOffset + i];
        }
    }
    توجه کنید که اطراف کپی واقعی، کارهای زیادی وجود دارد: fixed برای پین کردن (چون GC اشیاء مدیریت‌شده را جابه‌جا می‌کند)، بررسی‌های دستی مرز و مدیریت طول عمر. اشتباه در حسابگری به جای exception، منجر به buffer overrun می‌شود. اشاره‌گرها سریع و انعطاف‌پذیرند، اما مسئولیت بررسی مرز، مدیریت طول عمر و ایمنی کاملاً بر عهده توسعه‌دهنده است.

    ورود Span: ایمنی با عملکرد بالا
    Span یک نوع value type کوچک (از نوع ref struct) است که یک ناحیه پیوسته از حافظه را نشان می‌دهد: اشاره به عنصر اول به علاوه طول. ویژگی‌های کلیدی آن:
    • مستقل از محل حافظه: می‌تواند به آرایه مدیریت‌شده، بلوک stackalloc، فیلد داخل شیء، کاراکتر داخل رشته یا حافظه native اشاره کند.
    • بر پایه managed pointers (byrefs در CLR) کار می‌کند که GC آن‌ها را هنگام جابه‌جایی اشیاء به‌روزرسانی می‌کند. بنابراین نیازی به fixed نیست.
    • به دلیل محدودیت stack، نمی‌تواند در heap ذخیره شود (برای جلوگیری از dangling reference). کامپایلر این را با ref struct تضمین می‌کند.

    ساخت و slicing:
    var data = new byte[10];
    Span<byte> bytes = data;  // تبدیل ضمنی
    
    Span<byte> middle = bytes.Slice(start: 2, length: 4);
    middle[0] = 42;  // مستقیماً در data[2] نوشته می‌شود
    // middle[4] = 1; // IndexOutOfRangeException
    ReadOnlySpan نسخه فقط‌خواندنی آن است و اجازه slicing رشته بدون تخصیص حافظه جدید را می‌دهد.

    stackalloc بدون unsafe:
    Span<int> numbers = stackalloc int[3];
    الگوی رایج انتخاب stack برای بافرهای کوچک و heap برای بزرگ:
    const int StackLimit = 256;
    Span<byte> buffer = length <= StackLimit
        ? stackalloc byte[length]
        : new byte[length];
    کامپایلر با ref struct تضمین می‌کند که Span از stack خارج نشود. برای مواردی که نیاز به نگهداری در heap یا await دارید، از Memory استفاده کنید.

    بنچمارک: Reference در مقابل Span در مقابل Pointer
    در workloadهایی که بازتفسیر بایت‌ها به عرض بیشتر (مثل Int32، UInt64، Int16) لازم است، Pointer سریع‌ترین، Span در میانه و آرایه معمولی کندترین است. اما Span اغلب ۲-۵ برابر سریع‌تر از کد naive است، هیچ تخصیصی ندارد و ایمن است. هزینه pinning در pointerها در بنچمارک micro دیده نمی‌شود اما در GC برنامه واقعی تأثیرگذار است.


    تغییرات unsafe در C# 16: بازتعریف قرارداد ایمنی (بخش گسترش‌یافته)
    C# 16 به عنوان بخشی از تلاش گسترده مایکروسافت برای بهبود ایمنی حافظه، مدل unsafe را به طور اساسی بازطراحی کرده‌است. این تغییرات الهام‌گرفته از Rust است و هدف آن تبدیل unsafe از یک زمینه سینتکسی ساده به یک قرارداد صریح و قابل بررسی است که توسعه‌دهندگان و ابزارها بتوانند آن را به راحتی audit کنند. این مدل در ابتدا opt-in است (با پراپرتی MSBuild جدید EnableRequiresUnsafe) و به تدریج در .NET 11 (preview) و .NET 12 (production) عرضه می‌شود.

    دلایل این تغییرات
    • امنیت: باگ‌های حافظه (buffer overrun، dangling pointer، type confusion) منبع اصلی آسیب‌پذیری‌های امنیتی هستند. آژانس‌هایی مانند CISA، NSA و FBI بر لزوم حرکت به سمت زبان‌های memory-safe تأکید دارند. unsafe تنها جایی است که کامپایلر مسئولیت را به توسعه‌دهنده واگذار می‌کند. هدف جدید: شفاف، محدود و قابل بررسی کردن این مرز.
    • AI-assisted coding: با سرعت تولید کد توسط ابزارهای هوش مصنوعی، نیاز به قراردادهای صریح بیشتر احساس می‌شود.
    • سازگاری با اکوسیستم: runtime libraries مایکروسافت نیز بر اساس این مدل بازنویسی می‌شوند.

    تغییرات کلیدی و ملموس
    unsafe به عنوان قرارداد caller-facing:
    • فراخوانی یک عضو unsafe خارج از زمینه unsafe حالا خطای کامپایل تولید می‌کند (نه فقط warning).
    مثال:
    void Caller()
    {
        M();  // خطا: فراخوانی M() خارج از unsafe context
    }
    
    unsafe void M() { }

    اعمال unsafe در سطح عضو (per-member):
    • دیگر نمی‌توان کل نوع (class/struct) را unsafe کرد. unsafe فقط روی متدها، پراپرتی‌ها، فیلدها و local functions اعمال می‌شود.
    • این تغییر granularity را افزایش می‌دهد و کد را دقیق‌تر می‌کند.

    تفاوت بین pointer type و dereference:
    • صرفاً تعریف یا پاس دادن نوع pointer (مثل byte*) دیگر unsafe محسوب نمی‌شود.
    • فقط dereference (خواندن/نوشتن از طریق اشاره‌گر، مثل data[i]) نیاز به بلوک unsafe دارد.

    بلوک‌های inner unsafe:
    • تمام عملیات unsafe باید داخل بلوک unsafe { ... } قرار گیرند. این کار scoping را محدود و بررسی کد را آسان می‌کند.

    مستندسازی اجباری با :
    • هر عضو unsafe باید بخش /// داشته باشد که تعهدات caller را به وضوح توصیف کند.
    • داخل پیاده‌سازی، کامنت‌های // SAFETY: برای توضیح منطق ایمنی استفاده می‌شود.
    • analyzerها عدم وجود این مستندات را flag می‌کنند.

    کلمه کلیدی جدید safe:
    • برای اعلام صریح اینکه یک extern method یا LibraryImport ایمن است و unsafety را propagate نمی‌کند.

    مثال کد unsafe خوب در مدل جدید
    /// <safety>
    /// Callers must pass a non-null pointer to at least <paramref name="length"/>
    /// readable bytes. The memory must stay alive for the duration of the call.
    /// </safety>
    static unsafe int SumBytes(byte* data, int length)
    {
        // SAFETY: length توسط wrapper عمومی اعتبارسنجی شده و قرارداد caller
        // تضمین می‌کند data حداقل به این اندازه بایت معتبر اشاره دارد.
        int total = 0;
        unsafe
        {
            for (int i = 0; i < length; i++)
                total += data[i];
        }
        return total;
    }
    این ساختار سه لایه دارد: فیلد/عملیات unsafe با invariant، بلوک inner unsafe، و سطح عمومی که caller بدون unsafe از آن استفاده می‌کند.
    ابزارهایی مثل dotnet format برای مهاجرت خودکار (اضافه کردن بلوک‌ها و جابه‌جایی modifierها) در نظر گرفته شده، اما نوشتن مستندات همچنان نیاز به قضاوت انسانی دارد، که دقیقاً هدف مدل است.

    نتیجه‌گیری
    برای اکثر کدهای روزمره، نیازی به هیچ‌کدام نیست؛ آرایه‌ها و APIهای سطح بالا کافی‌اند. وقتی به فلز نزدیک‌تر می‌شوید، Span و ReadOnlySpan انتخاب پیش‌فرض باشند. آن‌ها slicing، تخصیص stack و عملکرد نزدیک به pointer را با ایمنی بالا ارائه می‌دهند و اکوسیستم .NET حول آن‌ها بازسازی شده است. به unsafe و pointerها فقط وقتی مراجعه کنید که Span واقعاً ناکافی باشد (interop خاص، fixed-size bufferها و غیره). مدل جدید C# 16 این کار را ایمن‌تر، شفاف‌تر و قابل نگهداری‌تر می‌کند. اشاره‌گرها حذف نمی‌شوند؛ فقط در جایی قرار می‌گیرند که تعلق دارند؛ پشت دری مشخص، با قرارداد صریح و توسط کسانی که می‌توانند دلایل ایمنی را به وضوح مستند کنند. این رویکرد تعادل مناسبی بین عملکرد، ایمنی و قابلیت نگهداری ایجاد می‌کند و توسعه‌دهندگان حرفه‌ای را قادر می‌سازد کدهایی بنویسند که هم سریع باشند و هم قابل اعتماد.
  • وحید نصیری در ۱۴۰۵/۰۳/۱۷ ۰۹:۳۳
    توضیح بیشتر «الهام‌گرفته از Rust» در تغییرات unsafe C# 16

    این عبارت به معنای کپی مستقیم یا تبدیل #C به Rust نیست، بلکه مایکروسافت از بهترین ایده‌های Rust در زمینه مدیریت و شفاف‌سازی کد unsafe الهام گرفته تا ایمنی حافظه را بدون از دست دادن انعطاف‌پذیری #C بهبود ببخشد.

    Rust در unsafe چه می‌کند؟
    در Rust، کل زبان بر پایه ایمنی حافظه طراحی شده‌است. بیشتر کد شما «safe» است و کامپایلر به صورت خودکار بررسی می‌کند که هیچ buffer overrun، dangling pointer یا استفاده بعد از آزادسازی حافظه رخ ندهد. اما برای مواردی که واقعاً نیاز به دسترسی خام به حافظه دارید (مثل interop، بهینه‌سازی شدید یا کار با hardware)، Rust کلمه کلیدی unsafe را معرفی کرده. داخل unsafe، کامپایلر تقریباً همه بررسی‌های ایمنی را خاموش می‌کند. در عوض:
    • توسعه‌دهنده باید صراحتاً تضمین دهد که کد ایمن است.
    • هر تابع unsafe باید در مستندات (/// # Safety) دقیقاً بنویسد که caller چه تعهداتی باید رعایت کند (مثلاً «این اشاره‌گر نباید null باشد»، «حافظه باید حداقل X بایت معتبر باشد»، «طول عمر حافظه باید تا پایان فراخوانی تضمین شود» و غیره).
    • کامنت‌های // SAFETY: داخل پیاده‌سازی برای توضیح منطق ایمنی استفاده می‌شود.
    • unsafe به صورت صریح propagate می‌شود (یعنی فراخوانی یک تابع unsafe، caller را هم unsafe می‌کند مگر اینکه boundary ایمن بسازید).
    این رویکرد باعث می‌شود unsafe «مرئی و قابل audit» بماند و تیم‌ها یا ابزارها بتوانند به راحتی کدهای پرریسک را پیدا کنند.

    C# 16 چه چیزی را از Rust اقتباس کرده؟
    مایکروسافت مدل قدیمی unsafe در #C (که بیشتر یک سوئیچ ساده بود) را به سمت همان فلسفه Rust برده است:
    • unsafe به عنوان قرارداد صریح: دیگر فقط یک بلاک نحوی نیست، بلکه یک قرارداد caller-facing است. فراخوانی عضو unsafe خارج از زمینه unsafe، خطای کامپایل می‌دهد.
    • مستندات اجباری : دقیقاً مشابه /// # Safety در Rust. هر متد unsafe باید توضیح دهد caller چه تضمین‌هایی باید بدهد.
    • کامنت‌های // SAFETY: داخل کد برای توضیح منطق.
    • محدود کردن دامنه: unsafe فقط در سطح member (متد، پراپرتی، فیلد) اعمال می‌شود، نه کل کلاس.
    • تفاوت بین نوع pointer و dereference: فقط عملیات خطرناک (dereference) نیاز به بلاک unsafe دارد.
    • propagation کنترل‌شده: unsafe فقط وقتی propagate می‌شود که صراحتاً روی signature عضو گذاشته شود.

    هدف نهایی: unsafe دیگر «مخفی» یا «گسترده» نباشد. توسعه‌دهندگان و ابزارهای static analysis بتوانند به راحتی بفهمند کجا حافظه به صورت دستی مدیریت می‌شود و چه تعهداتی وجود دارد.

    تفاوت مهم #C با Rust
    #C همچنان یک زبان managed با Garbage Collector است و اکثر کد شما safe باقی می‌ماند. Rust زبانی است که از پایه، بدون GC و با ownership/borrow checker کار می‌کند. C# 16 فقط مرز unsafe را «Rust-like»تر و حرفه‌ای‌تر کرده، نه اینکه کل زبان را تغییر دهد. این الهام‌گیری بخشی از حرکت بزرگ‌تر صنعت (از جمله فشار CISA و NSA) برای کاهش آسیب‌پذیری‌های حافظه است. Rust در این زمینه الگوی موفق و مدرنی بوده، پس مایکروسافت از آن ایده گرفته، بدون اینکه #C را به Rust تبدیل کند.