带泛型的类应如何互相转化(NET4.0+)?

【问】

以下代码问题出在哪里呢?

 [C#]

namespace Test
{
public class MainTest
{
public static void Main(String[] args)
{
Object obj = new string(new char[]{‘a’,’b’,’c’});
List<Object> objects = new List<string>();
}
}
}

[VB.NET]

Namespace Test
Public Class MainTest
Public Shared Sub Main(args As [String]())
Dim obj As [Object] = New String(New Char() {a, b, c})
Dim objects As List(Of [Object]) = New List(Of String)()
End Sub
End Class
End Namespace

【错误回答】

没有错误。因为object是一切类型的父类,自然object的任意类型均可以指向其子类。

【正解】

OOP(面向对象)在针对“类继承”中应用了里氏原则(大致意思是:如果两个类之间存在着“A”是“B”的关系,那么“A”应当可以转化为“B”——也就是说,B是A的父类)。因此说object是父类,指向子类string肯定是可以的。所以第一个语句肯定是“编译通过、运行也OK”。

不过正如“真理往前多走了一步,真理也会变成谬论”一样。里氏原则是仅针对“单父类引用=单父类(子类)实体”关系而言,而我们现在第二句话展示是一个集合的情况——要使得集合也能够像第一条语句一样运行顺畅,不出错误,就必须保证“每一个在List中的object”都能够转化成“string”(父类的object指针指向子类的string的List实例,这是正确的;但是问题在于当你使用父类的Add方法添加对象的时候,实质是添加到string的那个List里边的,因为引用类型自身不能存储数据,object类型的List仅是一个指向string类型List的指针,本质是存储string类型List的一个数组起始地址而已)。那么问题就来了——如果现在我是这样写代码:

[C#]

objects.Add(new MainTest());

[VB.NET]

objects.Add(New MainTest())

请问MainTest可以隐式转化成string类吗?显然不能!因为无论从“里氏原则”还是其它方面考虑,MainTest和string毫无任何发生关系的可能。那么试想一下:如果微软的.NET允许问题中的第二行代码编译通过,会发生什么情况呢?毫无疑问,会抛出类似“隐式转换异常”的错误。

上面我们只是从感性认识上知道了集合之间不能简单使用里氏原则作为是否可以转换的判定原则。下面我们进一步研究这种复杂情况下转换是否有规律可循。

方便期间,我们先使用数组举例。现假定存在着Object和String两个数组引用声明,N为大于零的正整数:

[C#]

Object[]obj=new String[N];

[VB.NET]

Dim obj()As [Object]=New String(N)

因为obj仅是引用String类型数组,因此相对String而言,obj的每一个元素是保存到String数组对应的位置的,这里就简化成obj->String,给每一个Object元素赋值其实真正是存储到String数组对应的索引中去的。因为Object是“一切类的鼻祖”,自然不能保证其下面的子类也一定是String的子类(这里假定String是一个允许被继承的类)。

那么如果我们调换一下——

[C#]

String[]s =new Object[N];

[VB.NET]

Dim s() As String=New Object(N)

那么这样一来,每一个Object元素必须接受String的每一个元素。自然地,根据里氏原则判定,String的“子类”(如果String可以被继承的话),也一定是其父类object的子类,所以此条件恒成立(尽管这编译根本无法通过,但是的确完全符合我们的理论推断)。

如果把上面解决方案转化成接口的形式就可以是这个样子(实现此接口的类省略)

[C#]

public interface In<T>where T:class
{
void Input(T item);
}
In<String> sIn = ……;
In<Object> oIn = ……;
sln=oln;

[VB.NET]

Public Interface [In](Of T As Class)
Sub Input(item As T)
End Interface

Dim sln As [In](Of String) = ……
Dim oln As [In](Of Object)=……
sln=oln

在实际运行中,接口的泛型T被分别替换了——

对于sln而言:

[C#]

void Input(String item)

[VB.NET]

Sub Input(item As String)

对于oln而言:

[C#]

void Input(Object item)

[VB.NET]

Sub Input(item As [Object])

当调用sln.Input时候,String强制接受了一个至少是String类型的实体,然后实际调用了oln.Input(因为要存储到oln中去)。那么String=>Object完全符合里氏原则,恒成立。

不过上述代码放入编译器后为什么还是编译不通过呢?这是因为我们只考虑了一种情况(由String->Object转化)。假设有以下类:

[C#]

public interface InOut<T>where T:class
{
void Input(T item);
T Output();
}

InOut <String> sIn = ……;
InOut <Object> oIn = ……;
sln=oln;

[VB.NET]

Public Interface InOut(Of T As Class)
Sub [Input](item As T)
Function OutPut() As T
End Interface

Dim sln As InOut(Of String)=……
Dim oln As InOut(Of [Object])=……
Sln=oln

注意我除了改变了接口名字(方便后面的讲解),还增加了一个Output。那么如果我调用oln.Add(某个实体),然后通过sln获取,双方在实际运行中变成这个样子:

对于sln而言:

[C#]

void Input(String item),String Output()

[VB.NET]

Sub Input(item As String),Function Output() As String

对于oln而言:

[C#]

void Input(Object item),Object Output()

[VB.NET]

Sub Input(item As Object),Function Output() As [Object]

那么现在如果我们从Object角度看(也就是使用oln.Input(某个实体)),然后通过sln.Output()输出(也就是说,这里还出现了一个“某个实体”像“String”的隐式转化问题)。显然也是不一定可行的。

所以我们说“集合A=集合B”存在着互相转化问题,并非单纯“A=B”那么简单,问题是在于系统(或者说“我们”根本不知道T是用于输出还是输入参数)。因此应该分两部考虑(假设B继承于A——

                        集合B=>集合AAB只能用于输出参数。

或者

                        集合A<=集合BAB只能用于输入参数

幸好,微软在NET4.0为我们提供了接口“输入”和“输出”参数强制定义。我们不妨这样做:

对于1情况:

[C#]

public interface In<in T>where T:class
{
void Input(T item); //T必须用于输入参数
}

[VB.NET]

Public Interface [In](Of In T As Class)
Sub Input(item As T) ‘T必须用于输入参数
End Interface

对于2情况:

[C#]

public interface Out<out T>where T:class
{
T Output(); //T必须用于输出参数
}

[VB.NET]

Public Interface Out(Of Out T As Class)
Function Output() As T ‘T必须用于输出参数
End Interface

那么:

String类型的sln=Object类型的oln恒成立;或者Object类型oln=String类型的sln也恒成立。

【总结】

        要使得泛型接口支持“父子类”一样的转换,必须考虑“输入”和“输出”两种情况,假设以赋值号为界,左边到右边是“输入”,右边到左边是“输出”。无论左边还是右边,“输出端”=>“输入端”总是符合“子类=>父类”转换原则。

        如果总是以“输出”端看(从左到右),那么in参数实际包含的类型总是要符合“父类=>子类”的(称为“反变”);out参数包含的实际类型总是符合“子类=>父类”(协变)。

其实1和2的原则性质是一样的,只是看问题的角度不同:

1)是根据赋值号“赋值”的本性来考虑(因为赋值号对于泛型集合具备两异性:如A=B当使用A的Input方法时候可以理解为A输出到B,;反之,使用B的Output同时也可以理解为B输出到A。因此in总是“A”=>“B”方向判定;反之out总是“B”=>“A”)。

2)是根据“从右到左”(输出端)看,那么同方向的(都是out的),要符合“协变”(父类=>子类),反之in方向(原来从左到右,目前与定义方向相反),则“反变”。

【扩展】

上面的例子我们仅考虑了“单项输入”(仅作为输入参数)和“单向输出”(仅作为输出参数)。实际情况接口往往同时包含“输入”和“输出”泛型参数类型,会产生什么情况呢?以下假设B:A,根据上面的判断,自然得到以下结果——

[C#]

public interface InOut<in V,out T>where T:class where V:class
{
void Input(V item);
T Output();
}

InOut<B,A> a1=……;
InOut<A,B>a2=……;
a1=a2;

[VB.NET]

Public Interface InOut(Of In V As Class,Out T As Class)
Sub Input(item As V)
Function Output() As T
End Interface

Dim a1 As InOut(Of A,B)=……
Dim a2 As InOut(Of B,A)=……
a1=a2

这样编译是通过了,但是运行是否有问题?假设我们如果这样调用a1的Input方法,并且存入一个C类实例(假设C:A)——这样是会发生问题的——因为在“a2”中的A是要向“a2”中的B输出的,但是B在a2体内实际输出的是A。因为C和B毫无关系,所以又会引发转换错误。可见,我们还要考虑“a2”中“A,B”之间的转化问题。VS只检测输入参数(从左至右)和输出参数(从右至左)是否可以正常转换,所以只靠VS语法检测并不保证运行时一定不出问题。一般地,要使得在“语法检测”和“运行时”都正确,对于两个泛型参数接口应该做这样地转换检查

假设有泛型接口A<K,V>=泛型接口B<T,U>,则必须满足:K=>T=>U=>V的关系。一般可以考虑类似K:T:U:V这样的继承关系。

类似VS做语法检测不保证运行时一定正确的例子还有“Object[]=new String[N];”因为你不能保证每一个Object都可以转化成string。这里VS是把Object看成是数组指针,判断这个数组指针是否是右边“String”的对象,并不检查Object数组每一个元素是否都可以转化成String

如果把这个表达式转换成接口看就很明显了:

[C#]

InOut<Object,Object> a1=……;
InOut<String,String>a2=……;
a1=a2;

[VB.NET]

Dim a1 As InOut(Of [Object],[Object])=……
Dim a2 As InOut(Of String,String)=……
a1=a2

接口定义同“【扩展】”部分,显然Object不满足向String的转换。

另外想说明的是:

                      1) 这里题目的List泛型类型是Object,虽然可以但实际上不建议这样做,泛型不应该用Object作为类型参数,因为这样仍然需要强制转换,等同于ArrayList类

                      2) 不是所有的泛型接口都必须指定“输入”和“输出”(如泛型List类)

                       3) 只有接口和委托才可以指定“输入in”和“输出out”.

                       4)协(反)变接口只支持类,不支持结构类型。因为结构之间不存在继承关系。

原文地址:https://www.cnblogs.com/ServiceboyNew/p/2227848.html