好久没用写博了,感觉工作的越久就越发的懒了,啦啦啦!德玛西亚!

感觉最近食欲不正,便想写写组织下自己的学习路程:

泛型,可能很多朋友在学习这个东西的时候都源于面向对象,当然我也不例外:从一个实体继承另一个实体就是我们最常见的,当然这些都是其对应的关系,实体相对应就可以抽象为T,当然这里的T可以为其他的哈

为什么要使用泛型
为了了解这个问题,我们先看下面的代码,代码省略了一些内容,但功能是实现一个栈,这个栈只能处理int数据类型:

public class Stack

{

private int[] m_item;

public int Pop(){...}

public void Push(int item){...}

public Stack(int i)

{

this.m_item = new int[i];

}

}

上面代码运行的很好,但是,当我们需要一个栈来保存string类型时,该怎么办呢?很多人都会想到把上面的代码复制一份,把int改成string不就行了。当然,这样做本身是没有任何问题的,但一个优秀的程序是不会这样做的,因为他想到若以后再需要long、Node类型的栈该怎样做呢?还要再复制吗?优秀的程序员会想到用一个通用的数据类型object来实现这个栈:

public class Stack

{

private object[] m_item;

public object Pop(){...}

public void Push(object item){...}

public Stack(int i)

{

this.m_item = new[i];

}

}

这个栈写的不错,他非常灵活,可以接收任何数据类型,可以说是一劳永逸。但全面地讲,也不是没有缺陷的,主要表现在:

当Stack处理值类型时,会出现装箱、折箱操作,这将在托管堆上分配和回收大量的变量,若数据量大,则性能损失非常严重。 
在处理引用类型时,虽然没有装箱和折箱操作,但将用到数据类型的强制转换操作,增加处理器的负担。 
在数据类型的强制转换上还有更严重的问题(假设stack是Stack的一个实例):
Node1 x = new Node1();

stack.Push(x);

Node2 y = (Node2)stack.Pop();

上面的代码在编译时是完全没问题的,但由于Push了一个Node1类型的数据,但在Pop时却要求转换为Node2类型,这将出现程序运行时的类型转换异常,但却逃离了编译器的检查。

针对object类型栈的问题,我们引入泛型,他可以优雅地解决这些问题。泛型用用一个通过的数据类型T来代替object,在类实例化时指定T的类型,运行时(Runtime)自动编译为本地代码,运行效率和代码质量都有很大提高,并且保证数据类型安全。

使用泛型
下面是用泛型来重写上面的栈,用一个通用的数据类型T来作为一个占位符,等待在实例化时用一个实际的类型来代替。让我们来看看泛型的威力:

public class Stack<T>

{

private T[] m_item;

public T Pop(){...}

public void Push(T item){...}

public Stack(int i)

{

this.m_item = new T[i];

}

}

类的写法不变,只是引入了通用数据类型T就可以适用于任何数据类型,并且类型安全的。这个类的调用方法:

//实例化只能保存int类型的类

Stack<int> a = new Stack<int>(100);

a.Push(10);

a.Push("8888"); //这一行编译不通过,因为类a只接收int类型的数据

int x = a.Pop();

//实例化只能保存string类型的类

Stack<string> b = new Stack<string>(100);

b.Push(10);    //这一行编译不通过,因为类b只接收string类型的数据

b.Push("8888");

string y = b.Pop();

这个类和object实现的类有截然不同的区别:

1.       他是类型安全的。实例化了int类型的栈,就不能处理string类型的数据,其他数据类型也一样。

2.       无需装箱和折箱。这个类在实例化时,按照所传入的数据类型生成本地代码,本地代码数据类型已确定,所以无需装箱和折箱。

3.       无需类型转换。

泛型类实例化的理论
C#泛型类在编译时,先生成中间代码IL,通用类型T只是一个占位符。在实例化类时,根据用户指定的数据类型代替T并由即时编译器(JIT)生成本地代码,这个本地代码中已经使用了实际的数据类型,等同于用实际类型写的类,所以不同的封闭类的本地代码是不一样的。按照这个原理,我们可以这样认为:

泛型类的不同的封闭类是分别不同的数据类型。

例:Stack<int>和Stack<string>是两个完全没有任何关系的类,你可以把他看成类A和类B,这个解释对泛型类的静态成员的理解有很大帮助。

泛型类中数据类型的约束
程序员在编写泛型类时,总是会对通用数据类型T进行有意或无意地有假想,也就是说这个T一般来说是不能适应所有类型,但怎样限制调用者传入的数据类型呢?这就需要对传入的数据类型进行约束,约束的方式是指定T的祖先,即继承的接口或类。因为C#的单根继承性,所以约束可以有多个接口,但最多只能有一个类,并且类必须在接口之前。这时就用到了C#2.0的新增关键字:

public class Node<T, V> where T : Stack, IComparable

where V: Stack

{...}

以上的泛型类的约束表明,T必须是从Stack和IComparable继承,V必须是Stack或从Stack继承,否则将无法通过编译器的类型检查,编译失败。

通用类型T没有特指,但因为C#中所有的类都是从object继承来,所以他在类Node的编写中只能调用object类的方法,这给程序的编写造成了困难。比如你的类设计只需要支持两种数据类型int和string,并且在类中需要对T类型的变量比较大小,但这些却无法实现,因为object是没有比较大小的方法的。 了解决这个问题,只需对T进行IComparable约束,这时在类Node里就可以对T的实例执行CompareTo方法了。这个问题可以扩展到其他用户自定义的数据类型。

如果在类Node里需要对T重新进行实例化该怎么办呢?因为类Node中不知道类T到底有哪些构造函数。为了解决这个问题,需要用到new约束:

public class Node<T, V> where T : Stack, new()

where V: IComparable

需要注意的是,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。

C#中数据类型有两大类:引用类型和值类型。引用类型如所有的类,值类型一般是语言的最基本类型,如int, long, struct等,在泛型的约束中,我们也可以大范围地限制类型T必须是引用类型或必须是值类型,分别对应的关键字是class和struct:

public class Node<T, V> where T : class

where V: struct

泛型方法
泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。看下面的类:

public class Stack2

{

public void Push<T>(Stack<T> s, params T[] p)

{

foreach (T t in p)

{

s.Push(t);

}

}

}

原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法,这个方法的调用示例如下:

Stack<int> x = new Stack<int>(100);

Stack2 x2 = new Stack2();

x2.Push(x, 1, 2, 3, 4, 6);

string s = "";

for (int i = 0; i < 5; i++)

{

s += x.Pop().ToString();

}    //至此,s的值为64321

泛型中的静态成员变量
在C#1.x中,我们知道类的静态成员变量在不同的类实例间是共享的,并且他是通过类名访问的。C#2.0中由于引进了泛型,导致静态成员变量的机制出现了一些变化:静态成员变量在相同封闭类间共享,不同的封闭类间不共享。

这也非常容易理解,因为不同的封闭类虽然有相同的类名称,但由于分别传入了不同的数据类型,他们是完全不同的类,比如:

Stack<int> a = new Stack<int>();

Stack<int> b = new Stack<int>();

Stack<long> c = new Stack<long>();

类实例a和b是同一类型,他们之间共享静态成员变量,但类实例c却是和a、b完全不同的类型,所以不能和a、b共享静态成员变量。

泛型中的静态构造函数
静态构造函数的规则:只能有一个,且不能有参数,他只能被.NET运行时自动调用,而不能人工调用。

泛型中的静态构造函数的原理和非泛型类是一样的,只需把泛型中的不同的封闭类理解为不同的类即可。以下两种情况可激发静态的构造函数:

1.       特定的封闭类第一次被实例化。

2.       特定封闭类中任一静态成员变量被调用。

泛型类中的方法重载
方法的重载在.Net Framework中被大量应用,他要求重载具有不同的签名。在泛型类中,由于通用类型T在类编写时并不确定,所以在重载时有些注意事项,这些事项我们通过以下的例子说明:

public class Node<T, V>

{

public T add(T a, V b)          //第一个add

{

return a;

}

public T add(V a, T b)          //第二个add

{

return b;

}

public int add(int a, int b)    //第三个add

{

return a + b;

}

}

上面的类很明显,如果T和V都传入int的话,三个add方法将具有同样的签名,但这个类仍然能通过编译,是否会引起调用混淆将在这个类实例化和调用add方法时判断。请看下面调用代码:

Node<int, int> node = new Node<int, int>();

object x = node.add(2, 11);

这个Node的实例化引起了三个add具有同样的签名,但却能调用成功,因为他优先匹配了第三个add。但如果删除了第三个add,上面的调用代码则无法编译通过,提示方法产生的混淆,因为运行时无法在第一个add和第二个add之间选择。

Node<string, int> node = new Node<string, int>();

object x = node.add(2, "11");

这两行调用代码可正确编译,因为传入的string和int,使三个add具有不同的签名,当然能找到唯一匹配的add方法。

由以上示例可知,C#的泛型是在实例的方法被调用时检查重载是否产生混淆,而不是在泛型类本身编译时检查。同时还得出一个重要原则:

当一般方法与泛型方法具有相同的签名时,会覆盖泛型方法。

泛型类的方法重写
方法重写(override)的主要问题是方法签名的识别规则,在这一点上他与方法重载一样,请参考泛型类的方法重载。

泛型的使用范围
本文主要是在类中讲述泛型,实际上,泛型还可以用在类方法、接口、结构(struct)、委托等上面使用,使用方法大致相同,就不再讲述。

小结
C# 泛型是开发工具库中的一个无价之宝。它们可以提高性能、类型安全和质量,减少重复性的编程任务,简化总体编程模型,而这一切都是通过优雅的、可读性强的语法完成的。尽管 C# 泛型的根基是 C++ 模板,但 C# 通过提供编译时安全和支持将泛型提高到了一个新水平。C# 利用了两阶段编译、元数据以及诸如约束和一般方法之类的创新性的概念。毫无疑问,C# 的将来版本将继续发展泛型,以便添加新的功能,并且将泛型扩展到诸如数据访问或本地化之类的其他 .NET Framework 领域。

c#进阶之泛型的更多相关文章

  1. C#进阶之泛型(Generic)

    1.泛型 泛型是framwork2.0推出的新语法,具有延迟声明的特点:把参数类型的声明推迟到调用的时候.泛型不是一个语法糖,是框架升级提供的功能.需要编辑器和JIT(just-in-time com ...

  2. Scala进阶之路-Scala中的泛型介绍

    Scala进阶之路-Scala中的泛型介绍 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 通俗的讲,比如需要定义一个函数,函数的参数可以接受任意类型.我们不可能一一列举所有的参数类 ...

  3. Java进阶 | 泛型机制与反射原理

    一.泛型的概念 1.基础案例 泛型在Java中的应用非常广泛,最常见则是在集合容器中,先看下基础用法: public class Generic01 { public static void main ...

  4. 【C#进阶系列】12 泛型

    泛型是CLR和编程语言提供的一种特殊机制,它用于满足“算法重用”  . 可以想象一下一个只有操作的参数的数据类型不同的策略模式,完全可以用泛型来化为一个函数. 以下是它的优势: 类型安全 给泛型算法应 ...

  5. Java 泛型进阶

    擦除 在泛型代码内部,无法获得任何有关泛型参数类型的信息. 例子1: //这个例子表明编译过程中并没有根据参数生成新的类型 public class Main2 { public static voi ...

  6. Java进阶(四)Java反射TypeToken解决泛型运行时类型擦除问题

    在开发时,遇到了下面这条语句,不懂,然习之. private List<MyZhuiHaoDetailModel> listLottery = new ArrayList<MyZhu ...

  7. 苹果新的编程语言 Swift 语言进阶(十六)--泛型

    泛型允许你定义一个宽松.可重用的函数或者类型,使用泛型能够避免代码的重复,也能以更清楚和抽象的方式来表达程序的意图. 泛型是Swift语言提供的强大功能之一,Swift提供的许多标准库都使用了泛型来创 ...

  8. C# 泛型详解---进阶编程(七)

    今天我们来学习在C#的泛型技巧,传统的课本都在讲解什么是泛型,然后列举一大堆代码示例告诉你什么是泛型,今天我们就来聊聊更加本质的东西,我为什么要用泛型?它是来解决什么问题的?底层原理是什么? 简单来说 ...

  9. JAVA进阶-泛型

    >泛型:泛型指代了參数的类型化类型,一般被用在接口.类.方法中 >作用:用来确定參数的范围,在书写代码的时候提前检查代码的错误性 >泛型的声明,下面给出类声明,依此类推: class ...

随机推荐

  1. 关于在虚拟机上安装ubuntu输入不了中文的问题

    打开终端后,无法输入中文,按照网络上的教程 1.安装语言包 System Settings–>Language Support–>Install/Remove Languages 选中ch ...

  2. StringBuild使用与原理

    StringBuild的使用: 1.创建: StringBuilder sb=new StringBuilder(); StringBuilder sb=new StringBuilder(200); ...

  3. mobile_缩放

    document.documentElement.clientWidth       不包含滚动条 window.innerWidth                                  ...

  4. [LeetCode] Max Increase to Keep City Skyline 保持城市天际线的最大增高

    In a 2 dimensional array grid, each value grid[i][j] represents the height of a building located the ...

  5. laravel之路由和控制器与视图

    路由和控制器: : 路由与视图: 访问:

  6. get与post请求问题

    req.url可以获取请求路径: 为避免浏览器自身发送的'/favicon.ico'的影响,获取路径后可利用if(req.url=='/favicon.ico')  return ;处理 url.pa ...

  7. IntelliJ IDEA 2017.2.6 x64 配置 tomcat 启动 maven 项目

    IntelliJ IDEA 2017.2.6 x64 配置 tomcat 启动 maven 项目 1.确认 IDEA 是否启用了 tomcat 插件 2.添加 tomcat 选择 tomcat 存放路 ...

  8. COCI 2018/2019 CONTEST #2 T4 Maja T5Sunčanje Solution

    COCI 2018/2019 CONTEST #2 T4 T5 Solution abstract 花式暴力 #2 T5 Sunčanje 题意 按顺序给你1e5个长方形(左下角坐标&& ...

  9. vue中路由懒加载实现amd加载文件

    一般我们配置路由的时候是import引入: import log from '@/components/login': { path: '/login', component: log , hidde ...

  10. Lambda Expression

    Java 8的一个大亮点是引入Lambda表达式,使用它设计的代码会更加简洁.当开发者在编写Lambda表达式时,也会随之被编译成一个函数式接口.下面这个例子就是使用Lambda语法来代替匿名的内部类 ...