解读经典《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?为例:
- 首先JIT编译器根据泛型类Nullable<int>,生成一个临时命名的新类,它包装了值类型int,是个“包装类”
 - 执行包装类的具体功能方法,输出结果
 
实际上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>的完整定义:

现在int类型的包装类已经实现了,那我还想实现long的包装类,想实现decimal的包装类,怎么办?写n多个类?显然有点啰嗦了。
.Net泛型类就为提供这种能力而创造的。
我们如果反编译Nullable<T>的源代码,我们会发现实现结构和我手写的包装类是类似的,只是用T代替了int:

最后,在贴一个我的产品框架中建立的泛型类实例(节选),它的实现的具体细节,在后面章节也会有分析:
/// <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的更多相关文章
- C#高级编程笔记之第三章:对象和类型
		
类和结构的区别 类成员 匿名类型 结构 弱引用 部分类 Object类,其他类都从该类派生而来 扩展方法 3.2 类和结构 类与结构的区别是它们在内存中的存储方式.访问方式(类似存储在堆上的引用类型, ...
 - 《Node.js 高级编程》简介与第二章笔记
		
<Node.js 高级编程> 作者简介 Pedro Teixerra 高产,开源项目程序员 Node 社区活跃成员,Node公司的创始人之一. 10岁开始编程,Visual Basic.C ...
 - C#高级编程学习一-----------------第五章泛型
		
三层架构之泛型应用 概述 1.命名约定 泛型类型以T开头或就是T. 2.泛型类 2.1.创建泛型类
 - C#高级编程第9版 第一章 .NET体系结构 读后笔记
		
.NET的CLR把源代码编译为IL,然后又把IL编译为平台专用代码. IL总是即时编译的,这一点的理解上虽然明白.当用户操作C#开发的软件时,应该是操作已经编译好的程序.那么此时安装在客户机上的程序是 ...
 - 20191105 《Spring5高级编程》笔记-第5章
		
第5章 Spring AOP 面向切面编程(AOP)是面向对象编程(OOP)的补充.AOP通常被称为实施横切关注点的工具.术语横切关注点是指应用程序中无法从应用程序的其余部分分解并且可能导致代码重复和 ...
 - 20191105 《Spring5高级编程》笔记-第12章
		
第12章 使用Spring远程处理 12.4 在Spring中使用JMS 使用面向消息的中间件(通常成为MQ服务器)是另一种支持应用程序间通信的流行方法.消息队列(MQ)服务器的主要优点在于为应用程序 ...
 - 20191105 《Spring5高级编程》笔记-第9章
		
第9章 事务管理 一些名词: 2PC(2 Phase Commit) XA协议 JTS(Java Transaction Service) JCA(Java EE Connector Architec ...
 - 20191105 《Spring5高级编程》笔记-第6章
		
第6章 Spring JDBC支持 Spring官方: 位于Spring Framework Project下. 文档: https://docs.spring.io/spring-framework ...
 - 20191103 《Spring5高级编程》笔记-第4章
		
第4章 详述Spring配置和Spring Boot 4.2 管理bean生命周期 通常,有两个生命周期事件与bean特别相关:post-initialization和pre-destruction. ...
 - C#高级编程笔记(11至16章)异步/托管/反射/异常
		
11.1.2LINQ语句 LINQ查询表达式以from子句开始,以select或者group子句结束.在这两个子句之间可以跟零个或者多个from.let.where.join或者orderby子句. ...
 
随机推荐
- leetcode-求众数
			
题目:求众数 给定一个大小为 n 的数组,找到其中的众数.众数是指在数组中出现次数大于 ⌊ n/2 ⌋ 的元素. 你可以假设数组是非空的,并且给定的数组总是存在众数. 示例 1: 输入: [3,2,3 ...
 - vscode1.30.1使用的electron3.0.10中的bug
			
PS C:\GitHub\vscode2\vscode> scripts\code.bat[13:07:19] Syncronizing built-in extensions...[13:07 ...
 - 如何理解opencv, python-opencv 和 libopencv?
			
转: OpenCV is a computer vision library written using highly optimized C/C++ code. It makes use of ...
 - CSS背景图片
			
1.背景图片插入 代码格式:background-image:url(): 括号内填写图片路径 2.背景图片设置大小 代码格式:background-size:宽.高 3.背景图片设置不平铺 代码格式 ...
 - APP研发录笔记
			
一.消灭全局变量 在内存不足时,系统会回收一部分闲置的资源,由于App被切换到后台,所以之前存放的全局变量很容易被回收,这时再切换到前台继续使用,会报空指针崩溃.想彻底解决这个问题,就要使用序列化. ...
 - [Swift]LeetCode235. 二叉搜索树的最近公共祖先 | Lowest Common Ancestor of a Binary Search Tree
			
Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BS ...
 - [Swift]LeetCode479. 最大回文数乘积 | Largest Palindrome Product
			
Find the largest palindrome made from the product of two n-digit numbers. Since the result could be ...
 - mybatis xml  <  >
			
[参考文章]:mybatis 中的 xml 配置文件中 ‘<’. ‘>’ 处理 1.使用转义字符将 ‘<’. ‘>’ 替换掉 描述 字符 转义字符小于号 < <大于 ...
 - SpringMVC接收json数组对象
			
最近帮一个妹子解决一个需求,就是前台使用ajax传三个相同的对象,再加一个form表单对象.然后遇到各种问题,终于解决了,@RequestBody接收Json对象字符串 以前,一直以为在Spring ...
 - bootcamp分区_BOOTCAMP 删除分区失败
			
mac 装了双系统,Mac OS X 分配的内存太少了,导致使用卡顿,要删掉windows系统. 在删除windows的时候出现 “您的磁盘不能恢复为单一的分区” 解决方案: 1.重启Mac,并按下 ...