پشتیبانی از انقیاد پویا در سی‌شارپ
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: شش دقیقه

زبان سی‌شارپ strongly typed و type safe است. کامپایلر بیشتر کد را از نظر صحت نوع (Type) بررسی میکند و در صورت بروز خطا، روند کامپایل متوقف خواهد شد. با این وجود سی‌شارپ اجازه میدهد که کدهای داینامیک نیز داشته باشیم؛ کدهایی که در زمان کامپایل برای کامپایلر ناشناس هستند و اگر خطای نوع در آنها وجود داشته باشد، در زمان اجرا مشخص شده و باعث توقف برنامه میشود. 

Type Safety

ایمنی نوع، قاعده‌ای است در زبانهای برنامه‌نویسی که اجازه نمیدهد متغیرها، مقادیری را دریافت کنند که متفاوت با نوع تعریف شده‌ی آنها باشد. اگر این بررسی وجود نداشت، در زمان اجرا مقادیر خوانده شده از حافظه باعث رفتاری غیر قابل پیش‌بینی میشد؛ مثلا در یک متغیر عددی، مقدار رشته‌ای ذخیره و در زمان اجرا با یک مقدار عددی دیگر جمع بسته و نمایش داده شود. کامپایلر همچنین بررسی اعضای اعلان نشده‌ی متغیرها را نیز انجام میدهد که در قطعه کد زیر آمده‌است:
string text = “String value”;
int textLength = text.Length;
int textMonth = text.Month; // won’t compile
با این حال ایمنی نوع در سی‌شارپ کاملا قابل اعتماد نیست و میشود به روشی آن را دور زد!  
public interface IGeometricShape
{
     double Circumference { get; }
     double Area { get; }
}
public class Square : IGeometricShape
{
     public double Side { get; set; }
     public double Circumference => 4 * Side;
     public double Area => Side * Side;
}
public class Circle : IGeometricShape
{
     public double Radius { get; set; }
     public double Circumference => 2 * Math.PI * Radius;
     public double Area => Math.PI * Radius * Radius;
}

IGeometricShape circle = new Circle { Radius = 1 };
Square square = ((Square)circle); // no compiler error
var side = square.Side;
در خط کدی که با کامنت مشخص شده، هر چند که دیده میشود نوع circle نمیتواند به نوع square تبدیل شود، اما این کد بدون خطا کامپایل و خطای InvalidCastException  در زمان اجرا رخ خواهد داد. به دلیل اینکه هر دو نوع circle و square از نوع پایه IGeometricShape هستند، کامپایلر خطایی نخواهد گرفت؛ اما در زمان اجرا و زمانیکه برنامه میخواهد اجزاء circle را به square تبدیل کند، مشخص میشود که امکان تبدیل کامل circle به square نیست و خطا رخ خواهد داد.

Dynamic Binding

توسط انقیاد پویا در سی‌شارپ، کامپایلر بررسی نوع را در زمان کامپایل انجام نخواهد داد. کامپایلر فرض را بر این میگیرد که کد معتبر است و تمام متغیرها به درستی قابل دسترسی هستند. بررسی‌ها در زمان اجرا خواهند بود و زمانی خطا رخ خواهد داد که مثلا دسترسی به یک عضو از یک متغیر امکانپذیر نباشد؛ به این دلیل که آن عضو برای آن نوع وجود ندارد. 
توسط کلمه کلیدی dynamic میتوان متغیرهایی را تعریف کرد که در زمان کامپایل از نظر نوع بررسی نشوند؛ مانند مثال زیر.
dynamic text = “String value”;
int textLength = text.Length;
int textMonth = text.Month; // throws exception at runtime
واضح است که مثال بالا بی‌فایده است؛  اولا خطا در زمان کامپایل مشخص نمیشود و ثانیا مدیریت خطا در زمان اجرا بر کارآیی برنامه تاثیر خواهد داشت. روش دیگر استفاده از dynamic که کارآیی پایینی دارد در مثال زیر آمده.  
public dynamic GetAnonymousType()
{
  return new
    {
        Name = “John”,
        Surname = “Doe”,
        Age = 42
    };
}

dynamic value = GetAnonymousType();
Console.WriteLine($”{value.Name} {value.Surname}, {value.Age}”);
در مثال بالا نوع بازگشتی متد و متغیری که برای نگهداری نوع بازگشتی تعریف شده از نوع dynamic هستند. هر چند که در زمان کامپایل میشود هر مقداری و نوعی را از متد بازگشت داد، اما مانند مثال قبل، تا زمان اجرا، صحت اینکه آیا واقعا چنین نوعی جهت بازگشت وجود دارد یا نه و همچنین اساسا نوع بازگشت داده شده قابل استفاده و تبدیل هست یا نه، بررسی نخواهد شد. مضاف بر این مشکلات، IntelliSense نخواهیم داشت و اگر بخواهیم از یک اسمبلی دیگر به متد بالا دسترسی پیدا کنیم با خطای RuntimeBinderException مواجه خواهیم شد؛ علت این است که  نوع‌های anonymous به صورت internal اعلان می‌شوند. اما میشود استفاده‌های بهتری از نوع dynamic داشت؛ برای مثال زمان استفاده از کتابخانه‌ی JSON.NET که نمونه‌ای از آن در زیر آمده.
string json = @"
{
     ""name"": ""John"",
     ""surname"": ""Doe"",
     ""age"": 42
}";

dynamic value = JObject.Parse(json);
Console.WriteLine($"{ value.name} { value.surname}, { value.age}");
مانند نوع anonymous در مثال قبل، متد Parse میتواند مقادیر را به صورت پویا برگشت دهد و میتوان از این مقادیر مانند خصوصیات شیء ایجاد شده، از JSON استفاده کرد، بدون آنکه کامپایلر از وجود آنها اطلاعی داشته باشد. به این ترتیب در زمان اجرا میشود اشیاء JSON را به برنامه داد و از مقادیر آن مانند دسترسی به یک property استفاده کرد؛ کاری که نمیشود با نوعهای anonymous که در مثال بالاتر آورده شد انجام داد. برای حل این مسئله میتوان از دو شیء کمکی در کتابخانه NET Framework. استفاده کرد.

ExpandoObject

بین این دو شیء، ExpandoObject ساده‌تر است. به همراه کلمه کلیدی dynamic، این شیء اجازه میدهد که به نوع ساخته شده از آن در زمان اجرا و به صورت پویا، عضوی اضافه یا حذف کنیم؛ این اعضا میتوانند متد هم باشند.
dynamic person = new ExpandoObject();
person.Name = "John";
person.Surname = "Doe";
person.Age = 42;
person.ToString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person. Age}”);

Console.WriteLine($"{ person.Name}{ person.Surname}, { person.Age}");

  برای اینکه ببینیم در زمان اجرا چه اعضایی به این شی اضافه شده، می‌توان نمونه ساخته شده از آن را به نوع <IDictionary<string, object تبدیل و در یک حلقه به آنها دسترسی پیدا کرد. از همین طریق هم میشود عضوی را حذف کرد.

var dictionary = (IDictionary<string, object>)person;
foreach (var member in dictionary)
{
     Console.WriteLine($”{member.Key} = {member.Value}”);
}
dictionary.Remove(“ToString”);

DynamicObject

از آنجایی که ExpandoObject برای سناریو‌های ساده کاربرد دارد و کنترل کمتری بر روی اعضا و نمونه‌های ایجاد شده‌ی توسط آن داریم، می‌توان از شیء DynamicObject استفاده کرد؛ البته نیاز به کدنویسی بیشتری دارد. پیاده‌سازی اعضا برای شیء DynamicObject در یک کلاس صورت میگیرد که در زیر آورده شده‌است:

class MyDynamicObject : DynamicObject
{
       private readonly Dictionary<string, object> members = new Dictionary<string, object>();

       public override bool TryGetMember(GetMemberBinder binder, out object result)
       {
              if (members.ContainsKey(binder.Name))
              {
                  result = members[binder.Name];
                  return true;
              }
              else
              {
                  result = null;
                  return false;
             }
       }

      public override bool TrySetMember(SetMemberBinder binder, object value)
      {
               members[binder.Name] = value;
              return true;
      }

      public bool RemoveMember(string name)
      {
            return members.Remove(name);
      }

}

dynamic person = new MyDynamicObject();
person.Name = “John”;
person.Surname = “Doe”;
person.Age = 42;
person.AsString = (Func<string>)(() => $”{person.Name} {person.Surname}, {person.
Age}”);
یک نکته در قطعه کد بالا وجود دارد. در شیء ExpandoObject، متد ToString را اضافه کردیم، اما برای شیء DynamicObject نام آن را تغییر داده و مثلا AsString گذاشتیم. اگر از نام ToString استفاده میکردیم در زمان فراخوانی، متد پیش‌فرض کلاس DynamicObject فراخوانی میشد. DynamicObject زمانی یک عضو پویا را فراخوانی میکند که آن عضو جدید از قبل وجود نداشته باشد. از آنجا که خود کلاس، متد ToString را دارد متد TryGetMember برای فراخوانی کردن آن اجرا نخواهد شد.
  • #
    ‫۶ سال و ۲ ماه قبل، شنبه ۲۳ تیر ۱۳۹۷، ساعت ۰۱:۵۲
    یک مثال از ExpandoObject
    public static class ExpandoXml
    {
        public static dynamic AsExpando(this XDocument document)
        {
            return CreateExpando(document.Root);
        }
    
        private static dynamic CreateExpando(XElement element)
        {
            var result = new ExpandoObject() as IDictionary<string, object>;
    
            if (element.Elements().Any(e => e.HasElements))
            {
                var list = new List<ExpandoObject>();
                result.Add(element.Name.ToString(), list);
                foreach (var childElement in element.Elements())
                {
                    list.Add(CreateExpando(childElement));
                }
            }
            else
            {
                foreach (var leafElement in element.Elements())
                {
                    result.Add(leafElement.Name.ToString(), leafElement.Value);
                }
            }
    
            return result;
        }
    }

    طریقه استفاده
    class Program
    {
        static void Main(string[] args)
        {
            var doc1 = XDocument.Load("Employees.xml");
            foreach (var element in doc1.Element("Employees").Elements("Employee"))
            {
                Console.WriteLine(element.Element("FirstName").Value);
            }
    
            var doc2 = XDocument.Load("Employees.xml").AsExpando();
            foreach (var employee in doc2.Employees)
            {
                Console.WriteLine(employee.FirstName);
            }
        }
    }
    همانطور که در مقاله ذکر شد، برای حالت پیشرفته‌تر آن می‌توان با استفاده از DynamicObject یک DynamicXml را تهیه کرد.