はじめに
Visual C# 2.0 で最も期待されている (そしておそらく最も困難な) 機能の 1 つは、ジェネリックスのサポートです。この記事では、ジェネリックがどのような種類の問題を解決するために使用されるのか、ジェネリックを使用してコードの品質を向上させる方法、およびジェネリックを恐れる必要がない理由について説明します。
ジェネリックとは何ですか?
多くの人はジェネリック医薬品を理解するのが難しいと感じています。これは、ジェネリックがどのような問題を解決するために使用されるのかを理解する前に、多くの理論と例を与えられることが多いためだと思います。結果として、解決策はありますが、解決策を必要とする問題は発生しません。
この記事では、この学習プロセスを変えることを試みます。まず、ジェネリックは何に使用されますか?という単純な質問から始めます。答えは、ジェネリックがなければ、タイプセーフなコレクションを作成するのが難しいということです。
C# はタイプ セーフな言語です。タイプ セーフにより、コンパイラーはプログラムの実行中に潜在的なエラーを検出するのではなく (確実に) 検出できます (製品の販売後に発生することがよくあります)。したがって、C# では、すべての変数には定義された型があり、その変数にオブジェクトを割り当てると、コンパイラは割り当てが正しいかどうかをチェックし、問題がある場合はエラー メッセージを表示します。
.Net バージョン 1.1 (2003) では、コレクションを使用するとこのタイプ セーフティが壊れます。 .Net クラス ライブラリによって提供されるすべてのコレクション クラスは、基本型 (Object) を格納するために使用され、.Net のすべては Object 基本クラスから継承されるため、すべての型をコレクションの中間に置くことができます。したがって、型検出がまったく行われないことと同じになります。
さらに悪いことに、コレクションからオブジェクトを取り出すたびに、それを正しい型にキャストする必要があり、この変換はパフォーマンスに影響を及ぼし、冗長なコードが生成されます (変換を忘れると例外が発生します)。投げられる)。さらに、値型をコレクション (整数変数など) に追加すると、整数変数は暗黙的にボックス化され (これもパフォーマンスが低下します)、コレクションから削除するときに使用されるときに明示的にボックス化解除が実行されます。再度実行します (別のパフォーマンスの低下と型変換)。
ボックス化とボックス化解除の詳細については、トラップ 4 を参照してください。暗黙的なボックス化とボックス化解除には注意してください。
単純な線形連結リストを作成する
これらの問題を鮮明に感じるために、できるだけ単純な線形連結リストを作成します。これを読んでいる方で、線形リンク リストを作成したことがない人のために説明します。線形リンク リストは、ボックスのチェーン (ノードと呼ばれます) と考えることができます。各ボックスには、いくつかのデータと、チェーン内の次のボックスへの参照が含まれます (もちろん、このボックスの参照は除きます)。次のボックスは NULL に設定されます)。
単純な線形リンク リストを作成するには、次の 3 つのクラスが必要です:
1. データと次のノードへの参照を含む Node クラス。
2. LinkedList クラスには、リンク リストの最初のノードと、リンク リストに関する追加情報が含まれます。
3. LinkedList クラスのテストに使用されるテスト プログラム。
リンク リストがどのように機能するかを確認するために、整数タイプと従業員タイプの 2 つのタイプのオブジェクトをリンク リストに追加します。 Employee タイプは、会社内の従業員に関するすべての情報が含まれるクラスと考えることができます。デモンストレーションの目的で、Employee クラスは非常に単純です。
public class Employee{ private string name; public Employee (string name){ this.name = name; } public override string ToString(){ return this.name; } }
このクラスには、従業員の名前を表す文字列型、従業員の名前を設定するコンストラクター、従業員の名前を返す ToString() メソッドのみが含まれています。
リンク リスト自体は多くのノードで構成されており、前述のように、これらのメモにはデータ (整数と従業員) とリンク リスト内の次のノードへの参照が含まれている必要があります。
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; } } }
コンストラクターはプライベート データ メンバーを渡されたオブジェクトに設定し、次のフィールドを null に設定することに注意してください。
このクラスには、Node タイプのパラメーターを受け入れるメソッド Append も含まれており、渡された Node をリストの最後の位置に追加します。プロセスは次のようになります。まず、現在のノードの次のフィールドをチェックして、それが null かどうかを確認します。そうであれば、現在のノードが最後のノードとなり、現在のノードの次の属性が、渡された新しいノードを指すようになります。このようにして、新しいノードをリンク リストの最後に挿入します。
現在のノードの次のフィールドが null でない場合は、現在のノードがリンク リストの最後のノードではないことを意味します。次のフィールドの型もノードであるため、次のフィールドの Append メソッドを呼び出し (注: 再帰呼び出し)、再度 Node パラメーターを渡します。これは最後のノードが見つかるまで続きます。
public void Append(Node newNode){ if ( this.next == null ){ this.next = newNode; }else{ next.Append(newNode); } }
Node クラスの ToString() メソッドもオーバーライドされ、データ内の値を出力し、次の Node の ToString() メソッドを呼び出すために使用されます (注釈: 別の再帰呼び出し)。
public override string ToString(){ string output = data.ToString(); if ( next != null ){ output += ", " + next.ToString(); } return output; }
このように、最初のNodeのToString()メソッドを呼び出すと、リンクされたリスト上のすべてのNodeの値が出力されます。
LinkedList クラス自体には 1 つのノードへの参照のみが含まれます。このノードは HeadNode と呼ばれ、リンク リストの最初のノードであり、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
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)!