Reflection روی Memberها در C#؛ MemberInfo، Late Binding و BindingFlags

فصل ۱۸: Reflection روی Memberها، Late Binding، BindingFlags و Assemblyها

فصل ۱۸: 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 دارد:

DeclaringType
Typeای را برمی‌گرداند که عضو را تعریف کرده است.
ReflectedType
Typeای را برمی‌گرداند که 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هایی است که در شکل ۱۸-۱ نشان داده شده‌اند.

شکل ۱۸-۱: سلسله‌مراتب MemberInfo
شکل ۱۸-۱ — نوع‌های Member (Member types)

می‌توانید یک MemberInfo را بر اساس propertyِ MemberType آن به زیرنوع مربوط cast کنید. اگر عضو را با GetMethod، GetField، GetProperty، GetEvent، GetConstructor یا GetNestedType—یا نسخه‌های جمع آن‌ها—گرفته باشید، cast لازم نیست. جدول ۱۸-۱ خلاصه می‌کند برای هر سازهٔ C# از چه متدی استفاده شود.

جدول ۱۸-۱ — بازیابی فرادادهٔ اعضا

سازهٔ C#متد مورد استفادهنام مورد استفادهنتیجه
MethodGetMethodنام متدMethodInfo
PropertyGetPropertyنام propertyPropertyInfo
IndexerGetDefaultMembersMemberInfo[] شامل PropertyInfoها در صورت کامپایل با C#
FieldGetFieldنام fieldFieldInfo
عضو EnumGetFieldنام عضوFieldInfo
EventGetEventنام eventEventInfo
ConstructorGetConstructorConstructorInfo
FinalizerGetMethod"Finalize"MethodInfo
OperatorGetMethod"op_" + operator nameMethodInfo
Nested typeGetNestedTypeنام typeType

هر زیرکلاس 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
PropertyPropertyget_XXX و set_XXX
IndexerPropertyget_Item و set_Item
EventEventadd_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");

ادامهٔ بخش «بازتاب اسمبلی‌ها» در مقالهٔ بعدی و از صفحهٔ ۸۲۸ منبع ادامه دارد.

منبع: C# 12 in a Nutshell, The Definitive Reference — Chapter 18: Reflection and Metadata — صفحات 813–827 کتاب.

امتیاز کاربران به این مقاله

☆☆☆☆☆

0 نفر امتیاز داده اند. میانگین: 0.0 از 5

 

0 نظر

نظر محترم شما در مورد مقاله های وب سایت برنامه نویسی و پایگاه داده

نظرات محترم شما در خدمات رسانی بهتر ما را یاری می نمایند. لطفا اگر مایل بودید یک نظر ما را مهمان فرمائید. آدرس ایمیل و وب سایت شما نمایش داده نخواهد شد.

0 / 500

اطلاعات تماس

  • آدرس:اصفهان-خیابان ام کلثوم غربی - بعد خیابان تخم چی - بیست متر بعد از پیتزا ننه شب - کوچه تعمیر گاه سمار زغالی - پلاک 354 - درب مشکی - طبقه هفتم
  • آدرس ایمیل:najafzade@gmail.com
  • وب سایت:http://www.a00b.com/
  • تلفن ثابت:(+98)9131253620
  • تلفن همراه:09131253620