【读书笔记】C#高级编程 第五章 泛型
(一)泛型概述
泛型不仅是C#编程语言的一部分,而且与程序集中的IL代码紧密地集成。泛型不仅是C#语言的一种结构,而且是CLR定义的。有了泛型就可以创建独立于被包含类型的类和方法了。
1、性能
泛型的一个主要优点就是性能。对值类型使用非泛型集合类,在把值类型转化为引用类型,和把引用类型转换为值类型时,需要进行装箱和拆箱操作。
下面的例子显示了System.Collections名称空间中的ArrayList类。ArrayList存储对象,Add()方法定义为需要把对象作为参数,所以要装箱一个整数类型。在读取ArrayList中的值时,要进行拆箱操作,把对象转化为整数类型。可以使用类型装置转换运算符把ArrayList集合的第一个元素赋予变量i1,在访问int类型的变量i2的foreach语句中,也要使用类型强制转换运算符:
var list = new ArrayList(); list.Add(44);//此处会装箱 int i1 = (int)list[0];//此处会拆箱 foreach (int i2 in list) { Console.WriteLine(i2);//此处会拆箱 }
System.Collections.Generic名称空间中的List<T>类不使用对象,而是在使用时定义类型。装箱和拆箱操作很容易,但性能损失比较大,遍历许多项尤其如此。
下面的例子。List<T>类的泛型类型定义为int,所以int类型在JIT编译器动态生成的类中使用,不再进行装箱和拆箱操作:
var list = new List<int>(); list.Add(44); int i1 = (int)list[0]; foreach (int i2 in list) { Console.WriteLine(i2); }
2、类型安全
泛型的另一个特性是类型安全。
在泛型类List<T>中,泛型类型T定义了允许使用的类型。有了List<int>的定义,就只能把整数类型添加到集合中。编译器不会编译这段代码,因为Add()方法无效,这样类型就安全了:
var list = new List<int>(); list.Add(44); list.Add("str");
这个时候编译器会报错:
3、二进制代码的重用
泛型允许更好地重用二进制代码。泛型类可以定义一次,并且可以用许多不同的类型实例化。
例如,System.Collections.Generic名称空间中的List<T>类用一个int、一个字符串和一个MyClass类实例化:
var intList = new List<int>(); intList.Add(1); var stringList = new List<string>(); stringList.Add("str"); var myClassList = new List<MyClass>(); myClassList.Add(new MyClass());
4、代码的扩展
在不同的特定类型实例化泛型时,会创建多少代码?因为泛型类的定义会放在程序集中,所以用特定类型实例化泛型类不会再IL代码中赋值这些类。但是,在JIT编译器把泛型类编译为本地代码时,会给每个值类型创建一个新类。引用类型共享同一个本地类的所有相同的实现代码。这是因为引用类型在实例化的泛型类中只需要4个字节的内存地址(32位系统),就可以引用一个引用类型。值类型包含在实例化的泛型类的内存中,同时因为每个值类型对内存的要求都不同,所以要为每个值类型实例化一个新类。
5、命名的约定
在程序中使用泛型,在区分泛型类型和非泛型类型时就会有一定的帮助。下面是泛型类型的命名规则:
l 泛型类型的名称用字母T作为前缀。
l 如果没有特殊的要求,泛型类型允许使用任意类替代,且只使用了一个泛型类型,就可以用字符T作为泛型类型的名称。
public class List<T> { } public class LinkedList<T> { }
如果泛型类型有特定的要求(例如,它必须实现一个接口或派生自基类),或者使用了两个或多个泛型类型,就应给泛型类型使用描述性的名称:
public class SortedList<TKey, TValue> { }
(二)创建泛型类
泛型提供了一种新的创建类型的机制,使用泛型创建的类型将带有类型形参。每个处理对象类型的类都可以有泛型实现方式。另外如果类使用了层次结构就非常有助于消除类型强制转换操作。
public class LinkedListNode<T> { public LinkedListNode(T value) { this.Value = value; } public T Value { get; private set; } public LinkedListNode<T> Next { get; internal set; } public LinkedListNode<T> Prev { get; internal set; } }
(三)泛型类的功能
1、默认值
通过default关键字,将null赋予引用类型,将0赋予值类型。
public T GetDocument() { T doc = default(T); return DealDocument(doc); }
default关键字根据上下文可以有多种含义。switch语句使用default定义默认情况。在泛型中,根据泛型类型是引用类型还是值类型,泛型default用于将泛型类型初始化为null或0。
2、约束
在定义泛型类时,可以对客户端代码能够在实例化类时用于类型参数的类型种类施加限制。如果客户端代码尝试使用某个约束所不允许的类型来实例化类,则会产生编译时错误。这些限制称为约束。约束是使用 where 上下文关键字指定的。下表列出了六种类型的约束:
约束 |
说明 |
where T:struct |
类型参数必须是值类型。可以指定除 Nullable 以外的任何值类型。 |
where T:Class |
类型参数必须是引用类型,包括任何类、接口、委托或数组类型。 |
where T:new() |
类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new() 约束必须最后指定。 |
where T:<基类名> |
类型参数必须是指定的基类或派生自指定的基类。 |
where T:<接口名称> |
类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。 |
where T1:T2 |
为 T1 提供的类型参数必须是为 T2 提供的参数或派生自为 T2 提供的参数。这称为裸类型约束。 |
使用约束的原因
如果要检查泛型列表中的某个项以确定它是否有效,或者将它与其他某个项进行比较,则编译器必须在一定程度上保证它需要调用的运算符或方法将受到客户端代码可能指定的任何类型参数的支持。这种保证是通过对泛型类定义应用一个或多个约束获得的。例如,基类约束告诉编译器:仅此类型的对象或从此类型派生的对象才可用作类型参数。一旦编译器有了这个保证,它就能够允许在泛型类中调用该类型的方法。约束是使用上下文关键字 where 应用的。
使用泛型类型还可以合并多个约束:
public class MyClass<T> where T : IClass, new() { }
3、继承
泛型类型可以实现泛型接口,也可以派生自一个类(要求是必须重复接口或基类的泛型类型):
public class LinkedList<T> : IEnumerable<T> { } public class MyClass<T> : MyBaseClass<T> { }
派生类也可以是泛型或非泛型的,其要求是必须制定基类的类型
public class IntClass : MyBaseClass<int> { }
4、静态成员
泛型类的静态成员只能在类的一个实例中共享:
public class StaticDemo<T> { public static int x; } static void Main(string[] args) { StaticDemo<string>.x = 1; StaticDemo<int>.x = 2; Console.WriteLine(StaticDemo<string>.x); Console.WriteLine(StaticDemo<int>.x); Console.ReadKey(); }
运行以上代码,结果如下:
当T类型不同时静态成员不共享。
(四)泛型接口
使用泛型可以定义接口,在泛型接口中定义的方法可以带泛型参数。
public interface IDeal<T> { T Deal(T value); }
协变和抗变指对参数和返回值类型进行转换。在.NET中参数类型时协变的,返回类型是抗变的。
1、协变和抗变
例子:
参数类型的协变
static void Main(string[] args) { string str = "测试参数的协变"; Show(str); Console.ReadKey(); } public static void Show(object value) { Console.WriteLine(value); }
返回类型的抗变
例子:
static void Main(string[] args) { int value = 1; string str = ConvertToString(value); Console.ReadKey(); } public static string ConvertToString(object value) { return value.ToString(); }
2、泛型接口的协变
如果泛型类型用out关键字进行标注,泛型接口就是协变的。(子到父是协变)
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IFactory<Chinese> ChineseFactory = new Factory<Chinese>(); 6 IFactory<People> PeopleFactory = ChineseFactory; //协变 7 People People = PeopleFactory.Create(); 8 Console.ReadKey(); 9 } 10 } 11 public class Chinese : People { } 12 public class People { } 13 public class Factory<T> : IFactory<T> 14 { 15 public T Create() 16 { 17 return (T)Activator.CreateInstance<T>(); 18 } 19 } 20 public interface IFactory<out T> 21 { 22 T Create(); 23 }
3、泛型接口的抗变
如果泛型类型用in关键字标注,泛型接口就是抗变的。
1 class Program 2 { 3 static void Main(string[] args) 4 { 5 IShow<People> ps = new PeopleShow(); 6 IShow<Chinese> cs = ps;//抗变 7 Console.ReadKey(); 8 } 9 } 10 public interface IShow<in T> 11 { 12 void Write(T t); 13 } 14 public class Chinese : People { } 15 public class People 16 { 17 public string Name { get; set; } 18 } 19 public class PeopleShow : IShow<People> 20 { 21 public void Write(People t) 22 { 23 Console.WriteLine("我的名字:" + t.Name + ",现在我表演写字!"); 24 } 25 }
(五)泛型结构
与类相似,结构也可以是泛型的。它们非常类似于泛型类,只是没有继承特性。.NET Framework中的一个泛型结构是Nullable<T>,结构Nullable<T>定义了一个约束:其中泛型类型T必须是一个结构。
因为可空类型使用得非常频繁,所以C#有一种特殊的语法,它用于定义可空类型的变量。定义这类变量时,不使用泛型结构的语法,而是用“?”运算符。
int? x;
可以使用合并运算符从可空类型转换为非可空类型。合并运算符“??”
int? x = null; int y = x ?? 0;
y的值显示为0,因为x是null。
(六)泛型方法
除了定义泛型类之外,还可以定义泛型方法。在泛型方法中,泛型类型用方法声明来定义。泛型方法可以在非泛型类中定义。
public T ReviseName<T>(T person) where T : People { person.Name = "改名后的:" + person.Name; return person; }
C#编译器会通过泛型方法来获取参数类型,所以不需要把泛型类型赋予方法的调用。
Chinese chinese = new Chinese(); chinese.Name = "张三"; Chinese.ReviseName(chinese);
泛型方法可以像泛型方法那样调用。
1、泛型方法示例
1 class Program 2 3 { 4 5 static void Main(string[] args) 6 7 { 8 9 var accounts = new List<Account>() 10 11 { 12 13 new Account("张三",2000), 14 15 new Account("李四",1300), 16 17 new Account("王麻子",800), 18 19 new Account("赵六",1000) 20 21 }; 22 23 decimal total = AccumulateSimple(accounts); 24 25 Console.ReadKey(); 26 27 } 28 29 public static decimal AccumulateSimple(IEnumerable<Account> source) 30 31 { 32 33 decimal sum = 0; 34 35 foreach (var item in source) 36 37 { 38 39 sum += item.Balance; 40 41 } 42 43 return sum; 44 45 } 46 47 } 48 49 public class Account 50 51 { 52 53 public string Name { get; set; } 54 55 public decimal Balance { get; set; } 56 57 public Account(string name, Decimal balance) 58 59 { 60 61 this.Name = name; 62 63 this.Balance = balance; 64 65 } 66 67 }
2、带约束的泛型方法
public T ReviseName<T>(T person) where T : People { person.Name = "改名后的:" + person.Name; return person; }
因为方法需要使用T参数的Name属性,所以为了确保程序不抛出异常,对T参数进行约束,使其必须继承自People类。
3、带委托的泛型方法
1 static void Main(string[] args) 2 { 3 int t1 = 1; 4 int t2 = 9; 5 Console.WriteLine(Cal(t1, t2, (i1, i2) => 6 { 7 return i1 + i2; 8 })); 9 Console.ReadKey(); 10 } 11 12 public static T Cal<T>(T t1, T t2, Func<T, T, T> calMethod) 13 { 14 return calMethod(t1, t2); 15 }
定义Cal方法,参数t1,t2是calMethod方法的参数(Func是内置的委托)。
4、泛型方法规范
泛型方法可以重载,为特定的类型定义规范。在编译期间会使用最佳匹配。
1 static void Main(string[] args) 2 { 3 string str = "str"; 4 int i = 0; 5 First(str); 6 First(i); 7 Console.ReadKey(); 8 } 9 10 public static void First<T>(T obj) 11 { 12 Console.WriteLine("obj:"+obj.GetType().Name); 13 } 14 15 public static void First(int obj) 16 { 17 Console.WriteLine("int"); 18 }
运行以上代码,结果如下:
需要注意的是,所调用的方法是在编译期间定义的,而不是运行期间。
static void Main(string[] args) { int i = 0; Second(i); Console.ReadKey(); } public static void Second<T>(T obj) { First(obj); }
运行以上代码,结果如下:
(七)小结
本章介绍了CLR中一个非常重要的特性:泛型。通过泛型类可以创建独立于类型的类,泛型方法是独立于类型的方法。接口、结构和委托也可以用泛型的方式创建。泛型引入了一种新的编程方式。我们介绍了如何实现相应的算法(尤其是操作和谓词)以用于不同的类,而且它们都是类型安全的。泛型委托可以去除集合中的算法。
博客园-本文作者(好先生FX http://www.cnblogs.com/hxsfx)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。