[CLR via C#]12. 泛型

时间:2022-09-24 10:12:56

  泛型(generic)是CLR和编程语言提供一种特殊机制,它支持另一种形式的代码重用,即"算法重用"。

  简单地说,开发人员先定义好一个算法,比如排序、搜索、交换等。但是定义算法的开发人员并不设定该算法要操作什么数据类型;该算法可广泛地应用于不同类型的对象。然后,另一个开发人员只要指定了算法要操作的具体数据类型,就可以使用这个现成的算法了。

  泛型有两种表现形式:泛型类型泛型方法

  泛型类型:大多数算法都封装在一个类型中,CLR允许创建泛型引用类型和泛型值类型,但不允许创建泛型枚举类型。除此之外,CLR还允许创建泛型接口和泛型委托。

  泛型方法:方法偶尔也封装有用的算法,所以CLR允许引用类型、值类型或接口中定义泛型方法。

  两者都是表示API的基本方法(不管是指一个泛型方法还是一个完整的泛型类型),以致平时期望出现一个普通类型的地方出现一个类型参数。比如,List<T>,在类名之后添加一个<T>,表明它操作的是一个未指定的数据类型。定义泛型类型和方法时,它为类型指定的任何变量(比如 T)都称为类型参数(type parameter)。T代表一个变量名,在源代码中能够使用一个数据类型的任何位置 ,都能使用T。

  类型参数是真实类型的占位符。在泛型声明中,类型参数要放在一堆尖括号内,并以逗号分隔。所以,在Dictionary<TKey, TValue>中,类型参数是TKey和TValue。使用泛型类型或方法时,要使用真实的类型代替。这些真实的类型称为类型实参(type argument)。

  泛型为开发人员提供了以下优势:

  1)源代码保护  使用一个泛型算法的开发人员不需要访问算法的源代码。然而,使用C++模板的泛型技术时,算法的源代码必须提供给准备使用算法的用户。

  2)类型安全  将一个泛型算法应用于一个具体的类型时,编译器和CLR能理解开发人员的意图,并保证只有与制定数据类型兼容的对象才能随同算法使用。

  3)更清晰的代码  由于编译器强制类型安全性,所以减少了源代码中必须进行的转型次数。

  4)更佳的性能  在有泛型之前,要想定义一个常规化的算法,它的所有成员都要定义成操作Object数据类型。这其中就要有装箱和拆箱之间的性能损失。由于现在能创建一个泛型算法来操作一个具体的值类型,所以值类型的实例能以传值的方式传递,CLR不再需要只需任何装箱操作。由于不再需要转型,所以CLR不必检查尝试一次转型操作是否类型安全,同样提高了代码的允许速度。

一、 Framework类库中的泛型

  泛型最明显的应用就是集合类。FCL已经定义了几个泛型集合类。其中大多数类能在Sysytem.Collections.Generic和System.Collections.ObjectModel命名空间中。要使用线程安全的泛型集合类,可以去System.Collections.Concurrent命名空间寻找。
 
  Microsoft建议开发人员使用泛型集合类,并基于几个方面的原因,不鼓励使用非泛型集合类。首先,非泛型无法获得类型安全性、更清晰的代码和更佳的性能。其次,泛型具有更好的对象模型。
 
  集合类实现了许多接口,放入集合中的对象也可能实现了接口,集合类可利用这些接口执行像排序这样的操作。FCL内建了许多泛型接口定义,所以在使用接口时,也能体会到泛型带来的好处。常用的接口包含在Sysytem.Collections.Generic命名空间中。
  
  新的泛型接口并不是设计用来完全取代非泛型接口。
 
  System.Array类(即所有数组的基类)提供了大量静态泛型方法,比如,AsReadonly、FindAll、Find、FindIndex等。
 

二、Wintellect的Power Collections库
  Power Collections库由Wintellect制作,这个库有一系列集合类构成,任何人都可以免费下载和使用。  

集合类名称 说明
BigList<T>  有序T对象集合。操作100个以上的数据项是,效率非常高
Bag<T> 无序T对象的集合,集合进行了哈希处理,并允许重复项
OrderedBag<T> 有序T对象的集合,允许重复值
Set<T> 无序T数据项集合,不允许重复项。添加重复项后,会只保留一个
OrderedSet<T> 有序T数据项的集合,不允许重复项
Deque<T> 双端队列(double-ending queue)。类似于一个列表,但在起始处添加/删除数据项时,比列表更高效
OrderedDictionary<TKey,TValue> 字典,其中的键进行了排序,每个键都有一个对应的值
MultiDictionary<TKey,TValue> 字典,其中每个键都可以有多个值,对键进行了哈希处理,允许重复,而且数据项是无序的
OrderedMultiDictionary<TKey,TValue>

字典,其中的键进行了排序,每个键都可以有多个值(同样进行了排序)。允许重复的键

 

三、泛型的基础结构

  为了是泛型能够工作,Microsoft必须完成以下工作:

    1)创建新的IL指令,使之能够识别类型实参
    2)修改现有元数据表的格式,以便表示具有泛型参数的类型名称和方法
    3)修改各种编程语言(C#等),以支持新的语法,允许开发人员定义个引入泛型类型和方法
    4)修改编译器,使之能生成新的IL指令和修改元数据格式
    5)修改JIT编译器,使之能够处理新的、支持类型实参的IL指令,以便生成正确的本地代码
    6)创建新的反射成员,使开发人员能查询类型和成员,以判断它们是否具有泛型参数。另外,还必须定义新的反射成员,使开发人员能在运行时创建泛型类型和方法定义。
    7)修改调试器以以显示和操作泛型类型、成员、字段以及局部变量。
    8)修改VisualStudio 的"智能感知"(IntelliSense)特性。
 
 1.开放类型封闭类型
  前面我们讨论过CLR如何为应用程序的每个类型创建一个内部数据结构,这种数据结构称为类型对象。
  具有泛型类型参数的类型仍然是类型,CLR同样会为它创建一个内部类型对象。无论是引用类型(类)、值类型(结构)、接口类型,还是委托类型,这一点都是成立的。
  如果没有为任何类型参数提供类型实参,声明的就是一个未绑定泛型类型
  如果指定了类型实参,该类型就称为已构造类型
  我们知道,类型可以看做是对象的蓝图。同样的,未绑定泛型类型是已构造类型的蓝图。它是一种额外的抽象层。
  已构造类型可以是开放类型封闭类型
  "开放类型"(open type)是指还包含一个类型参数,CLR禁止构造开放类型的任何实例。这一点类似于CLR禁止构造接口类型的实例。
  代码引用一个泛型类型时,可指定一组泛型类型实参。假如为所有类型实参传递的都是实际数据类型,类型就称为"封闭类型"(closed type)。也就是说,具有泛型"类型实参"的类型称为"封闭类型"。CLR允许构造封闭类型的实例。
  当代码引用一个泛型类型时,可能会留下一些泛型类型实参未指定。这会在CLR中创建一个新的开放类型的对象,而且不能创建该类型的实例。比如:
internal static class Program
{
private static void Main(string[] args)
{
Object o = null; // Dictionary<,> 是一个开放类型,有两个类型参数
Type t = typeof(Dictionary<,>); // 尝试创建该类型的一个实例 (失败)
o = CreateInstance(t);
Console.WriteLine(); // DictionaryStringKey<> 是一个开放类型,有一个类型参数
t = typeof(DictionaryStringKey<>); // 尝试创建该类型的一个实例 (失败)
o = CreateInstance(t);
Console.WriteLine(); // DictionaryStringKey<Guid> 是一个封闭类型
t = typeof(DictionaryStringKey<Guid>); // 尝试创建该类型的一个实例 (成功)
o = CreateInstance(t); // Prove it actually worked
Console.WriteLine("Object type=" + o.GetType()); Console.ReadKey();
} private static Object CreateInstance(Type t)
{
Object o = null;
try
{
o = Activator.CreateInstance(t);
Console.Write("已创建 {0} 的实例", t.ToString());
}
catch (ArgumentException e)
{
Console.WriteLine(e.Message);
}
return o;
} // A partially specified open type
internal sealed class DictionaryStringKey<TValue> :
Dictionary<String, TValue>
{
}
}

  最后显示地结果为:

[CLR via C#]12. 泛型

[CLR via C#]12. 泛型  可以看出,Activator的CreateInstance方法会在构造开发类型的实例时抛出一个ArgumentException异常。注意,在异常的字符串消息中,指明类型中仍然含有一些泛型参数。
 
  从输出结果可以看出,类型名是以一个"`"字符和一个数字结尾的。这个数字代表类型的元数,也就是类型要求的类型参数的个数。例如,Dictionary类的元数为2,它要求为TKey和TValue这两个类型参数指定具体类型。
 
  还要注意的是,CLR会在类型对象内部分配类型的静态字段。因此,每个封闭类型都有自己的静态字段。换言之,假如List<T>定义了任何静态字段,这些字段不会在一个List<DataTime>和List<String>之间共享;每个封闭类型对象都有它自己的静态字段。另外,假如一个泛型类型定义了一个静态构造器,那么针对每个封闭类型,这个构造器都会执行一次。在泛型类型上定义一个静态构造器的目的是保证传递的类型参数满足特定的条件。例如,如果希望一个泛型类型值用于处理枚举类型,可以如下定义:
internal sealed calss GenericTypeThatReqiresAnEnum<T> {
static GenericTypeThatReqiresAnEnum() {
if ( !typeof (T).IsEnum) {
throw new ArgumentException("T must be an enumerated type")
}
}
}

  CLR提供了一个名为"约束"(constraint)的功能,可利用它更好地定义一个泛型类型来指出哪个类型实参是有效的。

 
 2.泛型类型和继承
  泛型类型仍然是类型,所以它能从其他任何类型派生。使用一个泛型类型并指定类型实参时,实际上是在CLR中定义一个新的类型对象,新的类型对象是从派生该泛型类型的那个类型派生的。也就是说,由于List<T>是从Object派生的,那么List<String>和List<Guid>也是从Object派生的。
 
 3. 泛型类型同一性
  有的时候,泛型语法会将开发人员搞糊涂,所以有的开发人员定义了一个新的非泛型类类型,它从一个泛型类型派生,并指定了所有的类型实参。例如,为了简化一下代码:
List<DateTime> dt = new List<DateTime>();

一些开发人员可能首先定义下面这样的一个类:

internal sealed class DateTimeList : List<DataTime> {
//这里无需放任何代码!
}

然后就可以进一步简化创建:

DateTimeList  dt = new DateTimeList ();

  这样做表面上是方便了,但是决定不要单纯处于增强源代码的易读性类这样定义一个新类。这样会丧失类型同一性(identity)和相等性(equivalence)。如下:

Boolean sameType = (typeof(List<DateTime>) == (typeof(DateTimeList));

  上述代码运行时,sameType会初始化为false,因为比较的是两个不同类型的对象。也就是说,假如一个方法的原型接受一个DateTimeList,那么不能将一个List<DateTime>传给它。然而,如果方法的原型接受一个List<DateTime>,那么可以将一个DateTimeList传给它,因为DateTimeList是从List<DateTime>派生的。

  C#提供一种方式,允许使用简化的语法来引用一个泛型封闭类型,同时不会影响类的相等性——使用using指令。比如:
using DateTimeList = System.Collections.Generic.List<System.DateTime>;

  现在只想下面这行代码时,sameType会初始化为true:

Boolean sameType = (type(List<DateTime>) == (ypeof(DateTimeList));

  还有,可以使用C#的隐式类型局部变量功能,让编译器根据表达式的类型来推断一个方法的局部变量的类型。

 
 4.代码爆炸
 
  使用泛型类型参数的一个方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参进行替换,然后创建恰当的本地代码。然而,这样做有一个缺点:CLR要为每种不同的方法/类型组合生成本地代码。我们将这个现象称为"代码爆炸"。它可能造成引用程序集的显著增大,从而影响性能。
 
  CLR内建了一些优化措施,能缓解代码爆炸。首先,假如为一个特定的类型实参调用了一个方法,以后再次使用相同的类型实参来调用这个方法,CLR只会为这个方法/类型组合编译一次。所以,如果一个程序集使用List<DateTime>,一个完全不同的程序集也使用List<DateTime>,CLR只会为List<DateTime>编译一次方法。
 
  CLR还提供了一个优化措施,它认为所有引用类型实参都是完全相同的,所以代码能够共享。之所以能这样,是因为所有引用类型的实参或变量时间只是执行堆上的对象的指针,而对象指针全部是以相同的方式操作的。
 
  但是,假如某个类型实参是值类型,CLR就必须专门为那个值类型生成本地代码。因为值类型的大小不定。即使类型、大小相同,CLR仍然无法共享代码,可能需要用不同的本地CPU指令操作这些值。
 
四、泛型接口
 
  泛型的主要作用就是定义泛型的引用类型和值类型。然而,对泛型接口的支持对CLR来说也很重要。没有泛型接口,每次试图使用一个非泛型接口(如IComparable)来操作一个值类型,都会发生装箱,而且会失去编译时的类型安全性。这将严重制约泛型类型的应用。因此,CLR提供了对泛型接口的支持。一个引用类型或值类型可以通过指定类型实参的方式来实现泛型接口。也可以保持类型实参的未指定状态来实现一个泛型接口。
 
  以下是泛型接口定义是FCL的一部分:
public interface IEnumerator<T> : IDisposable, IEnumerator{
T Current { get; }
}

  下面的示例类型实现上述泛型接口,而且指定了类型实参。

internal sealed class Triangle : IEnumerator<Point> {
private Point[] m_Vertice; public Point Current { get { ... } }
}

  下面实现了相同的泛型接口,但保持类型实参的未指定状态:

internal sealed class ArrayEnumerator<T> :  IEnumerator<T> {
private T[] m_Vertice; public TCurrent { get { ... } }
}

五、泛型委托

  CLR支持泛型委托,目的是保证任何类型的对象都能以一种类型安全的方式传给一个回调方法。

此外,泛型委托允许一个值类型的实例在传给一个回调方法时不执行任何装箱操作。

  委托实际只提供了4个方法的一个类定义。这4个方法包括一个构造器、一个Invoke方法、一个BeginInvoke和一个EndInvoke方法。如果定义的一个委托类型指定了类型参数,编译器会定义委托类的方法,用指定的类型参数替代方法中的参数类型和返回值类型。
 
  例如,假定向下面这样定义一个泛型委托:
 public delegate TReturn CallMe<TReturn, TKey, TValue>(TKey key, TValue value);

  编译器会将它转化成一个类,该类在逻辑上可以这样表示:

public sealed class CallMe<TReturn, TKey, TValue> : MulticastDelegate {
public CallMe(Object object, IntPtr method);
public virtual TReturn Invoke(TKey key, TValue value);
public virtual IAsycResult BeginInvoke(TKey key, TValue value, AsyncCallback callback, Object object);
public virtual TReturn EndInvoke(IAsycResult result);
}

  反编译后

[CLR via C#]12. 泛型

  建议尽量使用在FVL中预定义的泛型Action和Func委托。

[CLR via C#]12. 泛型

  
 六、 委托和接口的逆变和协变泛型类型实参
 
  委托的每个泛型类型参数都可标识为协变量或者逆变量。利用这个功能,可将泛型委托类型的一个变量转型为同一个委托类型的另一个变量,后者的泛型参数类型不同。泛型类型参数可以是一下任何一种形式:
  1)不变量(invariant)    意味着泛型类型参数不能更改。
  2)逆变量(Contravarriant)    意味着泛型类型参数可以从一个基类更改为该类的派生类。在C#中,用 in 关键字标识逆变量形式的泛型类型参数。逆变量泛型参数只出现在输入位置,比如作为方法的参数。
  3)协变量(Convarianr)    意味着泛型类参数可以从一个派生类更改为它的基类。在C#中用 out 关键字标记协变量形式的泛型类型参数。协变量泛型参数只能出现在输出位置,比如作为方法的返回类型。
 
  例如,现在存在以下委托类型定义(它在FCL中是存在的) 
public delegate TResult Func<in T, Out TResult>(T arg);

  其中,泛型类型参数T用in关键字标记,这使它成为一个逆变量;泛型类型参数TResulr则用out关键字标记,这是它成为一个协变量。

  所以,如果像下面这样声明一个变量:
Func<Object,ArgumenException> fn1 = null;

  就可以将它转型为另一个泛型类型参数不同的Func类型:

Func<String,Exception> fn2 = fn1;    //不需要显示转型
Exception e = fn("");

  使用要获取泛型参数和返回值的委托时,建议尽量为逆变性和协变性指定in和out关键字。这样做不会有不良反应,并使你的委托能在更多的情形中使用。

 
  和委托相似,具有泛型类型参数的接口也可将它的类型参数标记为逆变量和协变量。比如:
public interface IEnumerator<out T> : IEnumerator {
Boolean MoveNext();
T Current{ get; }
}

  由于T是逆变量,所以以下代码可以顺利编译:

//这个方法接受任意引用类型的一个IEnumerable
Int32 Count(IEnumerable<Object> collection) { ... }
//以下调用向Count传递一个IEnumerable<String>
Int32 c = Count(new[] { "Grant" });

七、泛型方法

  定义泛型类、结构或接口时,这些类型中定义的任何方法都可引用由类型指定的一个类型参数。类型参数可以作为方法的参数,作为方法的返回值,或者作为方法内部定义的一个局部变量来使用。

  CLR还允许一个方法指定它独有的类型参数。这些类型参数可用于参数、返回值或者局部变量。
 
  在下面的例子中,一个类型定义了一个类型参数,一个方法则定义了它自己的专用类型参数:
internal sealed class FenericType<T> {
privete T m_value; public GenericType(T value) { m_value = value; } public TOutput Converter<TOutput>() {
TOutput resulr= (TOurput) Convert.ChangeType(m_value,typeof(TOutput));
return result;
}
}

 1.泛型方法和类型推断

   为了改进代码的创建,同事增强可读性和维护性,C#编译器支持在调用一个泛型方法时进行类型推断(type inference)。这意味着编译器会在调用一个泛型方法时自动判断出要使用的类型。
private static void CallingSwapUsingInference() {
Int32 n1 = , n2 = ;
Swap(ref n1, ref n2); //调用Swap<Int32> String s1 = "A";
Object s2 = "B";
Swap(ref s1, ref s2); //错误,不能推断类型
}

  执行类型推断时,C#使用变量的数据类型,而不是由变量引用的对象的实际类型。

八、泛型和其他成员

  在C#中,属性、索引器、事件、操作符方法、构造器和终结器(finalizer)本身不能有类型参数。但是,它们能在一个泛型类型中定义,而且这些成员中的代码能使用类型的类型参数。

九、可验证性和约束

  C#编译器和CLR支持一个称为"约束"(constraint)的机制,可利用它使泛型变得真正有用。约束的作用是限制能指定成泛型实参的参数数量。通过限制类型的数量,我们可以对那些类型执行更多的操作。
public static T Min<T>(T o1, T o2) where T : IComparable<T> {
if (o1.CompareTo(o2))< return o1;
return o2;
}

  C#的where关键字告诉编译器,为T指定的任何类型都必须实现同类型(T)的泛型IComparable接口。有了这个约束,就可以在方法中调用CompareTo,因为已知IComparable<T>接口定义了CompareTo。

   约束可应用于一个泛型类型的类型参数,也可应用于一个泛型方法的类型参数(就像Min所展示的)。CLR不允许基于类型参数名称或约束来进行重载;只能基于元数(类型参数的个数)对类型或方法进行重载。
  
internal sealed class OverloadingByArity {
// 可以定义一下类型
internal sealed class AType { }
internal sealed class AType<T> { }
internal sealed class AType<T1, T2> { } // 错误: 与没有约束的 AType<T> 起冲突
internal sealed class AType<T> where T : IComparable<T> { } // 错误: 与 AType<T1, T2> 起冲突
internal sealed class AType<T3, T4> { } internal sealed class AnotherType {
// 可以定义一下方法,参数个数不同:
private static void M() { }
private static void M<T>() { }
private static void M<T1, T2>() { } // 错误: 与没有约束的 M<T> 起冲突
private static void M<T>() where T : IComparable<T> { } // 错误: 与 M<T1, T2> 起冲突
private static void M<T3, T4>() { }
}
}
 
  重写一个虚泛型方法时,重写的方法必须指定相同数量的类型参数,而且这些类型参数会继承在基类方法上指定的约束。事实上,根本不允许为重写方法的类型参数指定任何约束。但是,类型参数的名称是可以改变的。类似的,实现一个接口方法时,方法必须指定与接口方法等量的类型参数,这些类型参数将继承由接口的方法在它们前面指定的约束。下例使用虚方法演示了这一规则:
internal static class OverridingVirtualGenericMethod {
internal class Base {
public virtual void M<T1, T2>()
where T1 : struct
where T2 : class {
}
} internal sealed class Derived : Base {
public override void M<T3, T4>()
/*where T3 : struct
where T4 : class */
{
}
}
}

 1 主要约束

  类型参数可以指定零个或一个主要约束。主要约束可以是一个引用类型,它标志了一个没有密封的类。不能指定以下特殊引用类型:System.Object,System.Array,System.Delagate,System.MulticastDelegate,System.ValueType,System.Enum和System.Void。
 
  指向一个引用类型约束时,相当于向编译器承诺:一个指定的类型实参要么是与约束类型相同的类型,要么是从约束类型派生的一个类型。如下泛型类:
 internal static class PrimaryConstraintOfStream<T> where T : Stream {
public static void M(T stream) {
stream.Close(); // OK
}
}

  有两个特殊的主要约束:class和struct。其中,class约束是指类型实参是一个引用类型。任何类类型、接口类型、委托类型或者数组类型都满足这个约束。例如:

internal static class PrimaryConstraintOfClass<T> where T : class {
public static void M() {
T temp = null; // 允许,T为引用类型
}
}

  struct约束向编译器承诺一个指定的类型实参是值类型。包括枚举在内的任何值类型都满足这个约束。然而,编译器和CLR将任何System.Nullable<T>值类型都视为特殊类型。

internal static class PrimaryConstraintOfStruct<T> where T : struct {
public static T Factory() {
// 允许,因为值类型都有一个隐式无参构造器
return new T();
}
}
 2 次要约束
  一个类型参数可以指定零个或者多个次要约束,次要约束代表的是一个接口类型。指定一个接口类型约束时,是向编译器承诺一个指定的类型实参是是实现了接口的一个类型。由于能指定多个接口约束,所以为类型实参指定的类型必须实现了所有接口约束。
 
  还有一种次要约束称为类型参数约束,有时也称裸类型约束。这种约束用的比接口约束少得多。它允许一个泛型类型或方法规定:在指定的类型实参之间,必须存在一个关系。一个类型参数可以指定零个或者多个类型参数约束。下面这个泛型方法演示了如何使用类型参数约束:
internal static class SecondaryConstraints
{
private static List<TBase> ConvertIList<T, TBase>(IList<T> list)
where T : TBase
{ List<TBase> baseList = new List<TBase>(list.Count);
for (Int32 index = ; index < list.Count; index++)
{
baseList.Add(list[index]);
}
return baseList;
} private static void CallingConvertIList()
{
//构造并初始化一个List<String>(它实现了IList<String>)
IList<String> ls = new List<String>();
ls.Add("A String"); // 将IList<String>转换成IList<Object>
IList<Object> lo = ConvertIList<String, Object>(ls); // 将IList<String>转换成IList<IComparable>
IList<IComparable> lc = ConvertIList<String, IComparable>(ls); // 将IList<String>转换成IList<IComparable<String>>
IList<IComparable<String>> lcs =
ConvertIList<String, IComparable<String>>(ls); // 将IList<String>转换成IList<Exception>
//IList<Exception> le = ConvertIList<String, Exception>(ls); // 错误
}
}
 3 构造器约束
  一个类型参数可以指定零个或者一个构造器约束。指定构造器约束相当于向编译器承诺:一个指定的类型实参是实现了公共无参构造器的一个非抽象类型。
internal sealed class ConstructorConstraints
{
internal sealed class ConstructorConstraint<T> where T : new()
{
public static T Factory()
{
// 允许,因为值类型都有隐式无参构造器
// 而约束要求任何引用类型也要有一个无参构造器
return new T();
}
}
}

  目前,Microsoft只支持无参构造器约束。

 
 4 其他可验证问题
 
  1)泛型类型变量的转型
  将一个泛型类型的变量转型为另一个类型是非法的,除非将其转型为与一个约束兼容的类型:
 private void CastingAGenericTypeVariable1<T>(T obj)
{
Int32 x = (Int32)obj; // 错误
String s = (String)obj; // 错误
}

  上述两行错误是因为T可以是任何任何类型,无法保证成功。

private void CastingAGenericTypeVariable2<T>(T obj)
{
Int32 x = (Int32)(Object)obj; // 不报错
String s = (String)(Object)obj; // 不报错
}

  现在虽然能编译通过,但运行时也无法保证是正确的。

  2)将一个泛型类型变量设为默认值
  将泛型类型变量设为null是非法的,除非将泛型 类型约束成一个引用类型:
internal sealed class SettingAGenericTypeVariableToADefaultValue
{
private void SettingAGenericTypeVariableToNull<T>()
{
//T temp = null; // 错误, 值类型不能设置为null,可考虑使用default('T')
} private void SettingAGenericTypeVariableToDefaultValue<T>()
{
T temp = default(T); // 正确
}
}
  3)将一个泛型类型变量与null进行比较
  无论泛型类型是否非约束,使用==或!=操作符将一个泛型类型变量与null进行比较都是合法的。
private void ComparingAGenericTypeVariableWithNull<T>(T obj)
{
if (obj == null) { /* 对值类型来说,永远不会执行 */ }
}

  如果T被约束成一个struct,C#编译器会报错。

 
  4)两个泛型类型变量相互比较
  如果泛型类型参数不是一个引用类型,对同一个泛型类型的两个变量进行比较是非法的:
private void ComparingTwoGenericTypeVariables<T>(T o1, T o2)
{
//if (o1 == o2) { } // 错误
}

  5)泛型类型变量作为操作书使用

  将操作符应用于泛型类型的操作数,会出现大量问题。不能将操作符应用于泛型,因为编译器在编译时无法确定类型。
internal static class UsingGenericTypeVariablesAsOperands {
private T Sum<T>(T num) where T : struct {
T sum = default(T);
for (T n = default(T); n < num; n++)
sum += n;
return sum;
}
}

  上面代码会出很多错误,比如:运算符"<"无法应用于"T"和"T"类型的操作数等。

  这是CLR的泛型支持体系的一个严重限制。
 
 

[CLR via C#]12. 泛型的更多相关文章

  1. &period;NET via C&num;笔记12——泛型

    12 泛型 使用值类型作为参数的泛型容器,传入值类型的参数时,不需要进行装箱 12.1 FCL中的泛型 System.Array中提供了很多泛型方法 AsReadOnly BinarySearch C ...

  2. 【C&num;进阶系列】12 泛型

    泛型是CLR和编程语言提供的一种特殊机制,它用于满足“算法重用”  . 可以想象一下一个只有操作的参数的数据类型不同的策略模式,完全可以用泛型来化为一个函数. 以下是它的优势: 类型安全 给泛型算法应 ...

  3. CLR via C&num;&lpar;16&rpar;--泛型

    泛型就像是一个模板,常常定义一些通用的算法,具体调用时再替换成实际的数据类型,提高了代码的可重用性. 一.初识泛型 1. 简单实例 以最常用的FCL中的泛型List<T >为例: stat ...

  4. 【Clr in c&num;】泛型

    使用泛型的好处是“代码重用”,极大的提高了开发效率,泛型为开发者提供了以下优势: 1,源代码保护  算法的源代码不需要提供给使用泛型算法的开发人员,使用c++模板的泛型技术需要提供.(目前c++模板的 ...

  5. CLR类型设计之泛型(二)

    在上一篇文章中,介绍了什么是泛型,以及泛型和非泛型的区别,这篇文章主要讲一些泛型的高级用法,泛型方法,泛型接口和泛型委托,协变和逆变泛型类型参数和约束性,泛型的高级用法在平时的业务中用的不多,多用于封 ...

  6. CLR类型设计之泛型(一)

    在讨论泛型之前,我们先讨论一下在没有泛型的世界里,如果我们想要创建一个独立于被包含类型的类和方法,我们需要定义objece类型,但是使用object就要面对装箱和拆箱的操作,装箱和拆箱会很损耗性能,我 ...

  7. CLR via C&num;关于泛型(Generics )的摘录

    泛型,是CLR和编程语言提供的一种特殊机制,它支持另一种形式的代码重用,即“算法重用”. 简单的说,开发人员先定义好一个算法,比如排序.搜索.交换.比较或者转换等.但是,定义算法的开发人员并不设改算法 ...

  8. 重温CLR(八 ) 泛型

    熟悉面向对象编程的开发人员都深谙面向对象的好处,其中一个好处是代码重用,它极大提高了开发效率.也就是说,可以派生出一个类,让他继承基类的所有能力.派生类只需要重写虚方法,或添加一些新方法,就可定制派生 ...

  9. 读&lt&semi;&lt&semi;CLR via C&num;&gt&semi;&gt&semi; 详谈泛型

    1,什么是泛型? 答:泛型是类型的模板,类型是实例(对象)的模板.C#提供了5种泛型:类,接口,委托,结构和方法. 2,使用泛型有什么好处? 答:继承实现的是"代码重用",而泛型实 ...

随机推荐

  1. struts2实现改变超链接的显示方式

    问题来源:我们看别人家专业的网站的时候,会发现他们的超链接都是这样的http://www.cnblogs.com/ivictor/p/5522383.html 这是一个连接到某一篇文章的超链接,我们一 ...

  2. Regex

    1. regex with variable example: find the number and put a parenthese around the number. output: a(52 ...

  3. iOS 10 因苹果健康导致闪退 crash-b

    如果在app中调用了苹果健康,iOS10中会出现闪退.控制台报出的原因是: Terminating app due to uncaught exception 'NSInvalidArgumentEx ...

  4. &lbrack;itint5&rsqb;最大子矩阵和

    http://www.itint5.com/oj/#39 最大子矩阵和,复杂度O(n^3).利用了最大子段和的方法. int maxRectSum(vector<vector<int&gt ...

  5. SpringMVC轻松学习-注解的使用(三)

    根据上一讲的例子,我们下面就注解的使用进行详细说明. 我们采用sprng MVC开发项目时,通常都会采用注解的方式,这样可以大大提高我们的开发效率.实现零配置.下面我们从零开始重新做一个spring ...

  6. 专访笨叔叔:2019年可能是Linux年?(转)

    链接:https://zhuanlan.zhihu.com/p/57815479 2017年9月<奔跑吧 Linux内核>一书出版后得到了广大Linux从业人员和爱好者(特别是从事Linu ...

  7. docker学习笔记(四)-持久化数据,安装docker-compose

    Docker 持久化数据 实战 compose 安装docker-compose

  8. Instant Django 1&period;5 Application Development Starter

    Django is a high-level Python Web framework that encourages rapid development and clean, pragmatic d ...

  9. JWT的介绍解析

    JWT的介绍解析 一.什么是JWT?了解JWT,认知JWT 首先jwt其实是三个英语单词JSON Web Token的缩写.通过全名你可能就有一个基本的认知了.token一般都是用来认证的,比如我们系 ...

  10. django使用JWT保存用户登录信息

    在使用前必须弄明白JWT的原理,原理可以看我的另一篇博文:https://www.cnblogs.com/chichung/p/9966027.html JWT的流程 1.签发JWT 在用户正确输入账 ...