نظرات مطالب
تنظیمات مورد نیاز جهت شروع به کار با C# 9.0
یک نکته‌ی تکمیلی: شماره نگارش زبان پیش‌فرض نگارش‌های مختلف NET.
Target framework       version         C# language version default
.NET                   5.x             C# 9.0
.NET Core              3.x             C# 8.0
.NET Core              2.x             C# 7.3
.NET Standard          2.1             C# 8.0
.NET Standard          2.0             C# 7.3
.NET Standard          1.x             C# 7.3
.NET Framework         all             C# 7.3
اشتراک‌ها
NET. چه چیزی است؟ پلتفرم یا فریم ورک؟

You'll often hear the terms framework and platform used interchangeably. This can be very confusing. You'll hear that .NET Core is a platform. Then you'll read that .NET Core is cross-platform. Next, you'll learn that .NET Core is a framework. And it doesn't help that framework is part of the name of the original full .NET Framework.

In this post, I explain the difference between frameworks and platforms and explain why sometimes you'll see .NET being referred to as a framework and other times see it referred to as a platform. 

NET. چه چیزی است؟ پلتفرم یا فریم ورک؟
مطالب
نکات کار با استثناءها در دات نت
استثناء چیست؟
واژه‌ی استثناء یا exception کوتاه شده‌ی عبارت exceptional event است. در واقع exception یک نوع رویداد است که در طول اجرای برنامه رخ می‌دهد و در نتیجه، جریان عادی برنامه را مختل می‌کند. زمانیکه خطایی درون یک متد رخ دهد، یک شیء (exception object) حاوی اطلاعاتی درباره‌ی خطا ایجاد خواهد شد. به فرآیند ایجاد یک exception object و تحویل دادن آن به سیستم runtime، اصطلاحاً throwing an exception یا صدور استثناء گفته می‌شود که در ادامه به آن خواهیم پرداخت.
بعد از اینکه یک متد استثناءایی را صادر می‌کند، سیستم runtime سعی در یافتن روشی برای مدیریت آن خواهد کرد.
خوب اکنون که با مفهوم استثناء آشنا شدید اجازه دهید دو سناریو را با هم بررسی کنیم.
- سناریوی اول:
فرض کنید یک فایل XML از پیش تعریف شده (برای مثال یک لیست از محصولات) قرار است در کنار برنامه‌ی شما باشد و باید این لیست را درون برنامه‌ی خود نمایش دهید. در این حالت برای خواندن این فایل انتظار دارید که فایل وجود داشته باشد. اگر این فایل وجود نداشته باشد برنامه‌ی شما با اشکال روبرو خواهد شد.
- سناریوی دوم:
فرض کنید یک فایل XML از آخرین محصولات مشاهده شده توسط کاربران را به صورت cache در برنامه‌تان دارید. در این حالت در اولین بار اجرای برنامه توسط کاربر انتظار داریم که این فایل موجود نباشد و اگر فایل وجود نداشته باشد به سادگی می‌توانیم فایل مربوط را ایجاده کرده و محصولاتی را که توسط کاربر مشاهده شده، درون این فایل اضافه کنیم.
در واقع استثناء‌ها بستگی به حالت‌های مختلفی دارد. در مثال اول وجود فایل حیاتی است ولی در حالت دوم بدون وجود فایل نیز برنامه می‌تواند به کار خود ادامه داده و فایل مورد نظر را از نو ایجاد کند.
 استثناها مربوط به زمانی هستند که این احتمال وجود داشته باشد که برنامه طبق انتظار پیش نرود.
برای حالت اول کد زیر را داریم:
public IEnumerable<Product> GetProducts()
{
    using (var stream = File.Read(Path.Combine(Environment.CurrentDirectory, "products.xml")))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}
همانطور که عنوان شد در حالت اول انتظار داریم که فایلی بر روی دیسک موجود باشد. در نتیجه نیازی نیست هیچ استثناءایی را مدیریت کنیم (زیرا در واقع اگر فایل موجود نباشد هیچ روشی برای ایجاد آن نداریم).
در مثال دوم می‌دانیم که ممکن است فایل از قبل موجود نباشد. بنابراین می‌توانیم موجود بودن فایل را با یک شرط بررسی کنیم:
public IEnumerable<Product> GetCachedProducts()
{
    var fullPath = Path.Combine(Environment.CurrentDirectory, "ProductCache.xml");
    if (!File.Exists(fullPath))
        return new Product[0];
         
    using (var stream = File.Read(fullPath))
    {
        var serializer = new XmlSerializer();
        return (IEnumerable<Product>)serializer.Deserialize(stream);
    }
}

چه زمانی باید استثناءها را مدیریت کنیم؟
زمانیکه بتوان متدهایی که خروجی مورد انتظار را بر می‌گردانند ایجاد کرد.
اجازه دهید دوباره از مثال‌های فوق استفاده کنیم:
IEnumerable<Product> GetProducts()
همانطور که از نام آن پیداست این متد باید همیشه لیستی از محصولات را برگرداند. اگر می‌توانید اینکار را با استفاده از catch کردن یک استثنا انجام دهید در غیر اینصورت نباید درون متد اینکار را انجام داد.
IEnumerable<Product> GetCachedProducts()
در متد فوق می‌توانستیم از FileNotFoundException برای فایل موردنظر استفاده کنیم؛ اما مطمئن بودیم که فایل در ابتدا وجود ندارد.
در واقع استثنا‌ها حالت‌هایی هستند که غیرقابل پیش‌بینی هستند. این حالت‌ها می‌توانند یک خطای منطقی از طرف برنامه‌نویس و یا چیزی خارج کنترل برنامه‌نویس باشند (مانند خطاهای سیستم‌عامل، شبکه، دیسک). یعنی در بیشتر مواقع این نوع خطاها را نمی‌توان مدیریت کرد.

اگر می‌خواهید استثناء‌ها را catch کرده و آنها را لاگ کنید در بالاترین لایه اینکار را انجام دهید.


چه استثناءهایی باید مدیریت شوند و کدام‌ها خیر؟ 
مدیریت صحیح استثناء‌ها می‌تواند خیلی مفید باشد. همانطور که عنوان شد یک استثناء زمانی رخ می‌دهد که یک حالت استثناء در برنامه اتفاق بیفتد. این مورد را بخاطر داشته باشید، زیرا به شما یادآوری می‌کند که در همه جا نیازی به استفاده از try/catch نیست. در اینجا ذکر این نکته خیلی مهم است:
تنها استثناء‌هایی را catch کنید که بتوانید برای آن راه‌حلی ارائه دهید.
به عنوان مثال اگر در لایه‌ی دسترسی به داده، خطایی رخ دهد و استثناءی SqlException صادر شود، می‌توانیم آن را catch کرده و درون یک استثناء عمومی‌تر قرار دهیم:
public class UserRepository : IUserRepository
{
    public IList<User> Search(string value)
    {
        try
        {
              return CreateConnectionAndACommandAndReturnAList("WHERE value=@value", Parameter.New("value", value));
        }
        catch (SqlException err)
        {
             var msg = String.Format("Ohh no!  Failed to search after users with '{0}' as search string", value);
             throw new DataSourceException(msg, err);
        }
    }
}
همانطور که در کد فوق مشاهده می‌کنید به محض صدور استثنای SqlException آن را درون قسمت catch به صورت یک استثنای عمومی‌تر همراه با افزودن یک سری اطلاعات جدید صادر می‌کنیم. اما همانطور که عنوان شد کار لاگ کردن استثناءها را بهتر است در لایه‌های بالاتر انجام دهیم.
اگر مطمئن نیستید که تمام استثناء‌ها توسط شما مدیریت شده‌اند، می‌توانید در حالت‌های زیر، دیگر استثناءها را مدیریت کنید:
ASP.NET: می‌توانید Aplication_Error را پیاده‌سازی کنید. در اینجا فرصت خواهید داشت تا تمامی خطاهای مدیریت نشده را هندل کنید.
WinForms: استفاده از رویدادهای Application.ThreadException و AppDomain.CurrentDomain.UnhandledException 
WCF: پیاده‌سازی اینترفیس IErrorHandler 
ASMX: ایجاد یک Soap Extension سفارشی
ASP.NET WebAPI


چه زمان‌هایی باید یک استثناء صادر شود؟ 
صادر کردن یک استثناء به تنهایی کار ساده‌ایی است. تنها کافی است throw را همراه شیء exception (exception object) فراخوانی کنیم. اما سوال اینجاست که چه زمانی باید یک استثناء را صادر کنیم؟ چه داده‌هایی را باید به استثناء اضافه کنیم؟ در ادامه به این سوالات خواهیم پرداخت.
همانطور که عنوان گردید استثناءها زمانی باید صادر شوند که یک استثناء اتفاق بیفتد.

اعتبارسنجی آرگومان‌ها
ساده‌ترین مثال، آرگومان‌های مورد انتظار یک متد است:
public void PrintName(string name)
{
     Console.WriteLine(name);
}
در حالت فوق انتظار داریم مقداری برای پارامتر name تعیین شود. متد فوق با آرگومان null نیز به خوبی کار خواهد کرد؛ یعنی مقدار خروجی یک خط خالی خواهد بود. از لحاظ کدنویسی متد فوق به خوبی کار خود را انجام می‌دهد اما خروجی مورد انتظار کاربر نمایش داده نمی‌شود. در این حالت نمی‌توانیم تشخیص دهیم مشکل از کجا ناشی می‌شود.
مشکل فوق را می‌توانیم با صدور استثنای ArgumentNullException رفع کنیم:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
     
     Console.WriteLine(name);
}
خوب، name باید دارای طول ثابت و همچنین ممکن است حاوی عدد و حروف باشد:
public void PrintName(string name)
{
    if (name == null) throw new ArgumentNullException("name");
    if (name.Length < 5 || name.Length > 10) throw new ArgumentOutOfRangeException("name", name, "Name must be between 5 or 10 characters long");
    if (name.Any(x => !char.IsAlphaNumeric(x)) throw new ArgumentOutOfRangeException("name", name, "May only contain alpha numerics");
     
     Console.WriteLine(name);
}
برای حالت فوق و همچنین جلوگیری از تکرار کدهای داخل متد PrintName می‌توانید یک متد Validator برای کلاسی با نام Person ایجاد کنید.
حالت دیگر صدور استثناء، زمانی است که متدی خروجی مورد انتظارمان را نتواند تحویل دهد. یک مثال بحث‌برانگیز متدی با امضای زیر است:
public User GetUser(int id)
{
}
کاملاً مشخص است که متدی همانند متد فوق زمانیکه کاربری را پیدا نکند، مقدار null را برمی‌گرداند. اما این روش درستی است؟ خیر؛ زیرا همانطور که از نام این متد پیداست باید یک کاربر به عنوان خروجی برگردانده شود.
با استفاده از بررسی null کدهایی شبیه به این را در همه جا خواهیم داشت:
var user = datasource.GetUser(userId);
if (user == null)
    throw new InvalidOperationException("Failed to find user: " + userId);
// actual logic here
به این چنین کدهایی معمولاً The null cancer گفته می‌شود (سرطان نال!) زیرا اجازه داده‌ایم متد، خروجی null را بازگشت دهد. به جای کد فوق می‌توانیم از این روش استفاده کنیم:
public User GetUser(int id)
{
    if (id <= 0) throw new ArgumentOutOfRangeException("id", id, "Valid ids are from 1 and above. Do you have a parsing error somewhere?");
    
    var user = db.Execute<User>("WHERE Id = ?", id);
    if (user == null)
        throw new EntityNotFoundException("Failed to find user with id " + id);
        
    return user;
}
نکته‌ایی که باید به آن توجه کنید این است که در هنگام صدور یک استثناء اطلاعات کافی را نیز به آن پاس دهید. به عنوان مثال در EntityNotFoundException مثال فوق پاس دادن "Failed to find user with id " + id کار دیباگ را برای مصرف کننده، راحتر خواهد کرد.


خطاهای متداول حین کار با استثناءها  


  • صدور مجدد استثناء و از بین بردن stacktrace

کد زیر را در نظر بگیرید:

try
{
    FutileAttemptToResist();
}
catch (BorgException err)
{
     _myDearLog.Error("I'm in da cube! Ohh no!", err);
    throw err;
}
مشکل کد فوق قسمت throw err است. این خط کد، محتویات stacktrace را از بین برده و استثناء را مجدداً برای شما ایجاد خواهد کرد. در این حالت هرگز نمی‌توانیم تشخیص دهیم که منبع خطا از کجا آمده است. در این حالت پیشنهاد می‌شود که تنها از throw استفاده شود. در این حالت استثناء اصلی مجدداً صادر گردیده و مانع حذف شدن محتویات stacktrace خواهد شد(+).
  • اضافه نکردن اطلاعات استثناء اصلی به استثناء جدید

یکی دیگر از خطاهای رایج اضافه نکردن استثناء اصلی حین صدور استثناء جدید است:

try
{
    GreaseTinMan();
}
catch (InvalidOperationException err)
{
    throw new TooScaredLion("The Lion was not in the m00d", err); //<---- استثناء اصلی بهتر است به استثناء جدید پاس داده شود
}
  • ارائه ندادن context information

در هنگام صدور یک استثناء بهتر است اطلاعات دقیقی را به آن ارسال کنیم تا دیباگ کردن آن به راحتی انجام شود. به عنوان مثال کد زیر را در نظر داشته باشید:

try
{
   socket.Connect("somethingawful.com", 80);
}
catch (SocketException err)
{
    throw new InvalidOperationException("Socket failed", err);  
}
هنگامی که کد فوق با خطا مواجه شود نمی‌توان تنها با متن Socket failed تشخیص داد که مشکل از چه چیزی است. بنابراین پیشنهاد می‌شود اطلاعات کامل و در صورت امکان به صورت دقیق را به استثناء ارسال کنید. به عنوان مثال در کد زیر سعی شده است تا حد امکان context information کاملی برای استثناء ارائه شود:
void IncreaseStatusForUser(int userId, int newStatus)
{
    try
    {
         var user  = _repository.Get(userId);
         if (user == null)
             throw new UpdateException(string.Format("Failed to find user #{0} when trying to increase status to {1}", userId, newStatus));
    
         user.Status = newStatus;
         _repository.Save(user);
    }
   catch (DataSourceException err)
   {
       var errMsg = string.Format("Failed to find modify user #{0} when trying to increase status to {1}", userId, newStatus);
        throw new UpdateException(errMsg, err);
   }

نحوه‌ی طراحی استثناءها 
برای ایجاد یک استثناء سفارشی می‌توانید از کلاس Exception ارث‌بری کنید و چهار سازنده‌ی آن را اضافه کنید:
public NewException()
public NewException(string description )
public NewException(string description, Exception inner)
protected or private NewException(SerializationInfo info, StreamingContext context)
سازنده اول به عنوان default constructor شناخته می‌شود. اما پیشنهاد می‌شود که از آن استفاده نکنید، زیرا یک استثناء بدون context information از ارزش کمی برخوردار خواهد بود.
سازنده‌ی دوم برای تعیین description بوده و همانطور که عنوان شد ارائه دادن context information از اهمیت بالایی برخوردار است. به عنوان مثال فرض کنید استثناء KeyNotFoundException که توسط کلاس Dictionary صادر شده است را دریافت کرده‌اید. این استثناء زمانی صادر خواهد شد که بخواهید به عنصری که درون دیکشنری پیدا نشده است دسترسی داشته باشید. در این حالت پیام زیر را دریافت خواهید کرد:
“The given key was not present in the dictionary.”
حالا فرض کنید اگر پیام به صورت زیر باشد چقدر باعث خوانایی و عیب‌یابی ساده‌تر خطا خواهد شد:
“The key ‘abrakadabra’ was not present in the dictionary.”
در نتیجه تا حد امکان سعی کنید که context information شما کاملتر باشد.
سازنده‌ی سوم شبیه به سازنده‌ی قبلی عمل می‌کند با این تفاوت که توسط پارامتر دوم می‌توانیم یک استثناء دیگر را catch کرده یک استثناء جدید صادر کنیم.
سازنده‌ی سوم زمانی مورد استفاده قرار می‌گیرد که بخواهید از Serialization پشتیبانی کنید (به عنوان مثال ذخیره‌ی استثناءها درون فایل و...)

خوب، برای یک استثناء سفارشی حداقل باید کدهای زیر را داشته باشیم:
public class SampleException : Exception
{
    public SampleException(string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
    }
 
    public SampleException(string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
    }
 
    public SampleException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
}

اجباری کردن ارائه‌ی Context information:
برای اجباری کردن context information کافی است یک فیلد اجباری درون سازنده تعریف کنیم. برای مثال اگر بخواهیم کاربر HTTP status code را برای استثناء ارائه دهد باید سازنده‌ها را اینگونه تعریف کنیم:
public class HttpException : Exception
{
    System.Net.HttpStatusCode _statusCode;
     
    public HttpException(System.Net.HttpStatusCode statusCode, string description)
        : base(description)
    {
        if (description == null) throw new ArgumentNullException("description");
        _statusCode = statusCode;
    }
 
    public HttpException(System.Net.HttpStatusCode statusCode, string description, Exception inner)
        : base(description, inner)
    {
        if (description == null) throw new ArgumentNullException("description");
        if (inner == null) throw new ArgumentNullException("inner");
        _statusCode = statusCode;
    }
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
    }
     
    public System.Net.HttpStatusCode StatusCode { get; private set; }
 
}
همچنین بهتر است پراپرتی Message را برای نمایش پیام مناسب بازنویسی کنید:
public override string Message
{
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
}
مورد دیگری که باید در کد فوق مد نظر داشت این است که status code قابلیت سریالایز شدن را ندارد. بنابراین باید متد GetObjectData را برای سریالایز کردن بازنویسی کنیم:
public class HttpException : Exception
{
    // [...]
 
    public HttpException(SerializationInfo info, StreamingContext context)
        : base(info, context)
    {
        // this is new
        StatusCode = (HttpStatusCode) info.GetInt32("HttpStatusCode");
    }
 
    public HttpStatusCode StatusCode { get; private set; }
 
    public override string Message
    {
        get { return base.Message + "\r\nStatus code: " + StatusCode; }
    }
 
    // this is new
    public override void GetObjectData(SerializationInfo info, StreamingContext context)
    {
        base.GetObjectData(info, context);
        info.AddValue("HttpStatusCode", (int) StatusCode);
    }
}
در اینحالت فیلدهای اضافی در طول فرآیند Serialization به خوبی سریالایز خواهند شد.

در حین صدور استثناءها همیشه باید در نظر داشته باشیم که چه نوع context information را می‌توان ارائه داد، این مورد در یافتن راه‌حل خیلی کمک خواهد کرد.


طراحی پیام‌های مناسب 
پیام‌های exception مختص به توسعه‌دهندگان است نه کاربران نهایی.
نوشتن این نوع پیام‌ها برای برنامه‌نویس کار خسته‌کننده‌ایی است. برای مثال دو مورد زیر را در نظر داشته باشید:
throw new Exception("Unknown FaileType");
throw new Exception("Unecpected workingDirectory");
این نوع پیام‌ها حتی اگر از لحاظ نوشتاری مشکلی نداشته باشند یافتن راه‌حل را خیلی سخت خواهند کرد. اگر در زمان برنامه‌نویسی با این نوع خطاها روبرو شوید ممکن است با استفاده از debugger ورودی نامعتبر را پیدا کنید. اما در یک برنامه و خارج از محیط برنامه‌نویسی، یافتن علت بروز خطا خیلی سخت خواهد بود.
توسعه‌دهندگانی که exception message را در اولویت قرار می‌دهند، معتقد هستند که از لحاظ تجربه‌ی کاربری پیام‌ها تا حد امکان باید فاقد اطلاعات فنی باشد. همچنین همانطور که پیش‌تر عنوان گردید این نوع پیام‌ها همیشه باید در بالاترین سطح نمایش داده شوند نه در لایه‌های زیرین. همچنین پیام‌هایی مانند Unknown FaileType نه برای کاربر نهایی، بلکه برای برنامه‌نویس نیز ارزش چندانی ندارد زیرا فاقد اطلاعات کافی برای یافتن مشکل است.
در طراحی پیام‌ها باید موارد زیر را در نظر داشته باشیم:
- امنیت:
یکی از مواردی که از اهمیت بالایی برخوردار است مسئله امنیت است از این جهت که پیام‌ها باید فاقد مقادیر runtime باشند. زیرا ممکن است اطلاعاتی را در خصوص نحوه‌ی عملکرد سیستم آشکار سازند.
- زبان:
همانطور که عنوان گردید پیام‌های استثناء برای کاربران نهایی نیستند، زیرا کاربران نهایی ممکن است اشخاص فنی نباشند، یا ممکن است زبان آنها انگلیسی نباشد. اگر مخاطبین شما آلمانی باشند چطور؟ آیا تمامی پیام‌ها را با زبان آلمانی خواهید نوشت؟ اگر هم اینکار را انجام دهید تکلیف استثناء‌هایی که توسط Base Class Library و دیگر کتابخانه‌های thirt-party صادر می‌شوند چیست؟ اینها انگلیسی هستند.

در تمامی حالت‌هایی که عنوان شد فرض بر این است که شما در حال نوشتن این نوع پیام‌ها برای یک سیستم خاص هستید. اما اگر هدف نوشتن یک کتابخانه باشد چطور؟ در این حالت نمی‌دانید که کتابخانه‌ی شما در کجا استفاده می‌شود.
اگر هدف نوشتن یک کتابخانه نباشد این نوع پیام‌هایی که برای کاربران نهایی باشند، وابستگی‌ها را در سیستم افزایش خواهند داد، زیرا در این حالت پیام‌ها به یک رابط کاربری خاص گره خواهند خورد.

خب اگر پیام‌ها برای کاربران نهایی نیستند، پس برای کسانی مورد استفاده قرار خواهند گرفت؟ در واقع این نوع پیام می‌تواند به عنوان یک documentation برای سیستم شما باشند.
فرض کنید در حال استفاده از یک کتابخانه جدید هستید به نظر شما کدام یک از پیام‌های زیر مناسب هستند:
"Unecpected workingDirectory"
یا:
"You tried to provide a working directory string that doesn't represent a working directory. It's not your fault, because it wasn't possible to design the FileStore class in such a way that this is a statically typed pre-condition, but please supply a valid path to an existing directory.

"The invalid value was: "fllobdedy"."
یافتن مشکل در پیام اول خیلی سخت خواهد بود زیرا فاقد اطلاعات کافی برای یافتن مشکل است. اما پیام دوم مشکل را به صورت کامل توضیح داده است. در حالت اول شما قطعاً نیاز خواهید داشت تا از دیباگر برای یافتن مشکل استفاده کنید. اما در حالت دوم پیام به خوبی شما را برای یافتن راه‌حل راهنمایی می‌کند.
همیشه برای نوشتن پیام‌های مناسب سعی کنید از لحاظ نوشتاری متن شما مشکلی نداشته باشد، اطلاعات کافی را درون پیام اضافه کنید و تا حد امکان نحوه‌ی رفع مشکل را توضیح دهید
مطالب
بررسی تفاوت Task و ValueTask

زمانیکه تصمیم میگیریم کدهای زده شده را بهینه کنیم، اکثرا دنبال راه حل‌های جدید نمیگردیم. این مورد کاملا غریزی است؛ چرا که به‌دنبال کم‌ترین انرژی و بیشترین بازدهی هستیم؛ این طبیعت انسان است. صرفا کدهای قبلی را بازبینی میکنیم و سعی میکنیم  نحوه‌ی نوشتن منطق‌های موجود را بهینه کنیم. در همین راستا درک عملکرد Task و ValueTask ‌ها شاید قدمی مهم در مورد بهینه کردن کد‌ها باشد؛ چرا استفاده درست و بجای این دو مورد می‌تواند تاثیر زیادی بر روی سرعت و استفاده از مصرف حافظه داشته باشد؟ در این مقاله سعی میکنیم تا درک درستی از این دو داشته باشیم.


Task<T>  چیست؟

Task یک کلاس در فضای نام System.Threading.Tasks است؛ به‌طوریکه کمک میکند تا یک قسمت از برنامه به صورت مستقل از Thread اصلی اجرا شود. به‌بیان دیگر می‌تواند یک Thread Pool را ایجاد و با توجه به روند کار، از یک مرحله‌ی اجرایی به مرحله‌ای دیگر منتقل می‌کند. همچنین هر Task می‌تواند یک مقدار برگشتی نیز داشته باشد.

 این درحالی‌است که می‌تواند صرفا یک فرآیند را اجرا کند، بدون اینکه خروجی داشته باشد. به‌عبارتی دیگر اگر فرآیندی داشته باشیم که در نهایت یک شناسه را برمیگرداند، از Task<int> و اگر فرآیندی داشته باشیم که صرفا فرآیند همگام سازی داده‌های قدیمی به جدید را انجام میدهد، می‌تواند از نوع Task باشد.

همانطور که اشاره شد، Task یک کلاس است که شامل متد‌ها و فیلد‌های مختلفی می‌باشد. با استفاده از این اعضا می‌توان نحوه‌ی اجرای کدها و وضعیت‌های مختلف اجرای آن را مدیریت کرد، تا در نهایت اجرای آن کامل شود.

به دلیل اینکه Task یک class است و class ‌ها از نوع ReferenceType می‌باشند، روی حافظه‌ی Heap ذخیره می‌شوند و به‌ازای هر بار فراخوانی متدی که خروجی Task دارد، شیء Task را روی Heap ذخیره میکند. این شیء وضعیت اجرای قسمتی از کد ما را که میتواند sync یا async باشد، در خود ذخیره میکند تا در نهایت اجرای آن کامل شود.


نحوه استفاده از Task<T>

برای درک بهتر، یک تکه کد را با بهره بردن از Task ایجاد میکنیم :

public static class DummyWeatherProvider
{
    public static async Task<Weather> Get(string city)
    {
        await Task.Delay(10);
        var weather = new Weather 
        { 
            City = city, 
            Date = DateTime.Now, 
            AvgTempratureF = new Random().Next(5, 70) 
        };
        
        return weather;
    }
}
همان طور که مشخص است، کلاس موجود یک متد به نام Get دارد تا اطلاعات آب و هوای  شهر مورد نظر را به صورت یک Task  برگرداند. حال کد زیر را جهت بررسی تغییر وضعیت‌های اجرایی این Task ایجاد می‌کنیم :
static async Task CheckTaskStatus()
{
   var task = DummyWeatherProvider.Get("Stockholm");
    LogTaskStatus(task.Status);
    await task;
    LogTaskStatus(task.Status);
}

static void LogTaskStatus(TaskStatus status)
{
    Console.WriteLine($"Task Status: {Enum.GetName(typeof(TaskStatus), status)}");
}
TaskStatus یک enumeration است، به‌طوری‌که بیانگر وضعیت‌های مختلف یک Task در حال اجرا می‌باشد. برای مثال: WaitingForActivation, Running, RanToCompletion. در کد بالا ابتدا متد را فراخوانی می‌کنیم. سپس منتظر می‌مانیم تا متد اجرا شده، تکمیل شود. در اولین لاگ وضعیت، به WaitingForActivation و در دومین لاگ به RanToCompletion تبدیل میشود. حال‌که با Task ها و نحوه‌ی اجرای فرآیند آن آشنا شدیم، در قسمت بعدی به بررسی ValueTask ها می‌پردازیم. 

ValueTask<T>  چیست؟

همانند Task ، ValueTask هم برای مدیریت وضعیت فرآیند استفاده میشود؛ با این تفاوت که ValueTask ‌ها از نوع struct هستند. به‌طوریکه نحوه‌ی ذخیره سازی آن‌ها در حافظه به نسبت class ‌ها کاملا متفاوت است. از نقطه نظر سرعت، تشخیص دادن اینکه کدامیک باید استفاده شود، باید با توجه به سناریو، بررسی و انتخاب شود؛ چرا که از نظر تخصیص حافظه متفاوت عمل می‌کنند. برای درک بهتر عملکرد ValueTask ‌ها کد زیر را بررسی میکنیم :

public class WeatherService
{
    private readonly ConcurrentDictionary<string, Weather> _cache;
    public WeatherService()
    {
        _cache = new();
    }

    public async Task<Weather> GetWeatherTask(string city)
    {
        if (!_cache.ContainsKey(city))
        {
            var weather = await DummyWeatherProvider.Get(city);
            _cache.TryAdd(city, weather);
        }
        return _cache[city];
    }

    public async ValueTask<Weather> GetWeatherValueTask(string city)
    {
        if (!_cache.ContainsKey(city))
        {
            var weather = await DummyWeatherProvider.Get(city);
            _cache.TryAdd(city, weather);
        }
        return _cache[city];   
  }

کلاس WeatherService شامل یک فیلد private از نوع collection و دو متد است. ما از _cache  جهت نگهداری اطلاعاتی که قبلا دریافت شده، استفاده می‌کنیم و به نوعی in-memory cache را پیاده سازی میکنیم. پیاده سازی منطق هر دو متد  GetWeatherTask و GetWeatherValueTask  کاملا شبیه به هم است؛ به‌طوری‌که اول بررسی میکنیم اطلاعات آب و هوای شهر مورد نظر در _cache وجود دارد یا خیر؟ اگر وجود داشت، اطلاعات به صورت مستقیم برگشت داده می‌شود؛ در غیر این صورت DummyWeatherProvider.Get()  فراخوانی خواهد شد. 

در قدم بعدی اطلاعات به‌دست آمده را در _cache ذخیره می‌کنیم. سپس مقدار ذخیره شده را برگشت میدهیم. در واقع تنها تفاوت دو متد ذکر شده، نوع خروجی آن می‌باشد؛ یکی از Taskو دیگری از ValueTask استفاده می‌کند.

برای مقایسه‌ی مصرف حافظه‌ی این دو روی هر دو متد، Benchmark میگیریم. برای پیاده سازی نیار به کد‌های زیر داریم : 

[MemoryDiagnoser]
public class TaskAndValueTaskBenchmark
{
    private readonly WeatherService _weatherService;
    public TaskAndValueTaskBenchmark()
    {
        _weatherService = new();
    }
    
    [Benchmark]
    [Arguments("Denver")]
    public async Task<Weather> TaskBenchmark(string city)
    {
        return await _weatherService.GetWeatherTask(city);
    }

    [Benchmark]
    [Arguments("London")]
    public async ValueTask<Weather> ValueTaskBenchmark(string city)
    {
        return await _weatherService.GetWeatherValueTask(city);
    }
}

نتیجه به دست آمده به شرح زیر است :

Allocated

Gen0

Method

144 B

0.0229

TaskBenchmark

------

----

ValueTaskBenchmark

  با توجه به نتیجه به‌دست آمده، متدی که خروجی ValueTask دارد، حافظه‌ای را تخصیص نداده‌است؛ این دقیقا مزیت مهم ValueTask نسبت به Task  می‌باشد.

مزیت  ValueTask<T>

به‌دلیل اینکه از نوع struct هستند، بر روی حافظه، در قسمت Stack ذخیره می‌شوند و به صورت خودکار بعد از اینکه نیازی به آنها نباشد، از حافظه حذف می‌شوند . به همین دلیل به شکل قابل توجهی، فشار را از روی GC کاهش می‌دهد .

 علاوه بر این، در سناریویی که اکثر کدها به صورت sync اجرا می‌شوند، در این مواقع استفاده از ValueTask، بهتر از Task می‌باشد .

این سری متد GetWeatherValueTask   را جهت تشخص اینکه  اغلب کدها به صورت sync یا async اجرا می‌شوند، بررسی می‌کنیم. در متد ذکر شده اگر اطلاعات شهر مورد نظر وجود داشته باشد، کار به صورت sync اجرا می‌شود و اگر شهر وجود نداشته باشد، کار به صورت async اجرا می‌شود. با بررسی دقیق‌تر متوجه می‌شویم اکثر مواقع در این متد کار به صورت sync  اجرا می‌شود؛ چرا که بعد ازدریافت اطلاعات، مجدد آن را دریافت نمیکند، بلکه از حافظه میخواند (همان _cache ) .


محدودیت‌های استفاده از    ValueTask<T>  

1. در اینجا تنها یکبار امکان استفاده از await وجود دارد. وقتی یکبار valueTask را await می‌کنیم، بهتر است کار دیگری بر روی آن انجام ندهیم؛ چراکه ممکن است از حافظه پاک شده باشد.

2. اگر در سناریویی لازم دارید چندین بار await را بر روی valueTask اجرا کنید، لازم است ابتدا آن را به Task تبدیل کنیم. برای این کار متد AsTask را فراخوانی میکنیم (بهتر است صرفا یکبار متد AsTask را فراخوانی کنیم).

3. نمیتوانیم به یک ValueTask به صورت هم زمان در حالت Multi threads دسترسی داشته باشیم.

4. به صورت پیش فرض خروجی عملیات async، نوع Task می‌باشد؛ مگر اینکه اغلب مراحل کار به صورت sync اجرا شود، مانند مثالی که بالاتر اشاره شد.


منابع :

اشتراک‌ها
تاریخ NET Conf 2024. مشخص شد، ۱۲ تا ۱۴ نوامبر، رهانش نهایی دات‌نت ۹

We are thrilled to announce the highly anticipated .NET Conf 2024, a free, three-day virtual developer event celebrating the release of .NET 9. Co-organized by the .NET community and Microsoft, this annual tradition continues to grow, and we’re more excited than ever to bring you the latest innovations in .NET. Mark your calendars for November 12th to 14th, 2024, and prepare to be inspired by a wealth of knowledge, creativity, and community engagement.

تاریخ NET Conf 2024. مشخص شد، ۱۲ تا ۱۴ نوامبر، رهانش نهایی دات‌نت ۹
اشتراک‌ها
Reflector 10 منتشر شد

What's new in this release?

  • Full C#7.0 support
  • .NET Core support

For more information, please see the release notes.

Upgrading to .NET Reflector 10
.NET Reflector 10 is a major upgrade from .NET Reflector 9. If your current license includes a valid support and upgrades package for .NET Reflector 10, the same license serial number will automatically activate .NET Reflector 10.

If your v9.x license does not include support and upgrades, Reflector will upgrade to a 14-day trial of v10.0. 

Reflector 10 منتشر شد