如何实现方法在结构上通用的C#通用类?

时间:2019-11-17 21:46:49

标签: c# struct

假设我们有这对结构,它们是广泛使用的交换格式的一部分(因此,我们无法修改源代码-理想情况下不希望:我们不尝试更改数据本身):

struct Vector2 { public int x; public int y; }
struct Vector3 { public int x; public int y; public int z; }

是一个类,其核心是一个或另一个的列表,并且包含许多算法,对于任何一个结构,该算法几乎都是相同的,但是必须引用3元素结构中的额外成员({{ }}):

z

...如何正确实现处理它们的方法套件?例如:

public class Mesh<T>
{
    private List<T> _myVectors;
}

尤其要注意:特定于结构的方法已经存在(数以百万计),由缺少任何内置泛型的API提供。因此,例如我们可以放心地编写一种算法来使用public int Average() { // if Vector2: int sum = 0, count = 0; foreach( var v2 in _MyVectors ) { sum += v2.x + v2.y; count++; } // if Vector3: int sum = 0, count = 0; foreach( var v3 in _MyVectors ) { sum += v3.x + v3.y + v3.z; count++; } return sum/count; } 中的方法,因为知道源代码的一个副本(但使用泛型)将以任何一种方式绑定到可接受的实现:

FOREIGN_API

我想缠住这个问题的大概是:

  1. 这是上面示例中的public float FOREIGN_API_Average( Vector2 input ); public float FOREIGN_API_Average( Vector3 input ); 概念部分,我不知道该如何使用C#。
  2. 我只是不确定如何构造它。感觉我必须做一些巧妙的技巧来说明“我具有任意的泛型参数。但是我有一些特定版本的此类将在内部实现。(所有其他非特定版本都隐含了非法,我将实现一组抛出异常的方法”)。但是...我不确定如何实现这一目标。
  3. 结构不能彼此“扩展”。如果它们是类,那么我将以// if Vector2:约束使用类(Mesh),然后从那里开始。但这对于结构是不可能的。
  4. 结构来自我不拥有的(十亿美元)软件;我希望有很多架构上的决定可以有所不同,但是TL; DR:可以根据我们已有的东西来工作。
  5. 这个问题不仅仅是两个结构:要支持3D数学,我必须为where T : BaseVectorVector1Vector2Vector3重新实现所有功能。 。我不想复制/粘贴很多代码!
  6. ...并且典型的Mesh类具有4个内部列表,每个列表可以是这4种类型中的任何一种。如果我必须手动编写每个组合,而不使用泛型,那么我将有 4 ^ 4 = 256个相同代码的副本进行编写和维护。

4 个答案:

答案 0 :(得分:1)

确实让您羡慕C ++及其愚蠢的性感模板,不是吗?

首先进行一些假设(如果错误,请更正):

您已经说过,网格类型可以有四个不同的列表,因此我假设其签名为Mesh<T1, T2, T3, T4>。我还假设您控制此类型,但不能控制VectorN类型。

问题是您缺乏对Vector的任何一般支持,并且不能以任何方式对它们使用多态。如您所说,将它们包装在接口中或引入自定义类作为包装器会降低性能。

因此,您要做的是对双调度进行修改-根据其参数类型调用其他方法。

想到的最简单的事情是现有FOREIGN_API调用的静态包装:

public static class VectorExtensions
{
    public static int Sum<TVector>(this IEnumerable<TVector> vectors)
    {
        var type = typeof(TVector);
        if (type == typeof(Vector1))
        {
            return FOREIGN_API.Sum((IEnumerable<Vector1>)vectors);
        }
        else if (type == typeof(Vector2))
        {
            return FOREIGN_API.Sum((IEnumerable<Vector2>)vectors);
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

然后,在网格上实现Average很容易(我假设平均值是所有列表的平均值):

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;

    public float Average()
    {
        var sum1 = _myVectors1.Sum();
        var sum2 = _myVectors2.Sum();
        var sum3 = _myVectors3.Sum();
        var sum4 = _myVectors4.Sum();

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

这种类型的类型检查应该很快,因为C#会大量优化typeof调用。

有一种更简单的编写方法,涉及dynamic

public static class VectorExtensions
{
    public static int Sum<TVector>(this IEnumerable<TVector> vectors) =>
        FOREIGN_API.Sum((dynamic)vectors);
}

dynamic基础结构的速度也比许多人预期的要高,因为缓存,因此您可能想先尝试一下此解决方案,然后仅在诊断出性能问题时再考虑其他问题。如您所见,这只需要少量的代码即可尝试。

================================================ =============================

现在让我们假设我们正在寻找性能最好的方法。我非常确信,没有完全避免运行时类型检查的方法。在上面的案例中,每个方法调用只有很少的类型检查。除非您数百万次调用Mesh<,,,>方法,否则应该没问题。但是,假设您可能想要这样做,那么有一种方法可以帮助我们摆脱困境。

这个想法是在实例化网格物体时执行所需的所有类型检查。让我们定义助手类型,我们将为VectorOperationsN类型中所有可能的N调用VectorN。它将实现一个接口IVectorOperations<TVector>,该接口将定义您想要的基本矢量操作。现在,以Sum为例,介绍一个或多个向量:

public interface IVectorOperations<TVector>
{
    public int Sum(TVector vector);

    public int Sum(IEnumerable<TVector> vectors);
}

public class VectorOperations1 : IVectorOperations<Vector1>
{
    public int Sum(Vector1 vector) => vector.x;

    public int Sum(IEnumerable<Vector1> vectors) => vectors.Sum(v => Sum(v));
}


public class VectorOperations2 : IVectorOperations<Vector2>
{
    public int Sum(Vector2 vector) => vector.x + vector.y;

    public int Sum(IEnumerable<Vector2> vectors) => vectors.Sum(v => Sum(v));
}

现在,我们需要一种获取适当实现的方法-这将涉及类型检查:

public static class VectorOperations
{
    public static IVectorOperations<TVector> GetFor<TVector>()
    {
        var type = typeof(TVector);

        if (type == typeof(Vector1))
        {
            return (IVectorOperations<TVector>)new VectorOperations1();
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

现在,当我们创建网格时,我们将获得适当的实现,然后在我们的方法中全部使用它:

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;
    private readonly IVectorOperations<T1> _operations1;
    private readonly IVectorOperations<T2> _operations2;
    private readonly IVectorOperations<T3> _operations3;
    private readonly IVectorOperations<T4> _operations4;

    public Mesh()
    {
        _operations1 = VectorOperations.GetFor<T1>();
        _operations2 = VectorOperations.GetFor<T2>();
        _operations3 = VectorOperations.GetFor<T3>();
        _operations4 = VectorOperations.GetFor<T4>();
    }

    public float Average()
    {
        var sum1 = _operations1.Sum(_myVectors1);
        var sum2 = _operations2.Sum(_myVectors2);
        var sum3 = _operations3.Sum(_myVectors3);
        var sum4 = _operations4.Sum(_myVectors4);

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

这仅在实例化网格时有效并且进行类型检查。成功!但是我们可以使用两个技巧来进一步优化。

一个,我们不需要IVectorOperations<TVector>实现的新实例。我们可以使它们成为单例,并且对于一种类型的向量,永远不要实例化多个对象。这是绝对安全的,因为无论如何这些实现​​始终都是无状态的。

public static class VectorOperations
{
    private static VectorOperations1 Implementation1 = new VectorOperations1();
    private static VectorOperations2 Implementation2 = new VectorOperations2();
    ... // etc.

    public static IVectorOperations<TVector> GetFor<TVector>()
    {
        var type = typeof(TVector);

        if (type == typeof(Vector1))
        {
            return (IVectorOperations<TVector>)Implementation1;
        }
        else if (...) // etc.

        throw new ArgumentException($"Invalid type of vector {typeof(TVector).Name}.");
    }
}

第二,我们每次实例化一个新的网格时并不需要进行类型检查。很容易看出,对于具有相同类型参数的网格类型的每个对象,实现都保持不变。就单个封闭的通用类型而言,它们是静态的。因此,我们真的可以将它们设为静态:

public class Mesh<T1, T2, T3, T4>
{
    private List<T1> _myVectors1;
    private List<T2> _myVectors2;
    private List<T3> _myVectors3;
    private List<T4> _myVectors4;
    private static readonly IVectorOperations<T1> Operations1 =
        VectorOperations.GetFor<T1>();
    private static readonly IVectorOperations<T2> Operations2 =
        VectorOperations.GetFor<T2>();
    private static readonly IVectorOperations<T3> Operations3 =
        VectorOperations.GetFor<T3>();
    private static readonly IVectorOperations<T4> Operations4 =
        VectorOperations.GetFor<T4>();

    public float Average()
    {
        var sum1 = Operations1.Sum(_myVectors1);
        var sum2 = Operations2.Sum(_myVectors2);
        var sum3 = Operations3.Sum(_myVectors3);
        var sum4 = Operations4.Sum(_myVectors4);

        return (float)(sum1 + sum2 + sum3 + sum4) / 
            (_myVectors1.Count + _myVectors2.Count + _myVectors3.Count + _myVectors4.Count);
    }
}

这样,如果存在N个不同的向量类型,我们将只实例化实现N的{​​{1}}个对象,并执行与存在不同网格类型一样多的附加类型检查,因此最多IVectorOperations<>。各个网格对象不会占用任何额外的内存,但是最多有4^N个引用指向矢量操作实现。

这仍然迫使您针对不同类型四次执行所有矢量操作。但是请注意,现在您已经解锁了所有选项-您拥有一个通用接口,该接口取决于您控制的4^N * 4类型。 TVector实现中的任何技巧都是允许的。在通过VectorOperations接口与Mesh分离的同时,您可以在其中灵活地进行操作。

哇,答案很长。感谢您参加我的TED演讲!

答案 1 :(得分:0)

(我不认为这可行,但这是我一开始尝试的方向-欢迎发表评论,也许会激发其他人提供更好的答案:)

我以为我可以做类似的事情(如果我没记错的话,在C ++中是可能的,并且C#在完全普通的情况下没有直接等价物,但我认为在这样的简单情况下可能有一个等价物):

public class Mesh<T1,T2>
{
 // This class is basically going to fail at runtime:
 //   it cannot/will not prevent you from instancing it
 //   as - say - a Mesh<string,int> - which simply cannot
 //   be sensibly implemented.
 //
 // So: many methods will throw Exceptions - but some can be implemented
 //   (and hence: shared amongst all the other variants of the class)

     public List<T1> internalList;
     public int CountElements<List<T1>>() { return internalList.Count; }
     public int DoSomethingToList1<T1>() { ... }
}

public class Mesh<Vector2,T2>
{
     // Now we're saying: HEY compiler! I'll manually override the
     //    generic instance of Mesh<T1,T2> in all cases where the
     //    T1 is a Vector2!

     public int DoSomethingToList1<Vector2>() { ... }
}

或者另一种尝试找到在语法上有效的方法来做同样的事情(参见@Gserg对主要问题的评论)-但显然这失败了,因为C#编译器禁止任意类型转换:

    private List<T1> data;
    public void Main()
    {
        if( typeof(T1) == typeof(Vector2) )
            Main( (List<Vector2>) data );
        else if( typeof(T1) == typeof(Vector3) )
            Main( (List<Vector3>) data );
    }

    public void Main( List<Vector2> dataVector2s )
    {
        ...
    }
    public void Main( List<Vector3> dataVector3s )
    {
        ...
    }

答案 2 :(得分:0)

我不确定这是否是您想要的,但是也许您可以通过一些运行时编译来解决。例如,您可以生成一个对结构的字段求和的委托;

        public Func<T, int> Sum { get; private set; }
        public void Compile()
        {
            var parm = Expression.Parameter(typeof(T), "parm");
            Expression sum = null;

            foreach(var p in typeof(T).GetFields())
            {
                var member = Expression.MakeMemberAccess(parm, p);
                sum = sum == null ? (Expression)member : Expression.Add(sum, member);
            }
            Sum = Expression.Lambda<Func<T, int>>(sum, parm).Compile();
        }

或者只是将结构转换为其他易于枚举的枚举的方法。

答案 3 :(得分:-2)

我的建议是让Vector2和Vector3带来他们自己的处理方法。接口是您要寻找的机器人:

  • 让他们使用所需功能实现接口
  • 使用该接口作为列表中所有内容的类型
  • 调用界面函数

显示过程的合适名称为“ Sumable”。

这些可能是Vector struct的内置实现。当然那两个不能被继承。但是MVVM的处理方式是:“如果您无法继承或修改它,请将其包装为您可以 继承和修改的东西。”

您只需要做一个简单的包装程序(可以是结构或类),即可围绕实现接口的向量之一。

另一个选择是使用LINQ进行处理。如果这只是一个一次性的过程,那么它通常要轻得多,然后要一路继承到继承,类,接口等。