C# in depth ( 第三章 用泛型实现参数化类型)
3.1 为什么需要泛型
- 避免了强制转换,使代码更易读易写,也就减少了出bug的几率。
- 提升了性能
- 由于编译时做了更多的检查,运行时的检查就可以少做很多。
- JIT能够聪明地处理值类型,能消除很多情况下的装箱和拆箱处理。
3.2 日常使用的简单泛型
3.2.1通过例子来学习: 泛型字典
class DictionaryDemo { static Dictionary<string,int> CountWords(string text) { Dictionary<string,int> frequencies; frequencies = new Dictionary<string,int>(); string[] words = Regex.Split(text, @"\W+"); foreach (string word in words) { if (frequencies.ContainsKey(word)) { frequencies[word]++; } else { frequencies[word] = 1; } } return frequencies; } static void Main() { string text = @"Do you like green eggs and ham? I do not like them, Sam-I-am. I do not like green eggs and ham."; Dictionary<string, int> frequencies = CountWords(text); foreach (KeyValuePair<string, int> entry in frequencies) { string word = entry.Key; int frequency = entry.Value; Console.WriteLine("{0}: {1}", word, frequency); } } }
3.2.2 泛型类型和类型参数
- 泛型有两种形式
- 泛型类型(类,接口,委托和结构)
- 泛型方法
- 类型参数 类型参数是真实类型的占位符 在Dictionary<TKey,TValue>中类型参数是TKey和TValue。
- 类型实参 使用泛型类型或方法时,要用真实的类型替代,这些真实的类型成为类型实参(type argument)在代码清单中类型实参是string(代替TKey)和int(代替TValue)
- 未邦定泛型类型(unbound generic type) 如果没有为泛型类型参数提供类型实参,那么这就是一个未邦定泛型类型(unbound generic type)
- 在C#代码中唯一能看见未邦定代码的地方就是typeof
var type= typeof(Dictionary<,>);
- 如果指定了类型实参,该类型就称为一个已构造的类型(constructed type), 已构造类型又可以是开放或封闭的。
- 开放类型(open type)还包含一个类型参数
- 封闭类型(closed type)则不是开放的,类型的每个部分都是明确的。
- 未绑定泛型类型相当于已构造类型的蓝图。已构造类型又是实际对相的蓝图,这一点和非泛型类型的作用是相似的
非泛型蓝图 | 泛型蓝图 | |
Dictionary<TKey,TValue>(未绑定泛型类型) | ||
指定类型参数 | 指定类型参数 | |
Dictionary<string,int>(已构造类型) | Dictionary<byte,long>(已构造类型) | |
实例化 | 实例化 | 实例化 |
Hashtable实例 | Dictionary<string,int>实例 | Dictionary<byte,long>实例 |
泛型类型中的方法签名 | 类型参数被替换之后的方法签名 |
void Add (TKey key,Tvalue value) | void Add (string key,int value) |
注意上表中的方法并不是泛型方法,只是泛型类型中的普通方法,只是凑巧使用了作为类型一部分声明的类型参数。
- 泛型类型可以重载MyType,MyType<T>,MyType<T,U>,MyType<T,U,V> 所有这些定义可以被放到同一个命名空间,类型参数的名称并不重要,重要的是个数,泛形方法也一样。
3.2.3 泛型方法和判读泛型声明
class ListConvertAll { static double TakeSquareRoot(int x) { return Math.Sqrt(x); } static void Main() { List<int> integers = new List<int>(); integers.Add(1); integers.Add(2); integers.Add(3); integers.Add(4); Converter<int, double> converter = TakeSquareRoot; List<double> doubles = integers.ConvertAll<double>(converter); foreach (double d in doubles) { Console.WriteLine(d); } } }
class GenericMethodDemo { static List<T> MakeList<T>(T first, T second) { List<T> list = new List<T>(); list.Add(first); list.Add(second); return list; } static void Main() { List<string> list = MakeList<string>("Line 1", "Line 2"); foreach (string x in list) { Console.WriteLine(x); } } }
3.3深化与提高
3.3.1类型约束
- 引用类型的约束
struct RefSample<T> where T: class RefSample<string>仍然是值类型,以这种方式约束了一个参数类型后,可以使用==来比较引用(包括null) 需要注意的是除非还存在其他约束,否则只能比较引用。
- 值类型的约束
class ValSample<T> where T:struct 可以确保使用的类实参是值类型,包括枚举(enums)。但是它将可空类型排除在外。
类型参数被约束为值类型后就不允许使用==和!=进行比较。 -
构造函数类型约束
T :new() 必须是所有类型参数的最后一个约束,它检查类型实参是否有一个可用于创建类型实例的无参构造函数。这适用于所有值类型,所有没有显示声明构造函数的非静态,非抽象类;所有显式声明了一个公共无参构造函数的非抽象类。 public T CreateInstance<T>() where T: new() { return new T(); } CreateInstance<int>() 和 CreateInstance<object>()都是有效的, 但是CreateInstance<string>()是无效的,因为string没有无参构造函数
- 转换类型约束
可以指定多个接口,但只能指定一个类。例如,以下声明毫无问题(尽管很难满足): Class Sample<T> where T: Stream, IEnumerable<string>, IComparable<int> 但以下声明就有问题了: Class Sample<T> where T: Stream, ArrayList, IComparable<int>
总之,任何类型都不能派生自多个类,对于这样一个约束要么是永远无法满足(如上),要么它的一部分是多余的(例如,规定类型必须从Stream和MemoryStream派生)
此外还有一系列的限制:指定的类不可以是结构,密封类,或者以下任何特殊类型:
System.Object
System.Enum
System.ValueType
System.Delegate -
组合约束
有效 class sample<T> where T: class, IDisposable, new() class sample<T> where T: struct, IDisposable class sample<T,U> where T :class where U :struct, T (T是object,或者是U实现的一个接口) class sample<T,U> where T: Stream where U : IDisposable 无效 class sample<T> where T: class, struct class Sample<T> where T: Stream, class class Sample<T> where T : new(), Stream (new 要放在最后) class Sample<T> where T : IDisposable, Stream class Sample<T> where T : XmlReader, IComparable, IComparable class Sample<T,U> where T: struct, where U:class, T (和上方有效的第三条不一样,相反) class Sample<T,U> where T: Stream, U:IDisposable (没有where)
- 约束可以分为 主要约束,次要约束,和构造函数约束。
- 主要约束可以为引用类型约束,值类型约束或使用类的转换类型约束。
- 主要约束是可选的,但是只能有一个
- 次要约束为使用接口或其他类型参数的转换类型约束。
- 次要约束则可以有多个
- 构造函数约束
- 构造函数也是可选的(如果拥有了值类型约束,就不能再使用构造函数约束)
3.3.2 泛型方法类型实参的类型推断 (类型推断只适用于泛型方法,不适用于泛型类型)
3.3.3 实现泛型
1.默认值表达式
class DefaultValueComparison { static int CompareToDefault<T>(T value) where T : IComparable<T> { return value.CompareTo(default(T)); } static void Main() { Console.WriteLine(CompareToDefault("x")); //1 Console.WriteLine(CompareToDefault(10)); //1 Console.WriteLine(CompareToDefault(0)); //0 Console.WriteLine(CompareToDefault(-10)); //-1 Console.WriteLine(CompareToDefault(DateTime.MinValue)); //0 } }
2.直接比较
- 如果一个类型参数是未约束的(即没有对其应用约束),那么且只能在将该类型的值与null进行比较时才能使用==和!=操作符。不能直接比较两个类型的值(因为值类型不知道如何比较)
- 如果类型参实参是一个引用类型,会进行正常的引用比较。
- 如果为T提供的类型实参是一个非可空值类型,与null进行比较的结果总是显示他们不相等(这样一来JIT编译器就可以移除这个比较)。如果类型参数是可空值类型,那么就会自然而然与类型的空值进行比较。
- 如果它只是一个引用类型,那么执行的是简单的引用比较。如果它被进一步约束成继承自某个重载了==和!=操作符的特定类型,就会使用重载的操作符。但要注意,假如调用者指定的类型实参恰巧也进行了重载,那么这个重载操作符是不会使用的。
class OperatorOverloading { static bool AreReferencesEqual<T>(T first, T second) where T : class { return first == second; (此处不会调用string的重载) } static void Main() { string name = "Jon"; string intro1 = "My name is " + name; string intro2 = "My name is " + name; Console.WriteLine(intro1 == intro2); //true Console.WriteLine(AreReferencesEqual(intro1, intro2));//false } }
- 并非只有操作符才有这个问题,遇到泛型类型时,编译器会在编译未绑定的泛型类型时,就解析好所有方法重载,而不是等到执行时
Console.WriteLine(default(T));这个语句总是被解析成调用Console.WriteLine(object value)。 即使为T传递的类型实参恰好是string,也不会调用Console.WriteLine(string value)
- 泛型接口比较
- IComparaer<T>和IComparable<T>用于排序,而IEqualityComparer<T>和IEquatable<T>通过某种标准来比较两个项的相等性,或查找出某个项的散列(通过与相等性概念匹配的方式)。
- IComparaer<T>和IEqualityComparer<T>的实例能够比较两个不同的值,而IComparable<T>和IEquatable<T>的实例则可以比较它的本身和其他值。
3.完整的比较例子:表示一对值
[Description("Listing 3.06")] public sealed class Pair<TFirst, TSecond> : IEquatable<Pair<TFirst, TSecond>> { private static readonly IEqualityComparer<TFirst> FirstComparer = EqualityComparer<TFirst>.Default; private static readonly IEqualityComparer<TSecond> SecondComparer = EqualityComparer<TSecond>.Default; private readonly TFirst first; private readonly TSecond second; public Pair(TFirst first, TSecond second) { this.first = first; this.second = second; } public TFirst First { get { return first; } } public TSecond Second { get { return second; } } public bool Equals(Pair<TFirst, TSecond> other) { return other != null && FirstComparer.Equals(this.First, other.First) && SecondComparer.Equals(this.Second, other.Second); } public override bool Equals(object o) { return Equals(o as Pair<TFirst, TSecond>); } public override int GetHashCode() { return FirstComparer.GetHashCode(first) * 37 + SecondComparer.GetHashCode(second); } }
- 使用包含泛型方法的非泛型类型进行类型推断
[Description("Listing 3.07")] public static class Pair { public static Pair<TFirst, TSecond> Of<TFirst, TSecond>(TFirst first, TSecond second) { return new Pair<TFirst, TSecond>(first, second); } }
Pair<int,string> pair = Pair.Of(10,"value");
3.4高级泛型
3.4.1静态字段和静态构造函数
- 每个封闭类型都有自己的静态字段集
class StaticFieldPerClosedType { class TypeWithField<T> { public static string field; public static void PrintField() { Console.WriteLine(field + ": " + typeof(T).Name); } } static void Main() { TypeWithField<int>.field = "First"; TypeWithField<string>.field = "Second"; TypeWithField<DateTime>.field = "Third"; TypeWithField<int>.PrintField(); TypeWithField<string>.PrintField(); TypeWithField<DateTime>.PrintField(); } }
- 静态初始化程序(static initializer)和静态构造函数也一样(static constructor)
class StaticConstructors { class Outer<T> { public class Inner<U, V> { static Inner() { Console.WriteLine("Outer<{0}>.Inner<{1},{2}>", typeof(T).Name, typeof(U).Name, typeof(V).Name); } public static void DummyMethod() { } } } static void Main() { Outer<int>.Inner<string, DateTime>.DummyMethod(); Outer<string>.Inner<int, int>.DummyMethod(); Outer<object>.Inner<string, object>.DummyMethod(); Outer<string>.Inner<string, object>.DummyMethod(); Outer<object>.Inner<object, string>.DummyMethod(); Outer<string>.Inner<int, int>.DummyMethod(); } }
3.4.2 JIT编译器如何处理泛型
- JIT为每个以值类型作为类型实参的封闭类型都创建不同的代码。
- 然而所有使用引用类型(string, Stream,StringBuilder等)作为类型实参的封闭类型都共享相同的本地代码。
- 之所以能这么做,是由于所有引用都具有相同的大小(32位CLR上是4字节,64位CLR上是8字节)无论实际引用的是什么,引用数组的大小是不会发生变化的。
3.4.3 泛型迭代
class CountingEnumerableExample { class CountingEnumerable : IEnumerable<int> { public IEnumerator<int> GetEnumerator() { return new CountingEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } class CountingEnumerator : IEnumerator<int> { int current = -1; public bool MoveNext() { current++; return current < 10; } public int Current { get { return current; } } object IEnumerator.Current { get { return Current; } } public void Reset() { throw new NotSupportedException(); } public void Dispose() { } } static void Main() { CountingEnumerable counter = new CountingEnumerable(); foreach (int x in counter) { Console.WriteLine(x); } } }
3.4.4 反射和泛型
- 对泛型类型使用typeof
- 反射的一切都是围绕“检查对象及其类型”展开的。所以,最重要的就是获取System.Type对象的引用。这样就可以访问与特定类型有关的所有信息。
class TypeofOperator { static internal void DemonstrateTypeof<X>() { Console.WriteLine(typeof(X)); //System.Int32 Console.WriteLine(typeof(List<>)); //System.Collections.Generic.List`1[T] Console.WriteLine(typeof(Dictionary<,>));//System.Collections.Dictionary'2[System.String,System.Int32]
Console.WriteLine(typeof(List<X>)); Console.WriteLine(typeof(Dictionary<string, X>));
Console.WriteLine(typeof(List<long>));
Console.WriteLine(typeof(Dictionary<long, Guid>)); }
static void Main()
{
DemonstrateTypeof<int>();
}
}
2.System.Type的属性和方法
[Description("Listing 3.12")] class GenericTypeReflection { static void Main() { string listTypeName = "System.Collections.Generic.List`1"; Type defByName = Type.GetType(listTypeName); Type closedByName = Type.GetType(listTypeName + "[System.String]"); Type closedByMethod = defByName.MakeGenericType(typeof(string)); Type closedByTypeof = typeof(List<string>); Console.WriteLine(closedByMethod == closedByName); Console.WriteLine(closedByName == closedByTypeof); Type defByTypeof = typeof(List<>); Type defByMethod = closedByName.GetGenericTypeDefinition(); Console.WriteLine(defByMethod == defByName); Console.WriteLine(defByName == defByTypeof); } }
无论怎样获取对一个特定类型对象的引用,都只涉及一个这样的对象
3.反射泛型方法
class GenericMethodReflection { public static void PrintTypeParameter<T>() { Console.WriteLine (typeof(T)); } static void Main() { Type type = typeof(GenericMethodReflection); MethodInfo definition = type.GetMethod("PrintTypeParameter"); MethodInfo constructed; constructed = definition.MakeGenericMethod(typeof(string)); constructed.Invoke(null, null); } }
3.5 泛型在C#和其他语言中的限制
3.5.1泛型可变性的缺乏
1.泛型为何不支持协变性 静态类型的全部意义在于在代码运行之前找出错误
Animal[] animals = new Cat[5];//编译可过 animals[0] = new Dog();//运行时报错 List<Animal> lstanimals = new List<Cat>(); //编译不可过 lstanimals.Add(new Dog());
2.协变性在什么时候有用
3.逆变在什么时候有用
3.5.2 缺乏操作符约束或者"数值"约束
3.5.3 缺乏泛型属性,索引器和其他成员类型。
3.5.4 同C++模板的对比
- C++只编译一次,一个C++程序以10种不同方式使用一个标准模板,就会在程序中包含代码10次。但是在C#中,一个类似程序如果以10种不同的方式来使用自框架的一个泛型类型,那么根本不会包含泛型类型的代码。相反,它只是引用一下泛型类型。执行时需要多少个不同的版本,JIT就会编译多少个。
3.5.5 和Java泛型的对比