【读书笔记】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中一个非常重要的特性:泛型。通过泛型类可以创建独立于类型的类,泛型方法是独立于类型的方法。接口、结构和委托也可以用泛型的方式创建。泛型引入了一种新的编程方式。我们介绍了如何实现相应的算法(尤其是操作和谓词)以用于不同的类,而且它们都是类型安全的。泛型委托可以去除集合中的算法。
【读书笔记】C#高级编程 第五章 泛型的更多相关文章
- 读书笔记 - js高级程序设计 - 第五章 引用类型
引用类型 和 类 不是一个概念 用typeof来检测属性是否存在 typeof args.name == "string" 需要实验 访问属性的方法 .号和[] 一般情况下要 ...
- 《C#从现象到本质》读书笔记(七)第9章 泛型
<C#从现象到本质>读书笔记(七)第9章 泛型 泛型的三大好处:类型安全,增强性能(避免装箱和拆箱),代码复用. 泛型方法是传入的参数至少有一个类型为T(尚未制定的类型,根据微软的命名规则 ...
- 《java编程思想》读书笔记(二)第五章(2)
成员初始化 Java尽力保证:所有变量在使用前都能得到恰当的初始化. 对于方法的局部变量,Java会以编译时报错的形式贯彻这种保证,比如: void f() { int i; //这里编译时就会报错, ...
- R in action读书笔记(2)-第五章:高级数据管理(下)
5.4 控制流 语句(statement)是一条单独的R语句或一组复合语句(包含在花括号{ } 中的一组R语 句,使用分号分隔): 条件(cond)是一条最终被解析为真(TRUE)或假(FAL ...
- R in action 读书笔记(1)--第五章:高级数据管理
5.2.1数学函数 函数 描述 abs(x) 绝对值 sqrt(x) 平方根 ceiling(x) 不小于x的最小整数 floor(x) 不大于x的最大整数 trunc(x) 向0的方向截取的X中的整 ...
- UNIX系统高级编程——第五章-标准I/O库-总结
基础: 标准I/O库在ANSI C中定义,可移植在不同的系统 文件指针(FILE):标准I/O库操作的不是文件描述符,而是流.FILE文件指针包含的是维护流所需的信息 通过函数fileno获取流的文件 ...
- 读书笔记 - js高级程序设计 - 第十一章 DOM扩展
对DOM的两个主要的扩展 Selectors API HTML5 Element Traversal 元素遍历规范 querySelector var body = document.query ...
- 读书笔记 - js高级程序设计 - 第七章 函数表达式
闭包 有权访问另一个函数作用域中的变量的函数 匿名函数 函数没有名字 少用闭包 由于闭包会携带包含它的函数的作用域,因此会比其它函数占用更多的内存.过度使用闭包可能会导致内存占用过多,我们建议读者 ...
- 读书笔记 - js高级程序设计 - 第六章 面向对象的程序设计
EcmaScript有两种属性 数据属性 和 访问器属性 数据属性有4个特性 Configurable Enumerable Writable Value 前三个值的默认值都为false ...
随机推荐
- 关于swiper插件在vue2的使用
最近做项目用到了vue-awesome-swiper,总结一下使用方法 第一步:安装依赖 npm install swiper vue-awesome-swiper --save or npm ins ...
- RPA应用场景-财务报表统计整合
场景概述 财务报表统计整合 所涉系统名称 邮储银行系统 人工操作(时间/次) 3小时 所涉人工数量 1 操作频率 每月 场景流程 1.登录各个区支行系统 2.机器人按照要求,自动复选多项业务参数,导出 ...
- 分享一款IDEA主题,很奈斯
主题名称:Dark Purple Theme: 更换主题:在Setting中设置更换
- NC50038 kotori和糖果
NC50038 kotori和糖果 题目 题目描述 kotori共有 \(n\) 块糖果,每块糖果的初始状态是分散的,她想把这些糖果聚在一堆.但她每次只能把两堆糖果合并成一堆. 已知把两堆数量为 \( ...
- 深入浅出理解SVM支持向量机算法
支持向量机是Vapnik等人于1995年首先提出的,它是基于VC维理论和结构风险最小化原则的学习机器.它在解决小样本.非线性和高维模式识别问题中表现出许多特有的优势,并在一定程度上克服了" ...
- 隐私计算FATE-多分类神经网络算法测试
一.说明 本文分享基于 Fate 使用 横向联邦 神经网络算法 对 多分类 的数据进行 模型训练,并使用该模型对数据进行 多分类预测. 二分类算法:是指待预测的 label 标签的取值只有两种:直白来 ...
- 攻防世界MISC进阶区 61-63
61.肥宅快乐题 得到swf文件,但是用PotPlayer打不开,用浏览器应该可以打开,打开后可以在npc的对话中看到一段base64 解密后就可以得到flag 62.warmup 得到一张png和一 ...
- Idea 编译jsp生成的class文件路径
找到work\Catalina\localhost\ 然后访问响应的JSP地址才会动态生成到这个路径下面,不访问不会生成,在org\apache\jsp 下面
- 如何用空气质量查询API接口进行快速开发
空气质量的好坏反映了空气污染程度,它是依据空气中污染物浓度的高低来判断的.空气污染是一个复杂的现象,在特定时间和地点空气污染物浓度受到许多因素影响.来自固定和流动污染物的人为污染物排放大小是影响空 ...
- led的进化
1.一个led亮100ns,灭400ns,循环 2.一个led亮2500ns,灭5000ns,亮7500ns,灭10000ns循环 3.以2500ns为变化周期,20000ns为一个循环,每个周期的亮 ...