泛型方法的重载分辨率无法按预期工作

时间:2018-06-14 13:22:10

标签: c# generics dynamic overload-resolution

去年我问过如何遍历和打印锯齿状数组,而不必为每个添加的维度编写重载函数。 Generic printing of jagged arrays
我再次接受了这个问题并且能够像这样解决它。它类似于我得到的答案之一,但不完全相同。

static string Print<T>(T[] array)
{
    string str = "[ ";

    for (int i = 0; i < array.Length; i++)
    {
        str += array[i];
        if (i < array.Length - 1)
            str += ", ";
    }

    return str + " ]\n";
}

static string Print<T>(T[][] array)
{
    string str = "";

    for (int i = 0; i < array.Length; i++)
    {
        var sub = array[i];

        if (sub.Length != 0 && sub[0] is Array)
            str += PrintDynamic(sub);
        else
            str += Print(sub);
    }

    return str + "\n";
}

private static string PrintDynamic(dynamic array)
{
    return Print(array);
}

它工作正常,我得到了正确的输出:

var twoDim = new int[][]
{ 
    new int[] { 0, 1, 2, 3 },
    new int[] { 0, 1, 2 },
    new int[] { 0 }
};
var threeDim = new int[][][] { twoDim, twoDim }

Console.WriteLine(Print(threeDim));
// Output:
// [ 0, 1, 2, 3]
// [ 0, 1, 2]
// [ 0 ]
// 
// [ 0, 1, 2, 3]
// [ 0, 1, 2]
// [ 0 ]

但是我仍然不满意,因为如果我不需要PrintDynamic()并且如果我可以写

那会更好
str += Print(sub);

而不是

str += PrintDynamic(sub);

这就是我的问题所在。如果我改变那一行,我没有得到任何错误,但输出变为

// [ System.Int32[], System.Int32[], System.Int32[], System.Int32[]]
// [ System.Int32[], System.Int32[], System.Int32[]]
// [ System.Int32[] ]
// 
// [ System.Int32[], System.Int32[], System.Int32[], System.Int32[]]
// [ System.Int32[], System.Int32[], System.Int32[]]
// [ System.Int32[] ]

因为Print<T>(T[] array)被调用而不是Print<T>(T[][] array)。当编译器从Print<T>()调用时,编译器如何知道要使用哪个PrintDynamic(dynamic array),而在Print<T>()内调用时却不知道?

3 个答案:

答案 0 :(得分:1)

回答您的原始问题。致电时:

str += Print(sub)

,该方法的原始对象是int[][][],则该方法的<T>int[]。因此,您用Print(sub)呼叫T[],其中Tint[]

因此,选择了T[]的{​​{1}}重载,其中PrintT-一切都按预期进行。这是解决编译时间的问题,并且是编译器可以利用其所拥有信息的最佳方法。

请记住,泛型方法只能编译到IL一次-您不会因为碰巧调用它的方式不同而获得“不同版本”(与C ++模板不同)。通用方法的行为必须对所有可能的输入均有效。因此,如果该方法接收到int[],并且您在内部提取了子元素,则它只能将子对象类型视为T[][]。它在运行时无法检测到“哦,实际上T是一个int [],所以我将调用另一个重载”。无论输入什么,该方法都只会调用T[]的{​​{1}}重载。

但是,在使用T[]的情况下,您将忽略编译时产生的所有通用类型信息,而是说“在运行时使用反射现在实际上是什么类型”。现在哪种方法最合适?使用那个!此行为的开销很大,因此必须使用Print(sub)关键字来明确请求。

答案 1 :(得分:0)

“当从PrintDynamic(动态数组)调用时,编译器如何知道要使用哪个Print()”

答案是虚函数表。由于您使用的是C#,因此所有类型都继承自“object”类。对Print()的简单调用会尝试打印对象本身而不是其内容。为什么?因为为对象调用ToString()方法,因为没有重载更合适的方法。每当您使用强类型OOP语言(如C#)时,每个对象都是指向其数据结构的指针(通常在堆上),此数据结构的第一个条目是指向该对象的虚函数表的指针。虚函数表本质上是类支持的每个相应函数的函数指针数组。因为通过调用PrintDynamic实际上是传递对象的指针,对象指针的分辨率会映射回其类的虚函数表。然后,可以调用适当的重载函数。这是该过程的高级描述。这个概念在C ++等语言中是类似的。我希望这有助于您更多地了解编译器在幕后实际执行的操作。我会推荐一些学术性阅读或者以下链接以获取更多细节。

https://en.wikipedia.org/wiki/Virtual_method_table

答案 2 :(得分:0)

如果我是你,因为这是一个在编译时无法在任意维度上解决的问题,我完全避免使用泛型:

public static string Print(Array array)
{
    string str = "[ ";
    for (int i = 0; i < array.Length; i++)
    {
        var element = array.GetValue(i);

        if (element is Array)
            str += Print(element as Array);
        else
        {
            str += element;
            if (i < array.Length - 1)
                str += ", ";
        }
    }

    return str + " ]";
}

这会产生嵌套输出,我认为它更好,随着深度的增加,它会随意嵌套。

  

[[[0,1,2,3] [0,1,2] [0]] [[0,1,2,3] [0,1,2] [0]]]