列表的头部。
(第 1 项
元素:student1
下一个------------> ( item2
) 元素:student2
下一个------------> ( item3
) 元素:student3
下一个:空
)
列表的尾部。
首先,为了能够编写 StudentList 类,您需要先编写客户端代码。客户端代码是使用您的学生列表的代码。另外,不要一次只写一件事然后扔掉。而是编写一大堆 [测试] 案例来练习与 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 非常基础。在内部,它跟踪第一个或头节点。显然总是需要跟踪第一个节点。
您可能还想知道为什么在 StudentList 中声明 ListNode。发生的情况是 ListNode 类只能由 StudentList 类访问。这样做是因为 StudentList 不想将细节透露给它的内部实现,因为它控制着对列表的所有访问。StudentList 从不透露列表是如何实现的。实现隐藏是一个重要的面向对象概念。
如果我们确实允许客户端代码直接操作列表,那么将 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;
}
现在,当我们添加时,我们不会迭代。我们只需要跟踪头部和尾部引用。
接下来: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 非常基本。但是代码只做我的第一个单元测试需要的,仅此而已。您可以进行更多优化,并且还有其他构建列表的方法。
展望未来:你需要做的是首先为你的代码需要做的事情创建[单元]测试,然后添加你需要的实现。
* 仅供参考,我还重写了 Student 类。来自 C# 透视的错误命名和奇怪的大小写,更不用说您提供的代码无法编译。我更喜欢_
作为领导者而不是私有成员变量。有些人不喜欢这样,但是你是新手,所以我会把它们留在里面,因为它们很容易被发现。
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; } }
}
- 检查参数
- 注意属性、类和变量的不同大小写。
- 隐藏默认构造函数。为什么我要创建没有真实数据的学生?
- 提供一些只读属性。
- 这个类在书面上是不可变的(即一旦你创建了一个学生,你就不能改变它)。