谁最终决定什么是通用类型?(涉及IT技术)

7

I have this function

 public static T2 MyFunc<T1, T2>( T1 a, T1 b, T2 c)
        {
            return c;
        }     

我正在创建2个 Persons 类实例:
 class Person
         {  }

            Person p = new Person();
            Person p2 = new Person();

我正在使用以下内容调用该函数:

 MyClass.MyFunc(p, p2, 5);

我的问题是:

到底是谁决定了T1类型?(p?p2?)

因为如果左边是苹果,那么他会检查第二个是否也是苹果。

如果第二个是橙子,则他应该检查第一个是否也是橙子。

enter image description here

这似乎有些奇怪,因为在编译时,如果不相同,它们将失败。

但是,到底是谁决定了类型呢?

其次,如果我将其更改为动态类型-在运行时-谁会决定T1类型应该是什么?


3
没有所谓的“决策”。要么找到匹配的结果,要么出现错误。只需尝试使用MyFunc(p1, "", 5)或者MyFunc("", p2, 5) - H H
6个回答

13

在较高层次上,方法类型推断的工作方式如下。

首先,我们列出所有的参数 -- 您提供的表达式 -- 以及它们对应的形式参数类型

让我们看一个比你提供的更有趣的例子。 假设我们有:

class Person {}
class Employee : Person {}
...
Person p = whatever;
Employee p2 = whatever;

并且进行相同的调用。因此我们建立对应关系:

p  --> T1
p2 --> T1
5  --> T2

然后我们列出每个类型参数的“边界”以及它们是否“固定”。我们有两个类型参数,并且一开始没有上限、下限或确切的边界。

T1: (unfixed) upper { }  lower { }  exact { }
T2: (unfixed) upper { }  lower { }  exact { }
(回想一下我们在另一个问题中讨论的类型相对大小的问题,类型的相对大小取决于其是否更具限制性; 更具限制性的类型比不那么限制性的类型更小。因为比起长颈鹿,动物的种类更多,所以动物比长颈鹿更小。"上"和"下"界限集就是这样: 对于给定类型参数的类型推导问题的解决方案必须大于或等于每个下界并且小于或等于每个上界,并且等于每个精确边界。)

然后我们看每个参数及其相应的类型。(如果参数是lambda表达式,则我们可能需要确定查看参数的顺序,但是你没有任何lambda表达式,因此让我们忽略该细节) 对于每个参数,我们对形式参数类型进行推导,并将我们推导出的关于该推导的事实添加到界限集合中。因此,在查看第一个参数之后,我们推导出以下界限:

T1: (unfixed) upper { }  lower { Person }  exact { }
T2: (unfixed) upper { }  lower { }  exact { }
在第二个参数之后,我们推导出界限。
T1: (unfixed) upper { }  lower { Person, Employee }  exact { }
T2: (unfixed) upper { }  lower { }  exact { }

在第三个参数之后,我们推断出边界:

T1: (unfixed) upper { }  lower { Person, Employee }  exact { }
T2: (unfixed) upper { }  lower { int }  exact { }
在我们尽可能地进行进展后,我们会“修复”边界,通过找到边界集中满足每个边界的最佳类型来实现。
对于T1,边界集中有两种类型,即PersonEmployee。是否有一个类型能够满足边界集中的所有边界条件呢?是的。Employee不能满足Person的边界条件,因为Employee是比Person更小的类型;Person是一个下边界——它表示没有比Person更小的类型是合法的Person可以满足所有的边界条件:PersonPerson相同,并且比Employee大,因此它同时满足这两个边界条件。对于T1,满足所有边界条件的最佳类型是Person,而对于T2,显然是int,因为在边界集中只有一种类型。因此,我们接下来修复类型参数。
T1: (fixed) Person
T2: (fixed) int

我们问: "对于每个类型参数,我们是否有一个固定的边界?"

如果我将第一个参数的类型更改为dynamic,那么T1是如何被推断出来的?

如果任何一个参数是动态的,那么T1和T2的推断会被推迟到运行时,此时语义分析器会将值的最具体可访问运行时类型视为动态参数提供的下限所需的类型。


如果您对此主题感兴趣并希望了解更多信息,可以在这里找到我解释C# 3版本算法的视频:

http://blogs.msdn.com/b/ericlippert/archive/2006/11/17/a-face-made-for-email-part-three.aspx

(C# 3没有上限,只有下限和精确边界;除此之外,算法基本相同。)

我写的有关类型推断问题的文章在这里:

http://blogs.msdn.com/b/ericlippert/archive/tags/type+inference/


1
我觉得编译器给了我一个答案。非常感谢。(再次感谢) - Royi Namir
@RoyiNamir:不用谢。如果您想了解更多关于这个主题的信息,我已经添加了一些有用的文章和视频链接。 - Eric Lippert
C# 4加入了上界?这是为什么?在哪些情况下它们是有用的? - svick
2
@svick:只有在涉及到逆变转换的通用类型推断时,上限才是可能存在的。由于C# 3仅具有协变或非变异转换,因此根本没有办法进入即使存在上界的情况,因此我们甚至不必在算法中提到它们。当我们在C# 4中为通用接口和委托添加协变和逆变转换时,我们将上限推断添加到了类型推断算法中。 - Eric Lippert
请问您能告诉我在哪里下载您在回答中提到的推理算法完整视频吗?链接如下:http://blogs.msdn.com/b/ericlippert/archive/2006/11/17/a-face-made-for-email-part-three.aspx,以及该博客文章中的直接链接http://wm.microsoft.com/ms/msdn/visualcsharp/eric_lippert_2006_11/EricLippert01.wmv。由于我的网络连接较慢,所以想问一下 :) - M3taSpl0it
@M3taSpl0it:不好意思,我不知道。 - Eric Lippert

7
可以在调用中省略类型的可能性
MyClass.MyFunc(p1, p2, 5);

这是一种语法糖(除非您使用匿名类型),它编译后与原文完全相同。

MyClass.MyFunc<Person, int>(p1, p2, 5);

编译器根据参数a、b和c的类型推断T1和T2的值。如果p1和p2的类型不兼容(参见svick的答案),编译器将无法推断T1,从而导致编译错误。请保留HTML标签。

1
我认为编译器并不关心类型是什么,它只检查第一个和第二个参数是否是相同的类型。 - Didier Ghys
1
@RoyiNamir,你先谈论人,然后又谈苹果和橙子……抱歉,我有点跟不上你的思路 :) - Ilya Kogan
1
t1 == t2 和 t2 == t1 是一样的,我们在这里讨论的是编译器角度下的“类型”,而不是任何实例的相等检查。 - doblak
@RoyiNamir 它不检查 p1 == p2。它检查的是 typeof(p1) == typeof(p2) - Ilya Kogan
1
如果您正在使用匿名类型,那么这不仅仅是语法糖。即使 p1p2 是不同类型,它仍然可以编译。 - svick
显示剩余4条评论

2

没有优先级,a和b应该是相同的,这是设计上的考虑,T1在编译时解决。如果你改成dynamic,你只是将类型解析推迟到运行时,如果类型不同,则会在运行时而不是编译时失败。如果你想让它们不同,你需要引入T3。

编辑:

有趣的部分:

Orange a = new Orange();
Apple b = new Apple();
string c = "Doh.";

MyFunc<dynamic, string>(a,b,c);

public static T2 MyFunc<T1, T2>( T1 a, T1 b, T2 c) where T2 : class
{
    return (a.ToString() + b.ToString() + c.ToString()) as T2;
}     

输出:

I am an orange. I am an apple. Doh.

但是这个:
dynamic a = new Orange();
dynamic b = new Apple();
string c = "Doh.";

MyFunc<Apple, string>(a,b,c);

会抛出以下异常:

RuntimeBinderException: The best overloaded method match for 'UserQuery.MyFunc<UserQuery.Apple,string>(UserQuery.Apple, UserQuery.Apple, string)' has some invalid arguments

然而,似乎我确实需要找一本关于C# 4.0动态类型的好书或资源,以理解这里发生的魔法。


1
我不确定我是否理解你的意思,但是...如果a.GetType()等于b.GetType(),那么反之亦然。只要它们都是T1即可(前提是没有动态类型,详见我的上面的编辑)。 - doblak
1
那种情况下的魔法非常简单:显式类型参数“dynamic”在运行时分析表达式时被视为完全等同于“object”。如果您对某些不理解的内容有具体问题,那么这是一个问答网站,请考虑发布问题。 - Eric Lippert
现在看起来很明显了,感谢您发布的解释。 - doblak

1
谁实际上决定 T1 类型?(p?p2?)
这不是很明显吗?两者都要。p 和 p2 的类型必须兼容。与其他答案所说的相反,它们不必相同。实际规则是其中一种类型必须隐式转换为另一种类型。
例如,MyFunc("a", new object(), 5) 等同于 MyFunc("a", new object(), 5),因为 string 隐式可转换为 object。另一个例子,MyFunc(42L, 42, 4) 等同于 MyFunc(42L, 42, 4),因为 int 隐式可转换为 long。
此外,有些情况下让编译器推断类型的能力不仅仅是好用,而是必要的。具体来说,当使用匿名类型时就会发生这种情况。例如,MyFunc(new { p = "p" }, new { p = "p2" }, 5)无法重写以明确指定类型。

1

谁实际上决定了T1类型?(p?p2?)

通常,C#编译器会决定这个。如果方法参数中有一个是动态的,则在运行时(由Microsoft.CSharp库)进行决策。 在这两种情况下,都应用了C#规范中描述的类型推断算法: 将pp2的类型都添加到T1下限集合中(也可能是上限,但仅当涉及逆变泛型时)。

然后,编译器从边界集合中选择一个类型,该类型还满足所有其他边界条件。 当只有一个边界时,因为pp2具有相同的类型,所以这个选择是微不足道的。 否则(假设只涉及下限),这意味着编译器选择一种类型,以便所有其他候选类型都可以隐式转换为该类型(svick的答案描述了这一点)。
如果没有唯一的选择,类型推断失败-如果可能,选择另一个重载,否则会出现编译器错误(当在运行时(动态)进行决策时,会抛出异常)。


你的意思是 逆变 泛型。 - Eric Lippert
@Eric Lippert:糟糕,我总是混淆协变性/逆变性的术语。实际上,在撰写这个问题时,我查了一下它们的定义,但不知怎么地还是写错了。 感谢在C#中使用in/out,这样记忆起来容易多了。 - Daniel

0

在编译时,如果类型是明确的,则编译器将检查传递的参数类型,并查看它们是否对应并可以匹配到泛型中的类型(无冲突)。

无论如何,真正的检查是在“运行时”进行的。泛型代码将始终作为泛型编译(不像C++模板)。然后,当JIT编译器编译该行时,它将检查并查看是否可以根据您提供的模板和发送的参数创建方法。


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