泛型方法的重载解析没有按预期工作

Overload resolution on generic method doesn't work as expected

去年我询问了如何遍历和打印锯齿状数组,而不必为添加的每个维度编写重载函数。 .
我又把问题捡了起来,竟然能这样解决。和我得到的其中一个答案类似,但又不完全相同。

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) 被调用。当编译器从 PrintDynamic(dynamic array) 中调用时,编译器如何知道要使用哪个 Print<T>(),而当它从 Print<T>() 中调用时却不知道?

"How does the compiler know which Print() to use, when it's called from PrintDynamic(dynamic array)"

答案是虚函数tables。由于您使用的是 C#,因此所有类型都继承自 class "object"。对 Print() 的简单调用尝试打印对象本身而不是它们的内容。为什么?因为没有重载更合适的方法,所以为对象调用了 ToString() 方法。每当您使用 C# 等强类型 OOP 语言时,每个对象都是指向其数据结构(通常在堆上)的指针,并且该数据结构的第一个条目是指向虚函数的指针 table对于那个对象。虚函数 table 本质上是 class 支持的每个函数的函数指针数组。由于调用 PrintDynamic 实际上是在传递对象的指针,因此对象指针的分辨率映射回其 class 的虚函数 table。然后,可以调用适当的重载函数。这是该过程的高级描述。这个概念在 C++ 等语言中是相似的。我希望这可以帮助您更多地了解编译器在幕后实际做了什么。我建议您阅读一些学术读物或以下内容 link 以获取更多详细信息。

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

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

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 ] ] ]

回答你原来的问题。当你打电话时:

str += Print(sub)

并且该方法的原始对象是 int[][][],那么该方法的 <T>int[]。所以你用 T[] 调用 Print(sub),其中 Tint[].

因此,选择 PrintT[] 重载,T 作为 int[] - 一切都如预期的那样进行。这是 编译时解决的问题,并且是编译器可以利用它所拥有的信息做的最好的事情。

请记住,泛型方法只被编译为 IL 一次 - 您不会因为调用它的不同方式而获得 'different versions'(与 C++ 模板不同)。泛型方法的行为必须对所有可能的输入有效。所以如果方法接收到T[][],你在内部提取子元素,它只能认为子对象类型是T[]。它无法在运行时检测到“哦,实际上,T 是一个 int[],所以我将调用一个不同的重载”。该方法只会调用 Print(sub)T[] 重载,无论输入是什么。

但是,在您使用 dynamic 的情况下,您忽略了编译时嵌入的所有通用类型信息,并说 'what is this type actually NOW, at runtime, using reflection. Which method is the best match now? Use that one!'。此行为有很大的开销,因此必须使用 dynamic 关键字明确请求。