如何使用通用变量执行数学运算?

4
我正在处理一个项目,需要将整数转换为使用奇数字基(例如base36、62、64等)的字符串表示形式。在.NET Framework中并不支持这些奇数数字基(据我所知)。因此我们决定编写一个通用的字符串转换系统,可以与任何数字基一起工作,因为这是一个非常简单的操作。
稍后,我们想创建一个自定义的IFormatProvider / ICustomFormatter来方便以后的使用。但首先,我们需要解决转换过程本身,通过编写静态方法来执行转换并返回一些基本结果。一旦我们使其正常运行,我们将添加IFormatProvider包装器。
由于我已经有些时间没有使用C#泛型了,我无法记住如何让编译器满意这个操作。我想创建一个私有的静态泛型方法ConvertInteger,然后从公共静态Convert方法调用它,以帮助强制类型化而不会使其难以使用。这样做的主要原因之一是要避免在转换有符号值与无符号值时出现符号位问题。目前,我们有public static convert方法,适用于long和ulong,以避免在有符号和无符号值之间转换时出现问题。
将来,如果我们能够让私有的静态泛型方法正常工作,我们希望扩展公共方法,包括int、uint、short、ushort、byte和sbyte,作为显式实现,以帮助执行大批量不同整数大小的值的方法时提高性能。这就是泛型方法设计派上用场的地方,因此我们不必反复复制完全相同的代码(这也使测试和调试更加简单)。
我遇到的问题是,编译器不允许我使用泛型类型进行比较或数学运算,因为它不知道在提供泛型类型参数值之前如何执行这些操作。这让我感到非常难过,因为我主要是C ++开发人员,而C ++通过简单地避免在提供类型参数值之前尝试解释通用代码来解决了这个问题。
我需要做什么来满足泛型方法设计的编译器?下面是代码,并标注了特定的编译错误。
public static class NumericStringConverter
{
    private static readonly string[] StandardDigits = new string[]
    {
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
        "_", "-"
    };



    private static string ConvertInteger<T>(T Value, T Base)
    {
        if (Base < 2) // error here: "Operator '<' cannot be applied to operands of type 'T' and 'int'"
            throw new ArgumentOutOfRangeException("Base", Base, "The NumericStringConverter.Convert(Value, Base) method was called, with the Base parameter set to a value less than 2.");

        if (Base > 64) // error here: "Operator '>' cannot be applied to operands of type 'T' and 'int'"
            throw new ArgumentOutOfRangeException("Base", Base, "The NumericStringConverter.Convert(Value, Base) method was called, with the Base parameter set to a value greater than 64.");

        if (Value == 0) // error here: "Operator '==' cannot be applied to operands of type 'T' and 'int'"
            return StandardDigits[0];

        string strResult = "";
        bool IsValueNegative = (Value < 0); // error here: "Operator '<' cannot be applied to operands of type 'T' and 'int'"

        while (Value != 0) // error here: "Operator '!=' cannot be applied to operands of type 'T' and 'int'"
        {
            strResult = strResult.Insert(0, StandardDigits[Math.Abs(Value % Base)]); // error here: "Operator '%' cannot be applied to operands of type 'T' and 'T'"
            Value /= Base; // error here: "Operator '/=' cannot be applied to operands of type 'T' and 'T'"
        }

        if (IsValueNegative)
            strResult = strResult.Insert(0, "-");

        return strResult;
    }



    public static string Convert(long Value, long Base)
    {
        return ConvertInteger<long>(Value, Base);
    }

    public static string Convert(ulong Value, ulong Base)
    {
        return ConvertInteger<ulong>(Value, Base);
    }

    public static T Convert<T>(string Value, T Base)
    {
        return default(T); // TODO: convert a string back into an integer value.
    }
}

没有通用的约束只允许整数类型 - 最好的方法是限制为 IConvertible 但它也会允许浮点类型。我会将可以泛型化的部分进行泛型处理,并对不同的整数类型进行重载。 - D Stanley
为什么Base必须是T?如果它只允许值在2到64之间,为什么不能是byte或其他整数类型? - D Stanley
@DStanley 我们最初考虑过这个问题,但是发现如果我们保持相同的类型,Value % Base 和 Value /= Base 操作会更加顺畅。 - Giffyguy
“更顺畅”指的是什么?long%bytelong/= byte不应该需要强制类型转换或其他转换。 - D Stanley
1
你说“...为了提高性能...”。我的建议是,除非你有必须达到的真实性能指标,并且你没有达到这些指标,并且仔细的分析告诉你这个转换是问题所在,那么你才可以看看是否制作一个通用方法有所帮助。否则,只需使用最适合你需要的最大类型,并使用重载将其转换回来。你正在花费时间处理可能根本不重要的事情。 - Ed T
显示剩余2条评论
2个回答

4

由于不能定义一个只允许整数类型的通用约束,因此我会为 long 编写一个可用的方法,并为 int 重载:

private static string ConvertInteger(long Value, byte Base)
{
    if (Base < 2)
        throw new ArgumentOutOfRangeException("Base", Base, "The NumericStringConverter.Convert(Value, Base) method was called, with the Base parameter set to a value less than 2.");

    if (Base > 64) 
        throw new ArgumentOutOfRangeException("Base", Base, "The NumericStringConverter.Convert(Value, Base) method was called, with the Base parameter set to a value greater than 64.");

    if (Value == 0) 
        return StandardDigits[0];

    string strResult = "";
    bool IsValueNegative = (Value < 0); 

    while (Value != 0) 
    {
        strResult = strResult.Insert(0, StandardDigits[Math.Abs(Value % Base)]); 
        Value /= Base; 
    }

    if (IsValueNegative)
        strResult = strResult.Insert(0, "-");

    return strResult;
}
public static string Convert(int Value, byte Base)
{
    return ConvertInteger((long)Value, Base);
}

public static string Convert(ulong Value, byte Base)
{
    return ConvertInteger((long)Value, Base); // check for overflow?
}

是的,我们想要避免溢出检查 - 这容易出错,并且当强类型检查本身就足够时,过于复杂。在我看来,强类型检查应该足够了。 - Giffyguy
那么你将不得不为有符号和无符号类型编写不同的方法。除非你只允许一个字节数组和一些其他指示符来支持longulong的范围,否则你不能在同一个方法中同时支持它们的范围。 - D Stanley

4

C# 10 和 .NET 6:从 2021 年 11 月起的解决方案

好消息:现在在 .NET 6 和 C# 10 中有一个解决方案,参见https://devblogs.microsoft.com/dotnet/preview-features-in-net-6-generic-math/#generic-math

为了执行数学运算,您可以接受任何数字类型 INumber<T> 并指定所需的操作符,例如:

public T Adding<T>(T a, T b)
   where T : INumber<T>
   where T : IAdditionOperators<T, T, T>
{
   return a + b;
}

注意:截至我回答时,此功能仅为预览版。微软将在.NET 6的最终版本中保留此功能,因为他们仍想允许破坏性变化。要使用此功能,必须在项目配置中启用预览功能:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <EnablePreviewFeatures>true</EnablePreviewFeatures>
    <LangVersion>preview</LangVersion>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="System.Runtime.Experimental" Version="6.0.0-preview.7.21377.19" />
  </ItemGroup>

</Project>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接