繁体   English   中英

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

[英]How to implement a C# generic class whose methods are generic on structs?

假设我们有这对结构,它是广泛使用的交换格式的一部分(所以我们不能修改源 - 理想情况下不应该这样做:我们不是试图改变数据本身):

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

还有一个 class ,其核心是一个或另一个列表,并包含许多算法,这些算法对于任一结构实现几乎相同,但必须引用 3 元素结构( z )中的额外成员:

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

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

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;
}

特别注意:结构特定的方法已经存在(数百万个),由缺少任何内置 Generics 的 API 提供。 因此,例如,我们可以自信地编写一个算法来使用FOREIGN_API中的方法,因为知道源代码的一个副本(但使用泛型)将以任何一种方式绑定到可接受的实现:

public float FOREIGN_API_Average( Vector2 input );
public float FOREIGN_API_Average( Vector3 input );

我试图在这里解决的问题大约是:

  1. 这是上面示例中的// if Vector2:概念部分,我无法弄清楚如何在 C# 中执行。
  2. 我只是不确定如何构建它。 感觉就像我必须做一些巧妙的技巧来说明“我有任意 generics 参数。但我有这个 class 的一些特定版本,我将在内部实现。(所有其他非特定版本暗示非法,我会实现一组抛出异常的方法)”。 但是...我不知道如何解决这个问题。
  3. 结构不能相互“扩展”。 如果它们是类,我将从使用where T: BaseVector和 go 约束使用类(Mesh)开始。 但这对于结构是不可能的。
  4. 这些结构来自我不拥有的(十亿美元)软件; 有很多架构决策我希望他们做出不同的决定,但是 TL;DR:必须使用我们所拥有的。
  5. 这个问题不仅仅是两个结构:为了支持 3D 数学,我必须重新实现Vector1Vector2Vector3Vector4的所有内容......我不想复制/粘贴很多代码!
  6. ...而典型的 Mesh class 有 4 个内部列表,每个列表都可以是这 4 种类型中的任何一种。 如果我必须手动编写每个组合,而不使用 Generics,我将有4^4 = 256 个相同代码的副本来编写和维护。

真的让你羡慕 C++ 和他们愚蠢的性感模板,不是吗?

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

你说过网格类型可以有四个不同的列表,所以我假设它的签名是Mesh<T1, T2, T3, T4> 我还假设您控制这种类型,但不是VectorN类型。

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

所以你想要做的事情是双重调度的变体 - 根据其 arguments 的类型调用不同的方法。

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

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<,,,>方法数百万次,否则应该没问题。 但是假设您可能想要这样做,有一种方法可以欺骗我们摆脱这种情况。

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

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>实现的新实例。 我们可以将它们设为单例,并且永远不会为一种类型的向量实例化多个 object。 这是非常安全的,因为无论如何实现总是无状态的。

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}.");
    }
}

第二,我们不需要在每次实例化新网格时都进行类型检查。 很容易看出,对于具有相同类型 arguments 的网格类型的每个 object,实现保持不变。 就单个封闭泛型而言,它们是 static。 因此,我们真的可以把它们做成 static:

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个实现IVectorOperations<>的对象,并执行与不同网格类型一样多的额外类型检查,因此最多4^N 单个网格对象不使用任何额外的 memory,但最多有4^N * 4对矢量操作实现的引用。

这仍然迫使您为不同类型实现所有向量操作四次。 但请注意,现在您已解锁所有选项 - 您拥有一个通用界面,该界面取决于您控制的TVector类型。 允许VectorOperations实现中的任何技巧。 在通过IVectorOperations<TVector>接口与Mesh分离的同时,您可以灵活地使用。

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

(我认为这行不通,但这是我最初尝试 go 的方向 - 欢迎评论,也许它会激励其他人提供更好的答案:)

我想我可以做类似的事情(如果我没记错的话,可能在 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 )
    {
        ...
    }

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

        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();
        }

或者也许只是一种将结构转换为其他类型的更易于使用的可枚举的方法。

我的建议是让 Vector2 和 Vector3 自带处理方法。 接口是您正在寻找的机器人:

  • 让他们实现具有您需要的功能的接口
  • 使用该接口作为提交到您列表中的任何内容的类型
  • 调用接口函数

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

这些可能是Vector struct的内置实现。 当然这两个是不能继承的。 但是MVVM的做事方式是:“如果你不能继承或修改它,就将它包装成你可以继承和修改的东西”。

一个简单的包装器(它可以是结构或类)围绕实现接口的那些向量之一就是您所需要的。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM