Visual C# 2.0 的一个最受期待的(也许也是最让人怕惧)的一个特征就是关于泛型的支撑。这篇文章将通知你泛型用来处理什么样的题目,以及怎样运用它们来进步你的代码质量,另有你没必要恐惊泛型的缘由。
泛型是什么?
许多人以为泛型很难明白。我置信这是由于他们一般在相识泛型是用来处理什么题目之前,就被灌输了大批的理论和范例。效果就是你有了一个处理方案,然则却没有须要运用这个处理方案的题目。
这篇文章将尝试着转变这类进修流程,我们将以一个简朴的题目作为最先:泛型是用来做什么的?答案是:没有泛型,将会很难建立范例平安的鸠合。
C# 是一个范例平安的言语,范例平安许可编译器(可信任地)捕捉潜伏的毛病,而不是在顺序运行时才发明(不可信任地,每每发作在你将产物出卖了今后!)。因而,在C#中,一切的变量都有一个定义了的范例;当你将一个对象赋值给谁人变量的时刻,编译器搜检这个赋值是不是准确,假如有题目,将会给出毛病信息。
在 .Net 1.1 版本(2003)中,当你在运用鸠应时,这类范例平安就失效了。由.Net 类库供应的一切关于鸠合的类满是用来存储基范例(Object)的,而.Net中一切的一切都是由Object基类继续下来的,因而一切范例都能够放到一个鸠合中。因而,相当于根本就没有了范例检测。
更糟的是,每一次你从鸠合中掏出一个Object,你都必需将它强迫转换成准确的范例,这一转换将对机能形成影响,而且发生冗杂的代码(假如你忘了举行转换,将会抛出异常)。更进一步地讲,假如你给鸠合中增加一个值范例(比方,一个整型变量),这个整型变量就被隐式地装箱了(再一次下降了机能),而当你从鸠合中掏出它的时刻,又会举行一次显式地拆箱(又一次机能的下降和范例转换)。
关于装箱、拆箱的更多内容,请接见 圈套4,小心隐式的装箱、拆箱。
建立一个简朴的线性链表
为了生动地感受一下这些题目,我们将建立一个尽量简朴的线性链表。关于浏览本文的那些从未建立过线性链表的人。你能够将线性链表想像成有一条链子栓在一起的盒子(称作一个结点),每一个盒子里包含着一些数据 和 链接到这个链子上的下一个盒子的援用(固然,除了末了一个盒子,这个盒子关于下一个盒子的援用被设置成NULL)。
为了建立我们的简朴线性链表,我们须要下面三个类:
1、Node类,包含数据以及下一个Node的援用。
2、LinkedList类,包含链表中的第一个Node,以及关于链表的任何附加信息。
3、测试顺序,用于测试 LinkedList 类。
为了检察链接表怎样运作,我们增加Objects的两种范例到链表中:整型 和 Employee范例。你能够将Employee范例设想成一个包含关于公司中某一个员工一切信息的类。出于演示的目标,Employee类异常的简朴。
public class Employee{ private string name; public Employee (string name){ this.name = name; } public override string ToString(){ return this.name; } }
这个类仅包含一个示意员工名字的字符串范例,一个设置员工名字的组织函数,一个返回Employee名字的ToString()要领。
链接表自身是由许多的Node组成,这些Note,如上面所说,必需包含数据(整型 和 Employee)和链表中下一个Node的援用。
public class Node{ Object data; Node next; public Node(Object data){ this.data = data; this.next = null; } public Object Data{ get { return this.data; } set { data = value; } } public Node Next{ get { return this.next; } set { this.next = value; } } }
注重组织函数将私有的数据成员设置成通报进来的对象,而且将 next 字段设置成null。
这个类还包含一个要领,Append,这个要领接收一个Node范例的参数,我们将把通报进来的Node增加到列表中的末了位置。这历程是如许的:起首检测当前Node的next字段,看它是不是是null。假如是,那末当前Node就是末了一个Node,我们将当前Node的next属性指向通报进来的新结点,如许,我们就把新Node插进去到了链表的尾部。
假如当前Node的next字段不是null,申明当前node不是链表中的末了一个node。由于next字段的范例也是node,所以我们挪用next字段的Append要领(注:递归挪用),再一次通报Node参数,如许继续下去,直到找到末了一个Node为止。
public void Append(Node newNode){ if ( this.next == null ){ this.next = newNode; }else{ next.Append(newNode); } }
Node 类中的 ToString() 要领也被掩盖了,用于输出 data 中的值,而且挪用下一个 Node 的 ToString()要领(译注:再一次递归挪用)。
public override string ToString(){ string output = data.ToString(); if ( next != null ){ output += ", " + next.ToString(); } return output; }
如许,当你挪用第一个Node的ToString()要领时,将打印出一切链表上Node的值。
LinkedList 类自身只包含对一个Node的援用,这个Node称作HeadNode,是链表中的第一个Node,初始化为null。
public class LinkedList{ Node headNode = null; }
LinkedList 类不须要组织函数(运用编译器建立的默许组织函数),然则我们须要建立一个大众要领,Add(),这个要领把 data存储到线性链表中。这个要领起首搜检headNode是不是是null,假如是,它将运用data建立结点,并将这个结点作为headNode,假如不是null,它将建立一个新的包含data的结点,并挪用headNode的Append要领,以下面的代码所示:
public void Add(Object data){ if ( headNode == null ){ headNode = new Node(data); }else{ headNode.Append(new Node(data)); } }
为了供应一点鸠合的觉得,我们为线性链表建立一个索引器。
public object this[ int index ]{ get{ int ctr = 0; Node node = headNode; while ( node != null &&ctr <= index ){ if ( ctr == index ){ return node.Data; }else{ node = node.Next; } ctr++; } return null; } }
末了,ToString()要领再一次被掩盖,用以挪用headNode的ToString()要领。
public override string ToString(){ if ( this.headNode != null ){ return this.headNode.ToString(); }else{ return string.Empty; } }
测试线性链表
我们能够增加一些整型值到链表中举行测试:
public void Run(){ LinkedList ll = new LinkedList(); for ( int i = 0; i < 10; i ++ ){ ll.Add(i); } Console.WriteLine(ll); Console.WriteLine(" Done.Adding employees..."); }
假如你对这段代码举行测试,它会如估计的那样事情:
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 Done. Adding employees...
但是,由于这是一个Object范例的鸠合,所以你一样能够将Employee范例增加到鸠合中。
ll.Add(new Employee("John")); ll.Add(new Employee("Paul")); ll.Add(new Employee("George")); ll.Add(new Employee("Ringo")); Console.WriteLine(ll); Console.WriteLine(" Done.");
输出的效果证明了,整型值和Employee范例都被存储在了同一个鸠合中。
0, 1, 2, 3, 4, 5, 6, 7, 8, 9 Done. Adding employees... 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, John, Paul, George, Ringo Done.
虽然看上去如许很轻易,然则负面影响是,你失去了一切范例平安的特征。由于线性链表须要的是一个Object范例,每一个增加到鸠合中的整型值都被隐式装箱了,犹如 IL 代码所示:
IL_000c: box [mscorlib]System.Int32 IL_0011: callvirt instance void ObjectLinkedList.LinkedList::Add(object)
一样,假如上面所说,当你从你的列表中掏出项目标时刻,这些整型必需被显式地拆箱(强迫转换成整型),Employee范例必需被强迫转换成Employee范例。
Console.WriteLine("The fourth integer is " +Convert.ToInt32(ll[3])); Employee d = (Employee) ll[11]; Console.WriteLine("The second Employee is " + d);
这些题目标处理方案是建立一个范例平安的鸠合。一个 Employee 线性链表将不能接收 Object 范例;它只接收 Employee类的实例(或许继续自Employee类的实例)。如许将会是范例平安的,而且不再须要范例转换。一个整型的 线性链表,这个链表将不再须要装箱和拆箱的操纵(由于它只能接收整型值)。
作为示例,你将建立一个 EmployeeNode,该结点晓得它的data的范例是Employee。
public class EmployeeNode { Employee employeedata; EmployeeNode employeeNext; }
Append 要领如今接收一个 EmployeeNode 范例的参数。你一样须要建立一个新的EmployeeLinkedList ,这个链表接收一个新的 EmployeeNode:
public class EmployeeLinkedList{ EmployeeNode headNode = null; }
EmployeeLinkedList.Add()要领不再接收一个 Object,而是接收一个Employee:
public void Add(Employee data){ if ( headNode == null ){ headNode = new EmployeeNode(data);} else{ headNode.Append(new EmployeeNode(data)); } }
相似的,索引器必需被修改成接收 EmployeeNode 范例,等等。如许确切处理了装箱、拆箱的题目,而且加入了范例平安的特征。你如今能够增加Employee(但不是整型)到你新的线性链表中了,而且当你从中掏出Employee的时刻,不再须要范例转换了。
EmployeeLinkedList employees = new EmployeeLinkedList(); employees.Add(new Employee("Stephen King")); employees.Add(new Employee("James Joyce")); employees.Add(new Employee("William Faulkner")); /* employees.Add(5); // try toadd an integer - won't compile */ Console.WriteLine(employees); Employee e = employees[1]; Console.WriteLine("The second Employee is " + e);
如许多好啊,当有一个整型试图隐式地转换到Employee范例时,代码以至连编译器都不能经由过程!
但它不好的处所是:每次你须要建立一个范例平安的列表时,你都须要做许多的复制/粘贴 。一点也不够好,一点也没有代码重用。同时,假如你是这个类的作者,你以至不能提早欲知这个链接列表所应当接收的范例是什么,所以,你不得不将增加范例平安这一机制的事情交给类的运用者---你的用户。
运用泛型来到达代码重用
处理方案,犹如你所猜测的那样,就是运用泛型。经由过程泛型,你从新获得了链接列表的 代码通用(关于一切范例只用完成一次),而当你初始化链表的时刻你通知链表所能接收的范例。这个完成是异常简朴的,让我们从新回到Node类:
public class Node{ Object data; ...
注重到 data 的范例是Object,(在EmployeeNode中,它是Employee)。我们将把它变成一个泛型(一般,由一个大写的T代表)。我们一样定义Node类,示意它能够被泛型化,以接收一个T范例。
public class Node <T>{ T data; ...
读作:T范例的Node。T代表了当Node被初始化时,Node所接收的范例。T能够是Object,也多是整型或许是Employee。这个在Node被初始化的时刻才肯定。
注重:运用T作为标识只是一种约定俗成,你能够运用其他的字母组合来替代,比方如许:
public class Node <UnknownType>{ UnknownType data; ...
经由过程运用T作为未知范例,next字段(下一个结点的援用)必需被声明为T范例的Node(意义是说接收一个T范例的泛型化Node)。
Node<T> next;
组织函数接收一个T范例的简朴参数:
public Node(T data) { this.data = data; this.next = null; }
Node 类的其余部分是很简朴的,一切你须要运用Object的处所,你如今都须要运用T。LinkedList类如今接收一个 T范例的Node,而不是一个简朴的Node作为头结点。
public class LinkedList<T>{ Node<T> headNode = null;
再来一遍,转换是很直白的。任何处所你须要运用Object的,如今改做T,任何须要运用Node的处所,如今改做Node<T>。下面的代码初始化了两个链接表。一个是整型的。
LinkedList<int> ll = new LinkedList<int>();
另一个是Employee范例的:
LinkedList<Employee> employees = new LinkedList<Employee>();
剩下的代码与第一个版本没有区分,除了没有装箱、拆箱,而且也不可能将毛病的范例保存到鸠合中。
LinkedList<int> ll = new LinkedList<int>(); for ( int i = 0; i < 10; i ++ ) { ll.Add(i); } Console.WriteLine(ll); Console.WriteLine(" Done."); LinkedList<Employee> employees = new LinkedList<Employee>(); employees.Add(new Employee("John")); employees.Add(new Employee("Paul")); employees.Add(new Employee("George")); employees.Add(new Employee("Ringo")); Console.WriteLine(employees); Console.WriteLine(" Done."); Console.WriteLine("The fourth integer is " + ll[3]); Employee d = employees[1]; Console.WriteLine("The second Employee is " + d);
泛型许可你不必复制/粘贴冗杂的代码就完成范例平安的鸠合。而且,由于泛型是在运行时才被扩大成特别范例。Just In Time编译器能够在差别的实例之间同享代码,末了,它显著地减少了你须要编写的代码。
以上就是C#明白泛型的内容,更多相关内容请关注ki4网(www.ki4.cn)!