リストの先頭。
( アイテム1
要素: 学生1
次へ------------> ( item2
) 要素: Student2
次へ------------> ( item3
) 要素: Student3
次へ: null
)
リストの末尾。
まず、StudentList クラスを作成できるようにするには、最初にクライアント コードを作成する必要があります。クライアント コードは、学生リストを使用するコードです。また、一度に1つのことだけを書いて捨てないでください。代わりに、StudentList を操作するために必要なさまざまな方法を実行する [テスト] ケースを大量に作成します。例外的なケースも書きます。しかし、できるという理由だけですべてを行うクラスのスイスアーミーナイフを書きたがる誘惑に駆られないでください。作業を完了するための最小限のコードを記述します。
クラスをどのように使用する必要があるかによって、クラスの構築方法が大きく決まります。これがTDDまたはテスト駆動設計の本質です。
私が見ることができるあなたの最大の問題は、クラスをどのように使用したいのかわからないことです。では、まずそうしましょう。
// create a list of students and print them back out.
StudentList list = new StudentList();
list.Add( new Student("Bob", 1234, 2, 'A') );
list.Add( new Student("Mary", 2345, 4, 'C') );
foreach( Student student in list)
{
Console.WriteLine(student.Name);
}
生徒をリストに追加してから、それらを印刷します。
クライアント コードで StudentList の内部を確認する必要はありません。したがって、StudentList はリンク リストの実装方法を隠します。StudentList の基本を書きましょう。
public class StudentList
{
private ListNode _firstElement; // always need to keep track of the head.
private class ListNode
{
public Student Element { get; set; }
public ListNode Next { get; set; }
}
public void Add(Student student) { /* TODO */ }
}
StudentList はかなり基本的なものです。内部的には、最初のノードまたはヘッド ノードを追跡します。最初のノードを追跡することは、明らかに常に必要です。
また、なぜ ListNode が StudentList の中で宣言されているのか疑問に思うかもしれません。何が起こるかというと、ListNode クラスは StudentList クラスからのみアクセス可能です。これは、リストへのすべてのアクセスを制御しているため、StudentList が内部実装に詳細を提供したくないために行われます。StudentList は、リストがどのように実装されているかを決して明らかにしません。実装の隠蔽は、OO の重要な概念です。
クライアント コードがリストを直接操作できるようにした場合、StudentList を最初に配置しても意味がありません。
Add() 操作を実装してみましょう。
public void Add(Student student)
{
if (student == null)
throw new ArgumentNullException("student");
// create the new element
ListNode insert = new ListNode() { Element = student };
if( _firstElement == null )
{
_firstElement = insert;
return;
}
ListNode current = _firstElement;
while (current.Next != null)
{
current = current.Next;
}
current.Next = insert;
}
Add 操作では、リスト内の最後の項目を見つけて、新しい ListNode を最後に配置する必要があります。しかし、ひどく効率的ではありません。現在は O(N) であり、リストが長くなるにつれて追加が遅くなります。
これを挿入用に少し最適化し、Add メソッドを書き直します。Add を高速化するには、StudentList にリストの最後の要素を追跡させるだけです。
private ListNode _lastElement; // keep track of the last element: Adding is O(1) instead of O(n)
public void Add(Student student)
{
if( student == null )
throw new ArgumentNullException("student");
// create the new element
ListNode insert = new ListNode() { Element = student };
if (_firstElement == null)
{
_firstElement = insert;
_lastElement = insert;
return;
}
// fix up Next reference
ListNode last = _lastElement;
last.Next = insert;
_lastElement = insert;
}
今、追加するとき、反復しません。head と tail の参照を追跡する必要があるだけです。
次は foreach ループです。StudentList はコレクションであり、それを列挙して C# の を使用したいコレクションですforeach
。C# コンパイラは魔法のように繰り返すことはできません。foreach ループを使用するには、記述したコードが明示的に列挙子を使用していないように見える場合でも、使用する列挙子をコンパイラに提供する必要があります。
最初に、リンクされたリストを反復処理する方法をもう一度見てみましょう。
// don't add this to StudentList
void IterateOverList( ListNode current )
{
while (current != null)
{
current = current.Next;
}
}
わかった。それでは、C# の foreach ループにフックして、列挙子を返しましょう。そのために、StudentList を変更して IEnumerable を実装します。これは少し進んでいますが、何が起こっているのかを理解できるはずです。
// StudentList now implements IEnumerable<Student>
public class StudentList : IEnumerable<Student>
{
// previous code omitted
#region IEnumerable<Student> Members
public IEnumerator<Student> GetEnumerator()
{
ListNode current = _firstElement;
while (current != null)
{
yield return current.Element;
current = current.Next;
}
}
#endregion
#region IEnumerable Members
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
#endregion
}
そこにリンクされたリストの反復を見つけることができるはずです。yield
キーワードに惑わされないでください。yield が行っているのは、現在の生徒を foreach ループに戻すことだけです。列挙子は、リンクされたリストの最後に到達すると、学生の返却を停止します。
以上です!コードは思い通りに機能します。
* リストを実装する方法はこれだけではありません。リスト ロジックを StudentList に配置し、ListNode を非常に基本的なものにすることにしました。しかし、コードは最初の単体テストで必要なことだけを行い、それ以上のことはしません。実行できる最適化は他にもあり、リストを作成する方法は他にもあります。
今後: まず、コードに必要な [単体] テストを作成し、次に必要な実装を追加する必要があります。
* fyi Student クラスも書き直しました。あなたが提供したコードがコンパイルされないことは言うまでもありません。_
私はプライベート メンバー変数よりもリーダーとしてを好みます。それを好まない人もいますが、あなたはこれに慣れていないので、簡単に見つけられるのでそのままにしておきます.
public class Student
{
private string _name;
private int _id;
private int _mark;
private char _letterGrade;
private Student() // hide default Constructor
{ }
public Student(string name, int id, int mark, char letterGrade) // Constructor
{
if( string.IsNullOrEmpty(name) )
throw new ArgumentNullException("name");
if( id <= 0 )
throw new ArgumentOutOfRangeException("id");
_name = name;
_id = id;
_mark = mark;
_letterGrade = letterGrade;
}
// read-only properties - compressed to 1 line for SO answer.
public string Name { get { return _name; } }
public int Id { get { return _id; } }
public int Mark { get { return _mark; } }
public char LetterGrade { get { return _letterGrade; } }
}
- パラメータをチェック
- プロパティ、クラス、および変数の大文字と小文字の違いに注意してください。
- デフォルトのコンストラクターを非表示にします。実際のデータなしで学生を作成したいのはなぜですか?
- いくつかの読み取り専用プロパティを提供します。
- このクラスは、書かれているように不変です (つまり、学生を作成すると、それを変更することはできません)。