C# 泛型方法选择

9

我正在尝试使用C#编写通用算法,可以处理不同维度的几何实体。

在下面的例子中,我有Point2Point3,它们都实现了一个简单的IPoint接口。

现在我有一个函数GenericAlgorithm,调用一个名为GetDim的函数。根据类型,有多个定义此函数的定义。还有一个对于任何实现IPoint的事物都适用的回退函数。

我最初希望以下程序的输出为2, 3。但是,实际上它是0, 0。

interface IPoint {
    public int NumDims { get; } 
}

public struct Point2 : IPoint {
    public int NumDims => 2;
}

public struct Point3 : IPoint {
    public int NumDims => 3;
}

class Program
{
    static int GetDim<T>(T point) where T: IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 0 !!
        Console.WriteLine("{0:d}", d2);        // returns 0 !!
    }
}

好的,由于某些原因,在GenericAlgorithm中具体类型信息丢失了。我不完全明白为什么会发生这种情况,但没关系。如果我无法通过这种方式实现,我有哪些其他选择呢?


2
还有一种备用函数,它的目的是什么呢?实现接口的整个目的就是保证 NumDims 属性可用。为什么在某些情况下会忽略它呢? - John Wu
基本上,它可以编译。最初,我认为如果在运行时JIT编译器找不到GetDim的专门实现(即我传递一个Point4但是GetDim<Point4>不存在),则需要回退函数。然而,编译器似乎并不费心去寻找专门的实现。 - mohamedmoussa
1
@woggy:你说“编译器似乎并不会寻找专门的实现”,好像这是设计师和实现者懒惰的问题。但事实并非如此,这是与.NET中泛型表示方式有关。这不同于C++中的模板化专业化。泛型方法不会为每个类型参数单独编译 - 它只编译一次。当然,这样做有利有弊,但这不是“烦恼”的问题。 - Jon Skeet
@jonskeet 如果我的语言选择不当,我很抱歉,我相信这里有一些我没有考虑到的复杂性。 我的理解是编译器不会为引用类型编译单独的函数,但对于值类型/结构体,它会这样做,这正确吗? - mohamedmoussa
@woggy:这是JIT编译器,与C#编译器完全不同 - 而且是C#编译器执行重载决策。通用方法的IL仅生成一次 - 而不是每个特化生成一次。 - Jon Skeet
4个回答

10
这个方法:
static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim(point);

该代码将始终调用GetDim<T>(T point)。重载决议是在编译时完成的,在那个阶段没有其他适用的方法。

如果您希望在执行时调用重载决议,则需要使用动态类型,例如:

static int GenericAlgorithm<T>(T point) where T : IPoint => GetDim((dynamic) point);

但通常更好的做法是使用继承 - 在您的示例中,显然您可以只有一个方法并返回point.NumDims。我想在您的实际代码中,相当的操作可能更加棘手,但如果没有更多上下文,我们无法建议如何使用继承来执行特化。这些是您的选项:

  • 继承(首选)用于基于目标的执行时类型进行特化
  • 动态类型用于执行时间重载分辨率

实际情况是我有一个 AxisAlignedBoundingBox2AxisAlignedBoundingBox3。我有一个 Contains 静态方法,用于确定一个集合的盒子是否包含 Line2Line3(取决于盒子的类型)。两种类型之间的算法逻辑完全相同,只是维度数不同。内部还需要调用 Intersect,需要专门针对正确的类型进行处理。我想避免虚函数调用/动态,这就是为什么我使用泛型的原因...当然,我也可以复制/粘贴代码并继续前进。 - mohamedmoussa
1
@woggy:仅从描述中很难将其可视化。如果您想使用继承来尝试完成此操作并需要帮助,我建议您创建一个新问题,并提供一个最小但完整的示例。 - Jon Skeet
好的,我会接受这个答案,因为似乎我没有提供一个好的例子。 - mohamedmoussa

6

从C# 8.0开始,您应该能够为您的接口提供默认实现,而不是要求使用通用方法。

interface IPoint {
    int NumDims { get => 0; }
}

实现一个通用的方法以及每个实现的重载也违反了Liskov置换原则(SOLID中的L)。你最好将算法推入每个实现中,这意味着你只需要一个单独的方法调用:
static int GetDim(IPoint point) => point.NumDims;

3

访问者模式

如果您不想使用dynamic,可以尝试使用下面的访问者模式

interface IPoint
{
    public int NumDims { get; }
    public int Accept(IVisitor visitor);
}

public struct Point2 : IPoint
{
    public int NumDims => 2;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public struct Point3 : IPoint
{
    public int NumDims => 3;

    public int Accept(IVisitor visitor)
    {
        return visitor.Visit(this);
    }
}

public class Visitor : IVisitor
{
    public int Visit(Point2 toVisit)
    {
        return toVisit.NumDims;
    }

    public int Visit(Point3 toVisit)
    {
        return toVisit.NumDims;
    }
}

public interface IVisitor<T>
{
    int Visit(T toVisit);
}

public interface IVisitor : IVisitor<Point2>, IVisitor<Point3> { }

class Program
{
    static int GetDim<T>(T point) where T : IPoint => 0;
    static int GetDim(Point2 point) => point.NumDims;
    static int GetDim(Point3 point) => point.NumDims;

    static int GenericAlgorithm<T>(T point) where T : IPoint => point.Accept(new Visitor());

    static void Main(string[] args)
    {
        Point2 p2;
        Point3 p3;
        int d1 = GenericAlgorithm(p2);
        int d2 = GenericAlgorithm(p3);
        Console.WriteLine("{0:d}", d1);        // returns 2
        Console.WriteLine("{0:d}", d2);        // returns 3
    }
}

1
为什么不在类和接口中定义GetDim函数? 实际上,您无需定义GetDim函数,只需使用属性NumDims即可。

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