عنوان:

‫دسترسی آسان‌تر به بازتاب با [UnsafeAccessorType] در NET 10.


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۸/۱۴ ۱۳:۰۶
آدرس: www.dntips.ir
گاهی اوقات نیاز به دسترسی به اعضای خصوصی کلاس‌ها یا انواع، مانند فیلدهای داخلی یا متدهای پنهان، پیش می‌آید. این کار معمولاً برای اهداف پیشرفته‌ای مانند تست واحد، instrumentation یا بهینه‌سازی عملکرد انجام می‌شود. در .NET 8، ویژگی [UnsafeAccessor] معرفی شد تا این دسترسی را بدون نیاز به APIهای بازتاب (reflection) سنتی، ساده‌تر و سریع‌تر کند. با این حال، در .NET 8 و 9، محدودیت‌هایی وجود داشت که مانع استفاده از آن در سناریوهای پیچیده می‌شد. در این مقاله، به بررسی بهبودهای .NET 10 می‌پردازیم، جایی که ویژگی جدید [UnsafeAccessorType] این محدودیت‌ها را برطرف می‌کند. این تغییرات، توسعه‌دهندگان را قادر می‌سازد تا با انعطاف بیشتری به اعضای خصوصی دسترسی پیدا کنند، بدون اینکه مجبور به بازگشت به روش‌های کندتر شوند. اگر با .NET آشنا هستید، این مقاله به شما کمک می‌کند تا از این ابزارهای جدید به طور مؤثر استفاده کنید؛البته با احتیاط، زیرا دسترسی به اعضای خصوصی می‌تواند کپسوله‌سازی (encapsulation) را نقض کند و کد را شکننده‌تر سازد.

توضیح مسئله
یکی از چالش‌های اصلی در کار با انواع در .NET، دسترسی به اعضای خصوصی یا داخلی (internal) است که visibility آن‌ها محدود شده. برای مثال، فرض کنید با یک کلاس عمومی (PublicClass) کار می‌کنید که یک فیلد خصوصی از نوع داخلی (PrivateClass) دارد. در .NET 8 و 9، با استفاده از [UnsafeAccessor]، باید بتوانید مستقیماً به تمام انواع درگیر در سیگنچر متد ارجاع دهید. اگر نوع PrivateClass داخلی باشد و نتوانید به آن ارجاع مستقیم دهید، [UnsafeAccessor] کار نمی‌کند.
این محدودیت دو سناریو اصلی ایجاد می‌کند:
  • محدودیت visibility: انواع داخلی یا خصوصی که نمی‌توانید در کد خود import کنید.
  • محدودیت کامپایل‌تایم: زمانی که انواع در لایبرری‌های دیگر هستند و به دلیل وابستگی‌های چرخه‌ای (circular dependencies) یا مسائل نسخه‌بندی، نمی‌توانید به آن‌ها ارجاع دهید. مثلاً در لایبرری‌های instrumentation مانند Datadog، یا حتی در خود runtime .NET برای لایبرری‌های HTTP و Cryptography.
علاوه بر این، کار با انواع ژنریک پیچیده‌تر می‌شود، زیرا باید سیگنچر دقیق را حفظ کنید. بدون حل این مسائل، توسعه‌دهندگان مجبور به استفاده از reflection سنتی، System.Reflection.Emit یا System.Linq.Expressions می‌شوند که نه تنها کندتر هستند (تا چندین برابر)، بلکه کد را پیچیده‌تر می‌کنند. این کندی می‌تواند در برنامه‌های عملکردمحور، مانند سرویس‌های وب یا ابزارهای مانیتورینگ، مشکل‌ساز باشد.

راه‌حل قدیمی: استفاده از [UnsafeAccessor] در .NET 8 و 9
در .NET 8، [UnsafeAccessor] به عنوان جایگزینی سریع‌تر برای reflection معرفی شد و در .NET 9 با پشتیبانی از ژنریک‌ها بهبود یافت. این ویژگی اجازه می‌دهد تا فیلدها، متدها، سازنده‌ها و اعضای استاتیک را بدون reflection فراخوانی کنید. برای مثال، دسترسی به فیلد خصوصی _items در کلاس List<T>:
با reflection سنتی:
var itemsFieldInfo = typeof(List<int>).GetField("_items", BindingFlags.NonPublic | BindingFlags.Instance);
var list = new List<int>(16);
var items = (int[])itemsFieldInfo.GetValue(list);
Console.WriteLine($"{items.Length} items"); // خروجی: 16 items
حالا با [UnsafeAccessor]:
var list = new List<int>(16);
int[] items = Accessors<int>.GetItems(list);
Console.WriteLine($"{items.Length} items"); // خروجی: 16 items

static class Accessors<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_items")]
    public static extern ref T[] GetItems(List<T> list);
}
توجه کنید که برای انواع ژنریک مانند List<T>، باید از یک کلاس کانتینر استفاده کنید و متد را extern تعریف کنید. این روش نه تنها خواناتر است، بلکه سریع‌تر اجرا می‌شود زیرا runtime مستقیماً به عضو دسترسی پیدا می‌کند.

برای متدهای استاتیک، مثلاً IsCompatibleObject درList<T>:
bool isCompat = Accessors<int?>.IsCompatibleObject(null, 123); // true

static class Accessors<T>
{
    [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "IsCompatibleObject")]
    public static extern bool IsCompatibleObject(List<T> instance, object? value);
}
این روش برای فیلدها ref برمی‌گرداند، پس می‌توانید فیلد را تغییر دهید:
var list = new List<int>(16);
Accessors<int>.GetItems(list) = Array.Empty<int>();
Console.WriteLine($"Capacity: {list.Capacity}"); // خروجی: 0
با این حال، همان‌طور که گفتیم، اگر نتوانید به انواع ارجاع دهید (مثل PrivateClass داخلی)، باید به reflection برگردید؛ که نقطه ضعف اصلی است.

راه‌حل جدید: [UnsafeAccessorType] در .NET 10
.NET 10 با معرفی [UnsafeAccessorType]، این محدودیت را برطرف می‌کند. این attribute اجازه می‌دهد تا انواع را به صورت string مشخص کنید، بدون نیاز به ارجاع مستقیم. این کار هر دو سناریو visibility و کامپایل‌تایم را حل می‌کند.

مثال: دسترسی به SomeValue در PrivateClass از طریق PublicClass:
public class PublicClass
{
    private readonly PrivateClass _private = new("Hello world!");
    internal PrivateClass GetPrivate() => _private;
}

internal class PrivateClass(string someValue)
{
    internal string SomeValue { get; } = someValue;
}
accessorها:
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GetPrivate")]
[return: UnsafeAccessorType("PrivateClass")]
static extern object GetByMethod(PublicClass instance);

[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "get_SomeValue")]
static extern string GetSomeValue([UnsafeAccessorType("PrivateClass")] object instance);
استفاده:
var publicClass = new PublicClass();
object privateClass = GetByMethod(publicClass);
string value = GetSomeValue(privateClass);
Console.WriteLine(value); // خروجی: Hello world!
نام نوع باید fully qualified باشد، مانند “PrivateLib.Class1, PrivateLib” برای assembly-qualified. برای ژنریک‌ها، از فرمت‌هایی مانند “List`1[[!0]]” استفاده کنید (!0 برای پارامترهای نوع باز).

مثال پیچیده‌تر با ژنریک‌ها و constraints:
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "GenericWithConstraints")]
public extern static bool CallGenericClassGenericWithConstraints<V, W>(
    [UnsafeAccessorType("PrivateLib.GenericClass`1[[!0]], PrivateLib")] object tgt,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[!0]]")] object a,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[!!0]]")] object b,
    List<W> c,
    [UnsafeAccessorType("System.Collections.Generic.List`1[[PrivateLib.Class1, PrivateLib]]")] object d) where W : T;
این روش همچنان سریع است و عملکرد بهتری نسبت به reflection دارد. نکته اضافی: برای امنیت بیشتر، همیشه بررسی کنید که آیا استفاده از این ویژگی ضروری است، زیرا می‌تواند به وابستگی‌های ناخواسته منجر شود. همچنین، در محیط‌های توزیع‌شده، اطمینان حاصل کنید که assemblyها در دسترس باشند.

با این حال، محدودیت‌هایی باقی مانده:
  • نمی‌توانید برای انواع ژنریک که پارامتر نوع غیرقابل ارجاع است، instance بسازید.
  • فیلدها یا متدهای ref-returning که نوعشان نیاز به [UnsafeAccessorType] دارد، پشتیبانی نمی‌شوند (خطای NotSupportedException).

نتیجه‌گیری
ویژگی [UnsafeAccessor] در .NET 8 و 9، دسترسی به اعضای خصوصی را ساده‌تر کرد، اما محدودیت‌های ارجاع به انواع، آن را محدود می‌کرد. با [UnsafeAccessorType] در .NET 10، حالا می‌توانید حتی به انواع غیرقابل ارجاع دسترسی پیدا کنید، که این ویژگی را برای سناریوهای پیشرفته مانند instrumentation یا حل وابستگی‌های پیچیده، ایده‌آل می‌سازد. این بهبود نه تنها کد را خواناتر نگه می‌دارد، بلکه عملکرد را حفظ می‌کند. با این حال، همیشه با احتیاط استفاده کنید تا کپسوله‌سازی را نقض نکنید.


مشاهده مطلب اصلی