解读经典《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>的完整定义:

现在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)、技术专题、视野知识、源码下载等内容。


扫描二维码关注

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

原文地址:https://www.cnblogs.com/holyknight17/p/10451897.html

时间: 2024-11-03 20:50:49

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

UNIX环境高级编程学习笔记(第一章UNIX基础知识)

总所周知,UNIX环境高级编程是一本很经典的书,之前我粗略的看了一遍,感觉理解得不够深入. 听说写博客可以提高自己的水平,因此趁着这个机会我想把它重新看一遍,并把每一章的笔记写在博客里面. 我学习的时候使用的平台是Windows+VMware+debian,使用secureCRT来连接(可以实现多个终端连接). 因为第一章是本书大概的描述,所以第一章的我打算写得详细一点,而且书本的原话占的比例会比较多,重点的东西会用粗体显示出来. 1.1  引言 所有操作系统都为他们所运行的程序提供服务.典型的

C#高级编程笔记之第三章:对象和类型

类和结构的区别 类成员 匿名类型 结构 弱引用 部分类 Object类,其他类都从该类派生而来 扩展方法 3.2 类和结构 类与结构的区别是它们在内存中的存储方式.访问方式(类似存储在堆上的引用类型,而结构是存储在栈上的值类型)和它们的一些特征. 语法上,类与结构非常相似,主要区别是结构使用关键字 struct 代替 class 来声明. 3.3 类成员 3.3.1 数据成员 数据成员是包含类数据—字段.常量和事件的成员. 3.3.2 函数成员 函数成员提供了操作类中数据的某些功能,包括方法.属

《C#高级编程》【第五章】泛型 -- 学习笔记

 泛型是高级程序设计语言的一种特性.泛型的引入使得程序开发的效率得到提高,代码的重用性大大的提升.有了泛型,我们可以创建独立于被包含类型的类和方法,我们不必给不同的类编写功能相同的很多方法或者类,只创建一个方法或类就可以了.现在我们看看泛型的优点 性能上,泛型不需要进行类型转换(也就是拆箱和装箱). 类型安全,和Object类相比,Object类属于非类型安全的,而泛型使用泛型类型,可以根据需要用特定的类型替换泛型类型,这样就保证了类型安全类.在编译时,只有与泛型类型T定义的允许使用的类型不

C#高级编程第9版 第一章 .NET体系结构 读后笔记

.NET的CLR把源代码编译为IL,然后又把IL编译为平台专用代码. IL总是即时编译的,这一点的理解上虽然明白.当用户操作C#开发的软件时,应该是操作已经编译好的程序.那么此时安装在客户机上的程序是什么状态呢?如果是已经编译为平台专用代码了. 那么IL的即时编译的优点是体现在哪里?如果安装在客户机上的代码是IL代码.那么就能说通了.这一点可能要在后续的读书中学到. 语言的互操作性,这一点就我所知,应该不会有太多的人在乎.可能是没有接触到这种的项目. F#是函数编程语言.常用于财务和科学应用程序

《C#高级编程》【第六章】数组 -- 学习笔记

       为了解决大量的同类型元素,于是数组就孕育而生了.数组是具有一定顺序关系的若干对象的集合体,一维数组可以看作是定长的线性表.反之,n为的数组可以看作线性表的推广.从存储结构上来看,数组是一段连续的存储空间.现在我们看看在C#中的数组: 1.普通数组 在C#中普通数组又可以分为一维数组.多维数组和锯齿数组. <1>一维数组 我们现在先看看一维数组的声明语法: 类型[] 变量名; 知道怎么声明了,现在我们继续看看数组的初始化吧,在C#中有4种初始化的方式: //n为数组长度,an为

C#高级编程第11版 - 第二章

导航 C# 全版本特性一览 全书目录 第二章 Core C 2.1 C#基础 29 2.2 变量 31 2.2.1 初始化变量 31 2.2.2 类型推断 32 2.2.3 变量的作用域 33 2.2.4 常量 34 2.3 预定义数据类型 35 2.3.1 值类型和引用类型 35 2.3.2 .NET 类型 36 2.3.3 预定义的值类型 36 2.3.4 预定义的引用类型 40 2.4 程序流控制 42 2.4.1 条件语句 42 2.4.2 循环 44 2.4.3 跳转语句 47 2.5

《C#高级编程》【第三章】对象和类型 -- 学习笔记

在看过C++之后,再看C#的面向对象感觉就不难了,只是有一些区别而已. 1.类定义 使用class关键字来声明类,其和C++不同的地方是在大括号之后不需要冒号 class 类名 { //类的内部 } //C++这里有一个冒号,而C#没有 2.类成员 3.字段与属性 首先我们先区分一下C#数据成员中的字段.常量与事件成员.字段.常量是与类的相关变量.事件是类的成员,在发生某些行为时(如:改变类的字段或属性,或进行某种形式的用户交互操作),它可以让对象通知调用方. 那么现在我们在来看看字段与属性,属

《C#高级编程》【第7章】运算符与类型强制转换 -- 学习笔记

       运算符编程语言的基本元素,它使得我们的代码更加的简洁明了.然而我们为了解决操作数类型不同的问题,我们又有引入了强制转换的概念.我们先看看本章的内容构成吧. 1.运算符 我们来看看一些常见的运算符: <1>条件运算符 其语法如下: Condition ? true_Expression : false_Expression 当条件Condition为真时,其将执行true_Expression,否则执行false_Expression. <2> checked 和

《C#高级编程》【第四章】继承 -- 学习笔记

计算机程序,在很大的程度上是为了描述和解决现实问题.在面向对象语言中的类很好的采用了人类思维中抽象和分类的方法,类和对象的关系很好的反映了个体与同类群体的共同特征的关系.但是在诸多共同点之下还是存在着些许差异.于是面向对象语言中设计了继承机制,允许我们在保持原有类特性的基础上,进行拓展.由于类的继承和派生机制的引入,使得代码的重用性和可扩充性大大提高.利用这个机制我们还可以站在巨人的肩膀上就行开发---利用别人写好的类进行扩充,这样又可以提高我们的开发效率.在派生新类的过程一般来说有三个步骤:吸

C#高级编程第9版 第二章 核心C# 读后笔记

System命名空间包含了最常用的.NET类型.对应前面第一章的.NET基类.可以这样理解:.NET类提供了大部分的功能,而C#语言本身是提供了规则. pseudo-code,哈哈,秀逗code.伪代码. 必须初始化变量才能编译,否则报错.有些情况下,没有显示初始化,会被编译器默认为0去编译. 实例化一个对象需要用new关键字. 类型推断使用var 关键字. var i= 0: 系统会推断出 i 为C#的int类型,为.NET的System.Int32类型. for while或类似语句中声明的