C#秘密武器之泛型
一、简介:
很多初学者在刚开始接触泛型的时候会比较难理解泛型,在这里先把 “泛型”当作一个形容词,这样就方便理解了,因为很多东西都可以是泛型的!比如:“泛型的类”,“泛型的方法”,“泛型的接口”,“泛型的委托” 等,很多时候我们使用泛型可以极大减少代码重复,使程序更加清爽,也可以避免不必要的‘装箱’和‘拆箱’过程。
为什么要有泛型?
我们常常会遇到这样的情况:为了实现某一个功能,我们一开始把方法写好,但后来我们发现同样的功能需要我们再写一次,但是这次方法的参数类型和上次不一样了,这个时候按照敏捷软件开发的思想,不要过早的进行抽象和应对变化,当变化第一次出现时,使用最快的方法解决它,但变化第二次出现时,再进行更好的架构设计,这样的目的是为了避免过度设计,因为有可能第二次变化永远也不会出现。考虑到功能一样,我们通常会直接复制原方法的代码,然后修改一下参数类型即可快速解决。这样做确实没错,但是有时候不仅出现了第二次变化 ,还出现了第三次...或者是更多次变化,继续使用CV大法修改方法的签名将会导致大量重复代码的出现,于是我们就会想,要是存在一个可以传递任何数据类型的方法那该多好,即把这个方法的实现当成模板,把方法的签名抽象出来,于是我们引入了泛型。
下面我们来看一下具体的例子:
1.1 使用CV大法
输入多个 int类型,进行冒泡排序让它们依次重小到大输出,代码如下:
public class SortHelper
{
public void BubbleSort(int[] arr)
{
int length = arr.Length;
for (int i = 0; i < length-1; i++)
{
for (int j = 0; j < length-1-i; j++)
{
if (arr[j]>arr[j+1])
{
int temp=arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
测试:
static void Main(string[] args)
{
SortHelper sorter = new SortHelper();
int[] a = { 4,5,1,3,2,8,5,0,2};
sorter.BubbleSort(a);
//输出省略
}
输出为:0,1,2,2,3,4,5,5,8
输入多个 Byte类型,进行冒泡排序让它们依次重小到大输出,代码如下:
这个时候我只要复制一下原来的方法改一下签名就可以了
public class SortHelper
{
public void BubbleSort(byte[] arr)
{
int length = arr.Length;
for (int i = 0; i < length-1; i++)
{
for (int j = 0; j < length-1-i; j++)
{
if (arr[j]>arr[j+1])
{
byte temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
这样做虽然可以,但是以后若要处理N次各种其它数据类时就要大量重复复制,严重影响代码的简洁度,而且当功能要扩展时,每个方法都要修改,维护起来非常不方便。
1.2 使用泛型
我们自然而然的会这样想了:如果可以把方法中的参数类型用一个 ”占位符“表示每次传入什么类型它就变成什么类型,这样就可以将这个方法当成一个模板用了。
这里我们用 “T” 来代表这个特殊的参数类型,于是代码就变成了这样:
public class SortHelper
{
public void BubbleSort(T[] arr)
{
int length = arr.Length;
for (int i = 0; i < length-1; i++)
{
for (int j = 0; j < length-1-i; j++)
{
if (arr[j]>arr[j+1])
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
这里 T 代表 ”类型的类型“ 和 int ,string ...等数据类型相似,T 就是类型本身。让人兴奋的是真的有像 “T” 这样的特别存在,在.NET中叫做类型参数.
下面我们看看规范的代码:这里我们把BubbleSort定义成泛型类,定义泛型类的一种方法是在类后面加上“<T>”
//定义泛型类SortHelper 这里“where T:IComparable” 是给类型参数T一个限制 -- 参数类型必须实现IComparable接口,否则无法通过编译
public class SortHelper<T> where T:IComparable
{
public void BubbleSort(T[] arr)
{
int length = arr.Length;
for (int i = 0; i < length-1; i++)
{
for (int j = 0; j < length-1-i; j++)
{
if (arr[j].CompareTo(arr[j+1])>0)
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
测试:
static void Main(string[] args)
{
SortHelper<byte> sorter = new SortHelper<byte>();
byte[] a = { 4,5,1,3,2,8,5,0,2}; sorter.BubbleSort(a); SortHelper<int> sorter1 = new SortHelper<int>();
int[] b = { 4, 5, 1, 3, 2, 8, 5, 0, 2 };
sorter1.BubbleSort(b);
//输出省略
}
输出为:
0,1,2,2,3,4,5,5,8
0,1,2,2,3,4,5,5,8
输入多个自定义类型的实例,进行冒泡排序让它们依次从小到大输出,下面我们来模拟一下宠物店卖的猫,按价格排序:
猫类:
public class cat:IComparable
{
public string name;
public int price; public int CompareTo(object obj)
{
cat catT = (cat)obj;
return this.price.CompareTo(catT.price);
} public cat(string name, int price)
{
this.price = price;
this.name = name;
}
}
测试:
static void Main(string[] args)
{
SortHelper<cat> sorter2 = new SortHelper<cat>();
cat cat1=new cat("猫1",1000);
cat cat2=new cat("猫2",1400);
cat cat3=new cat("猫3",400);
cat[] c = { cat1, cat2, cat3 }; sorter2.BubbleSort(c);
//输出
for (int i = 0; i < c.Length; i++)
{
Console.WriteLine("Name:"+c[i].name+" Price:"+c[i].price);
}
}
结果如图:
二、泛型与集合类型
通过泛型可以大大提高集合类型的的性能恶化安全性。
2.1 非泛型的集合类
先是往集合里存放3个数据 :
ArrayList list = new ArrayList();
int listSize = 3; for (int i = 0; i < listSize; i++)
{
list.Add(i);
} for (int i = 0; i < listSize; i++)
{
int value = (int)list[i];
Console.WriteLine(value);
}
测试:
输出
0
1
2
这样子写虽然能运行通过,但是在这里 list每次调用Add方法时就做了一次 ” 装箱 “ 操作,接着每次取数据时对list的元素进行一次强制转换 (int)list[i],同时也做了一次 “ 拆箱 ”操作,这两个操作对.NET来说是比较耗时的,当操作的次数越多效果就越明显。
2.2 下面我们将 listSize 设置成 1000000 ,看看消耗的时间
ArrayList list = new ArrayList();
int listSize = 1000000;
long StarTime = DateTime.Now.Ticks;
for (int i = 0; i < listSize; i++)
{
list.Add(i);
} for (int i = 0; i < listSize; i++)
{
int value = (int)list[i];
}
long EndTime = DateTime.Now.Ticks; Console.WriteLine("使用ArrayList,耗时:{0} Ticks", EndTime - StarTime);
测试:
结果
2.3 使用泛型集合类型(泛型数组)
List<int> list = new List<int>();
int listSize = 1000000;
long StarTime = DateTime.Now.Ticks;
for (int i = 0; i < listSize; i++)
{
list.Add(i);
} for (int i = 0; i < listSize; i++)
{
int value =list[i];
}
long EndTime = DateTime.Now.Ticks; Console.WriteLine("使用List<int>,耗时:{0} Ticks", EndTime - StarTime);
测试:
比较上述2次执行的结果我们会发现,使用 ArrayList 的耗时是使用 List<int> 的2倍多 ,随着次数的增大差距会越来越明显!
总结:
1.可以避免同种功能代码的大幅度重复出现,使我们的代码更加简洁、可读性更高
2.方便扩展维护,灵活度高
3.避免隐式的装箱拆箱,提高程序运行速度
三、泛型方法与泛型接口
3.1 泛型方法
上面说到用一个泛型类 SortHelper 来做一个冒泡排序的处理,现在回顾一下之前的代码:
public class SortHelper<T> where T:IComparable
{
public void BubbleSort(T[] arr)
{
int length = arr.Length;
for (int i = 0; i < length-1; i++)
{
for (int j = 0; j < length-1-i; j++)
{
if (arr[j].CompareTo(arr[j+1])>0)
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
这里我们为了调用 BubbleSort,而将参数T传递给类 SortHelper,假如 SortHelper 类中,除了 BubbleSort 方法之外,还有其它的没有使用到参数T的方法 比如:
public class SortHelper<T> where T:IComparable
{
public void BubbleSort(T[] arr)
{
//省略
} public void OtherMethod1()
{
//省略
}
//此处省略N个OtherMethod
}
这里有 N 个 OtherMethod方法。
测试调用 OtherMethod1:
static void Main(string[] args)
{
SortHelper<int> UserOtherMethod = new SortHelper<int>();
UserOtherMethod.OtherMethod1();
}
在实例化 UserOtherMethod 对象时,必须给 SortHelper 传一个类型参数,这里是 int 。显然这种情况下,我们仅仅是为了 BubbleSort 方法能够正常使用而定义泛型类,但是却强迫调用 SortHelper 类其它方法的实例也传递这样一个参数,很明显这是没必要的,于是这个时候 ’泛型方法‘ 就出现了 ,修改代码如下:
public class SortHelper2
{
public void BubbleSort<T>(T[] arr) where T:IComparable
{
int length = arr.Length;
for (int i = 0; i < length - 1; i++)
{
for (int j = 0; j < length - 1 - i; j++)
{
if (arr[j].CompareTo(arr[j + 1]) > 0)
{
T temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
}
总结:
泛型方法和普通方法没多大区别,仅仅在方法名后面加了“<T>”, 接着是括号里面的参数,最后是限定语句。
上述的这种情况,我们通过使用泛型方法,就可以避免每次使用泛型类传参了。
3.2 泛型接口
回顾前面我们自己写了一个 “cat” 类,并调用了一个接口“IComparable” ,代码如下:
public class cat:IComparable
{
public string name;
public int price; public int CompareTo(object obj)
{
cat catT = (cat)obj;
return this.price.CompareTo(catT.price);
}
public cat(string name, int price)
{
this.price = price;
this.name = name;
}
}
这里粉红色标记的部分我们实现了 “IComparable” 接口的 “CompareTo” 方法,这里没有进行装箱和拆箱。因为cat是引用类型,而且“ return this.price.CompareTo(catT.price) ”这里调用的是 price (int型) 的 ComparaTo 方法,右键对int转到定义查看代码。这里可以清楚的看到int类型实现了 IComparable 与 IComparable<int> 这两个接口,也就是说它分别实现了这两个接口的 ComparaTo() 方法,那这里又是调用哪个接口的 ComparaTo() 方法呢?
实际上这里传过来的参数是int型,那么优先调用的是 ComparaTo(int value) 方法,这样就避免了不必要的拆装箱了。
上图中是.NET 中 int 类型实现 IComparable 接口的 ComparaTo(object value) 方法,不难看出“红箭头”标记部分是执行装箱拆箱操作的过程。
下图是 int类型实现泛型版本的 IComparable<int> 的 ComparaTo(int value) 方法,如果对2种方法进行大量重复操作,不难发现使用泛型接口实现的方法性能比较好。
总结:
程序设计中应该尽量避免装箱和拆箱操作,为了避免多次装箱拆箱造成的性能影响 ,通过上面的例子我们可以知道使用泛型接口可以有效的避免这个问题:使用 IComparable<T> 而不使用 IComparable,这样可以避免值类型的装箱和取消装箱操作。
四、 泛型的协变和逆变以及常用的接口IEnumerable 及其泛型版的IEnumerable<out T>
4.1 泛型的协变与逆变|泛型修饰符‘out’与‘in’
首先这2个拗口的名词先不用去管它,先知道协变和逆变主要是用在泛型的接口和委托上就可以了,下面我们通过一个例子来看看。在这之前我们插点别的东西,我们知道接口是可以体现多态的,当然接口体现的多态注重的功能上的多态,这和抽象类不同,抽象类更注重的是建立在血缘关系上的多态。
知道接口是可以体现多态的之后,我们来看看一个相关的例子:鸟和飞机都会飞,把飞定义成一个借口,在定义2个类
public interface IFlyable
{
void fly();
}
class Bird:IFlyable
{
public void fly()
{
Console.WriteLine("鸟儿飞!");
}
}
class Plane:IFlyable
{
public void fly()
{
Console.WriteLine("飞机飞!");
}
}
下面看看接口体现的多态性:
IFlyable ifly; ifly = new Bird();
ifly.fly(); ifly = new Plane();
ifly.fly();
运行结果:
鸟儿飞!
飞机飞!
了解了接口的多态性后我们再来看一个例子:
这里定义了2个类 Animal 和 Cat (Cat继承了Animal)
public class Animal
{
} public class Cat:Animal
{
}
继续往下看:
Cat cat = new Cat();
下面这句代码,cat向animal转,子类向父类转换,这时cat会隐式转换为animal 我们说“儿子像父亲” 这是完全可以理解的
Animal animal = cat;
但是 说”父亲像儿子“ 这是说不过去的 ,但是有的时候如果儿子坑爹,强制转换了一下还是可以的
cat = (Cat)animal;
(协变)
List<Cat> catArray = new List<Cat>();
List<Animal> animalArray = catArray;
如果是上面说的类,这样写是可以的,但是这里是会报错的 如图
继续往下看 这样写却可以
IEnumerable<Cat> lCat = new List<Cat>();
IEnumerable<Animal> lAnimal = lCat;
对 IEnumerable<Cat> 转到定义 如图 我们发现这里多了一个 “out” 关键字
概念引入:
对于泛型类型参数,out 关键字指定该类型参数是协变的。 可以在泛型接口和委托中使用 out 关键字。“协变”是指能够使用与原始指定的派生类型相比,派生程度更大的类型。对于 “协变”, 笔者是这样理解的:就是”说的通变化“ ,就像 “儿子像父亲一样”(假定父亲派生程度0那么儿子的派生程度就是1了,所以父亲可以使用派生程度更大的儿子)
(逆变)
我们知道IComparable<T>接口中,T的修饰符是‘in’,下面我们修改一下上面的代码演示一下 :
class Cat : Animal, IComparable<Cat>
{
//仅演示
public int CompareTo(Cat other)
{
return 1;
}
} class Animal : IComparable<Animal>
{
//仅演示
public int CompareTo(Animal other)
{
return 1;
}
}
这里Cat和Animal都实现了IComparable<T>接口,然后我们这样写
IComparable<Cat> ICat = new Cat();
IComparable<Animal> IAnimal = new Animal();
ICat = IAnimal;
代码中ICat(高派生程度)使用 IAnimal(低派生程度) “父亲像儿子” 和上面的例子完全相反。
概念引入:
对于泛型类型参数,in 关键字指定该类型参数是逆变的。 可以在泛型接口和委托中使用 in 关键字。“逆变”则是指能够使用派生程度更小的类型。对于 “逆变” 笔者的理解则是: “坑爹儿子” 反过来硬说 “父亲像儿子”, 这是 “说不过去的” ,只是利用了强硬的手段。
在了解了上面的内容后,我们来看看“out” 与 “in” 关键字的特性
IEnumerable<T>接口的IEnumerator<T> GetEnumerator()方法返回了一个迭代器 ,不难发现T如果用 out 标记,则T代表了输出,也就说只能作为结果返回。
IComparable<T>接口的CompareTo(T other)方法传入了一个T类型的Other参数,不难发现T如果用 in 标记,则T代表了输入,也就是它只能作为参数传入。
下面我们演示一个例子
将动物会叫这功能,定义成一个泛型借口用 out 修饰
这里会出现一个错误
把第二个带参数的setSound方法,去掉后编译可以正常通过
下面我们把 out 改成 in
这里会出现一个错误
把第一个setSound方法,去掉后编译可以正常通过,或者把第一个方法的返回值,改成其它非T类型,编译也可通过
这个演示充分说明了:out 修饰 T 则 T只能作为结果输出而不能作为参数 ; in 修饰 T 则 T只能作为参数而不能作为结果返回;
4.2 IEnumerable接口及其泛型版
为什么要用IEnumerable接口? 下面我们通过一个例子看看:
//定义Person类
public class Person
{
public Person(string _name)
{
this.name = _name;
} public string name;
} //定义People类
public class People
{
private Person[] _people; public People(Person[] pArray)
{
//实例化数组 用于存Person实例
_people = new Person[pArray.Length]; for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
}
}
上面的代码我们定义了一个 Person 类和一个 People 类,显然 People是用来存放多个Person实例的集合,下面我们尝试用 Foreach 遍历集合的每个元素输出:
static void Main(string[] args)
{
Person[] personArray = new Person[3]{
new Person("Keiling1"),
new Person("Keiling2"),
new Person("Keiling3"),
}; People people = new People(personArray);
foreach (Person item in people)
{
Console.WriteLine(item.name);
}
}
这里编译不能通过,出现了一个错误
GetEnumerator:是IEnumerable接口中的一个方法,它返回一个 IEnumerator(迭代器),如下图
IEnumerator内部规定了,实现一个迭代器的所有基本方法,包括 如下图
为了在foreach中使用 People的实例, 我们给People实现IEnumerable接口,代码如下:
public class People:IEnumerable
{
private Person[] _people; public People(Person[] pArray)
{
//实例化数组 用于存Person实例
_people = new Person[pArray.Length]; for (int i = 0; i < pArray.Length; i++)
{
_people[i] = pArray[i];
}
} ////IEnumerable和IEnumerator通过IEnumerable的GetEnumerator()方法建立了连接,可以通过IEnumerable的GetEnumerator()得到IEnumerator对象。
IEnumerator IEnumerable.GetEnumerator()
{
return (IEnumerator)GetEnumerator();
} public PeopleEnum GetEnumerator()
{
return new PeopleEnum(_people);
}
} public class PeopleEnum:IEnumerator
{
public Person[] _people; public PeopleEnum(Person [] pArray)
{
_people = pArray;
}
//游标
int position = -1; //是否可以往下 移
public bool MoveNext()
{
position++;
return (position < _people.Length);
} //集合的所有元素取完了之后 重置position
public void Reset()
{
position = -1;
} //实现 IEnumerator的 Current方法 返回当前所指的Person对象
object IEnumerator.Current
{
get
{
return Current;
}
} //Current是返回Person类实例的只读方法
public Person Current
{
get
{
try
{
return _people[position];
}
catch (IndexOutOfRangeException)
{
throw new InvalidOperationException();
}
}
}
}
测试运行:
static void Main(string[] args)
{
Person[] personArray = new Person[3]{
new Person("Keiling1"),
new Person("Keiling2"),
new Person("Keiling3"),
}; People people = new People(personArray);
foreach (Person item in people)
{
Console.WriteLine(item.name);
}
}
结果:
总结:
1.一个集合要支持foreach方式的遍历,必须实现IEnumerable接口,描述这类实现了该接口的对象,我们叫它‘序列’。
比如 List<T> 支持 foreach 遍历 是因为它实现了IEnumerable接口和其泛型版,如图--
2. IEnumerator对象具体实现了迭代器(通过MoveNext(),Reset(),Current)。
3. 从这两个接口的用词选择上,也可以看出其不同:IEnumerable是一个声明式的接口,声明实现该接口的class是“可枚举(enumerable)”的,但并没有说明如何实现迭代器,而IEnumerator是一个实现式的接口,IEnumerator对象就是一个迭代器。
关于IEnumerable<T>我们来了解一下它的代码:
C#秘密武器之泛型的更多相关文章
- TypeScript: Angular 2 的秘密武器(译)
本文整理自Dan Wahlin在ng-conf上的talk.原视频地址: https://www.youtube.com/watch?v=e3djIqAGqZo 开场白 开场白主要分为三部分: 感谢了 ...
- C#秘密武器之扩展方法
原文:C#秘密武器之扩展方法 为何要用扩展方法? 作为一个.NET程序猿,我们经常要跟.net自带类库或者第三方dll类库打交道,有时候我们未必能够通过反编译来查看它们的代码,但是我们通常需要给它们扩 ...
- 第一章-第七题( 有人认为,“中文编程”, 是解决中国程序员编程效率一个秘密武器,请问它是一个 “银弹” 么? )--By 侯伟婷
首先,“银弹”在百度百科中的解释是银色的子弹,我们更熟知的“银弹”一词,应该是在<人月神话>中提到的.银弹原本应该是指某种策略.技术或者技巧可以极大地提高程序员的生产力[1].此题目中关于 ...
- margin负值 – 一个秘密武器
CSS盒模型中,margin是我们老熟悉的一个属性了, 它的负值你用过吗? 你知道 margin负值的秘密武器吗?我们一起看看吧! 1.带竖线分隔的横向列表(例如:网站底部栏目) 传统的分隔符是使用 ...
- 团队高效率协作开发的秘密武器-APIDOC
团队高效率协作开发的秘密武器 1.前言 在团队协作开发中,不知道各位有没有遇到这样的问题: l 新人接手了项目代码,因没有项目文档,只能靠追踪路由,寻读代码分析业务逻辑 l 前端同学写好了页面,苦等后 ...
- 当3D打影人头”成为黑客的秘密武器,隐私该如何保护?
在<碟中谍>系列电影中,除了超级敬业又帅气的阿汤哥之外,最让人津津乐道的桥段就是用3D打印做出来的"人头".通过这些惟妙惟肖的"人头",阿汤哥完成了 ...
- C#秘密武器之异步编程
一.概述 1.什么是异步? 异步操作通常用于执行完成时间可能较长的任务,如打开大文件.连接远程计算机或查询数据库.异步操作在主应用程序线程以外的线程中执行.应用程序调用方法异步执行某个操作时,应用程序 ...
- C#秘密武器之委托
在C#的世界里,委托是无处不在,尤其在.NET自己封装的框架里到处都有其身影,所以掌握委托就很有必要了!那什么是委托呢?其实委托就是一种数据类型,跟int等东东差不多,只不过在使用时要自己先去构建一个 ...
- gitignore.io-程序猿值得拥有的智能生成gitignore文件的秘密武器
gitignore.io Create useful .gitignore files for your project by selecting from 360 Operating System, ...
随机推荐
- django学习随笔:ManagementUtility
ManagementUtility类,位于django.core.management目录下的__init__.py文件. 这个类,在其init中: def __init__(self, argv=N ...
- activiti-ui源码构建
修改数据库链接:
- 正则表达式、re、常用模块
阅读目录 正则表达式 字符 量词 . ^ $ * + ? { } 字符集[][^] 分组 ()与 或 |[^] 转义符 \ 贪婪匹配 re 总结 正则 re 常用模块 namedtuple deque ...
- poj 1192(树形DP)
最优连通子集 Time Limit: 1000MS Memory Limit: 10000K Total Submissions: 2589 Accepted: 1382 Descriptio ...
- 【hdoj_1257】最小拦截系统
题目:http://acm.hdu.edu.cn/showproblem.php?pid=1257 可以这样理解题意:给出一组数字,给它们划分组数,划分的依据是,每一组的元素必须是单调递减的顺序,只有 ...
- ionic3 学习记录
1生命周期 ionViewDidLoad(){ console.log("1.0 ionViewDidLoad 当页面加载的时候触发,仅在页面创建的时候触发一次,如果被缓存了,那么下次再打开 ...
- php必备树状数组处理方法
thinkphp必备公共方法 /** * 子元素计数器 * @param array $array * @param int $pid * @return array */ function arra ...
- 2017中南大学暑期集训day1 : debug&STL-A
A - Surprising Strings 题意就是给你一个字符串,例如ZGBG,有一种称谓叫D-unique 这个字符串 在D=0时, 有三个子串 ZG GB BG,因为这三个都不同,也就是uni ...
- 【推广】实用命令——tldr
碎碎念 如题,通常遇到一个新的命令需要查询其帮助的时候,一般使用`command -h`或者`man command`来查询,但是,有时候仅仅想知道这个命令怎么用,并不想知道具体含义啊(这个命令可能不 ...
- BZOJ 1878 [SDOI2009]HH的项链(扫描线+树状数组)
[题目链接] http://www.lydsy.com/JudgeOnline/problem.php?id=1878 [题目大意] 给出一个数列,给出m个查询,每次查询一个区间中不相同的数字个数 [ ...