عنوان:

‫خداحافظی با Overloadهای بی‌پایان: قدرت Generic Math با INumber در دات‌نت مدرن


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۶/۲۲ ۱۵:۱۰
آدرس: www.dntips.ir
آیا تا به حال مجبور شده‌اید یک متد ساده ریاضی را برای انواع عددی مختلفی مثل int, long, double و decimal به صورت جداگانه پیاده‌سازی (Overload) کنید؟ این یک کار تکراری و خسته‌کننده‌است که خوانایی کد را کاهش داده و احتمال خطا را بالا می‌برد. خوشبختانه، از دات‌نت ۷ به بعد، با معرفی Generic Math و اینترفیس INumber، این دوران به پایان رسیده است. این مطلب به شما نشان می‌دهد که چگونه با استفاده از این قابلیت مدرن، کدهای عددی خود را یک بار برای همیشه بنویسید: تمیزتر، انعطاف‌پذیرتر و آینده‌نگرانه‌تر.

مشکل قدیمی: دنیای قبل ازINumber

بیایید یک سناریوی ساده را در نظر بگیریم. می‌خواهیم متدی بنویسیم که یک عدد را به تعداد دفعات مشخصی بر 1024 تقسیم کند. این عملیات در محاسبات مربوط به حجم داده‌ها (مانند تبدیل بایت به کیلوبایت، مگابایت و...) بسیار رایج است. در گذشته، یک پیاده‌سازی ساده برای نوع long به شکل زیر بود:
// روش سنتی و محدود به نوع long
public static long DivideBy1024(this long number, byte times = 1)
{
    long divisor = (long)Math.Pow(1024, times);
    return number / divisor;
}
این کد کاملاً درست کار می‌کند، اما یک مشکل اساسی دارد: فقط برای long قابل استفاده است. اگر بخواهیم همین کار را برای int، double یا decimal انجام دهیم، باید متدهای مشابهی را برای هر کدام از این انواع بازنویسی کنیم. این کار نه تنها حجم کد را افزایش می‌دهد، بلکه نگهداری آن را نیز دشوار می‌سازد.

راه‌حل مدرن: معرفیINumber

اینترفیس INumber که در فضای نام System.Numerics قرار دارد، یک تغییر بزرگ در برنامه‌نویسی دات‌نت ایجاد کرده است. این اینترفیس یک قرارداد مشترک برای تمام انواع عددی اصلی فراهم می‌کند و به ما اجازه می‌دهد تا متدهایی بنویسیم که به صورت ژنریک (Generic) روی هر نوع عددی کار می‌کنند. INumber شامل تعاریف اپراتورهای ریاضی پایه (+, -, *, /)، مقادیر ثابت (مانند One و Zero) و متدهای استاتیک برای ساختن مقادیر از انواع دیگر است. حالا بیایید متد DivideBy1024 را با استفاده از این قابلیت بازنویسی کنیم:
using System.Numerics;

public static class NumericExtensions
{
    public static T DivideBy1024<T>(this T number, byte times = 1) where T : INumber<T>
    {
        // 1. ساختن مقدار ثابت 1024 از نوع ژنریک T
        T divisorBase = T.CreateChecked(1024);

        // 2. پیاده‌سازی ژنریک برای عمل توان (جایگزین Math.Pow)
        T divisor = Power(divisorBase, times);

        // 3. استفاده از اپراتور تقسیم که برای همه انواع INumber تعریف شده
        return number / divisor;
    }

    /// <summary>
    /// یک پیاده‌سازی ژنریک و ساده از عمل توان برای انواع عددی.
    /// </summary>
    private static T Power<T>(T baseValue, byte exponent) where T : INumber<T>
    {
        if (exponent == 0)
            return T.One; // استفاده از مقدار ثابت T.One

        T result = baseValue;
        for (int i = 1; i < exponent; i++)
        {
            result *= baseValue; // استفاده از اپراتور *=
        }
        return result;
    }
}

کالبدشکافی کد جدید: چه چیزی تغییر کرد؟

بیایید نگاهی عمیق‌تر به جادوی پشت این کد بیندازیم:
  • قید ژنریک where T : INumber: این مهم‌ترین بخش کد است. ما به کامپایلر می‌گوییم که نوع T باید حتماً اینترفیس INumber را پیاده‌سازی کرده باشد. این قید به ما اطمینان می‌دهد که تمام اپراتورها و متدهای مورد نیاز در دسترس هستند.
  • T.CreateChecked(1024): از آنجایی که نمی‌توانیم عدد 1024 را مستقیماً به نوع نامشخص T تبدیل کنیم، از متد استاتیک CreateChecked استفاده می‌کنیم. این متد به صورت امن، یک مقدار را به نوع T تبدیل می‌کند.
  • متد کمکی Power: Math.Pow برای انواع ژنریک طراحی نشده است. بنابراین، یک متد کمکی ساده نوشتیم که با استفاده از اپراتور *= و مقدار T.One (که هر دو بخشی از INumber هستند)، عمل توان را برای هر نوع عددی شبیه‌سازی می‌کند.


چگونه از آن استفاده کنیم؟
زیبایی این رویکرد در سادگی استفاده از آن است. اکنون می‌توانید متد DivideBy1024 را روی انواع مختلفی فراخوانی کنید، بدون آنکه نگران نوع داده باشید:
long memoryInBytes = 4_294_967_296; // 4 گیگابایت
long memoryInKilobytes = memoryInBytes.DivideBy1024();       // نتیجه: 4194304
long memoryInMegabytes = memoryInBytes.DivideBy1024(times: 2); // نتیجه: 4096
long memoryInGigabytes = memoryInBytes.DivideBy1024(3);      // نتیجه: 4

Console.WriteLine($"Memory in GB: {memoryInGigabytes}");

// --- استفاده برای انواع دیگر ---

double preciseValue = 1536.5;
double resultDouble = preciseValue.DivideBy1024(); // کار می‌کند!
Console.WriteLine($"Double Result: {resultDouble}"); // نتیجه: 1.50048828125

decimal financialValue = 204800.00m;
decimal resultDecimal = financialValue.DivideBy1024(); // کار می‌کند!
Console.WriteLine($"Decimal Result: {resultDecimal}"); // نتیجه: 200.00
همانطور که می‌بینید، یک متد واحد توانست با long, double و decimal بدون هیچ تغییری کار کند. این یعنی کد کمتر، نگهداری آسان‌تر و خوانایی بالاتر.

نتیجه‌گیری: به آینده برنامه‌نویسی عددی خوش آمدید

در سال ۲۰۲۵ و پس از آن، استفاده از Generic Math و اینترفیس INumber دیگر یک انتخاب نیست، بلکه یک روش استاندارد و بهینه برای کار با داده‌های عددی در دات‌نت است. با پذیرش این الگو، شما می‌توانید:
  • از تکرار کد جلوگیری کنید.
  • خوانایی و قابلیت نگهداری کد خود را به شدت افزایش دهید.
  • کتابخانه‌هایی بنویسید که برای هر نوع عددی قابل استفاده باشند.
  • کدی بنویسید که در برابر انواع عددی جدیدی که در آینده به دات‌نت اضافه می‌شوند، مقاوم باشد.
وقت آن رسیده که با سربارگذاری‌های (overloads) بی‌پایان خداحافظی کرده و قدرت واقعی ژنریک‌ها را در آغوش بگیرید.