您现在的位置是:主页 > news > 桂林建网站/做企业推广

桂林建网站/做企业推广

admin2025/6/28 7:03:46news

简介桂林建网站,做企业推广,广西钦州有人帮做网站的公司吗,网站建设信 信科网络前言 今天看了两篇讲协变/反变的文章,写得很好也很有意思。不过我猜应该有不少人可能还是很难理解这个新概念——每一次推出新的概念的时候,都会或多或少造成我们的困惑:这是个什么东西?为什么要出这么复杂的东西?我们…

桂林建网站,做企业推广,广西钦州有人帮做网站的公司吗,网站建设信 信科网络前言 今天看了两篇讲协变/反变的文章,写得很好也很有意思。不过我猜应该有不少人可能还是很难理解这个新概念——每一次推出新的概念的时候,都会或多或少造成我们的困惑:这是个什么东西?为什么要出这么复杂的东西?我们…

前言

今天看了两篇讲协变/反变的文章,写得很好也很有意思。不过我猜应该有不少人可能还是很难理解这个新概念——每一次推出新的概念的时候,都会或多或少造成我们的困惑:这是个什么东西?为什么要出这么复杂的东西?我们什么时候应该用这种东西,什么时候不该用?

有这样的困惑没关系,我想绝大多数人都经历过这个过程。我在这里呢,也说说从我的角度是如何看这个新鲜事物的,也许对理解这个东西有帮助。不过先声明一下,我没有装过,更没有用过.NET 4.0,因此我写的内容基本是自己推导出来的,如果有什么不正确的地方,也希望大家能够指出。

先给出刚才提到的两篇文章,因为也许有人是通过搜索引擎过来的:

http://www.cnblogs.com/Hush/archive/2008/11/22/1339140.html

http://www.cnblogs.com/Ninputer/archive/2008/11/22/generic_covariant.html

其中第一篇文章的内容相对简单一点,也好理解一点,后者更加理论化一些。
本来呢,我看完Ninputer的文章产生了两个想法:
1、好!
2、有点太理论化了,恐怕难以理解。
基于第二点想法,我就产生了要从更容易理解的角度去描述这个问题的想法,结果一刷屏幕,已经出来了第二篇了——有人捷足先登了。这让我郁闷了一下会儿,不过后来还是发现了一些有趣的事情大家都没有提到,于是又重燃了我码一堆文字的热情。

 


什么是协变和反变?

其实前面的文章里面已经有很清晰和正规的定义,我这里不打算再写得更详细了。相反,我想把问题简化,因此我会给出一个较简单但不太准确的说法:

interface IFoo<in TIn, out TOut> // TIn 就是反变,TOut就是协变
{
    TOut Output();
    
void Intput(TIn value);
}

 

这个定义估计足够简单明了了,那么他们是干什么用的呢?

提示:理解协变和反变,需要从泛型之间的类型转换入手,而不要把注意力定格在泛型中某个函数的类型参数T上面。比如:IEnumerable<object> objs = new List<string>();

 

 

为什么要有协变和反变?

这个问题嘛,跟继承和派生的概念是有一定关联的。比方说:

ContractedBlock.gifExpandedBlockStart.gif对派生和继承了解的就别看了,浪费时间!
class Animal // 父类
{
   
// 返回某一种子类对象
   public static Animal CreateOne(string type)
   {
      
// 
   }
}
class Human:Animal // 子类
{
   
// 
}
class Dog:Animal // 另一个子类
{
   
// 
}
Animal foo 
= new Human(); // 这样是可以的
Human boo = Animal.CreateOne("dog"); // 这样是不可以的,因为你不能断定“动物”就一定是“人”,它还可能是“狗”。

这个和协变好像还有点远,哦,对,还跟泛型有关系:我们在泛类型上是否也可以像刚才那样使用呢?我们看一个例子:

IList<string> source = new List<string>;
IList
<object> target = source;  // 可以这么干吗?

我们先撇开“能不能”不说,至少我们是很期望能够这么干的,比如说:

public void RemoveNull(IList<object> objList)
{
  
// 把中间员素值为null的元素删掉
}

IList
<string> stringList = GetItFromSomewhere();
RemoveNull(stringList); 
// 嗯,看着很诱人的样子!
// 如果这样做是被允许的话,那么我们就不用再写一个针对
// IList<string>版本的RemoveNull函数了

那到底能不能呢?这个问题很有趣,在Ninputer的文章里面提到了另外一个类似的例子:

如果两个类型T和U之间存在一种安全的隐式转换,那么对应的数组类型T[]和U[]之间是否也存在这种转换呢?
……
举个例子,就是String类型继承自Object类型,所以任何String的引用都可以安全地转换为Object引用。我们发现String[]数组类型的引用也继承了这种转换能力,它可以转换成Object[]数组类型的引用,…… 

我这里只是节选了其中一部分,是因为有一部分的描述是不准确的。为什么说是不准确的呢?我们来看这么一段例子:

string[] source = { "A""B""C" };
object[] target = source; // 编译会出错吗?不会!
target[1= 1// 能运行通过吗?不能!

由于编译是可以通过的,所以上面我节选的那一段话是正确的。但由于实际上运行是不通过的,原因也很显而易见。

ContractedBlock.gifExpandedBlockStart.gif关于Ninputer的原话,以及不那么显而易见的“显而易见”的解释可以看这里

既然Ninputer点到了,我也多解释一下。没说原话不对,只是后面说是协变不准确(不是不正确)。我不反对这么说,不过我觉得是有条件的。因为这么转换实际上违反了强类型的类型安全:隐式类型转换下,应该是不会发生类型相关的运行时错误的。
比如说:

object[] a=new object[1];
a[
0= 1// int -> object 隐式转换,不应该有运行时错误,事实上也没有。
a[0= "a"// string -> object 隐式转换,不应该有运行时错误,事实上也没有。

这样肯定没问题对吧?
但是如果是:

string[] problem = new string[1];
test(problem); 
// 隐式转换或者强制转换,没关系,问题不在这里
void test(object[] a)
{
  a[
0= 1// int -> object 隐式转换导致运行时错误!
}

这却有可能发生运行时类型错误,实际上有点不太对劲,这才是我要表达的内容。

而我认为之所以是允许编译通过,很可能是有一些迫不得已的原因。举string.Format的例子可能不太恰当,但也可以说是有这个可能性的。我进一步解释一下:

string.Format(string format, params object[] objs) 这个函数不仅仅可以这么调用:

string.Format("{0}{1}{2}{3}"0123);

还可以这么调用:

object[] vals = new object[]{0123};
string.Format("{0}{1}{2}{3}", vals);

那么,当然也有可能这么调用:

string[] vals = new string[]{0123};
string.Format("{0}{1}{2}{3}", vals);

如果迫于完全尊从强类型原则,不允许string[]类型能够转换成object[]类型,上述的调用就会很麻烦,必须先创建一个object[]数组,然后一个个元素复制过去。上面这么直接写,也许很难理解为什么要这么调用,好像没有什么道理。但是如果我们考虑下面的情况:

ContractedBlock.gifExpandedBlockStart.gifCode
List<string> foo = GetItFromSomeWhere();
string.Format("{0}{1}{2}{3}", foo.ToArray());


这也许就会比较常见了,因为我可没有办法控制GetItFromSomeWhere返回的是什么,比如说强制对方一定返回一个object[]数组,这样可能导致某些地方的性能开销增大。于是当我要用Format的时候就会遇到讨厌的转换问题。

而泛型就关不了这么多了,干脆就不允许便已通过,请参考接下来我给出的IArray例子,这个接口是否可以认为是近似于一个数组的定义呢?

 

刚才的object数组的例子,能够给我们带来很多的思考:

一、为什么数组就可以编译通过,那么泛型呢?

ContractedBlock.gifExpandedBlockStart.gif泛型和数组的对比
interface IArray<T>
{
  T 
this[int index]{get;set;}
}

IArray
<string> source = GetFromSomewhere();
IArray
<object> target = source; // 编译不通过!
object[] boo = new string[0]; // 竟然可以……气死人了

泛型的类型转换是不能编译通过的!可为什么不行呢?其实前面数组的那个例子已经给出了答案:运行的时候如果我们试图对某个元素做赋值操作,是有可能出现运行时错误的。实际上数组本身也没有解决这个问题,只是忽略了这个问题。忽略这个问题的原因也很简单,比如说我们看看string.Format(string format, params object[] objs)这个函数,如果不忽略又怎么提供这种方法呢?可以说数组允许这种情况的转换,其实是一种不得不作出的妥协,而并不是真正的协变(按照泛型的协变/反变规则,其实是不允许这么做的)。


二、如果我们想要编译及运行通过,应该怎么去做?

前面曾经举了一个例子,说明我们是那么期望这种泛型之间的转换,协变和反变就是为了解决这一问题的。让我们回顾最开始的那个例子:

interface IFoo<in TIn, out TOut> // TIn 就是反变,TOut就是协变
{
    TOut Output();
    
void Intput(TIn value);
}

我们考虑有如下的代码:

ContractedBlock.gifExpandedBlockStart.gifCode
class Foo<TIn, TOut> : IFoo<TIn, TOut>
{
    
public TOut Output(){}
    
public void Intput(TIn value){}
}

// 反变
IFoo<objectstring> source = new Foo<objectstring>();
source.Input(
"ABC"); // 这样显然是可以的,因为string->object是可以的
IFoo<stringstring> contra = source; // 显然这样也是可以的:
// 既然能接受object对象,显然也可以接受string对象

// 反例:
IFoo<objectstring> foo = new Foo<stringstring>();
// 这样显然是不可能的,Input(string value)当然不能传入object参数。

// 协变
object result = source.Output(); // 返回的字符串用object变量来承载,
// 显然是可以的。
IFoo<objectobject> co = source; // 同理,这样也是可以的。

// 反例:
IFoo<objectstring> boo = new Foo<objectobject>();
// 这样显然是不行的,object Output()的返回值类型是object,
// 除非强制转换,不可能赋值到string类型的变量上。

通过这段代码,应该能理解,如果我们需要在泛型之间能够达成安全的隐式类型转换,是会有一定的前提条件限制的。在应用这些限制之前,泛型之间的隐式转换是不可能的事情,即使类型参数T之间是有继承关系的。而协变/反变就是为了完成泛型间类型转换而提供的,用于明确限制转换方向、给编译器验证条件的语法工具。

 

什么时候使用协变/反变?

我的经验是,先看看框架里面是怎么用的,用多了之后再总结,别轻易在自己设计的泛型中使用。原因很简单:刚学会新特性的时候,很容易把它当作金锤子四处滥用,最后可能反而会增加了整个程序的复杂度。 

 

后记

大家考虑一下,前面举的例子IList<T>能通过协变/反变来达到目的吗?答案在Ninputer的文章中。