Heim >Backend-Entwicklung >C#.Net-Tutorial >C# versteht Generika
Einführung
Eine der am meisten erwarteten (und vielleicht entmutigendsten) Funktionen von Visual C# 2.0 ist die Unterstützung von Generika. In diesem Artikel erfahren Sie, welche Probleme mit Generika gelöst werden, wie Sie damit die Qualität Ihres Codes verbessern und warum Sie vor Generika keine Angst haben müssen.
Was sind Generika?
Viele Menschen finden Generika schwer zu verstehen. Ich glaube, das liegt daran, dass sie oft mit einer Menge Theorie und Beispielen gefüttert werden, bevor sie verstehen, welches Problem durch Generika gelöst wird. Das Ergebnis ist, dass Sie eine Lösung haben, aber kein Problem, das die Lösung erfordert.
Dieser Artikel versucht, diesen Lernprozess zu ändern. Wir beginnen mit einer einfachen Frage: Wofür werden Generika verwendet? Die Antwort lautet: Ohne Generika wäre es schwierig, typsichere Sammlungen zu erstellen.
C# ist eine typsichere Sprache, die es dem Compiler ermöglicht, potenzielle Fehler (zuverlässig) zu erkennen, anstatt sie erst zu entdecken, wenn das Programm ausgeführt wird (was oft passiert, wenn Sie das Produkt verkaufen). . Daher haben in C# alle Variablen einen definierten Typ. Wenn Sie dieser Variablen ein Objekt zuweisen, prüft der Compiler, ob die Zuweisung korrekt ist, und gibt bei einem Problem eine Fehlermeldung aus.
In .Net Version 1.1 (2003) ist diese Typsicherheit gebrochen, wenn Sie Sammlungen verwenden. Alle von der .Net-Klassenbibliothek bereitgestellten Sammlungsklassen werden zum Speichern von Basistypen (Object) verwendet, und alles in .Net wird von der Object-Basisklasse geerbt, sodass alle Typen in eine Sammlungsmitte eingefügt werden können. Daher ist es gleichbedeutend damit, dass es überhaupt keine Typerkennung gibt.
Erschwerend kommt hinzu, dass Sie jedes Mal, wenn Sie ein Objekt aus der Sammlung entfernen, es in den richtigen Typ umwandeln müssen. Diese Konvertierung wirkt sich negativ auf die Leistung aus und erzeugt ausführlichen Code (falls Sie dies vergessen). Wenn Sie die Konvertierung durchführen, wird eine Ausnahme ausgelöst. Wenn Sie der Sammlung außerdem einen Werttyp hinzufügen (z. B. eine Ganzzahlvariable), wird die Ganzzahlvariable implizit geboxt (was wiederum die Leistung verringert), und wenn Sie sie aus der Sammlung entfernen, wird bei ihrer Verwendung ein explizites Unboxing durchgeführt erneut (eine weitere Leistungsreduzierung und Typkonvertierung).
Weitere Informationen zum Ein- und Auspacken finden Sie unter Falle 4. Seien Sie vorsichtig bei implizitem Ein- und Auspacken.
Erstellen Sie eine einfache lineare verknüpfte Liste
Um diesen Problemen ein lebendiges Gefühl zu verleihen, erstellen wir eine möglichst einfache lineare verknüpfte Liste. Für diejenigen unter Ihnen, die dies lesen und noch nie eine linear verknüpfte Liste erstellt haben. Sie können sich eine lineare verknüpfte Liste als eine Kette von Boxen vorstellen (Knoten genannt). Jede Box enthält einige Daten und einen Verweis auf die nächste Box in der Kette (natürlich mit Ausnahme der letzten Box, auf die diese Box verweist). das nächste Feld ist auf NULL gesetzt).
Um unsere einfache lineare verknüpfte Liste zu erstellen, benötigen wir die folgenden drei Klassen:
1. Knotenklasse, die Daten und einen Verweis auf den nächsten Knoten enthält.
2. LinkedList-Klasse, enthält den ersten Knoten in der verknüpften Liste und alle zusätzlichen Informationen über die verknüpfte Liste.
3. Testprogramm, das zum Testen der LinkedList-Klasse verwendet wird.
Um zu sehen, wie die verknüpfte Liste funktioniert, fügen wir der verknüpften Liste zwei Objekttypen hinzu: Ganzzahl- und Mitarbeitertypen. Sie können sich den Typ „Mitarbeiter“ als eine Klasse vorstellen, die alle Informationen über einen Mitarbeiter im Unternehmen enthält. Zu Demonstrationszwecken ist die Employee-Klasse sehr einfach.
public class Employee{ private string name; public Employee (string name){ this.name = name; } public override string ToString(){ return this.name; } }
Diese Klasse enthält nur einen String-Typ, der den Namen des Mitarbeiters darstellt, einen Konstruktor, der den Namen des Mitarbeiters festlegt, und eine ToString()-Methode, die den Namen des Mitarbeiters zurückgibt.
Die verknüpfte Liste selbst besteht aus vielen Knoten, wie oben erwähnt, muss Daten (Ganzzahl und Mitarbeiter) und einen Verweis auf den nächsten Knoten in der verknüpften Liste enthalten.
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; } } }
Beachten Sie, dass der Konstruktor die privaten Datenelemente auf das übergebene Objekt setzt und das nächste Feld auf Null setzt.
Diese Klasse enthält auch eine Methode, Append, die einen Node-Typ-Parameter akzeptiert. Wir fügen den übergebenen Node an der letzten Position in der Liste hinzu. Der Vorgang ist wie folgt: Überprüfen Sie zunächst das nächste Feld des aktuellen Knotens, um festzustellen, ob es null ist. Wenn dies der Fall ist, ist der aktuelle Knoten der letzte Knoten, und wir verweisen mit dem nächsten Attribut des aktuellen Knotens auf den neuen übergebenen Knoten. Auf diese Weise fügen wir den neuen Knoten am Ende der verknüpften Liste ein.
Wenn das nächste Feld des aktuellen Knotens nicht null ist, bedeutet dies, dass der aktuelle Knoten nicht der letzte Knoten in der verknüpften Liste ist. Da der Typ des nächsten Felds ebenfalls „Node“ ist, rufen wir die Append-Methode des nächsten Felds auf (Hinweis: rekursiver Aufruf) und übergeben den Node-Parameter erneut. Dies wird fortgesetzt, bis der letzte Node gefunden wird.
public void Append(Node newNode){ if ( this.next == null ){ this.next = newNode; }else{ next.Append(newNode); } }
Die ToString()-Methode in der Node-Klasse wird ebenfalls überschrieben, um den Wert in Daten auszugeben und die ToString()-Methode des nächsten Node aufzurufen (Anmerkung: ein weiterer rekursiver Aufruf).
public override string ToString(){ string output = data.ToString(); if ( next != null ){ output += ", " + next.ToString(); } return output; }
Auf diese Weise werden beim Aufruf der ToString()-Methode des ersten Knotens die Werte aller Knoten in der verknüpften Liste ausgedruckt.
Die LinkedList-Klasse selbst enthält nur einen Verweis auf einen Knoten. Dieser Knoten heißt HeadNode, ist der erste Knoten in der verknüpften Liste und wird auf Null initialisiert.
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的地方,现在改做Node8742468051c85b06f0a0af9e3e506b5c。下面的代码初始化了两个链接表。一个是整型的。
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#理解泛型的内容,更多相关内容请关注PHP中文网(www.php.cn)!