C#2.0新特性之泛型与迭代器
泛型
1、 什么是泛型?
泛型允许你在编译时间实现类型安全。它们允许你创建一个数据结构而不限于特定的数据类型。即通过参数化类型来实现在同一份代码上操作多种数据类型。
泛型编程是一种编程范式,它利用“参数化类型”将类型抽象化,从而实现更为灵活的复用。
之所以我们称它为泛型,是因为我们在定义的时候,只为它所包含的对象指派了一个类型占位符,而不是将它定义为确定的类型,只有在创建该集合的实例时,才会给它指派一个确定的类型。
2、泛型的优点
我们都知道,泛型是.NET2.0里面新引入的特性,至于泛型的作用,我们都知道,它只是使C#里面的类型免去了相互转换的麻烦和复杂度。而且,我们也知道,.NET是一个单根继承系统,即.NET里面所有的类都是单继承的,一个类不能继承多个类,即一个派生类不能继承多个基类,这一点和C,C++是不同的。另外,在C#里面,所有的类型都可以作为Object来进行传递,也就是说,Object类实际上完全可以实现泛型的功能。既然如此,那么Java和.NET为什么还要引入泛型呢?泛型的引入究竟有什么作用呢?
我们来归纳一下,泛型和非泛型相比,主要有二个优点。
首先,泛型更加安全。为什么这么说呢,虽然所有的类型都可以作为Object来传递,但是在传递的过程中无法避免进行类型转换,而类型转换在运行的时候是不安全的,在某个时候,它和程序的环境配置以及具体的数据有关,比如曾经我碰到的一个问题就是,将string类型转换成int型的时候,用int.parse进行转换,结果在本地运行正常,但是上传到服务器的时候却无法显示网页,结果后来用Convert.ToInt32转化就正常了。在C#中,仅整数就有九种类型,它们的范围各不相同,再加上其他类型,如此多的类型,它们相互之间的转换是存在不安全性的因素的,而泛型的使用避免了不必要的类型转换,从而提高了安全性。
比如,我们来看下面这个例子
Stack myStack=new Stack();
myStack.Push("ustbwuyi");
myStack.Push(4);
Array myArray = myStack.ToArray();
foreach (string item in myArray)
{
label1.Text += item + "<br/>";
}上述的这个例子,我们在编译该函数的时候,程序是不会报错的,而当程序执行的时候,程序才会抱错,这样的话我们很难在编译的时候发现程序中的错误。那么,如果我们用泛型情况又如何呢?
Stack<string> myStack = new Stack<string>();
myStack.Push("ustbwuyi1");
myStack.Push(5);
Array myArray = myStack.ToArray();
foreach (string item in myArray)
{
label1.Text += item + "<br/>";
}
像上面这种情况在编译的时候就会报错,因为创建该泛型的时候就指定了特定类型string。
当然,对于C#和VB.NET来说,在原理上还是有一些区别。比如我来看一个例子。
ArrayList list = new ArrayList();
list.Add(3);
list.Add(4);
list.Add(5.0);
int total = 0;
foreach (int i in list)
{
total = total + i;
}
Response.Write(total);
对于上面的代码,我们可以看到明显是出错的,类型转换上有错误,那么这个错误是怎么引起的,对于C#来说,当执行list.Add(3);时,已经将3装箱了,而在循环的时候,该元素将拆箱成int型,而对于
list.Add(5.0);来说,程序先将double型5.0装箱,然后再循环的时候该double值被拆箱成int型,所以引发程序的异常。
对于VB来说,这个过程是有所不同的,因为在VB中不使用装箱机制,而是激活一个将double型转化成int类型的方法。但是如果这个值不能被转化为整形,那么这个过程也是失败的。
其次,泛型的效率更高,在非泛型编程中,将简单类型作为Object传递会引起装箱和拆箱操作,这两个过程是具有很大的系统开销的,而用泛型就不会存在这个问题。比如在栈中添加值类型时,如整数,在添加之前必须装箱。装箱的时候在栈上分配一个对象,该对象包括该值类型的一个引用。
.NET中的泛型和其它平台的泛型有一些区别,他们具有很好的二进制重用性。这一点主要是因为.NET将泛型内建在CLR之中。而C++和Java中的泛型所依靠的是它们各自的编译器所提供的特性,编译器在编译泛型代码时将确切的类型展开,这就难免会出现代码膨胀的问题。而.NET的泛型代码是在运行时由JIT(just-in-time,CLR所使用的编译器,负责把MSIL翻译的代码编译成真正的机器代码.)即时编译的,这样CLR就可以为不同类型重用大部分的即时编译代码了。
除此之外,泛型极大的节省了我们的时间,提高了我们的开发效率。我们在创建自定义集合的时候,只需要一行代码,就可以创建一个自定义集合的匹配物,而不用为每种存储类型分别创建不同的独立的集合。只需要在初始化泛型集合的时候将其初始化为一个你需要用到的类型。
在什么情况下使用泛型?
一般来说,在通常情况下,在集合类型中,我们都建议使用泛型,因为它具有类型安全的优点,而不需要从基集合类型派生并实现类型特定的成员。另外,如果集合元素为值类型,泛型集合类型一般都会优于非泛型集合类型,因为在使用泛型的时候不必对元素进行装箱。
泛型集合的种类?
泛型一般来说有下面这些集合类型:
1 List,这是我们应用最多的泛型种类,它对应ArrayList集合。
2 Dictionary,这也是我们平时运用比较多的泛型种类,对应Hashtable集合。
3 Collection对应于CollectionBase
4 ReadOnlyCollection 对应于ReadOnlyCollectionBase,这是一个只读的集合。
5 Queue,Stack和SortedList,它们分别对应于与它们同名的非泛型类。
除此之外,还有一些没有对应非泛型类型的泛型种类,这个在我们平时中应用比较少,在这就不多阐述了。
泛型中的关键字default
在泛型的使用过程中,我们经常会遇到这样一种情况。怎样给泛型方法中的参数化类型T设置默认值。因为对于T来说,它的类型是不定的,它可能是值类型,也可能是引用类型。即使知道它是值类型,也有数值和结构之分。这样就给我们定义它的默认值带来了困难,如果我们定义t=null,这只有在T为引用类型时才有效。如果我们定义t=0,这只有在T为数值类型而不是结构时才有效。怎么解决这个问题呢?泛型的解决方案是利用default关键字,这个关键字有什么作用呢?它对引用类型会返回null,对数值类型会返回0,也就是说,它会根据每个结构成员的不同类型来返回不同的默认值。
public class GenericList<T>
{
private class Node
{
public Node Next;
public T Data;
}
private Node head;
public T GetNext()
{
T temp = default(T);
Node current = head;
if (current != null)
{
temp = current.Data;
current = current.Next;
}
return temp;
}
}
虽然泛型的引入虽然给我们带来很多好处,但是同时也使C#中的语法有一些变化,这是我们在使用泛型的过程中应该注意的。
下面我们着重在类中来讲叙泛型,在实际中它在类方法、接口、委托和结构上都是可以使用的。
1、泛型中的静态成员变量
对于类的静态成员变量来说,在C#1.1中,类的静态成员变量在不同的类实例中是共享的,并且都是通过类名来访问的。在2.0中,这个机制有了一些变化,即静态成员变量在相同封闭类间共享,在不同的封闭类间不共享。
为什么呢?因为在泛型中,不同的封闭类虽然有相同的类名称,但是传入的数据类型是不同的,它在本质上来说是不同的类。
比如下面:
Test<int> a=new Test<int>();
Test<int> b=new Test<int>();
Test<string> c=new Test<string>();
a和b是同一类型,但是c和a,b是完全不同的类实例,所以它们之间不能共享静态成员变量。
2、泛型中的方法重载
我们知道,方法重载在C#中应用非常广泛,对方法进行重载的时候,要求它们具有不同的签名。但是在对泛型进行重载的时候,通用类型T在编写的时候是不确定的,这是和传统的重载方法所不同的地方,对这种情况我们怎样进行处理呢?下面我们来看这个例子。
public class Example<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方法时编译器才能判断是否会引起调用混淆。
下面我们来看第一种情况,看是否会引起混淆。
Example<int,int> example1=new Example<int,int>();
object x=example1.add(1,1);
如果我们按照上面的情况实例化,类中的三个方法都符合,因为它们具有相同的签名。但是在这里,是可以调用成功的,因为在这里有一个原则:
如果一般方法和泛型方法具有相同的签名,一般方法会覆盖泛型方法。
也就是说,在上面进行实例化的并且调用类中的方法时,第三个方法已经覆盖了前面两个泛型方法,所以在这里是可以调用成功的。
如果我们删除了第三个add方法,那么上面的代码编译的时候是无法通过的,因为前两个add方法引起了混淆。
3、泛型类中数据类型的约束
在泛型的使用过程中我们经常会遇到这种问题,虽然这个数据类型T是通用类型,但是在有些特殊情况下我们必须对传入的数据进行一定的限制,即我们经常说的泛型的数据类型约束。约束的方式是指定T的祖先,即它继承的接口或者类,这里要注意的是C#是单根继承的,所以约束可以有多个接口,但是最多只能有一个类,并且类必须在接口之前。下面我们来看一个约束。
Public class Test<T,V> where T:Stack,IComparable
Where V:Stack
{…}
上面这个约束说明,T必须从Stack和Icomparable继承,V必须是Stack或者从Stack继承。否则它是无法通过编译器的类型检查的。
看到这里有人也许会问,既然T是通用类型,各种数据类型都支持,那还需要对它进行约束干什么呢?不是多此一举吗?其实不然,约束在泛型中是有很重要的作用的,举个例子,比如现在我设计的类只需要两种数据类型int和string,并且需要对T类型的变量比较大小。但是对于object来说,它是没有比较大小的方法的,这是很显然的。这时候我们就可以利用约束来进行处理。
比如说下面的方法:
public static T Max<T>(T op1, T op2)
{
if (op1.CompareTo(op2) < 0)
return op1;
return op2;
}
这个例子在编译的时候是不能通过的,因为没有对它添加IComparable约束,没有实现IComparable接口所以不支持CompareTo方法。
public static T Max<T>(T op1, T op2) where T : IComparable
{
if (op1.CompareTo(op2) < 0)
return op1;
return op2;
}
我们知道,在C#里数据类型有两大类,值类型和引用类型,在泛型的约束里面,我们也可以指定通用类型T是值类型还是引用类型,值类型用关键字struct来标识,而引用类型用关键字class来标识。如:
Public class Test<T,V> where T:class
Where V:struct
{……}
如果在类Test里需要对T重新进行实例化,而我们又不知道类Test中类T到底有哪些构造函数。这个时候需要用到new约束:
如:
public class Test<T, V> where T : Stack, new()
where V: IComparable
要注意的一点是,当与其他约束一起使用时,new() 约束必须最后指定。另外,new约束只能是无参数的,所以也要求相应的类Stack必须有一个无参构造函数,否则编译失败。
泛型方法:
泛型不仅能作用在类上,也可单独用在类的方法上,他可根据方法参数的类型自动适应各种参数,这样的方法叫泛型方法。
对于下面的泛型方法,原来的类Stack一次只能Push一个数据,这个类Stack2扩展了Stack的功能(当然也可以直接写在Stack中),他可以一次把多个数据压入Stack中。其中Push是一个泛型方法。
class Program
{
static void Main(string[] args)
{
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();
}
}
public class Stack2
{
//一个泛型方法,将多个数据压入堆栈
public void Push<T>(Stack<T> s, params T[] p)
{
foreach (T t in p)
s.Push(t);
}
}
迭代器(Iterator)
迭代器和泛型一样,也是C#2.0中的新功能,迭代器是方法、get访问器或者运算符。它是用来干什么的呢?它的主要作用是使类或者结构,包括我们自己定义的数据集等中能够支持foreach遍历。在C#中,我们主要通过继承IEnumerable接口和实现该接口的GetEnumrator方法来创建迭代器。只要我们在程序中提供一个迭代器,就可以遍历类中的数据结构,在程序进行编译的时候,当编译器检测到迭代器,它会自动生成IEnumerable或者IEnumerable接口的Current、MoveNext和Dispose方法。
下面我们看一个简单的例子,来看C#是如何实现迭代的。
例一:
public class Example1:IEnumerable
{
public IEnumerator GetEnumerator()
{
for (int i = 0; i < 10; i++)
{
yield return i;
}
}
}
class Program
{
static void Main(string[] args)
{
Example1 test = new Example1();
foreach (int i in test)
Console.WriteLine(i);
}
}
输出结果:0123456789
关于迭代器:
2 迭代器是可以返回相同类型的值的有序序列的一段代码。
3 迭代器可用作方法、运算符或get访问器的代码体。
4 迭代器使用yield return语句依次返回每个元素,到达yield break将终止迭代。
5 在一个类中可以实现多个迭代器。但是在一个类里面的迭代器必须有唯一的名称。
6 迭代器的返回类型必须为IEnumerable和IEnumerator类型。
现在,我们大概知道了迭代器的一些概念,也知道迭代器是用来做什么的,简单的说就是实现对一个类(主要是容器类)或者结构的循环。而它的主要用途也在这里,如果我们不做容器类,并且如果我们不需要实现容器类循环这个需求,那迭代器对我们来说就没有什么用处了。前者没有必要,后者可以直接用容器内部的GetEnumerator方法。
迭代器的实现模式。
通常情况下,实现IEnumrable接口的类是作为要遍历的集合类型的嵌套类来提供。将嵌套类作为枚举器。这样既可以保证它访问所包含的类的其他私有成员,又对迭代客户端隐藏了底层数据结构的实现细节。
迭代器中有三个很重要的属性和方法,即Current、MoveNext和Dispose方法。下面我们先介绍一下这三个方法。
MoveNext方法封装了迭代器块的代码,调用MoveNext方法将执行迭代器内的代码。并且将对象的current属性设为合适的值。至于设为什么值,由MoveNext方法被调用时枚举器对象的状态决定。比如如果当前枚举器对象状态是before或者suspended,那么调用MoveNext将把对象的状态改为running。如果当前枚举器对象状态是running,那么调用MoveNext方法的结果是未知的。
Current属性受yield return语句影响,它的值是最后一次调用MoveNext时被设置的值,当枚举器对象处于before、after或者running时,Current的值是不确定的。
Dispose方法通过将枚举器对象的状态置为after,以清理迭代结果。如果状态是before,调用Dispose将改变其状态为after;如果状态为running,调用Dispose的结果是为指定的;如果枚举器对象状态为suspended,调用Dispose将先改变其状态为running,并且执行finally块,最后改变其状态为after;如果枚举器对象状态为after,那么调用Dispose没有效果。
使用迭代器有一个问题我们必须有清晰的认识,那就是,迭代器的主要功能是实现容器类和自定义集合的循环。但是,在我们的实际开发中,容器类和自定义集合都不会是单一的数据类型,肯定包含了多种数据类型。如果包含了值类型,那么需要对它们进行装箱和拆箱才能获得选项。因为IEnumerator.Current返回一个Object类的对象。这必然会导致潜在的性能降低和托管堆上的压力增大。如果包含的是引用类型,那么必然存在类型之间的转换,这样同样是会带来效率的损失和存在一定的安全性问题。那么这个问题怎么解决呢?
前面我们讲泛型的时候曾经提到了泛型的优点,恰好是避免装箱拆箱,并且避免在类型转换中的安全问题,可以说,泛型的优点正好可以补迭代器在容器类中使用的缺点。所以,在实际的开发过程中,我们一般将迭代器和泛型结合起来使用。
最后,我们综合泛型和迭代器来看一个例子,这个例子是微软提供的,我稍微做了一下改动。
public class Ustbwuyi<T> : IEnumerable<T>
{
protected Node Node1;
protected Node current = null;
//该嵌套类型同样为T上的泛型
protected class Node
{
public Node next;
public Node Next
{
get { return next; }
set { next = value; }
}
private T data;
public T Data
{
get { return data; }
set { data = value; }
}
//在非泛型构造函数中使用T
public Node(T t)
{
next = null;
data = t;
}
}
public Ustbwuyi()
{
Node1 = null;
}
public void AddHead(T t)
{
Node n = new Node(t);
n.Next = Node1;
Node1 = n;
}
// 实现 GetEnumerator 以返回 IEnumerator<T>,从而启用列表的
// foreach 迭代。请注意,在 C# 2.0 中,
// 不需要实现 Current 和 MoveNext。
// 编译器将创建实现 IEnumerator<T> 的类。
public IEnumerator<T> GetEnumerator()
{
Node current = Node1;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
//必须实现次方法,因为IEnumerable<T> 继承 IEnumerable
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
/// <summary>
/// 一个排序算法
/// </summary>
/// <typeparam name="T"></typeparam>
public class SortedList<T> : Ustbwuyi<T> where T : IComparable<T>
{
public void BubbleSort()
{
if (Node1 == null || Node1.Next == null)
return;
bool swapped;
do
{
Node previous = null;
Node current = Node1;
swapped = false;
while (current.next != null)
{
if (current.Data.CompareTo(current.next.Data) > 0)
{
Node tmp = current.next;
current.next = current.next.next;
tmp.next = current;
if (previous == null)
{
Node1 = tmp;
}
else
{
previous.next = tmp;
}
previous = tmp;
swapped = true;
}
else
{
previous = current;
current = current.next;
}
}
}
while (swapped);
}
}
public class Person : IComparable<Person>
{
string name;
int age;
public Person(string s, int i)
{
name = s;
age = i;
}
public int CompareTo(Person p)
{
return age - p.age;
}
public override string ToString()
{
return name + "的年龄是:" + age;
}
public bool Equals(Person p)
{
return (this.age == p.age);
}
}
class Program
{
static void Main(string[] args)
{
SortedList<Person> list = new SortedList<Person>();
string[] names = new string[] { "邬琳伟", "吴娟", "吴倚", "胡娭毑", "唐志科" };
int[] ages = new int[] { 31, 25, 23, 24, 25 };
//填充列表
for (int i = 0; i < names.Length; i++)
{
list.AddHead(new Person(names[i], ages[i]));
}
Console.WriteLine("未排序的列表:");
//输出未排序的列表
foreach (Person p in list)
{
Console.WriteLine(p.ToString());
}
//对列表进行排序
list.BubbleSort();
Console.WriteLine(String.Format("{0}排序的列表:", Environment.NewLine));
//输出排序的列表
foreach (Person p in list)
{
Console.WriteLine(p.ToString());
}
Console.WriteLine("Filished");
}
}
该程序的输出结果为:
未排序的列表:
唐志科的年龄是:25
胡娭毑的年龄是:24
吴倚的年龄是:23
吴娟的年龄是:25
邬琳伟的年龄是:31
排序的列表:
吴倚的年龄是:23
胡娭毑的年龄是:24
唐志科的年龄是:25
吴娟的年龄是:25
邬琳伟的年龄是:31
Filished
请按任意键继续. . .