فصل ۱۸: Reflection و Metadata؛ Type، TypeInfo، Activator و نوعهای Generic
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
نشان تصویری آغاز فصل ۱۸ در منبع
فصل ۱۸: بازتاب و فراداده (Reflection and Metadata)
همانطور که در فصل ۱۷ دیدیم، یک برنامهٔ C# به یک اسمبلی (Assembly) کامپایل میشود که شامل فراداده (Metadata)، کد کامپایلشده و منابع است. بازرسی فراداده و کد کامپایلشده در زمان اجرا، بازتاب (Reflection) نامیده میشود.
کد کامپایلشده در یک اسمبلی تقریباً تمام محتوای کد منبع اصلی را در خود دارد. بخشی از اطلاعات، مانند نام متغیرهای محلی، توضیحات (Comments) و دستورهای پیشپردازنده (Preprocessor Directives)، از دست میرود. با این حال، Reflection تقریباً به هر چیز دیگری دسترسی دارد، تا جایی که حتی نوشتن یک دیکامپایلر (Decompiler) را ممکن میکند.
بسیاری از سرویسهای موجود در .NET که از طریق C# در دسترساند—مانند اتصال پویا (Dynamic Binding)، سریالسازی (Serialization) و اتصال داده (Data Binding)—به وجود فراداده وابستهاند. برنامههای خود شما نیز میتوانند از این فراداده بهره ببرند و حتی با استفاده از صفتهای سفارشی (Custom Attributes) اطلاعات تازهای به آن بیفزایند. فضای نام System.Reflection میزبان API مربوط به Reflection است. همچنین در زمان اجرا میتوان از طریق کلاسهای فضای نام System.Reflection.Emit، فرادادهٔ جدید و دستورهای اجرایی در زبان میانی (Intermediate Language یا IL) را بهصورت پویا ایجاد کرد.
مثالهای این فصل فرض میکنند که علاوه بر فضای نام System، فضاهای نام System.Reflection و System.Reflection.Emit را نیز import کردهاید.
بازتاب و فعالسازی نوعها (Reflecting and Activating Types)
در این بخش بررسی میکنیم چگونه یک Type را بهدست آوریم، فرادادهٔ آن را بررسی کنیم و از آن برای نمونهسازی پویا از یک شیء استفاده کنیم.
بهدستآوردن یک Type
یک نمونه از System.Type فرادادهٔ یک نوع را نمایش میدهد. از آنجا که Type بسیار پرکاربرد است، بهجای فضای نام System.Reflection در فضای نام System قرار دارد.
میتوانید با فراخوانی GetType روی هر شیء یا با عملگر typeof در C# یک نمونه از System.Type بهدست آورید:
Type t1 = DateTime.Now.GetType(); // Type obtained at runtime
Type t2 = typeof (DateTime); // Type obtained at compile time
میتوانید از typeof برای بهدستآوردن نوعهای آرایهای و نوعهای Generic نیز استفاده کنید:
Type t3 = typeof (DateTime[]); // 1-d Array type
Type t4 = typeof (DateTime[,]); // 2-d Array type
Type t5 = typeof (Dictionary<int,int>); // Closed generic type
Type t6 = typeof (Dictionary<,>); // Unbound generic type
همچنین میتوانید یک Type را بر اساس نام آن بازیابی کنید. اگر به Assembly آن ارجاع دارید، Assembly.GetType را فراخوانی کنید؛ این موضوع را در بخش «بازتاب اسمبلیها» در صفحهٔ ۸۲۷ بیشتر توضیح میدهیم:
Type t = Assembly.GetExecutingAssembly().GetType ("Demos.TestProgram");
اگر شیء Assembly ندارید، میتوانید نوع را از طریق نام واجد شرایط اسمبلی (Assembly-Qualified Name)—یعنی نام کامل نوع که پس از آن نام کامل یا تاحدیواجدشرایط اسمبلی آمده است—بهدست آورید. اسمبلی بهطور ضمنی همانند حالتی بارگذاری میشود که Assembly.Load(string) را فراخوانی کرده باشید:
Type t = Type.GetType ("System.Int32, System.Private.CoreLib");
پس از آنکه یک شیء System.Type دارید، میتوانید از propertyهای آن برای دسترسی به نام نوع، اسمبلی، نوع پایه، سطح دسترسی و موارد دیگر استفاده کنید:
Type stringType = typeof (string);
string name = stringType.Name; // String
Type baseType = stringType.BaseType; // typeof(Object)
Assembly assem = stringType.Assembly; // System.Private.CoreLib
bool isPublic = stringType.IsPublic; // true
یک نمونهٔ System.Type پنجرهای رو به تمام فرادادهٔ آن نوع—و اسمبلیای که نوع در آن تعریف شده است—فراهم میکند.
TypeInfo
اگر .NET Core 1.x (یا یک پروفایل قدیمیتر Windows Store) را هدف قرار دهید، متوجه میشوید بیشتر اعضای Type وجود ندارند. این اعضای مفقود در کلاسی به نام TypeInfo عرضه میشوند که با فراخوانی GetTypeInfo آن را بهدست میآورید. بنابراین برای اجرای مثال قبلی به این شکل عمل میکنید:
Type stringType = typeof(string);
string name = stringType.Name;
Type baseType = stringType.GetTypeInfo().BaseType;
Assembly assem = stringType.GetTypeInfo().Assembly;
bool isPublic = stringType.GetTypeInfo().IsPublic;
TypeInfo در .NET Core 2 و 3 و .NET 5+ (و نیز .NET Framework 4.5+ و همهٔ نسخههای .NET Standard) وجود دارد؛ بنابراین کد بالا تقریباً در همهجا کار میکند. TypeInfo همچنین propertyها و متدهای بیشتری برای Reflection روی اعضا دارد.
بهدستآوردن نوعهای آرایهای
همانطور که دیدیم، typeof و GetType با نوعهای آرایهای کار میکنند. همچنین میتوانید با فراخوانی MakeArrayType روی نوع عنصر، نوع آرایه را بهدست آورید:
Type simpleArrayType = typeof (int).MakeArrayType();
Console.WriteLine (simpleArrayType == typeof (int[])); // True
برای ساخت نوع آرایهٔ چندبعدی، یک آرگومان عدد صحیح به MakeArrayType بدهید:
Type cubeType = typeof (int).MakeArrayType (3); // cube shaped
Console.WriteLine (cubeType == typeof (int[,,])); // True
GetElementType عمل معکوس را انجام میدهد و نوع عنصر یک نوع آرایهای را بازیابی میکند:
Type e = typeof (int[]).GetElementType(); // e == typeof (int)
GetArrayRank تعداد ابعاد یک آرایهٔ مستطیلی را برمیگرداند:
int rank = typeof (int[,,]).GetArrayRank(); // 3
بهدستآوردن نوعهای تودرتو
برای بازیابی نوعهای تودرتو (Nested Types)، روی نوع دربرگیرنده GetNestedTypes را فراخوانی کنید:
foreach (Type t in typeof (System.Environment).GetNestedTypes())
Console.WriteLine (t.FullName);
OUTPUT: System.Environment+SpecialFolder
یا:
foreach (TypeInfo t in typeof (System.Environment).GetTypeInfo()
.DeclaredNestedTypes)
Debug.WriteLine (t.FullName);
تنها نکته دربارهٔ نوعهای تودرتو این است که CLR آنها را دارای سطحهای دسترسی ویژهٔ «تودرتو» در نظر میگیرد:
Type t = typeof (System.Environment.SpecialFolder);
Console.WriteLine (t.IsPublic); // False
Console.WriteLine (t.IsNestedPublic); // True
نام نوعها (Type Names)
یک نوع propertyهای Namespace، Name و FullName دارد. در بیشتر موارد، FullName ترکیبی از دو مورد اول است:
Type t = typeof (System.Text.StringBuilder);
Console.WriteLine (t.Namespace); // System.Text
Console.WriteLine (t.Name); // StringBuilder
Console.WriteLine (t.FullName); // System.Text.StringBuilder
این قاعده دو استثنا دارد: نوعهای تودرتو و نوعهای Generic بسته (Closed Generic Types).
نام نوعهای تودرتو
در نوعهای تودرتو، نوع دربرگیرنده فقط در FullName ظاهر میشود:
Type t = typeof (System.Environment.SpecialFolder);
Console.WriteLine (t.Namespace); // System
Console.WriteLine (t.Name); // SpecialFolder
Console.WriteLine (t.FullName); // System.Environment+SpecialFolder
نماد + نوع دربرگیرنده را از یک فضای نام تودرتو متمایز میکند.
نام نوعهای Generic
نام نوعهای Generic با نماد backtick یعنی ` و سپس تعداد پارامترهای نوع پسوند میگیرد. اگر نوع Generic نامقید (Unbound) باشد، این قاعده هم بر Name و هم بر FullName اعمال میشود:
Type t = typeof (Dictionary<,>); // Unbound
Console.WriteLine (t.Name); // Dictionary`2
Console.WriteLine (t.FullName); // System.Collections.Generic.Dictionary`2
اما اگر نوع Generic بسته باشد، فقط FullName دنبالهای طولانی دریافت میکند. نام کامل واجد شرایط اسمبلیِ هر پارامتر نوع فهرست میشود:
Console.WriteLine (typeof (Dictionary<int,string>).FullName);
// OUTPUT:
System.Collections.Generic.Dictionary`2[[System.Int32,
System.Private.CoreLib, Version=4.0.0.0, Culture=neutral,
PublicKeyToken=7cec85d7bea7798e],[System.String, System.Private.CoreLib,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
این کار تضمین میکند AssemblyQualifiedName—که ترکیبی از نام کامل نوع و نام اسمبلی است—اطلاعات کافی برای شناسایی کامل خود نوع Generic و پارامترهای نوع آن داشته باشد.
نام نوعهای آرایه و اشارهگر
آرایهها همان پسوندی را نمایش میدهند که در یک عبارت typeof استفاده میکنید:
Console.WriteLine (typeof ( int[] ).Name); // Int32[]
Console.WriteLine (typeof ( int[,] ).Name); // Int32[,]
Console.WriteLine (typeof ( int[,] ).FullName); // System.Int32[,]
نوعهای اشارهگر (Pointer Types) نیز مشابهاند:
Console.WriteLine (typeof (byte*).Name); // Byte*
نام نوع پارامترهای ref و out
یک Type که پارامتر ref یا out را توصیف میکند، پسوند & دارد:
public void RefMethod (ref int p)
{
Type t = MethodInfo.GetCurrentMethod().GetParameters()[0].ParameterType;
Console.WriteLine (t.Name); // Int32&
}
بعدتر در بخش «بازتاب و فراخوانی اعضا» در صفحهٔ ۸۱۳ بیشتر دربارهٔ این موضوع صحبت میکنیم.
نوعهای پایه و Interfaceها (Base Types and Interfaces)
Type یک property به نام BaseType ارائه میکند:
Type base1 = typeof (System.String).BaseType;
Type base2 = typeof (System.IO.FileStream).BaseType;
Console.WriteLine (base1.Name); // Object
Console.WriteLine (base2.Name); // Stream
متد GetInterfaces رابطهایی را برمیگرداند که یک نوع پیادهسازی میکند:
foreach (Type iType in typeof (Guid).GetInterfaces())
Console.WriteLine (iType.Name);
IFormattable
IComparable
IComparable`1
IEquatable`1
(متد GetInterfaceMap یک struct برمیگرداند که نشان میدهد هر عضو یک interface چگونه در یک class یا struct پیادهسازی شده است؛ کاربرد این قابلیت پیشرفته را در بخش «فراخوانی اعضای Static Virtual/Abstract در Interface» در صفحهٔ ۸۲۶ نشان میدهیم.)
Reflection سه معادل پویا برای عملگر ایستای is در C# فراهم میکند:
IsInstanceOfType- یک نوع و یک نمونه را میپذیرد.
IsAssignableFrom و—از .NET 5—IsAssignableTo- دو نوع را میپذیرند.
مثالی از مورد اول:
object obj = Guid.NewGuid();
Type target = typeof (IFormattable);
bool isTrue = obj is IFormattable; // Static C# operator
bool alsoTrue = target.IsInstanceOfType (obj); // Dynamic equivalent
IsAssignableFrom انعطاف بیشتری دارد:
Type target = typeof (IComparable), source = typeof (string);
Console.WriteLine (target.IsAssignableFrom (source)); // True
متد IsSubclassOf بر همان اصل IsAssignableFrom کار میکند، با این تفاوت که interfaceها را کنار میگذارد.
نمونهسازی نوعها (Instantiating Types)
برای نمونهسازی پویا از یک شیء بر اساس نوع آن دو راه وجود دارد:
- فراخوانی متد static یعنی
Activator.CreateInstance - فراخوانی
Invoke روی یک شیء ConstructorInfo که از GetConstructor روی یک Type بهدست آمده است—برای سناریوهای پیشرفته.
Activator.CreateInstance یک Type و آرگومانهای اختیاری میپذیرد و آنها را به سازنده پاس میدهد:
int i = (int) Activator.CreateInstance (typeof (int));
DateTime dt = (DateTime) Activator.CreateInstance (typeof (DateTime),
2000, 1, 1);
CreateInstance گزینههای دیگری نیز در اختیار میگذارد؛ مثلاً مشخصکردن اسمبلیای که نوع باید از آن بارگذاری شود یا تعیین اینکه اتصال به سازندهٔ nonpublic مجاز باشد یا نه. اگر Runtime نتواند سازندهٔ مناسبی پیدا کند، MissingMethodException رخ میدهد.
فراخوانی Invoke روی ConstructorInfo زمانی لازم است که مقدار آرگومانهای شما نتوانند بین سازندههای Overloadشده ابهامزدایی کنند. فرض کنید کلاس X دو سازنده دارد: یکی با پارامتر string و دیگری با پارامتر StringBuilder. اگر null را به Activator.CreateInstance بدهید، مقصد مبهم میشود. در این حالت باید از ConstructorInfo استفاده کنید:
// Fetch the constructor that accepts a single parameter of type string:
ConstructorInfo ci = typeof (X).GetConstructor (new[] { typeof (string) });
// Construct the object using that overload, passing in null:
object foo = ci.Invoke (new object[] { null });
یا اگر .NET Core 1 یا یک پروفایل قدیمی Windows Store را هدف گرفتهاید:
ConstructorInfo ci = typeof (X).GetTypeInfo().DeclaredConstructors
.FirstOrDefault (c =>
c.GetParameters().Length == 1 &&
c.GetParameters()[0].ParameterType == typeof (string));
برای بهدستآوردن یک سازندهٔ nonpublic باید BindingFlags را مشخص کنید؛ بخش «دسترسی به اعضای Nonpublic» در صفحهٔ ۸۲۲، در ادامهٔ بخش «بازتاب و فراخوانی اعضا» در صفحهٔ ۸۱۳ را ببینید.
برای نمونهسازی پویا از آرایهها فقط بر مبنای نوع عنصر، ابتدا MakeArrayType را فراخوانی کنید. همچنین میتوانید نوعهای Generic را نمونهسازی کنید؛ این موضوع در بخش بعد توضیح داده میشود.
برای نمونهسازی پویا از یک delegate، Delegate.CreateDelegate را فراخوانی کنید. مثال زیر هم یک delegate نمونه (instance delegate) و هم یک delegate ایستا (static delegate) را نمونهسازی میکند:
class Program
{
delegate int IntFunc (int x);
static int Square (int x) => x * x; // Static method
int Cube (int x) => x * x * x; // Instance method
static void Main()
{
Delegate staticD = Delegate.CreateDelegate
(typeof (IntFunc), typeof (Program), "Square");
Delegate instanceD = Delegate.CreateDelegate
(typeof (IntFunc), new Program(), "Cube");
Console.WriteLine (staticD.DynamicInvoke (3)); // 9
Console.WriteLine (instanceD.DynamicInvoke (3)); // 27
}
}
میتوانید شیء Delegate برگرداندهشده را مانند مثال بالا با DynamicInvoke فراخوانی کنید یا آن را به delegate تایپدار cast کنید:
IntFunc f = (IntFunc) staticD;
Console.WriteLine (f(3)); // 9 (but much faster!)
بهجای نام متد میتوانید یک MethodInfo به CreateDelegate بدهید. اندکی بعد در بخش «بازتاب و فراخوانی اعضا» در صفحهٔ ۸۱۳، MethodInfo را توضیح میدهیم و علت cast کردن یک delegate که بهصورت پویا ساخته شده است به نوع delegate ایستا را نیز بیان میکنیم.
نوعهای Generic
یک Type میتواند نوع Generic بسته یا نامقید را نمایش دهد. همانند زمان کامپایل، نوع Generic بسته قابل نمونهسازی است، اما نوع نامقید نیست:
Type closed = typeof (List<int>);
List<int> list = (List<int>) Activator.CreateInstance (closed); // OK
Type unbound = typeof (List<>);
object anError = Activator.CreateInstance (unbound); // Runtime error
متد MakeGenericType یک نوع نامقید را به نوع Generic بسته تبدیل میکند. کافی است آرگومانهای نوع موردنظر را به آن بدهید:
Type unbound = typeof (List<>);
Type closed = unbound.MakeGenericType (typeof (int));
متد GetGenericTypeDefinition کار معکوس را انجام میدهد:
Type unbound2 = closed.GetGenericTypeDefinition(); // unbound == unbound2
propertyِ IsGenericType اگر یک Type Generic باشد true برمیگرداند و IsGenericTypeDefinition اگر نوع Generic نامقید باشد true میدهد. مثال زیر بررسی میکند که یک نوع، value type تهیپذیر (Nullable) هست یا نه:
Type nullable = typeof (bool?);
Console.WriteLine (
nullable.IsGenericType &&
nullable.GetGenericTypeDefinition() == typeof (Nullable<>)); // True
GetGenericArguments آرگومانهای نوعِ نوعهای Generic بسته را برمیگرداند:
Console.WriteLine (closed.GetGenericArguments()[0]); // System.Int32
Console.WriteLine (nullable.GetGenericArguments()[0]); // System.Boolean
برای نوعهای Generic نامقید، GetGenericArguments شبهنوعهایی (Pseudotypes) را برمیگرداند که نمایندهٔ placeholderهای مشخصشده در تعریف نوع Generic هستند:
Console.WriteLine (unbound.GetGenericArguments()[0]); // T