解决我的类层次结构问题的更好方法,而不是 Unsafe.As 强制转换为子类



(我写的是矩阵,但别担心,这不是数学问题,假设没有数学知识。

我有一个Matrix类,有三个字段

double[] Data;
int Rows;
int Columns;

并定义了数百种数学运算。

此外,我有一个没有自己的实例字段的SymmetricMatrix : Matrix子类,它通过继承提供上述所有操作,提供一些额外的操作(如 EigenvalueDecomposition),最后使用new关键字重复一些定义以将返回类型更改为SymmetricMatrix

例如,如果Matrix

public Matrix MatrixMultiplication(Matrix otherMatrix)
public Matrix ScaledBy(double scalar)

然后SymmetricMatrix将简单地继承MatrixMultiplication但它会更改ScaledBy以返回SymmetricMatrix

我没有重新实现ScaledBy,而是通过以下方式定义它

// SymmetricMatrix.cs
public new SymmetricMatrix ScaledBy(double scalar) => base.ScaledBy(scalar).AsSymmetricMatrix()
// Matrix.cs
public SymmetricMatrix AsSymmetricMatrix() => Unsafe.As<SymmetricMatrix>()

(我使用new而不是virtual+override,原因对于这个问题的目的无关紧要)。

我发现这种方法的效果出奇地好,它让我在定义SymmetricMatrix.cs时非常简洁。明显的缺点是它可能会利用不受支持的行为(?),并且会混淆我经常使用的调试器(强制转换结果的运行时类型是Matrix它不是其编译时类型的子类SymmetricMatrix,但是,上定义的所有操作SymmetricMatrix成功,因为两个类保存的数据相同)

问题

  1. 您是否预见到我的方法存在我没有遇到的任何问题?

  2. 我的方法可能会与下一个 dotnet 版本中断吗?

  3. 你认为有更清洁的方法来实现我想要的东西吗?我有一些想法,但似乎没有一个干净利落。例如,我无法通过以下方式对哪些操作保留父类中的子类进行编码

class Matrix<T> where T : Matrix
...
T ScaledBy(double other)

因为操作是否保留子类是只有子类才能拥有的知识。例如,SymmetricPositiveDefiniteMatrix不会被ScaledBy保留。

或者,我可以使用封装而不是继承并定义

class BaseMatrix
{
...lots of operations
}
...
class Matrix : BaseMatrix
{
private BaseMatrix _baseMatrix;
...lots of "OperationX => new Matrix(_baseMatrix.OperationX)"
}
class SymmetricMatrix : BaseMatrix
{
private BaseMatrix _baseMatrix;
...lots of "OperationX => preserving ? new SymmetricMatrix(_baseMatrix.OperationX) : new Matrix(_baseMatrix.OperationX);"
}

不过,编码起来非常难过,因为我必须手动将我所做的每个更改传播到BaseMatrix,目前,四个额外的类,并且用户必须在很多情况下手动将SymmetricMatrix转换为Matrix

最后,我不能简单地提供子类,而是使用像bool _isSymmetric这样的标志,并对保持对称/积极性/...通过更改/设置我所有方法中的标志。这也是可悲的,因为:

  1. 然后,用户将能够编写Matrix.EigenvalueDecomposition()只是为了在运行时出现You have to promise me that your matrix is symmetric because I only implemented EigenvalueDecomposition for that case错误而

    中断
  2. 当标志
  3. 未保留时,很容易忘记重置标志,然后意外运行假定对称的操作(BLAS 通过简单地忽略矩阵的一半来做到这一点)

  4. 我喜欢能够在类型系统中指定而不是注释,例如,我从CholeskyFactor返回的矩阵是下三角形的(约定不同,有些人可能会期望上三角形矩阵)

  5. 用户不会看到当前设置了哪些标志,因此他们不知道我是否使用了给定算法的"专用"版本,并且为了安全起见,最终可能会到处不必要地使用.SetSymmetric()

除非我错过了要求,否则您可以使用泛型实现您想要的内容

一个(不太简短)的例子:

public abstract class BaseMatrix<TMatrix> where TMatrix : BaseMatrix<TMatrix>
{
// Replace with how you actuallt are storing you matrix values
protected object _valueStore;
// common initialization (if required), if not keep empty
protected BaseMatrix()
{
}
// necessary to copy the value store.
protected BaseMatrix(object valueStore)
{
this._valueStore = valueStore;
}
public virtual TMatrix ScaledBy(double scalar) 
{
// implementation
return default;
}
}
public class Matrix : BaseMatrix<Matrix>
{
}
public class SymmetricMatrix : BaseMatrix<SymmetricMatrix>
{
public SymmetricMatrix() : base(){}
// allows to build from another matrix, could be internal
public SymmetricMatrix(object valueStore) : base(valueStore){}
public override SymmetricMatrix ScaledBy(double scalar)
{
return base.ScaledBy(scalar);
}
}
public class SymmetricPositiveDefiniteMatrix : BaseMatrix<SymmetricPositiveDefiniteMatrix> 
{
// If we *know* the type is not the same after this method call, we mask with a new one retirning the target type
// A custom cast operator will handle the conversion
public new SymmetricMatrix ScaledBy(double scalar)
{
return base.ScaledBy(scalar);
}
// Added to allow a cast between SymmetricPositiveDefiniteMatrix  and SymmetricMatrix
// in this example, we keep the value store to not have to copy all the values
// Depending on project rules could be explicit instead
public static implicit operator SymmetricMatrix(SymmetricPositiveDefiniteMatrix positiveSymmetricMatrix)
{
return new SymmetricMatrix(positiveSymmetricMatrix._valueStore);
}
}

这将使ScaledBy(double)方法在每种类型的矩阵中可用,并返回调用它的矩阵的类型。

并且它将符合您的要求,不必在每个类中重新定义大多数方法/属性。

显然,您可以重写 child 中的方法,以便在base()调用之前或之后执行更多操作,甚至可以定义完全不同的实现。

一个缺点是,如果您需要一个类型从继承自BaseMatrix<TMatrix>的类继承。您无法更改泛型类型参数。

或者,如果这确实不符合您的需求并且更喜欢采用封装路线,您可以查看源生成器,因为代码似乎非常重复。

当我们和同事谈论过程编程时,这是我关于你的问题的 2 美分。 底层数据结构具有(并保持其)特定形式:

struct DataMatrix
{
public double[] Data;
public int Rows;
public int Columns;
}

由于您根据类型具有特定的行为,因此可以在专用类型中标记它,而不是强类型:

struct Matrix
{
public DataMatrix DataMatrix;
public bool isSymmetrical;
public bool isDefinite;
public bool isUpperTriangular;
}

然后,您可以使用所述标记类型从一个地方调用您的方法:

// MatrixManager acts solely as a namespace for convenient method grouping
public static class MatrixManager
{
//Consider a void return for each method, as matrix.DataMatrix is(/may be) the only affected field
public static Matrix Multiplication(Matrix matrix, Matrix otherMatrix) => /*..multiply matrix.DataMatrix with otherMatrix.DataMatrix then return matrix.*/;
public static Matrix ScaledBy(Matrix matrix, double scalar) => /*..scale matrix.DataMatrix with scalar then return matrix.*/;
public static void SetSymmetrical(Matrix matrix) { if (!matrix.isSymmetrical) matrix.isSymmetrical = true; }
public static Matrix UnavailableOperationIfSymmetricalExample(Matrix matrix) => matrix.isSymmetrical ? matrix : EigenvalueDecomposition(matrix);
/*..hundreds of Matrix-based methods here..*/
public static Matrix EigenvalueDecomposition(Matrix matrix) { if (matrix.isSymmetrical) return matrix; /*..otherwise do decompose matrix.DataMatrix then return matrix.*/}
}

这样,您可以在调用具有任何标记Matrix类型的方法时指定实际行为(抛出异常?调用另一个方法?返回?),而不必为行为类型的纠结结构而烦恼。

希望这能满足您的需求。

我认为使用Unsafe.As()确实不安全。

一个主要问题是:

Matrix mat = new Matrix(/* whatever */);
SymmetricMatrix smat = mat.AsSymmetricMatrix();
Console.WriteLine(smat.GetType().FullName); // "Matrix"

因此,您声明了SymmetrixMatrix的东西,但其底层类型实际上是Matrix。我相信你可以想象可能导致的那种可怕的问题......

这种方法还要求基类知道其派生类 - 这是一个很大的禁忌。

解决此类问题的一种方法是确保您可以以继承的方式克隆对象。然后,您可以克隆原始对象并返回它们,如有必要,可以使用强制转换。

例如:

public class Matrix
{
public Matrix()
{
// Whatever
}
// Copy constructor; used for cloning.
public Matrix(Matrix other)
{
_data = other._data.ToArray();
}
public virtual Matrix ScaledBy(double scalar)
{
var result = Clone();
result.ScaleBy(scalar);
return result;
}
protected void ScaleBy(double scalar)
{
for (int i = 0; i < _data.Length; ++i)
{
_data[i] *= scalar;
}
}
protected virtual Matrix Clone()
{
return new Matrix(this);
}
readonly double[] _data = new double[16]; // For illustration.
}
public class SymmetricMatrix: Matrix
{
public SymmetricMatrix()
{
// Whatever
}
// Copy constructor; used for cloning.
public SymmetricMatrix(SymmetricMatrix other): base(other)
{
// No new fields to copy.
// Any newly added fields should be copied here.
}
public override SymmetricMatrix ScaledBy(double scalar)
{
// This cast will work because the underlying type returned from 
// base.ScaledBy() really is SymmetricMatrix
return (SymmetricMatrix)base.ScaledBy(scalar);
}
protected override SymmetricMatrix Clone()
{
return new SymmetricMatrix(this);
}
}
public class SymmetricPositiveDefiniteMatrix: Matrix
{
// Doesn't override ScaledBy() so SymmetricPositiveDefiniteMatrix.ScaledBy() will return a Matrix.
protected override SymmetricPositiveDefiniteMatrix Clone()
{
// If SymmetricMatrix ever adds new fields to be cloned, they should be cloned here.
return Unsafe.As<SymmetricPositiveDefiniteMatrix>(base.Clone());
}
}

现在像这样的代码返回预期的类型:

var smatrix = new SymmetricMatrix();
var sresult = smatrix.ScaledBy(1.0);
Console.WriteLine(sresult.GetType().FullName); // "SymmetricMatrix"
var spmatrix = new SymmetricPositiveDefiniteMatrix();
var spresult = spmatrix.ScaledBy(1.0);
Console.WriteLine(spresult.GetType().FullName); // "Matrix"

作为可能导致奇怪结果Unsafe.As()代码的示例,请考虑以下代码:

using System;
using System.Runtime.CompilerServices;
namespace ConsoleApp1;
static class Program
{
public static void Main()
{
var myBase = new MyBaseClass();
var pretendDerived = myBase.AsMyDerivedClass();
// Given the definition of MyDerivedClass, what do you think this will print?
Console.WriteLine(pretendDerived.Name());
}
}
public class MyBaseClass
{
public MyDerivedClass AsMyDerivedClass() => Unsafe.As<MyDerivedClass>(this);
}
public class MyDerivedClass
{
readonly string _name = "TEST";
public string Name() => _name;
}

正如Unsafe.As()的文档所述:

Unsafe.As 只有当典型的 "安全"铸造操作(T)o会成功。使用此 API 来 规避否则会失败的强制转换不受支持,并且 可能导致运行时不稳定。

感谢所有创造性的答案,所有这些都是我从中汲取灵感的。 我最终将封装和继承结合起来,使用纯数据TwoDimensionalArray和仅方法(加上单个TwoDimensionalArray字段)Matrix类的继承:

class TwoDimensionalArray
{
double[] data;
int Rows;
int Columns
double Scale;
}
class Matrix
{
internal readonly TwoDimensionalArray _array;
public int Rows => _array.Rows;
public int Columns => _array.Columns;
public static Ones(int Rows, int Columns)
{
var data = new double[Rows * Column];
data.AsSpan().Fill(1);
return new Matrix(data, Rows, Columns, 1);
}
internal Matrix(TwoDimensionalArray array)
{
_array = array;
}
public Matrix ScaledBy(double scalar)
{
var output = Clone();
output.ScaleBy(scalar);
return output;
}
public void ScaleBy(double scalar)
{
_array.Scale *= scalar;
}
public Matrix Clone()
{ 
var newData = new double[array.data.Length];
array.data.AsSpan().CopyTo(newData);
return new Matrix(new TwoDimensionalArray(newData, Rows, Columns);
}
}
public class SymmetricMatrix : Matrix 
{ 
internal SymmetricMatrix(TwoDimensionalArray array) => base(array);
public new SymmetricMatrix ScaledBy(double scalar) => base.ScaledBy(scalar).AsSymmetricMatrix();

public (Matrix, double[]) EigenvalueDecomposition()
{
//implementation
}

public new SymmetricMatrix Clone() => base.Clone().AsSymmetricMatrix();
}
class SymmetricPositiveDefiniteMatrix : SymmetricMatrix
{
internal SymmetricPositiveDefiniteMatrix(TwoDimensionalArray array) => base(array);
public Matrix CholeskyFactor()
{
//implementation
}

public new SymmetricPositiveDefiniteMatrix Clone() => base.Clone().AsSymmetricMatrix();
}
public static class MatrixExt 
{
public SymmetricMatrix AsSymmetricMatrix(this Matrix matrix)
{
return new SymmetricMatrix(matrix._array);
}
public SymmetricPositiveDefiniteMatrix AsSymmetricMatrix(this Matrix matrix)
{
return new SymmetricPositiveDefiniteMatrix(matrix._array);
}
}

请注意,.AsSymmetricMatrix()的结果是一个视图,而不是克隆(类似于,例如,myArray.AsSpan()数组的视图,而不是副本)。我需要TwoDimensionalArray是一个类,而不是一个结构,以便对视图的所有更改(包括重新缩放和重塑)都反映在原始视图中。

最新更新