فصل ۱۸: Reflection روی Memberها، Late Binding، BindingFlags و Assemblyها
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
بازتاب و فراخوانی اعضا (Reflecting and Invoking Members)
متد GetMembers اعضای یک نوع را برمیگرداند. کلاس زیر را در نظر بگیرید:
class Walnut
{
private bool cracked;
public void Crack() { cracked = true; }
}
میتوانیم روی اعضای public آن Reflection انجام دهیم:
MemberInfo[] members = typeof (Walnut).GetMembers();
foreach (MemberInfo m in members)
Console.WriteLine (m);
نتیجه چنین است:
Void Crack()
System.Type GetType()
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
Void .ctor()
وقتی GetMembers بدون آرگومان فراخوانی شود، همهٔ اعضای public یک نوع و نوعهای پایهٔ آن را برمیگرداند. GetMember یک عضو مشخص را بر اساس نام بازیابی میکند—با این حال همچنان آرایه برمیگرداند، زیرا ممکن است اعضا overload شده باشند:
MemberInfo[] m = typeof (Walnut).GetMember ("Crack");
Console.WriteLine (m[0]); // Void Crack()
MemberInfo همچنین propertyای به نام MemberType از نوع MemberTypes دارد. این یک enum پرچمی است و مقادیر زیر را دارد:
All Custom Field NestedType TypeInfo
Constructor Event Method Property
هنگام فراخوانی GetMembers میتوانید یک نمونهٔ MemberTypes بدهید تا نوع عضوهایی که برگردانده میشوند محدود شود. راه دیگر این است که مجموعهٔ نتیجه را با فراخوانی GetMethods، GetFields، GetProperties، GetEvents، GetConstructors یا GetNestedTypes محدود کنید. نسخهٔ مفرد هر یک نیز برای هدفگرفتن یک عضو مشخص وجود دارد.
یک شیء MemberInfo یک property به نام Name و دو property از نوع Type دارد:
DeclaringTypeTypeای را برمیگرداند که عضو را تعریف کرده است.ReflectedTypeTypeای را برمیگرداند که GetMembers روی آن فراخوانی شده است.
این دو وقتی روی عضوی تعریفشده در نوع پایه فراخوانی شوند متفاوتاند: DeclaringType نوع پایه را برمیگرداند و ReflectedType زیرنوع را. مثال زیر این تفاوت را نشان میدهد:
// MethodInfo is a subclass of MemberInfo; see Figure 18-1.
MethodInfo test = typeof (Program).GetMethod ("ToString");
MethodInfo obj = typeof (object) .GetMethod ("ToString");
Console.WriteLine (test.DeclaringType); // System.Object
Console.WriteLine (obj.DeclaringType); // System.Object
Console.WriteLine (test.ReflectedType); // Program
Console.WriteLine (obj.ReflectedType); // System.Object
Console.WriteLine (test == obj); // False
چون test و obj دارای ReflectedType متفاوتاند، با هم برابر نیستند. با این حال، تفاوت آنها صرفاً ساختهٔ API مربوط به Reflection است؛ نوع Program ما در سیستم نوع زیربنایی، متد مجزایی برای ToString ندارد. میتوانیم با یکی از دو روش زیر تأیید کنیم که دو شیء MethodInfo به همان متد اشاره میکنند:
Console.WriteLine (test.MethodHandle == obj.MethodHandle); // True
Console.WriteLine (test.MetadataToken == obj.MetadataToken // True
&& test.Module == obj.Module);
یک MethodHandle برای هر متد واقعاً متمایز در یک process یکتا است؛ یک MetadataToken نیز در میان همهٔ typeها و memberهای داخل یک ماژول اسمبلی یکتا است. MemberInfo همچنین متدهایی برای برگرداندن custom attributeها تعریف میکند؛ بخش «بازیابی Attributeها در زمان اجرا» در صفحهٔ ۸۳۲ را ببینید.
نوعهای عضو (Member Types)
خود MemberInfo اعضای کمی دارد، زیرا یک پایهٔ abstract برای typeهایی است که در شکل ۱۸-۱ نشان داده شدهاند.
شکل ۱۸-۱ — نوعهای Member (Member types)
میتوانید یک MemberInfo را بر اساس propertyِ MemberType آن به زیرنوع مربوط cast کنید. اگر عضو را با GetMethod، GetField، GetProperty، GetEvent، GetConstructor یا GetNestedType—یا نسخههای جمع آنها—گرفته باشید، cast لازم نیست. جدول ۱۸-۱ خلاصه میکند برای هر سازهٔ C# از چه متدی استفاده شود.
جدول ۱۸-۱ — بازیابی فرادادهٔ اعضا
| سازهٔ C# | متد مورد استفاده | نام مورد استفاده | نتیجه |
| Method | GetMethod | نام متد | MethodInfo |
| Property | GetProperty | نام property | PropertyInfo |
| Indexer | GetDefaultMembers | — | MemberInfo[] شامل PropertyInfoها در صورت کامپایل با C# |
| Field | GetField | نام field | FieldInfo |
| عضو Enum | GetField | نام عضو | FieldInfo |
| Event | GetEvent | نام event | EventInfo |
| Constructor | GetConstructor | — | ConstructorInfo |
| Finalizer | GetMethod | "Finalize" | MethodInfo |
| Operator | GetMethod | "op_" + operator name | MethodInfo |
| Nested type | GetNestedType | نام type | Type |
هر زیرکلاس MemberInfo مجموعهٔ بزرگی از propertyها و متدها دارد که همهٔ جنبههای فرادادهٔ عضو را آشکار میکنند؛ از جمله visibility، modifierها، آرگومانهای نوع Generic، پارامترها، نوع بازگشتی و custom attributeها.
مثالی از استفاده از GetMethod:
MethodInfo m = typeof (Walnut).GetMethod ("Crack");
Console.WriteLine (m); // Void Crack()
Console.WriteLine (m.ReturnType); // System.Void
همهٔ نمونههای *Info در نخستین استفاده توسط API مربوط به Reflection cache میشوند:
MethodInfo method = typeof (Walnut).GetMethod ("Crack");
MemberInfo member = typeof (Walnut).GetMember ("Crack") [0];
Console.Write (method == member); // True
Cache شدن علاوه بر حفظ هویت شیء، عملکرد APIای را که در غیر این صورت نسبتاً کند است بهتر میکند.
اعضای C# در برابر اعضای CLR
جدول قبلی نشان میدهد بعضی از سازههای کاربردی C# نگاشت یکبهیک با سازههای CLR ندارند. این منطقی است، زیرا CLR و API مربوط به Reflection با در نظر گرفتن همهٔ زبانهای .NET طراحی شدهاند—حتی از Visual Basic نیز میتوان Reflection را بهکار برد.
بعضی سازههای C#—یعنی indexerها، enumها، operatorها و finalizerها—از دید CLR ساختارهایی قراردادی هستند. بهطور مشخص:
- یک indexer در C# به propertyای تبدیل میشود که یک یا چند آرگومان میپذیرد و با
[DefaultMember] نوع علامتگذاری میشود.
- یک enum در C# به زیرنوعی از
System.Enum تبدیل میشود که برای هر عضو یک fieldِ static دارد. - یک operator در C# به متد static با نامی ویژه تبدیل میشود که با
op_ شروع میشود؛ برای مثال op_Addition. - یک finalizer در C# به متدی تبدیل میشود که
Finalize را override میکند.
پیچیدگی دیگر این است که propertyها و eventها در واقع از دو چیز تشکیل میشوند:
- فرادادهای که property یا event را توصیف میکند و در
PropertyInfo یا EventInfo کپسوله شده است. - یک یا دو متد پشتیبان (Backing Method).
در برنامهٔ C#، متدهای پشتیبان داخل تعریف property یا event کپسوله میشوند. اما پس از کامپایل به IL، این متدهای پشتیبان مانند متدهای عادی ظاهر میشوند و میتوانید مانند هر متد دیگری آنها را فراخوانی کنید. بنابراین GetMethods متدهای پشتیبان property و event را در کنار متدهای معمولی برمیگرداند:
class Test { public int X { get { return 0; } set {} } }
void Demo()
{
foreach (MethodInfo mi in typeof (Test).GetMethods())
Console.Write (mi.Name + " ");
}
// OUTPUT:
get_X set_X GetType ToString Equals GetHashCode
میتوانید این متدها را از طریق propertyِ IsSpecialName در MethodInfo شناسایی کنید. IsSpecialName برای accessorهای property، indexer و event و نیز operatorها مقدار true دارد. تنها برای متدهای قراردادی C#—و در صورت تعریف finalizer، متد Finalize—مقدار false میدهد.
متدهای پشتیبانی که C# تولید میکند چنیناند:
| سازهٔ C# | نوع عضو | متدهای IL |
|---|
| Property | Property | get_XXX و set_XXX |
| Indexer | Property | get_Item و set_Item |
| Event | Event | add_XXX و remove_XXX |
هر متد پشتیبان، شیء MethodInfo مرتبط خودش را دارد. بهصورت زیر میتوانید به آنها دسترسی پیدا کنید:
PropertyInfo pi = typeof (Console).GetProperty ("Title");
MethodInfo getter = pi.GetGetMethod(); // get_Title
MethodInfo setter = pi.GetSetMethod(); // set_Title
MethodInfo[] both = pi.GetAccessors(); // Length==2
GetAddMethod و GetRemoveMethod کار مشابهی را برای EventInfo انجام میدهند.
برای رفتن در جهت معکوس—از یک MethodInfo به PropertyInfo یا EventInfo مرتبط با آن—باید یک query اجرا کنید. LINQ برای این کار مناسب است:
PropertyInfo p = mi.DeclaringType.GetProperties()
.First (x => x.GetAccessors (true).Contains (mi));
Propertyهای فقط-init (Init-only properties)
Propertyهای init-only که در C# 9 معرفی شدند، از طریق object initializer قابل تنظیماند، اما پس از آن کامپایلر آنها را read-only در نظر میگیرد. از دید CLR، یک accessor از نوع init دقیقاً مانند یک accessor معمولی set است، با این تفاوت که یک پرچم خاص روی نوع بازگشتی متد set اعمال میشود و این پرچم برای کامپایلر معنا دارد.
جالب اینکه این پرچم بهصورت یک convention attribute کدگذاری نشده است. در عوض، سازوکاری نسبتاً ناشناخته به نام modreq استفاده میشود که تضمین میکند نسخههای قبلی کامپایلر C#—که modreq جدید را نمیشناسند—بهجای اینکه property را قابلنوشتن تلقی کنند، accessor را نادیده بگیرند.
modreq مربوط به accessorهای init-only، IsExternalInit نام دارد و میتوانید به شکل زیر آن را query کنید:
bool IsInitOnly (PropertyInfo pi) => pi
.GetSetMethod().ReturnParameter.GetRequiredCustomModifiers()
.Any (t => t.Name == "IsExternalInit");
NullabilityInfoContext
از .NET 6 میتوانید از کلاس NullabilityInfoContext برای بهدستآوردن اطلاعات مربوط به annotationهای تهیپذیری (Nullability) یک field، property، event یا parameter استفاده کنید:
void PrintPropertyNullability (PropertyInfo pi)
{
var info = new NullabilityInfoContext().Create (pi);
Console.WriteLine (pi.Name + " read " + info.ReadState);
Console.WriteLine (pi.Name + " write " + info.WriteState);
// Use info.Element to get nullability info for array elements
}
اعضای نوعهای Generic
میتوانید فرادادهٔ عضو را هم برای نوعهای Generic نامقید و هم برای نوعهای Generic بسته بهدست آورید:
PropertyInfo unbound = typeof (IEnumerator<>) .GetProperty ("Current");
PropertyInfo closed = typeof (IEnumerator<int>).GetProperty ("Current");
Console.WriteLine (unbound); // T Current
Console.WriteLine (closed); // Int32 Current
Console.WriteLine (unbound.PropertyType.IsGenericParameter); // True
Console.WriteLine (closed.PropertyType.IsGenericParameter); // False
اشیای MemberInfo که از نوعهای Generic نامقید و بسته برگردانده میشوند همیشه متمایزند؛ حتی برای اعضایی که امضای آنها هیچ پارامتر نوع Genericای ندارد:
PropertyInfo unbound = typeof (List<>) .GetProperty ("Count");
PropertyInfo closed = typeof (List<int>).GetProperty ("Count");
Console.WriteLine (unbound); // Int32 Count
Console.WriteLine (closed); // Int32 Count
Console.WriteLine (unbound == closed); // False
Console.WriteLine (unbound.DeclaringType.IsGenericTypeDefinition); // True
Console.WriteLine (closed.DeclaringType.IsGenericTypeDefinition); // False
اعضای نوعهای Generic نامقید را نمیتوان بهصورت پویا invoke کرد.
فراخوانی پویای یک عضو (Dynamically Invoking a Member)
پس از آنکه یک شیء MethodInfo، PropertyInfo یا FieldInfo دارید، میتوانید آن را بهصورت پویا فراخوانی کنید یا مقدارش را get/set کنید. این کار اتصال دیرهنگام (Late Binding) نام دارد، زیرا عضو مورد فراخوانی را بهجای زمان کامپایل در زمان اجرا انتخاب میکنید.
برای نمونه، کد زیر از Static Binding معمول استفاده میکند:
string s = "Hello";
int length = s.Length;
همین کار بهصورت پویا با Late Binding:
object s = "Hello";
PropertyInfo prop = s.GetType().GetProperty ("Length");
int length = (int) prop.GetValue (s, null); // 5
GetValue و SetValue مقدار یک PropertyInfo یا FieldInfo را میگیرند و تنظیم میکنند. آرگومان اول نمونه است که برای عضو static میتواند null باشد. دسترسی به indexer مانند دسترسی به propertyای با نام Item است، با این تفاوت که هنگام فراخوانی GetValue یا SetValue مقدارهای indexer را بهعنوان آرگومان دوم میدهید.
برای فراخوانی پویا از یک متد، روی MethodInfo متد Invoke را فراخوانی کنید و آرایهای از آرگومانها را برای پاسدادن به متد بدهید. اگر نوع هرکدام از آرگومانها اشتباه باشد، در زمان اجرا exception رخ میدهد. با فراخوانی پویا ایمنی نوع در زمان کامپایل را از دست میدهید، اما همچنان ایمنی نوع در زمان اجرا را دارید—درست مانند کلیدواژهٔ dynamic.
پارامترهای متد (Method Parameters)
فرض کنید میخواهیم متد Substring از string را بهصورت پویا فراخوانی کنیم. بهصورت static این کار چنین است:
Console.WriteLine ("stamp".Substring(2)); // "amp"
معادل پویا با Reflection و Late Binding:
Type type = typeof (string);
Type[] parameterTypes = { typeof (int) };
MethodInfo method = type.GetMethod ("Substring", parameterTypes);
object[] arguments = { 2 };
object returnValue = method.Invoke ("stamp", arguments);
Console.WriteLine (returnValue); // "amp"
چون متد Substring overload شده است، مجبور بودیم آرایهای از نوعهای پارامتر را به GetMethod بدهیم تا مشخص شود کدام نسخه را میخواهیم. بدون نوعهای پارامتر، GetMethod یک AmbiguousMatchException پرتاب میکند.
متد GetParameters که روی MethodBase—کلاس پایهٔ MethodInfo و ConstructorInfo—تعریف شده است، فرادادهٔ پارامترها را برمیگرداند. مثال قبلی را میتوان اینگونه ادامه داد:
ParameterInfo[] paramList = method.GetParameters();
foreach (ParameterInfo x in paramList)
{
Console.WriteLine (x.Name); // startIndex
Console.WriteLine (x.ParameterType); // System.Int32
}
کار با پارامترهای ref و out
برای پاسدادن پارامترهای ref یا out، پیش از گرفتن متد، MakeByRefType را روی نوع فراخوانی کنید. برای نمونه، میتوانید این کد را بهصورت پویا اجرا کنید:
int x;
bool successfulParse = int.TryParse ("23", out x);
بهشکل زیر:
object[] args = { "23", 0 };
Type[] argTypes = { typeof (string), typeof (int).MakeByRefType() };
MethodInfo tryParse = typeof (int).GetMethod ("TryParse", argTypes);
bool successfulParse = (bool) tryParse.Invoke (null, args);
Console.WriteLine (successfulParse + " " + args[1]); // True 23
همین روش هم برای نوع پارامتر ref و هم out کار میکند.
بازیابی و فراخوانی متدهای Generic
مشخصکردن صریح نوع پارامترها هنگام فراخوانی GetMethod میتواند برای ابهامزدایی بین متدهای overloadشده ضروری باشد. با این حال، مشخصکردن نوع پارامترهای Generic ممکن نیست.
برای نمونه، کلاس System.Linq.Enumerable متد Where را به شکل زیر overload میکند:
public static IEnumerable<TSource> Where<TSource>
(this IEnumerable<TSource> source, Func<TSource, bool> predicate);
public static IEnumerable<TSource> Where<TSource>
(this IEnumerable<TSource> source, Func<TSource, int, bool> predicate);
برای بازیابی یک overload مشخص، باید همهٔ متدها را بگیریم و سپس overload موردنظر را بهصورت دستی پیدا کنیم. Query زیر overload اول Where را بازیابی میکند:
from m in typeof (Enumerable).GetMethods()
where m.Name == "Where" && m.IsGenericMethod
let parameters = m.GetParameters()
where parameters.Length == 2
let genArg = m.GetGenericArguments().First()
let enumerableOfT = typeof (IEnumerable<>).MakeGenericType (genArg)
let funcOfTBool = typeof (Func<,>).MakeGenericType (genArg, typeof (bool))
where parameters[0].ParameterType == enumerableOfT
&& parameters[1].ParameterType == funcOfTBool
select m
فراخوانی .Single() روی این query، شیء MethodInfo صحیح را با پارامترهای نوع نامقید میدهد. گام بعدی بستن پارامترهای نوع با فراخوانی MakeGenericMethod است:
var closedMethod = unboundMethod.MakeGenericMethod (typeof (int));
در اینجا TSource را با int بستهایم و بنابراین میتوانیم Enumerable.Where را با منبعی از نوع IEnumerable<int> و predicateای از نوع Func<int,bool> فراخوانی کنیم:
int[] source = { 3, 4, 5, 6, 7, 8 };
Func<int, bool> predicate = n => n % 2 == 1; // Odd numbers only
اکنون متد Generic بسته را میتوانیم invoke کنیم:
var query = (IEnumerable<int>) closedMethod.Invoke
(null, new object[] { source, predicate });
foreach (int element in query) Console.Write (element + "|"); // 3|5|7|
استفاده از Delegateها برای کارایی (Using Delegates for Performance)
فراخوانیهای پویا نسبتاً ناکارآمدند و معمولاً سرباری در محدودهٔ چند میکروثانیه دارند. اگر متدی را بارها در یک حلقه فراخوانی میکنید، میتوانید با فراخوانی یک delegate که بهصورت پویا نمونهسازی شده و متد پویا را هدف میگیرد، سربار هر فراخوانی را به محدودهٔ نانوثانیه منتقل کنید. در مثال زیر متد Trim از string را یک میلیون بار بهصورت پویا و بدون سربار قابلتوجه فراخوانی میکنیم:
MethodInfo trimMethod = typeof (string).GetMethod ("Trim", new Type[0]);
var trim = (StringToString) Delegate.CreateDelegate
(typeof (StringToString), trimMethod);
for (int i = 0; i < 1000000; i++)
trim ("test");
delegate string StringToString (string s);
این روش سریعتر است، چون Late Binding پرهزینه فقط یکبار انجام میشود.
دسترسی به اعضای Nonpublic
همهٔ متدهایی که روی typeها برای کاوش فراداده استفاده میشوند—مانند GetProperty و GetField—overloadهایی دارند که enumِ BindingFlags را میپذیرند. این enum مانند یک فیلتر فراداده عمل میکند و اجازه میدهد معیار انتخاب پیشفرض را تغییر دهید. رایجترین کاربرد آن بازیابی اعضای nonpublic است؛ این کار فقط در برنامههای Desktop کار میکند.
برای نمونه، کلاس زیر را در نظر بگیرید:
class Walnut
{
private bool cracked;
public void Crack() { cracked = true; }
public override string ToString() { return cracked.ToString(); }
}
میتوانیم گردو را دوباره «نشکسته» کنیم:
Type t = typeof (Walnut);
Walnut w = new Walnut();
w.Crack();
FieldInfo f = t.GetField ("cracked", BindingFlags.NonPublic |
BindingFlags.Instance);
f.SetValue (w, false);
Console.WriteLine (w); // False
استفاده از Reflection برای دسترسی به اعضای nonpublic قدرتمند است، اما خطرناک نیز هست، زیرا میتواند encapsulation را دور بزند و وابستگی غیرقابلمدیریتی به پیادهسازی داخلی یک نوع ایجاد کند.
Enumِ BindingFlags
BindingFlags برای ترکیب bitwise طراحی شده است. برای اینکه اصلاً matchای بهدست آید باید با یکی از چهار ترکیب زیر شروع کنید:
BindingFlags.Public | BindingFlags.Instance
BindingFlags.Public | BindingFlags.Static
BindingFlags.NonPublic | BindingFlags.Instance
BindingFlags.NonPublic | BindingFlags.Static
NonPublic شامل internal، protected، protected internal و private است.
مثال زیر همهٔ اعضای public static از نوع object را بازیابی میکند:
BindingFlags publicStatic = BindingFlags.Public | BindingFlags.Static;
MemberInfo[] members = typeof (object).GetMembers (publicStatic);
مثال بعد همهٔ اعضای nonpublic از نوع object، چه static و چه instance، را برمیگرداند:
BindingFlags nonPublicBinding =
BindingFlags.NonPublic | BindingFlags.Static | BindingFlags.Instance;
MemberInfo[] members = typeof (object).GetMembers (nonPublicBinding);
پرچم DeclaredOnly توابع ارثرسیده از نوعهای پایه را کنار میگذارد، مگر آنکه override شده باشند.
متدهای Generic
نمیتوانید متدهای Generic را مستقیماً invoke کنید؛ کد زیر exception ایجاد میکند:
class Program
{
public static T Echo<T> (T x) { return x; }
static void Main()
{
MethodInfo echo = typeof (Program).GetMethod ("Echo");
Console.WriteLine (echo.IsGenericMethodDefinition); // True
echo.Invoke (null, new object[] { 123 } ); // Exception
}
}
یک گام اضافه لازم است: MakeGenericMethod را روی MethodInfo فراخوانی کنید و آرگومانهای نوع Generic concrete را مشخص کنید. این کار یک MethodInfo دیگر برمیگرداند که میتوانید آن را invoke کنید:
MethodInfo echo = typeof (Program).GetMethod ("Echo");
MethodInfo intEcho = echo.MakeGenericMethod (typeof (int));
Console.WriteLine (intEcho.IsGenericMethodDefinition); // False
Console.WriteLine (intEcho.Invoke (null, new object[] { 3 } )); // 3
فراخوانی بینام اعضای یک Interface Generic
Reflection زمانی مفید است که لازم باشد عضوی از یک interface Generic را فراخوانی کنید و پارامترهای نوع را تا زمان اجرا نمیدانید. از نظر نظری اگر typeها کاملاً طراحی شده باشند، چنین نیازی بهندرت پیش میآید؛ البته typeها همیشه کاملاً طراحی نشدهاند.
فرض کنید میخواهیم نسخهٔ قدرتمندتری از ToString بنویسیم که بتواند نتیجهٔ queryهای LINQ را باز کند. میتوانیم از اینجا شروع کنیم:
public static string ToStringEx <T> (IEnumerable<T> sequence)
{
...
}
این همین حالا هم محدودکننده است. اگر sequence شامل collectionهای تودرتو باشد که بخواهیم آنها را نیز enumerate کنیم چه؟ باید برای پوشش این حالت متد را overload کنیم:
public static string ToStringEx <T> (IEnumerable<IEnumerable<T>> sequence)
اگر sequence شامل groupingها یا projectionهای sequenceهای تودرتو باشد چه؟ راهحل static مبتنی بر overload متد غیرعملی میشود؛ به روشی نیاز داریم که برای یک object graph دلخواه مقیاسپذیر باشد، مانند این:
public static string ToStringEx (object value)
{
if (value == null) return "<null>";
StringBuilder sb = new StringBuilder();
if (value is List<>) // Error
sb.Append ("List of " + ((List<>) value).Count + " items"); // Error
if (value is IGrouping<,>) // Error
sb.Append ("Group with key=" + ((IGrouping<,>) value).Key); // Error
// Enumerate collection elements if this is a collection,
// recursively calling ToStringEx()
// ...
return sb.ToString();
}
متأسفانه این کد کامپایل نمیشود: نمیتوانید اعضای یک نوع Generic نامقید مانند List<> یا IGrouping<,> را invoke کنید. در مورد List<> میتوانیم با استفاده از interface غیرGeneric یعنی IList مسئله را حل کنیم:
if (value is IList)
sb.AppendLine ("A list with " + ((IList) value).Count + " items");
راهحل برای IGrouping<,> به این سادگی نیست. تعریف interface چنین است:
public interface IGrouping <TKey,TElement> : IEnumerable <TElement>,
IEnumerable
{
TKey Key { get; }
}
هیچ نوع non-genericای وجود ندارد که بتوانیم برای دسترسی به propertyِ Key از آن استفاده کنیم؛ بنابراین اینجا باید Reflection به کار ببریم. راهحل این نیست که اعضای یک نوع Generic نامقید را invoke کنیم—که غیرممکن است—بلکه باید اعضای یک نوع Generic بسته را invoke کنیم که آرگومانهای نوع آن را در زمان اجرا تعیین میکنیم.
گام نخست این است که بفهمیم آیا value، IGrouping<,> را پیادهسازی میکند یا نه و اگر بله، interface Generic بستهٔ آن را بهدست آوریم. سادهترین راه اجرای یک query از LINQ است. سپس propertyِ Key را بازیابی و invoke میکنیم:
public static string ToStringEx (object value)
{
if (value == null) return "<null>";
if (value.GetType().IsPrimitive) return value.ToString();
StringBuilder sb = new StringBuilder();
if (value is IList)
sb.Append ("List of " + ((IList)value).Count + " items: ");
Type closedIGrouping = value.GetType().GetInterfaces()
.Where (t => t.IsGenericType &&
t.GetGenericTypeDefinition() == typeof (IGrouping<,>))
.FirstOrDefault();
if (closedIGrouping != null) // Call the Key property on IGrouping<,>
{
PropertyInfo pi = closedIGrouping.GetProperty ("Key");
object key = pi.GetValue (value, null);
sb.Append ("Group with key=" + key + ": ");
}
if (value is IEnumerable)
foreach (object element in ((IEnumerable)value))
sb.Append (ToStringEx (element) + " ");
if (sb.Length == 0) sb.Append (value.ToString());
return "\r\n" + sb.ToString();
}
این رویکرد مقاوم است: چه IGrouping<,> بهصورت implicit و چه explicit پیادهسازی شده باشد کار میکند. مثال زیر متد را نشان میدهد:
Console.WriteLine (ToStringEx (new List<int> { 5, 6, 7 } ));
Console.WriteLine (ToStringEx ("xyyzzz".GroupBy (c => c) ));
List of 3 items: 5 6 7
Group with key=x: x
Group with key=y: y y
Group with key=z: z z z
فراخوانی اعضای Static Virtual/Abstract در Interface
از .NET 7 و C# 11، interfaceها میتوانند اعضای static virtual و abstract تعریف کنند؛ بخش «Static virtual/abstract interface members» در صفحهٔ ۱۵۳ را ببینید. یک مثال، interfaceِ IParsable<TSelf> در .NET است:
public interface IParsable<TSelf> where TSelf : IParsable<TSelf>
{
static abstract TSelf Parse (string s, IFormatProvider provider);
...
}
با یک type parameter مقید، اعضای static abstract در interface را میتوان بهصورت polymorphic فراخوانی کرد:
T ParseAny<T> (string s) where T : IParsable<T> => T.Parse (s, null);
برای فراخوانی یک عضو static abstract از interface از طریق Reflection، باید یک MethodInfo را از نوع concreteای که interface را پیادهسازی میکند بگیرید—نه از خود interface. راهحل بدیهی این است که عضو concrete را بر اساس signature بازیابی کنیم:
MethodInfo GetParseMethod (Type concreteType) =>
concreteType.GetMethod ("Parse",
new[] { typeof (string), typeof (IFormatProvider) });
اما اگر عضو بهصورت explicit پیادهسازی شده باشد، این راه شکست میخورد. برای حل عمومی مسئله، ابتدا تابعی مینویسیم که MethodInfo نوع concrete را که یک متد مشخص interface را پیادهسازی میکند بازیابی کند:
MethodInfo GetImplementedInterfaceMethod (Type concreteType,
Type interfaceType, string methodName, Type[] paramTypes)
{
var map = concreteType.GetInterfaceMap (interfaceType);
return map.InterfaceMethods
.Zip (map.TargetMethods)
.Single (m => m.First.Name == methodName &&
m.First.GetParameters().Select (p => p.ParameterType)
.SequenceEqual (paramTypes))
.Second;
}
کلید کار، فراخوانی GetInterfaceMap است. این متد struct زیر را برمیگرداند:
public struct InterfaceMapping
{
public MethodInfo[] InterfaceMethods; // These arrays each
public MethodInfo[] TargetMethods; // have the same length.
...
}
این struct نشان میدهد اعضای interface پیادهسازیشده—InterfaceMethods—چگونه به اعضای نوع concrete—TargetMethods—نگاشت میشوند.
سپس از متد Zip در LINQ استفاده کردیم تا عناصر دو آرایه همردیف شوند و بتوانیم بهسادگی متد مقصد متناظر با متد interface با signature موردنظر را بهدست آوریم.
اکنون میتوانیم از این قابلیت برای نوشتن متد ParseAny مبتنی بر Reflection استفاده کنیم:
object ParseAny (Type type, string value)
{
MethodInfo parseMethod = GetImplementedInterfaceMethod (type,
type.GetInterface ("IParsable`1"),
"Parse",
new[] { typeof (string), typeof (IFormatProvider) });
return parseMethod.Invoke (null, new[] { value, null });
}
Console.WriteLine (ParseAny (typeof (float), ".2")); // 0.2
هنگام فراخوانی GetImplementedInterfaceMethod باید نوع interface بسته را فراهم میکردیم که با فراخوانی GetInterface("IParsable`1") روی نوع concrete بهدست آوردیم. چون در این سناریو interface موردنظر را در زمان کامپایل میدانستیم، میتوانستیم بهجای آن از عبارت زیر استفاده کنیم:
typeof (IParsable<>).MakeGenericType (type)
بازتاب اسمبلیها (Reflecting Assemblies)
میتوانید با فراخوانی GetType یا GetTypes روی یک شیء Assembly، یک اسمبلی را بهصورت پویا Reflection کنید. مثال زیر نوعی به نام TestProgram در فضای نام Demos را از اسمبلی جاری بازیابی میکند:
Type t = Assembly.GetExecutingAssembly().GetType ("Demos.TestProgram");
همچنین میتوانید یک اسمبلی را از یک نوع موجود بهدست آورید:
typeof (Foo).Assembly.GetType ("Demos.TestProgram");
ادامهٔ بخش «بازتاب اسمبلیها» در مقالهٔ بعدی و از صفحهٔ ۸۲۸ منبع ادامه دارد.