امکان تعریف اعضای static abstract در اینترفیس‌های C# 11
اندازه‌ی قلم متن
تخمین مدت زمان مطالعه‌ی مطلب: پنج دقیقه

امکان داشتن اعضای static abstract در اینترفیس‌ها شاید عجیب به‌نظر برسد یا حتی غیرضروری؛ اما در C# 11، پایه‌ی قابلیت جدیدی به نام «ریاضیات جنریک» شده‌است. به همین جهت در ابتدا نیاز است با اعضای static abstract آشنا شد و در قسمتی دیگر به «ریاضیات جنریک» پرداخت.


مثالی جهت توضیح علت نیاز به اعضای static abstract در اینترفیس‌ها

فرض کنید قصد داریم حاصل جمع اعضای یک آرایه‌ی int را محاسبه کنیم:
namespace CS11Tests;

public class StaticAbstractMembers
{
    public static void Test()
    {
        var sum = AddAll(new[] { 1, 2, 3, 4 });
        Console.WriteLine(sum);
    }

    private static int AddAll(int[] values)
    {
        int result = 0;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
}
روش متداول اینکار را در اینجا ملاحظه می‌کنید که حلقه‌ای بر روی عناصر آرایه، جهت یافتن حاصل جمع آن‌ها تشکیل شده‌است. اکنون فرض کنید بجای آرایه‌ای که در متد Test استفاده شده، از آرایه‌ی زیر استفاده شود:
var sum = AddAll(new[] { 1, 2, 3, 4, 0.68 });
اینبار با خطای زیر متوقف می‌شویم:
Argument 1: cannot convert from 'double[]' to 'int[]' [CS11Tests]csharp(CS1503)
عنوان می‌کند که آرایه‌ی مدنظر از نوع []double تشخیص داده شده‌است و متد AddAll، تنها آرایه‌های از نوع int را قبول می‌کند. در جهت رفع این مشکل شاید بهتر باشد نمونه‌ی جنریک متد AddAll را ایجاد کنیم، تا بتوان انواع و اقسام نوع‌های ممکن را به آن ارسال کرد:
private static T AddAll<T>(T[] values)
    {
        T result = 0;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
اما اینکار میسر نیست. چون زمانیکه از T استفاده می‌شود، مفهوم و امکان وجود «عدد صفر» در آن نوع، مشخص نیست. یک روش حل این مشکل، مقید و محدود کردن نوع T است. برای مثال عنوان کنیم که T، عددی است و از نوع INumber (فرضی/خیالی) است و این INumber فرضی، به همراه مفهوم عدد صفر هم هست. یعنی اولین سطر بدنه‌ی متد AddAll را باید بتوان به صورت زیر بازنویسی کرد:
T result = T.Zero;
یعنی باید بتوان از طریق یک «نوع» عمومی مانند T (نه وهله‌ای/نمونه‌ای/instance ای از آن نوع؛ دقیقا خود آن نوع) به خاصیت Zero آن نوع، دسترسی یافت و آن خاصیت هم باید از نوع استاتیک باشد و چون تا C# 10 و دات نت 6، چنین امکانی مهیا نشده بود (البته در حالت preview قرار داشت)، تنها راه ممکن، تهیه‌ی یک نمونه‌ی جدید double متد AddAll است/بود.
در C# 11 و دات نت 7، با معرفی اینترفیس جدید INumber، می‌توان قید <where T : INumber<T را به T اعمال کرد (مانند نمونه‌ی زیر) و همچنین با استفاده از اعضای static abstract این اینترفیس، به مقدار T.Zero هم دسترسی یافت و اینبار قطعه کد زیر، بدون مشکل در C# 11 کامپایل می‌شود:
using System.Numerics;

namespace CS11Tests;

public class StaticAbstractMembers
{
    public static void Test()
    {
        //var sum = AddAll(new[] { 1, 2, 3, 4 });
        var sum = AddAll(new[] { 1, 2, 3, 4, 0.68 });
        Console.WriteLine(sum);
    }

    private static T AddAll<T>(T[] values) where T : INumber<T>
    {
        T result = T.Zero;
        foreach (var value in values)
        {
            result += value;
        }
        return result;
    }
}
اگر به تعاریف INumber جدید مراجعه کنیم، نه فقط به خواص abstract static جدیدی می‌رسیم (که امکان دسترسی به T.Zero را میسر کرده‌اند)،
abstract static TSelf One { get; }
abstract static TSelf Zero { get; }
بلکه امکان تعریف اپراتورهای abstract static هم میسر شده‌اند (به همین جهت است که در کدهای فوق سطر result += value، هنوز هم کار می‌کند):
abstract static TResult operator +(TSelf left, TOther right);


مثال دیگری از کاربرد اعضای abstract static در اینترفیس‌ها

فرض کنید اینترفیس ISport را به همراه دو پیاده سازی از آن، به صورت زیر تعریف کرده‌ایم:
public interface ISport
{
    bool IsTeamSport();
}

public class Swimming : ISport
{
    public bool IsTeamSport() => false;
}

public class Football : ISport
{
    public bool IsTeamSport() => true;
}
اکنون جهت کار با متد IsTeamSport و تعریف جنریک این متد، می‌توان به صورت متداول زیر عمل کرد که در آن T، مقید به ISport شده‌است:
public class StaticAbstractMembers
{
    public static void Display<T>(T sport) where T : ISport
    {
        Console.WriteLine("Is Team Sport:" + sport.IsTeamSport());
    }
}
برای کار با آن هم باید حتما نمونه‌ای از ()new Football و یا ()new Swimming را به آن ارسال کرد:
Display(new Football());
سؤال: آیا با توجه به مشخص بودن و محدود بودن نوع T، می‌توان با حذف پارامتر T sport، به متد IsTeamSport اینترفیس ISport دسترسی یافت؟ یعنی تعریف متد Display را طوری تغییر داد تا دیگر نیاز به نمونه سازی ()new Football نداشته باشد. همینقدر که نوع Football مشخص بود، بتوان متد IsTeamSport آن‌را فراخوانی کرد.
پاسخ: تا پیش‌از C# 11 یکی از روش‌‌های انجام اینکار، استفاده از reflection بود. اما در C# 11 با کمک static abstractها می‌توان تعاریف این اینترفیس و پیاده سازی‌های آن‌را به صورت زیر تغییر داد:
public interface ISport
{
    static abstract bool IsTeamSport();
}

public class Swimming : ISport
{
    public static bool IsTeamSport() => false;
}

public class Football : ISport
{
    public static bool IsTeamSport() => true;
}
تا اینبار جهت دسترسی به متد IsTeamSport،‌مستقیما بتوان به خود «نوع»، «بدون نیاز به نمونه سازی آن» مراجعه کرد و قطعه کد زیر در C# 11 معتبر است:
public class StaticAbstractMembers
{
    public static void Display<T>() where T : ISport
    {
        Console.WriteLine("Is Team Sport:" + T.IsTeamSport());
    }
}
  • #
    ‫۱ سال و ۹ ماه قبل، شنبه ۲۸ آبان ۱۴۰۱، ساعت ۱۸:۳۷
    اضافه شدن Generic Parsing به دات نت 7

    تا قبل از دات نت 7، متدهای Parse و TryParse جزو استاندارد اغلب نوع‌ها در دات نت بودند؛ اما امکان استفاده‌ی جنریک از آن‌ها وجود نداشت. این مشکل به لطف وجود اعضای استاتیک اینترفیس‌ها در دات نت 7 و C# 11 برطرف شده‌است. برای این منظور دو اینترفیس جدید System.IParsable و System.ISpanParsable به دات نت 7 اضافه شده‌اند که امکان دسترسی به متد T.Parse را میسر می‌کنند.
    دو نمونه مثال از نحوه‌ی استفاده‌ی از این API جدید را در ادامه مشاهده می‌کنید:
    public static T ParseIt<T>(string content, IFormatProvider? provider) where T : IParsable<T>
    {
       return T.Parse(content, provider);
    }
    
    public IEnumerable<T> ParseCsvRow<T>(string content, IFormatProvider? provider) where T : IParsable<T>
    {
       return content.Split(',').Select(str => T.Parse(str, provider));
    }
    اگر می‌خواستیم متد ParseIt را به صورت جنریک و بدون استفاده از ویژگی‌های جدید زبان #C و دسترسی مستقیم به T.Parse بنویسیم، یک روش آن، استفاده از Reflection به صورت زیر می‌بود:
    public static T ParseIt<T>(string content, IFormatProvider? provider)
    {
       var type = typeof(T);
       var method = type.GetMethod("Parse", BindingFlags.Static | BindingFlags.Public,
         new[] { typeof(string), typeof(IFormatProvider) });
       return (T)method!.Invoke(null, new object?[] { content, provider })!;
    }
  • #
    ‫۱ سال و ۹ ماه قبل، یکشنبه ۲۹ آبان ۱۴۰۱، ساعت ۱۴:۲۰
    پشتیبانی از ریاضیات جنریک در C# 11

    در C# 11، امکان انجام عملیات ریاضی بر روی نوع‌های جنریک میسر شده‌است که قسمتی از آن‌را در مطلب جاری مطالعه کردید. این ویژگی به همراه دو مزیت زیر است:
    - اکنون اعضای استاتیک اینترفیس‌ها می‌توانند abstract هم باشند؛ یعنی کلاس‌های پیاده ساز آن‌ها باید این اعضای استاتیک را پیاده سازی کنند.
    -  امکان تعریف عملگرهای استاتیک ریاضی در اینترفیس‌ها ممکن شده‌است:
     static T Add<T>(T left, T right) where T : INumber<T> => left + right;
    در این مثال، نوع T، تنها می‌تواند از نوع <INumber<T باشد که یکی از اینترفیس‌های جدید NET 7. است. این اینترفیس نیز پیاده سازی کننده‌ی IAdditionOperators است که در آن امکان دسترسی به عملگر + وجود دارد. متد فوق را می‌توان تقریبا توسط تمام نوع‌های ریاضی توکار دات نت مورد استفاده قرار داد؛ از این جهت که تعاریف آن‌ها نیز در دات نت 7 برای پشتیبانی از INumber به‌روز رسانی شده‌اند.
    یکی از مهم‌ترین مزیت‌های چنین امکانی، کاهش تعداد overload هایی است که باید توسعه دهندگان کتابخانه‌ها برای پشتیبانی از انواع و اقسام نوع‌های ریاضی ارائه دهند.
     

    تغییرات API دات نت 7 در جهت پشتیبانی از ریاضیات جنریک

    در مطلب جاری با اینترفیس جدید INumber آشنا شدیم که توسط آن مفاهیمی مانند صفر و یک و همچنین سربارگذاری عملگرهای ریاضی میسر شده‌است. تعداد این اینترفیس‌های توکار در دات نت 7، فراتر از یک مورد فوق است. برای مثال اینترفیس جدید IMinMaxValue امکان دسترسی به T.MinValue و T.MaxValue را میسر می‌کند.
    یک مثال: امضای نوع Int32 در دات نت 7 به صورت زیر در آمده‌است:
    public readonly struct Int32 : 
      IComparable, 
      IComparable<int>, 
      IConvertible, 
      IEquatable<int>, 
      IFormattable, 
      IParsable<int>, 
      ISpanFormattable, 
      ISpanParsable<int>, 
      IAdditionOperators<int, int, int>, 
      IAdditiveIdentity<int, int>, 
      IBinaryInteger<int>, 
      IBinaryNumber<int>, 
      IBitwiseOperators<int, int, int>, 
      IComparisonOperators<int, int, bool>, 
      IEqualityOperators<int, int, bool>, 
      IDecrementOperators<int>, 
      IDivisionOperators<int, int, int>, 
      IIncrementOperators<int>, 
      IModulusOperators<int, int, int>, 
      IMultiplicativeIdentity<int, int>, 
      IMultiplyOperators<int, int, int>, 
      INumber<int>, 
      INumberBase<int>, 
      ISubtractionOperators<int, int, int>, 
      IUnaryNegationOperators<int, int>, 
      IUnaryPlusOperators<int, int>, 
      IShiftOperators<int, int, int>, 
      IMinMaxValue<int>, 
      ISignedNumber<int>
    یعنی تمام نوع‌های ریاضی توکار دات نت 7 از ریاضیات جنریک پشتیبانی می‌کنند. البته نوع‌ها زیر هنوز از یک چنین پشتیبانی برخوردار نیستند:
    - System.Half
    - System.Numerics.BigInteger
    - System.Numerics.Complex
    - System.Runtime.InteropServices.NFloat
    - System.Int128
    - System.UInt128

    نوع‌های Int128 و UIn128 جزو تازه‌های دات نت 7 هستند (128-bit signed integer و 128-bit unsigned integer).

    البته عموم ما از همان اینترفیس INumber و IBinaryInteger استفاده خواهیم کرد که خود آن نیز به صورت زیر تعریف شده‌است:
    public interface INumber<TSelf> : 
    IComparable, 
    IComparable<TSelf>, 
    IEquatable<TSelf>, 
    IFormattable, 
    IParsable<TSelf>, 
    ISpanFormattable, 
    ISpanParsable<TSelf>, 
    IAdditionOperators<TSelf, TSelf, TSelf>, 
    IAdditiveIdentity<TSelf, TSelf>, 
    IComparisonOperators<TSelf, TSelf, bool>, 
    IEqualityOperators<TSelf, TSelf, bool>, 
    IDecrementOperators<TSelf>, 
    IDivisionOperators<TSelf, TSelf, TSelf>, 
    IIncrementOperators<TSelf>, 
    IModulusOperators<TSelf, TSelf, TSelf>, 
    IMultiplicativeIdentity<TSelf, TSelf>, 
    IMultiplyOperators<TSelf, TSelf, TSelf>, 
    INumberBase<TSelf>, 
    ISubtractionOperators<TSelf, TSelf, TSelf>, 
    IUnaryNegationOperators<TSelf, TSelf>, 
    IUnaryPlusOperators<TSelf, TSelf> where TSelf : INumber<TSelf>?