只针对某些类型的泛型类的隐式转换

5
假设我们有一个通用类的想法,名为Matrix<T>,其中T是数字类型(Complexdoublefloatint等)。
在C#中,从float转换为double,从double转换为Complex等,都是天然的隐式转换。通常规则是,从较小的类型到较大的类型存在隐式转换。到目前为止,一切都很好。
现在,想象我们正在实现我们的Matrix<T>类型。由于这个新类型在某种程度上也是数值型的(或者至少它持有数值),从Matrix<float>Matrix<double>,从Matrix<double>Matrix<Complex>等之间的隐式转换是很自然的。至少对于像乘法、加法等数学操作来说是很不错的。然而,这似乎无法正确地实现,因为隐式操作符至少要求一个类型与我们正在实现它的类相同。
例如:下面的代码即使可以解决我的问题,但编译器仍然无法编译。
public abstract partial class Matrix<T>
{
    /// <summary>
    /// Implicitly converts a matrix to double precision complex numbers.
    /// </summary>
    public static implicit operator Matrix<Complex64>(Matrix<double> matrix)
    {
        matrix.ToComplex();
    }

    /// <summary>
    /// Implicitly converts a matrix to double precision real numbers.
    /// </summary>
    public static implicit operator Matrix<double>(Matrix<float> matrix)
    {
        matrix.ToDouble();
    }
}

它无法编译,因为“CS0556用户定义的转换必须转换为或从封闭类型转换”,假设我对此感到满意,因为它是语言规范的一部分,但是难道没有其他方法可以实现吗?

例如,这也不会编译。

public abstract partial class Matrix<double>
{
    /// <summary>
    /// Implicitly converts a matrix to single precision real numbers.
    /// </summary>
    public static implicit operator Matrix<double>(Matrix<float> matrix)
    {
        matrix.ToDouble();
    }
}

有没有办法实现这件事,感觉很自然,所以我认为应该是可以实现的?

目前我已经创建了一个解决方法,使所有类型都能隐式转换为最大的类型,但是这并不能解决从Matrix<float>Matrix<double>的转换,它只能解决到Matrix<Complex>的转换。

public abstract partial class Matrix<T>
{
    /// <summary>
    /// Implicitly converts a matrix to double precision complex numbers.
    /// </summary>
    public static implicit operator Matrix<Complex64>(Matrix<T> matrix)
    {
        return matrix.Map(x =>
        {
            if (x is Numerics.Complex32)
            {
                var xc32 = (Numerics.Complex32)(object)x;
                return new Complex64(xc32.Real, xc32.Imaginary);
            }
            return new Complex64(Convert.ToDouble(x), 0);
        }, Zeros.AllowSkip);
    }
}

如果有人对这个问题的背景感兴趣,可以看一下https://github.com/mathnet/mathnet-numerics/issues/304
另一个解决此问题的选项可能是使用类似于“扩展运算符”的东西(类似于扩展方法),但这些在C#中不存在。

说实话,作为一个使用这种 API 的用户,我会担心那样的隐式转换。那么显式转换呢?对你来说可行吗? - Ondrej Tucny
@OndrejTucny 不好意思,我认为隐式转换是唯一的解决方法。 - Pawel Troka
2个回答

1
首先,我不确定在使用数字原始类型的泛型是一个好的选择。这是语言中相当严重的缺陷,短期内似乎没有任何计划解决它。阅读此SO答案获取更多信息。
  1. 没有数字限制,最好的方法是使用相当糟糕的struct
  2. 对于任何算术支持,如果你正在实现一个矩阵,你需要定义一个带有AddMultiply等的IArithmetic接口,而且你将在各个地方进行装箱和拆箱,这可能会对性能产生很大的影响。

    你的代码比这还要糟糕;因为你似乎缺乏一个通用的T接口,所以你需要将其强制转换为对象才能使通用转换成功。

    此外,如果你没有一个通用的接口代码类似于if (typeof(T) == typeof(Complex)) ...就开始出现了,这是在使用泛型时的一个很大的警告信号;一个泛型类/方法应该适用于无限数量的类型,而不仅仅是一些预设的类型,这就是“泛型”的含义。

我认为你应该退一步重新思考你的方法。当语言类型系统似乎与你作对而没有任何帮助时,这是你做错了什么的明确标志。

为什么不直接实现一个最大类型的非通用矩阵?一个复数矩阵。拥有浮点数或双精度浮点数矩阵的好处是什么?这不能是性能或内存效率,因为任何非通用解决方案都比你目前的方法更好,其中所有的装箱和拆箱都在进行中。
更新:在查看您所基于的库之后,我不确定为什么您不首先按照预期使用 Matrix 作为基本类型。
public class DoubleMatrix : Matrix<double>
{
    //now this is legal
    public static implicit operator DoubleMatrix(FloatMatrix matrix)
        => matrix.ToDouble();
}

在这里,FloatMatrix 显然是 FloatMatrix: Matrix<Float>


首先,我并不是从头开始实现这个功能。这是MathNet.Numerics库的分支(我已经链接到我正在尝试解决的问题)。说实话,这个库的代码相当不错,但我真的需要这个隐式转换能够工作。据我所知,他们允许使用float等数据类型是出于性能方面的考虑(与诸如Intel MKL之类的本地提供程序一起使用)。 - Pawel Troka
@PawelTroka 首先,这是你应该在问题中提前说明的相关信息。其次,你转换中的代码仍然是你自己的,将其强制转换为 object 只是为了让你的代码编译通过,在性能方面是可怕的。 - InBetween
有时很难决定什么更重要。就性能而言,我想我的实现建议中的选项2应该更快,但代码更丑陋(https://github.com/mathnet/mathnet-numerics/issues/304)。 - Pawel Troka
关于更新的答案 - 是的,这将起作用,但只有在我们使用从Matrix<T>继承的类型时才会起作用。编译器仍然不会编译以下代码(因为没有将其转换为Matrix<double>的Matrix<T>的隐式运算符):var floatMatrix = Matrix.Build.Dense(Size, Size); var doubleMatrix = Matrix.Build.Dense(Size, Size); var result = floatMatrix*doubleMatrix;CS0019 运算符“*”无法应用于类型为“Matrix<float>”和“Matrix<double>”的操作数这也是“从基类的转换”。 - Pawel Troka
好的,我已经确认了,它不会起作用,因为它是编译器错误“从基类转换”。 - Pawel Troka
@PawelTroka 哎呀!太糊涂了,没注意到,抱歉。你不能在运算符中使用泛型形式,必须使用其他具体实现。已编辑答案。这是唯一的解决方法,也是最好的解决方法,“Matrix<T>”并不是以开放形式来使用的,我认为它的目的仅仅是作为具体封闭类型的基础实现。 - InBetween

1
我知道的唯一创建通用向量或矩阵类并使其执行代数运算(加法,减法,乘法)的方法是发出调用运算符(例如 operator +)的 MSIL 代码。然后它可以与任何具有已定义 static MyType operator + (MyType a, MyTYpe b) 的类型以及内置类型一起使用。
有关更多详细信息,请参见 我的这个答案 中类似问题的解答。
使用 Operation<T> 中的静态方法,您可以拥有以下示例代码
public class Matrix<T>
{
    T[,] elements;

    static readonly Func<T,T> add = Operation<T>.Add;

    public static Matrix<T> operator + (Matrix<T> A, Matrix<T> B)
    {
       Matrix<T> result = new Matrix<T>(rows,cols);
       for(int i=0; i<rows; i++)
       {
         for(int j=0; j<cols; j++)
         {
            result[i,j] = add(A[i,j], B[i,j]);
         }
       }
       return result;
    }    
    // rest of algebra
}

所以如果您定义了一个具有定义的operator +Complex64类,您可以声明Matrix<Complex64>并直接执行像var C = A+2*B这样的代数运算。运行时会为内置类型或自定义类型调用适当的运算符。速度影响很小,因为查找要调用的适当方法的反射仅在首次使用时完成一次。

是的,这看起来是一种有趣的方法,但请注意,我正在分叉已经实现了大多数运算符的现有库。所以我实现所需行为的唯一方法是通过隐式转换,这是令人遗憾的。 - Pawel Troka
好的,不,那不是唯一的方法。那只是你想要存在的简单方式。但是,还有其他方式,只是不容易。 - quetzalcoatl

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