解读经典《C#高级编程》泛型 页114-122.章4

前言

本章节开始讲解泛型。.Net从2.0开始支持泛型,泛型不仅是C#的一部分,也与IL代码紧密集成。所以C#中泛型的实现非常优雅。相对于C#,Java是后期引入的泛型,受限于最初的设计架构,就实现的比较别扭,遭到不少人的吐槽,比如“类型擦除”问题。使用C#还是幸福的。

使用泛型最典型的应用,可能是List<T>了,从List<T>我们可以看到,使用泛型最明显的优点是:

  • 将List的功能和包含类T的功能分开,功能各自实现。使得代码内聚性、复用性、灵活性都更好。
  • 使用强类型的T代替了原先的Object,不再需要类型强转,使用强类型更加安全便捷。

名词约定

对于Lis<T>定义: List<T>叫做“泛型类”,而T叫做“泛型类型”。泛型类型和泛型类后面会经常提到。

概述

泛型和C++的模板类似,不同之处在于C++实例化模板时,需要模板的源代码。而泛型是一种内化在CLR中的结构,可以认为它是CLR封装好的一种“类型”,使用起来更加简单安全。以下逐个讲述使用泛型的优点以及它所解决的问题。

性能

泛型性能的优势体现在装箱和拆箱上。在泛型出现之前,要填充一个列表使用的是非泛型集合如ArrayList。ArrayList存储的是Object对象,因此将值类型存储到ArrayList时需要经过装箱的操作,数据取出处理时又需要拆箱的操作。在遍历操作时,性能损耗是比较明显的。

名词解释

装箱:值类型转换为引用类型

拆箱:将引用类型转换为值类型

ArrayList list = new ArrayList();
list.Add(10); //装箱
int value = (int)list[0]; //拆箱 List<int> listg = new List<int>();
listg.Add(10); //没有装箱

类型安全

ArrayList中存储的是Object,什么类型都可以添加,那么拆箱时如果出现类型不符就会导致程序异常。这样一来,代码的准确性就只能完全靠程序员的能力和责任了。

ArrayList list2 = new ArrayList();
list2.Add(10); //列表实际定义为int列表
list2.Add("11"); //不小心加入了字符串类型的数字
foreach(int item in list2)
{
Console.WriteLine(item); //遍历时将出现异常
}

二进制代码重用 & 代码的扩展

前面也已经讲到,泛型是内化在CLR中的,也就是说泛型类型中可重用的部分已经内化在.Net框架中了,不需要编程人员根据不同的泛型类型去写代码实现多种不同的类。

实际上,假如我们程序员自己写代码来实现同样强类型列表功能,但不使用泛型,就要用不同的类型实现不同的包装处理类,比如写一个包含int的处理类,以及一个包含string的处理类的代码。而.Net提供了List解决了问题,但实际上代码量是少了吗?并不一定。因为本质上,JIT编译器会根据List<int>和List<string>创造出两个临时的新类。我们可以认为,类似C++模板的工作,被隐藏到.Net框架底层去处理了,机器处理的代码可能没少,但最终程序员写的代码是减少了。

命名约定

这里,少见的匈牙利命名法又出现了。前面我们讲到,接口命名采用是匈牙利命名法。泛型类型也是。

  • 泛型类型名称以字母T作为前缀
  • 泛型类型可以是任意类型,如果泛型类中只定义一个类型,可以直接命名为T,如List<T>
  • 如果有多个泛型类型,或者T不足以表达含义,可以使用T开头的命名,比如:SortedList<TKey, TValue>

创建泛型类

泛型类的创建,实际应用场景其实不多。如果你写的是通用类库,那可能会比较常用。但如果是应用层面的代码,实际上很少会需要你自己去建立泛型类。我应用在多个产品的基础框架里,似乎也只建立过一个泛型类,而且使用很少,是非常边缘化的一个功能。为什么会这样?还是因为.Net框架已经将常用的泛型类封装的很好了,拿来用就足够应付99%的应用场景。

也因为如此,可能不少人对为什么要创建泛型类,以及如何创建一个泛型类,并不是很了解。

首先我从原理上说明一下,实际上泛型类可以理解为:支持多种泛型类型的“包装类”。包装类是Java的概念,比如从int -> Integer,Integer就是int的包装类。其实相应的,在C#中,int?也可以认为是int的包装类,但C#不叫包装类,它刚好是个泛型,int? 就是 Nullable<int>泛型类。

但我们仍然可以做一个大脑体操,我们引入一下包装类的概念,然后推理一下:程序员定义了一个泛型类,它在CLR执行时的执行原理是怎样的?以int?为例:

  1. 首先JIT编译器根据泛型类Nullable<int>,生成一个临时命名的新类,它包装了值类型int,是个“包装类”
  2. 执行包装类的具体功能方法,输出结果

实际上JIT编译器生成的“包装类”代码是怎么样的呢?它的样子,我手写模仿了一下(部分实现):

/// <summary>
/// 允许Null的Int类型
/// </summary>
public class NullInt
{
private int value;
private bool hasValue; /// <summary>
/// NullInt的Int值
/// </summary>
public int Value
{
get
{
return value;
}
} /// <summary>
/// 输出
/// </summary>
/// <returns></returns>
public override string ToString()
{
if (!hasValue) return null;
return value.ToString();
} /// <summary>
/// 赋值操作符
/// </summary>
/// <param name="value"></param>
public static implicit operator NullInt(int value)
{
return new NullInt
{
value = value,
hasValue = true
};
}
} Main()方法:测试输出
NullInt age = 10; //赋值
Console.WriteLine(age.Value); //输出10
Console.WriteLine(age); //输出10

只实现了部分代码,它实际上是Nullable<T>的裁剪版本,以下是Nullable<T>的完整定义:

解读经典《C#高级编程》泛型 页114-122.章4

现在int类型的包装类已经实现了,那我还想实现long的包装类,想实现decimal的包装类,怎么办?写n多个类?显然有点啰嗦了。

.Net泛型类就为提供这种能力而创造的。

我们如果反编译Nullable<T>的源代码,我们会发现实现结构和我手写的包装类是类似的,只是用T代替了int:

解读经典《C#高级编程》泛型 页114-122.章4

最后,在贴一个我的产品框架中建立的泛型类实例(节选),它的实现的具体细节,在后面章节也会有分析:

/// <summary>
/// 单一事务处理服务,用于单表的数据读写事务
/// </summary>
/// <typeparam name="TViewModel"></typeparam>
/// <typeparam name="TEntity"></typeparam>
/// <typeparam name="TDbContext"></typeparam>
public class EFRepository<TViewModel, TEntity, TDbContext> : IDisposable
where TEntity : class,new()
where TViewModel : class,new()
where TDbContext : DbContext,new()
{
private DbContext dbContext;
private DbSet<TEntity> dbSet; /// <summary>
/// 构造方法
/// </summary>
public EFRepository()
{
dbContext = new TDbContext();
dbSet = dbContext.Set<TEntity>();
} /// <summary>
/// 根据主键获取单条数据
/// </summary>
/// <param name="keyValues"></param>
/// <returns></returns>
public TViewModel Get(params object[] keyValues)
{
return dbSet.Find(keyValues)
.MapTo<TEntity, TViewModel>();
} /// <summary>
/// 新增单条数据
/// </summary>
/// <param name="model"></param>
public void Add(TViewModel model)
{
var entity = model.MapTo<TViewModel, TEntity>();
dbSet.Add(entity);
dbContext.SaveChanges();
} /// <summary>
/// 根据主键删除单条数据
/// </summary>
/// <param name="keyValues"></param>
public void Delete(params object[] keyValues)
{
TEntity entity = dbSet.Find(keyValues);
dbSet.Remove(entity);
dbContext.SaveChanges();
}
}

下一篇,我们继续讲泛型的使用细节。

觉得文章有意义的话,请动动手指,分享给朋友一起来共同学习进步。


欢迎关注本人如下公众号 “产品技术知与行” ,打造全面的结构化知识库,包括原创文章、免费课程(C#,Java,Js)、技术专题、视野知识、源码下载等内容。

解读经典《C#高级编程》泛型 页114-122.章4

扫描二维码关注

回到目录,再看看相关文章

上一篇:【liunx】sftp常用命令


下一篇:结合源码看nginx-1.4.0之nginx事件驱动机制详解