如何在.NET 7中为INumber创建一个通用变量?

ibps3vxo  于 2022-11-26  发布在  .NET
关注(0)|答案(4)|浏览(156)

我们可以使用.NET 7中的新INumber<TSelf> interface来引用任何数值类型,如下所示:

using System.Numerics;

INumber<int> myNumber = 72;
INumber<float> mySecondNumber = 93.63f;

然而,由于INumber中的类型约束,我们不能拥有一个可以容纳任何数值类型的泛型引用。下面的代码是无效的:

using System.Numerics;

INumber myNumber = 72;
myNumber = 93.63f;

我如何拥有一个任意数字对象的数组,并调用一个需要INumber<TSelf>对象的方法。

using System.Numerics;

object[] numbers = new object[] { 1, 2.5, 5, 0x1001, 72 };

for (int i = 0; i < numbers.Length - 1; i++)
{
    Console.WriteLine("{0} plus {1} equals {2}", numbers[i], numbers[i + 1], AddNumbers(numbers[i], numbers[i + 1]));
}

static T AddNumbers<T>(T left, T right) where T : INumber<T> => left + right;
jyztefdp

jyztefdp1#

医生:你不能。
您注意到INumber<TSelf>没有实现非泛型类型INumber,因为这会造成严重破坏。
当您必须声明

var numbers = new object[] { 1, 2.5, 5, 0x1001, 72 };

来保存INumber<T>值。
您也不能声明,例如类似于

var numbers = new INumber<>[] { 1, 2.5, 5, 0x1001, 72 };

因为您会遇到CS7003:“* 意外使用未绑定的泛型名称 *”
假设存在一个非泛型接口类型INumber,它是INumber<TSelf>的基类型(就像IEnumerableIEnumerable<T>的基类型一样)。
要使INumber有用,它必须包含操作符之类的东西--但这些操作符也必须是非泛型的--因此,例如,必须存在IAdditionOperators<TSelf,TOther,TResult> interface(它定义+操作符)的非泛型版本--它必须将INumber, INumber作为其参数。
现在,假设您有一个类型UserDefinedNumber : INumber<UserDefinedNumber>,并且您有

INumber a = 1d;
INumber b = new UserDefinedNumber(...);
var c = a + b;

那么,您希望a + b做什么呢?
由于运算符的左边和右边都是INumber类型,编译器会在a(即double)上使用+运算符的实现,但由于这是一个内置类型,它不可能有任何逻辑来处理将double添加到UserDefinedNumber的操作。

p1tboqfb

p1tboqfb2#

我如何拥有一个任意数字对象的数组,并调用一个需要INumber对象的方法。
问题是数组中所有元素的类型必须相同,因为数组只是一个内存块,第i个元素是内存中的一个位置,位于地址arrayStart + i*(elementSize),如果大小不同,它就不起作用。
因此,对于值类型,这是不可能的(它们可能具有不同的大小),但可以具有对象数组,然后每个元素可以具有任何类型(在值类型的情况下将被装箱)。
所以你需要创建一个对象数组,你可以在里面放float,int,等等.
我也不认为为所有的数字提供一个通用的接口有多大的意义,因为如果你想把float加到long上,你应该怎么做呢?转换成float还是long?把数字转换成最方便的类型会更清楚。

xxe27gdn

xxe27gdn3#

这是不可能的。INumber<TSelf>类型是这样声明的:

public interface INumber<TSelf> : 
    IComparable, 
    IComparable<TSelf>, 
    IEquatable<TSelf>, 
    IParsable<TSelf>, 
    ISpanParsable<TSelf>,
    System.Numerics.IAdditionOperators<TSelf,TSelf,TSelf>,
    System.Numerics.IAdditiveIdentity<TSelf,TSelf>,
    System.Numerics.IComparisonOperators<TSelf,TSelf,bool>,
    System.Numerics.IDecrementOperators<TSelf>,
    System.Numerics.IDivisionOperators<TSelf,TSelf,TSelf>,
    System.Numerics.IEqualityOperators<TSelf,TSelf,bool>,
    System.Numerics.IIncrementOperators<TSelf>,
    System.Numerics.IModulusOperators<TSelf,TSelf,TSelf>, 
    System.Numerics.IMultiplicativeIdentity<TSelf,TSelf>, 
    System.Numerics.IMultiplyOperators<TSelf,TSelf,TSelf>, 
    System.Numerics.INumberBase<TSelf>,        
    System.Numerics.ISubtractionOperators<TSelf,TSelf,TSelf>, 
    System.Numerics.IUnaryNegationOperators<TSelf,TSelf>, 
    System.Numerics.IUnaryPlusOperators<TSelf,TSelf> 
    where TSelf : INumber<TSelf>

正如您所看到的,所有接口都使用TSelf类型,因此INumber接口没有支持不同类型之间操作的协定,而只支持同一类型内的操作。
因为您有一个混合类型的列表,所以编译器没有机会在运行时检查操作数的实际类型是否是受支持的组合。

bejyjqdl

bejyjqdl4#

正如@阿威Ha Lee指出的,我们事先并不知道所有的具体实现,所以我们无法涵盖所有的可能性,如果可能性的数量是穷尽的,我们可以为每对可能性实现IAdditionOperators<TSelf,TOther,TResult>
然而,如果我们准备作出让步,还有另一种可能性。
引用arithmetic operators (C# reference)的一句话:
当算子为其他整数型别(sbyte、byte、short、ushort或char)时,它们的值会转换成int型别,也就是运算的结果型别。当算子为不同的整数或浮点型别时,它们的值会转换成最接近的包含型别(如果有这种型别存在)。
我们可能都同意这是有道理的。然而,为了论证起见,假设OP中的case对我们来说比舍入误差更重要。例如,接口只存在于我们的库中,我们想用它来修饰几个结构体,这些结构体都应该可以转换为相同的类型(例如double)。

using System.Numerics;

namespace ConsoleApp
{
    internal class Program
    {
        static void Main(string[] _)
        {
            var numbers = new IMyNumber[] { new MyInt(1), new MyDouble(2.5), new MyInt(5), new MyInt(72) };

            for (int i = 0; i < numbers.Length - 1; i++)
            {
                Console.WriteLine($"{numbers[i]} plus {numbers[i+1]} equals {numbers[i].Add(numbers[i + 1])}");
            }
        }
    }

    interface IMyNumber
    {
        double ToDouble();

        double Add(IMyNumber number);
    }

    interface IMyNumber<TSelf> : IMyNumber, IEquatable<TSelf> where TSelf : struct
    {
    }

    readonly struct MyDouble : IMyNumber<MyDouble>, IAdditionOperators<MyDouble, IMyNumber, double>
    {
        public MyDouble(double value)
        {
            Value = value;
        }

        public double Value { get; }

        public double ToDouble() => Value;

        #region Equality

        public override int GetHashCode() => Value.GetHashCode();

        public override bool Equals(object? obj)
        {
            var areEqual = obj switch
            {
                MyDouble other => this.Equals(other.Value),
                IMyNumber other => this.ToDouble() == other.ToDouble(),
                _ => false,
            };
            return areEqual;
        }

        public bool Equals(MyDouble other) => this.Value == other.Value;

        #endregion Equality

        #region Addition

        public double Add(IMyNumber number) => this + number;

        public static double operator +(MyDouble left, IMyNumber right)
            => left.ToDouble() + right.ToDouble();

        #endregion Addition

        public override string ToString() => Value.ToString();
    }

    readonly struct MyInt : IMyNumber<MyInt>, IAdditionOperators<MyInt, IMyNumber, double>
    {
        public MyInt(int value)
        {
            Value = value;
        }

        public int Value { get; }

        public double ToDouble() => Value; // implicit cast

        #region Equality

        public override int GetHashCode() => Value.GetHashCode();

        public override bool Equals(object? obj)
        {
            var areEqual = obj switch
            {
                MyInt other => this.Equals(other.Value),
                IMyNumber other => this.ToDouble() == other.ToDouble(),
                _ => false,
            };
            return areEqual;
        }

        public bool Equals(MyInt other) => this.Value == other.Value;

        #endregion Equality

        #region Addition

        public double Add(IMyNumber number) => this + number;

        public static double operator +(MyInt left, IMyNumber right)
            => left.ToDouble() + right.ToDouble();

        #endregion Addition

        public override string ToString() => Value.ToString();
    }
}

我们现在可以轻松地将MyFloat添加到混合中,而不必触及IMyNumber<>的以前实现。
使用类型别名会使它更加干净。

using MyNumber = System.Double;

但后来我开始看到重影。

相关问题