سير تكامل و تاريخچه #c
شناسه پست: 513
بازدید: 1303

سیر تکامل c#

چکیده: #C از زمان ورژن اولیه خود در سال 2002 تا بحال شامل ویژگی های جدیدی بوده است. در این مقاله ما می خواهیم به هریک از ورژن های این زبان  و ویژگی های هر ورژن بپردازیم.

اگرچه از زمان ورود این زبان تا اکنون 16 سال می گذرد ولی هنوز نمی توان از آن به عنوان یک زبان قدیمی یاد کرد. دلیلش هم این است که به طور منظم به روز شده و ویژگی های جدیدی در هر ورژن آن آمده است.

هر دو یا سه سال، یک ورژن جدید با ویژگی های اضافی منتشر شده است. از زمان انتشار c# 7.0 در ابتدای سال 2017، ورژن های جزئی دیگری ازاین ورژن منتشر شده است.

در طول زمان یک سال، سه ورژن جزئی جدید از ورژن 7 منتشر شده است (c# 7.1 , 7.2 , 7.3).

اگر ما نگاهی به کدهای نوشته شده در ورژن اول c# در سال 2002 بیندازیم تفاوت بسیار زیادی با کدهای نوشته شده امروزی دارد.

در طول زمان توسعه این زبان، کلاس های جدیدی به .Net Framework اضافه شده اند که مزیت هایی را برای این زبان به ارمغان آورده است که همه اینها باعث می شوند که این زبان رساتر و مفیدتر باشد.

می خواهیم نگاهی به تاریخچه این زبان و تمام ورژن های آن داشته باشیم.

برای هرورژن ما بر روی مهم ترین تغییرات آن تمرکز می کنیم و کدهای قبل و بعد از هر ورژن را با یکدیگر مقایسه خواهیم کرد. در طول زمان هرچه بیشتر به سمت ورژن اولیه این زبان می رویم خواهیم دید که سازماندهی کردن کدها سخت تر خواهد بود.

ورژن های #C و سیر تکاملی آن

C# 7.0

در حال حاضر که این مقاله را می نویسیم، آخرین ورژن این زبان 7 می باشد که در سال 2017 منتشر شده است. این ورژن هنوز تقریبا جدید است و اغلب ویژگی های جدید آن استفاده نمی شود.

اکثر ما هنوز این زبان را بدون در نظر گرفتن مزیت هایی که  دارد استفاده می کنیم.

 

نقطه قوت اصلی ورژن 7، تطبیق الگو (pattern matching) می باشد که از چک کردن نوع ها (types) در بدنه switch پشتیبانی می کند

switch (weapon)
{
    case Sword sword when sword.Durability > 0:
        enemy.Health -= sword.Damage;
        sword.Durability--;
        break;
    case Bow bow when bow.Arrows > 0:
        enemy.Health -= bow.Damage;
        bow.Arrows--;
        break;
}

در قطعه کد بالا، چند ویژگی جدید وجود دارد:

  • در قسمت case، نوع مغیر weapon بررسی می شود.
  • در همان قسمت case، ما یک متغیر از نوع مورد نظرمان تعریف کرده ایم که این متغیر در همان بلاک مبوط به خود آن case استفاده می شود.
  • در آخرین قسمت از case، بعد از کلمه کلیدی When، ما یک شرط اضافی را برای محدودیت بیشتر مشخص می نماییم.

علاوه بر این، عمل کننده is  نیز از pattern matching پشتیبانی می کند. بنابراین از آن هم می توان به عنوان یک متغیر شبیه به عملگر case استفاده نمود.

if (weapon is Sword sword)
{
    // code with new sword variable in scope
}

در ورژن های قبل تر، بدون این ویژگی های جدید، بلاک کد طولانی تر بود:

if (weapon is Sword)
{
    var sword = weapon as Sword;
    if (sword.Durability > 0)
    {
        enemy.Health -= sword.Damage;
        sword.Durability--;
    }
}
else if (weapon is Bow)
{
    var bow = weapon as Bow;
    if (bow.Arrows > 0)
    {
        enemy.Health -= bow.Damage;
        bow.Arrows--;
    }
}

همچنین چند ویژگی جزئی دیگر در c# 7.0 اضافه شده اند که ما اینجا به دو نمونه دیگر از آن اشاره می کنیم:

  1. Out variables که اجازه می دهد متغیرها در همان جایی که از آنها به عنوان آرگومانهای از نوع out در متد استفاده می شوند تعریف شوند:
if (dictionary.TryGetValue(key, out var value))
{
    return value;
}
else
{
    return null;
}

قبل از این ویژگی، ما مجبور بودیم متغیر را از قبل تعریف نماییم:

string value;
if (dictionary.TryGetValue(key, out value))
{
    return value;
}
else
{
    return null;
}
  1. Tuples برای گروه بندی کردن چند متغیر در قالب یک مقدار استفاده می شود. به عنوان مثال برگرداندن مقدارهایی از یک متد:
public (int weight, int count) Stocktake(IEnumerable<IWeapon> weapons)
{
    return (weapons.Sum(weapon => weapon.Weight), weapons.Count());
}

بدون آنها، ما مجبور بودیم یک نوع جدیدی را برای انجام این کار تعریف نماییم حتی اگر فقط یکبار به آن نیاز داشتیم:

public Inventory Stocktake(IEnumerable<IWeapon> weapons)
{
    return new Inventory
    {
        Weight = weapons.Sum(weapon => weapon.Weight),
        Count = weapons.Count()
    };
}

C# 6.0

این نسخه از سی شارپ در سال 2015 انتشار یافت. قسمت مهم و قابل توجه انتشار این نسخه بر روی خدمان کامپایلری اشاره دارد که به صورت قابل توجهی در ویژوال استودیو و دیگر ادیتورها مورد استفاده قرار می گیرد. در این نسخه، ویژوال استودیو 2015 و 2017 از ویژگی های highlighting، code navigation، refactoring و دیگر ویژگی های ویرایشی استفاده می نمایند.

در این نسخه، تغییرات بسیار کمی صورت گرفت. این تغییرات بیشتر مربوط به ساختار نحوی زبان می باشد که بیشتر آنها امروزه هنوز بیسیار زیاد استفاده می شوند.

– Dictionary initializer برای set کردن مقدار اولیه Dictionary استفاده می شود.

var dictionary = new Dictionary<int, string>
{
    [1] = "One",
    [2] = "Two",
    [3] = "Three",
    [4] = "Four",
    [5] = "Five"
};

بدون آن، کالکشن باید به صورت زیر استفاده میشد:

var dictionary = new Dictionary<int, string>()
{
    { 1, "One" },
    { 2, "Two" },
    { 3, "Three" },
    { 4, "Four" },
    { 5, "Five" }
};

– اپراتور nameof نام یک symbol را برمیگرداند:

public void Method(string input)
{
    if (input == null)
    {
        throw new ArgumentNullException(nameof(input));
    }
    // method implementation
}

این یک راه کار عالیست برای اینکه مانند کد زیر از رشته ها استفاده نکنیم تا اگر زمانی ما نام symbol را تغییر دادیم به راحتی بتوان آن را sync نمود:

public void Method(string input)
{
    if (input == null)
    {
        throw new ArgumentNullException("input");
    }
    // method implementation
}

– Null conditional operator شرط چک کردن Null را مختصر می نماید:

var length = input?.Length ?? 0;

بدون این ویژگی ما باید این کد را به صورت زیر بازنویسی نماییم:

int length;
if (input == null)
{
    length =0;
}
else
{
    length = input.Length;
}

– Static import به ما اجازه می دهد تا به صورت مستقیم از متدهای موجود در یک کلاس استفاده نماییم:

using static System.Math;
var sqrt = Sqrt(input);

قبل از آن باید به صورت زیر کار می کردیم:

var sqrt = Math.Sqrt(input);

– String interpolation قالب بندی رشته را ساده تر می کند:

var output = $"Length of '{input}' is {input.Length} characters.";

این ویژگی نه تنها باعث می شود که String.Format استفاده نشود بلکه باعث خوانایی بیشتر کد نیز می گردد.

قبلا بدون استفاده از این ویژگی، کد باید به صورت زیر نوشته می شد:

var output = String.Format("Length of '{0}' is {1} characters.", input, input.Length);

در این کد، به دلیل استفاده آرگومانها خارج از کد، احتمال اشتباه در جابه جایی آرگومانها زیاد بود.

C# 5.0

مایکروسافت این نسخه از زبان را در سال 2012 منتشر نمود و یک ویژگی مهم به نام async/await را برای برنامه های غیرهمزمانی در آن قرار داد.

این ویژگی، برنامه نویسی غیرهمزمانی را برای برنامه نویسان آسان تر می سازد.

با این syntax جدید، کد asynchronous خیلی شبیه به کدهای synchronous می باشد:

public async Task<int> CountWords(string filename)
{
    using (var reader = new StreamReader(filename))
    {
        var text = await reader.ReadToEndAsync();
        return text.Split(' ').Length;
    }
}

کلمه کلیدی await، تا زمانی که عملیات خوتندن فایل تکمیل گردد، thread را برای کارهای دیگر آزاد می سازد. سپس اجرا به همان thread برمی گردد (در بیشتر اوقات).

بدون async/await، نوشتن و درک کد سخت تر می شود:

public Task<int> CountWords(string filename)
{
    var reader = new StreamReader(filename);
    return reader.ReadToEndAsync()
        .ContinueWith(task =>
        {
            reader.Close();
            return task.Result.Split(' ').Length;
        });
}

توجه داشته باشید که چطور باید ادامه کار را با متد Task.ContinueWith کامپایل نماییم.

بنابراین ما نمی توانیم از using برای بستن stream اینجا استفاده نماییم. زیرا بدون استفاده از کلمه کلیدی await، برای متوقف کردن اجرای متد، stream می تواند قبل از اینکه عملیات خواندن فایل به صورت غیرهمزمان تکمیل گردد بسته شود.

همچنین در این کد از متد ReadToEndAsync استفاده شده است که در زمان C# 5.0  منتشر شد. قبل از آن فقط یک ورژن همزمانی از این متد موجود بود. برای فراخوانی thread، کد به صورت زیر نوشته می شد:

public Task<int> CountWords(string filename)
{
    return Task.Run(() =>
    {
        using (var reader = new StreamReader(filename))
        {
            return reader.ReadToEnd().Split(' ').Length;
        }
    });
}

این کد فقط به نظر می رسد که به صورت asynchronous است در صورتی که به همان صورت synchronous در هسته خود کار می کند. برای انجام عملیات asynchronous واقعی، Api قدیمیتر و  پایه ای تر باید مورد استفاده قرار گیرد:

public Task<int> CountWords(string filename)
{
    var fileInfo = new FileInfo(filename);
    var stream = new FileStream(filename, FileMode.Open);
    var buffer = new byte[fileInfo.Length];
    return Task.Factory.FromAsync(stream.BeginRead, stream.EndRead, buffer, 0, buffer.Length, null)
        .ContinueWith(_ =>
        {
            stream.Close();
            return Encoding.UTF8.GetString(buffer).Split(' ').Length;
        });
}

کد بالا کل فایل را در یک زمان می خواند که برای خواندن فایل های بزرگ مناسب نیست و ما هنوز از  متد FromAsync helper که در  .NET framework 4 با کلاس  Task خودش معرفی شد،  استفاده می کنیم.

C# 4.0

این نسخه از #c در سال 2010 منتشر شد.

این نسخه از این زبان بر روی dynamic binding متمرکز شده است که کار برنامه نویسی داینامیک را آسان تر می سازد.

یک ویژگی مهم که در این نسخه منتشر شده بود و خیلی حائز اهمیت است و عضوی جداناشدنی از زبان به شمار می آید پارامترهای هم نام و اختیاری (optional and named parameters) می باشد. این یک جایگزین مناسب برای نوشتن متدهای زیادی می باشد:

public void Write(string text, bool centered = false, bool bold = false)
{
    // output text
}

این متد می تواند به طرق زیر و با پارامترهای اختیاری فراخوانی شود.

Write("Sample text");
Write("Sample text", true);
Write("Sample text", false, true);
Write("Sample text", bold: true);

ما قبل از C# 4.0 مجبور بودیم سه overload متفاوت بنویسیم:

public void Write(string text, bool centered, bool bold)
{
    // output text
}
public void Write(string text, bool centered)
{
    Write(text, centered, false);
}
public void Write(string text)
{
    Write(text, false);
}

بدون ویژگی named parameters ما مجبور بودیم  یک متد اضافی با پارامترهای متفاوت بنویسیم. برای مثال اگه متدی با دو پارامتر text و bold نیاز داشتیم باید متدی فقط مخصوص آن می نوشتیم:

public void WriteBold(string text, bool bold)
{
    Write(text, false, bold);
}

C# 3.0

این نسخه از زبان از سال 2007، نقطه عطف بزرگی در توسعه زبان بود. در این نسخه، ویژگی هایی حول محور تکنولوژی LINQ (Language INtegrated Query) به وجود آمد:

  • Extension methods: عملکردهای جدیدی است که به یک نوع داده ای اضافه می گردد.
  • Lambda expressions: سینتکس کوتاهتری را برای متدهای anonymous ارائه می دهد.
  • Anonymous types: اینها نوع هایی هستند که نیاز نیست از قبل تعریف شوند.

همه اینها در LINQ به کار می روند که ما تا به امروز از آنها استفاده می کنیم.

var minors = persons.Where(person => person.Age < 18)
    .Select(person => new { person.Name, person.Age })
    .ToList();

قبل از این نسخه، همچین راهی برای نوشتن اینگونه کدها نبود. قبل از آن، این کد باید به صورت زیر نوشته می شد:

List<NameAndAge> minors = new List<NameAndAge>();
foreach(Person person in persons)
{
    if (person.Age > 18)
    {
        minors.Add(new NameAndAge(person.Name, person.Age));
    }
}

همانطور که میبینید نوع داده ای را در خط اول از این کد ما تعریف نموده ایم. کلمه کلیدی var  را بیشتر اوقات همه ما استفاده می نماییم که در  C# 3.0 معرفی شده بود. اگرچه این کد نسبت به ورژن LINQ خودش طولانی تر به نظر نمی رسد، اما این را در نظر داشته باشید که نوع NameAndAge باید حتما تعریف گردد:

public class NameAndAge
{
    private string name;
    public string Name
    {
        get
        {
            return name;
        }
        set
        {
            name = value;
        }
    }
    private int age;
    public int Age
    {
        get
        {
            return age;
        }
        set
        {
            age = value;
        }
    }
    public NameAndAge(string name, int age)
    {
        Name = name;
        Age = age;
    }
}

C# 2.0

ما راهمان را در سال 2005 هموار ساختیم زمانی که نسخه 2 از این زبان منتشر شد. خیلی ها این ورژن را اولین ورژن بالغ در این زبان می دانستند و عقیده داشتند که به اندازه کافی در پروژه های واقعی کافی می باشد. این نسخه خیلی از ویژگی ها را که امروزه ما بدون آنها نمی توانیم کدنویسی کنیم معرفی نمود. اما مهمترین آنها قطعا پشتیبانی از generics می باشد.

هیچ کدام از ما #C  را نمیتوانیم بدون Generic ها تصور نماییم. خیلی از کالکشن هایی که ما هنوز استفاده می نماییم generic می باشد:

List<int> numbers = new List<int>();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += numbers[i];
}

بدون generic ها کالکشن های strongly typed در .NET framework وجود نداشت. به جای کد بالا ما باید از کد زیر استفاده می کردیم:

ArrayList numbers = new ArrayList();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += (int)numbers[i];
}

اگرچه این کد خیلی شبیه به نظر میرسه اما یک تفاوت مهمی وجود دارد: این کد از لحاظ نوع داده ای امن نیست. من به راحتی می توانم هر مقداری از نوع های داده ای دیگر را به غیر از int نیز در کالکشن ذخیره کنم. توجه نمایید که چطور من مقدار را قبل از اینکه از آن استفاده نمایم cast کرده ام. من باید این کار را انجام دهد چون آن نوعی از object در کالکشن می باشد.

البته چنین کدی می تواند خیلی خطاپذیر باشد. خوشبختانه راه دیگری نیز وجود داشت اگر میخواستم نوع امن تری داشته باشم. من میتوانستم کالکشن از نوع داده ای خودم را داشته باشم:

public class IntList : CollectionBase
{
    public int this[int index]
    {
        get
        {
            return (int)List[index];
        }
        set
        {
            List[index] = value;
        }
    }
    public int Add(int value)
    {
        return List.Add(value);
    }
    public int IndexOf(int value)
    {
        return List.IndexOf(value);
    }
    public void Insert(int index, int value)
    {
        List.Insert(index, value);
    }
    public void Remove(int value)
    {
        List.Remove(value);
    }
    public bool Contains(int value)
    {
        return List.Contains(value);
    }
    protected override void OnValidate(Object value)
    {
        if (value.GetType() != typeof(System.Int32))
        {
            throw new ArgumentException("Value must be of type Int32.", "value");
        }
    }
}

کدی که از این کلاس استفاده می نماید هنوز شبیه به کد قبلی می باشد اما حداقل مزیت آن این است که امن می باشد. مانند این است که ما از generic ها استفاده نموده ایم:

IntList numbers = new IntList();
numbers.Add(1);
numbers.Add(2);
numbers.Add(3);
int sum = 0;
for (int i = 0; i < numbers.Count; i++)
{
    sum += numbers[i];
}

اگرچه کالکشن IntList می تواند فقط برای ذخیره int باشد اما اگر من بخواهم نوع های داده ای دیگری را ذخیره نمایم باید strongly typed متفاوتی را ایجاد نمایم که این باز خود عیب بزرگی می باشد.

ویژگی های مهم دیگری نیز وجود دارند که ما بدون آنها امروزه نمیتوانیم کدنویسی نماییم:

  • Nullable value types,
  • Iterators,
  • Anonymous methods

نتیجه گیری

#C زبان اصلی توسعه دهندگان دات نت از ورژن 1 خود بوده است. اما آن رفته رفته در طول زمان با پیدایش ویژگی های که ورژن به ورژن آمد توسعه یافت و آن با رویکردهای جدید در دنیای برنامه نویسی به روز ماند طوری که هنوز یک جایگزین خوب برای زبانهای جدیدتر است که در حال حاضر موجود می باشند.

همچنین هرچند هر از گاهی، آن یک رویکردی جدید را اتخاذ میکند مانند رویکرد استفاده از کلمات کلیدی async و await. با پشتیبانی از انواع nullable reference و خیلی دیگر از ویژگی هایی که در C# 8.0 خواهد آمد جای هیچ گونه نگرانی جهت اینکه تحول این زبان متوقف خواهد شد وجود ندارد.

نویسنده

امید عباسی
من امید عباسی هستم. سالهاست که در زمینه برنامه نویسی با تکنولوژی دات نت فعالیت میکنم و عاشق این هستم که تجربیات و دانش خودم را در این زمینه با دیگران به اشتراک بزارم. خیلی دوست دارم که نظر و انتقاد خودتون رو در مورد این نوشته برای من بنویسید تا بتونم در آینده، مطالب بهتر و ارزشمندتری را برای شما فراهم کنم. در صورت داشتن هرگونه سوال هم در قسمت دیدگاه ها میتونید با بنده در ارتباط باشید