自定义集合的乐趣!






4.91/5 (71投票s)
从IEnumerable(T)到IDictionary(T)及中间的一切,创建自定义集合!
引言
.NET Framework自.NET 1.0起就提供了不同类型的集合(位于System.Collections
命名空间)。随着.NET 2.0中引入泛型,Microsoft也包含了几乎所有.NET集合类型的泛型版本(包含在System.Collections.Generic
命名空间中)。集合是.NET Framework的一项重要功能。它如此重要,以至于每个.NET版本都至少包含一对新的(泛型)集合类型。是的,每个.NET开发人员迟早都要使用某种集合。然而,并非每个人都知道如何处理这些集合(尤其是泛型集合)。数组
(最终也是集合)的出现比List<T>
(VB中为List(Of T)
)早得多,许多程序员仍然偏爱数组
而不是.NET集合。然而,数组
有其局限性,在许多情况下,它们就是无法完成List<T>
能做到的事情。如果您还不熟悉List<T>
的基础知识,请查看C#解决方案中的ListTUsage项目,或VB解决方案中的ListOfTUsage项目。请注意,如果您想运行此示例,则需要手动将其设置为启动项目。另外,如果您需要,请不要忘记将启动项目设置回FunWithCollectionsCSharp或FunWithCollectionsVB。
在本文中,我想探讨.NET Framework中一些基本且最常用的集合类型。我假设本文的读者至少在.NET中接触过某种集合,并且对如何、为何以及何时使用接口有一定的了解。我将解释它们是如何工作的以及如何创建自己的集合。这些包括IEnumerable<T>
、ICollection<T>
、IList<T>
和IDictionary<TKey, TValue>
(VB中用(Of T)
替换<T>
)。您可能想知道<T>
和(Of T)
代表什么?这被称为泛型。泛型是使用任何类型创建类型安全类的方法。在没有泛型之前,您会使用Object
。然而,Object
不是类型安全的,并且需要装箱/拆箱和/或转换为特定类型。泛型解决了这些问题。例如,一个集合可以是String
、Integer
、Form
...的集合,您自己命名。<T>
或(Of T)
语法允许任何类型。您可以创建一个List<Form>
或List(Of Form)
。这意味着您只能将Form
放入列表中。这也意味着如果您从列表中获取一项,编译器将知道它是一个Form
,无需转换。不用说,如果泛型类型集合可用,(几乎)总是最好使用它们。话虽如此,IEnumerable
(每个.NET集合的基础)将与IEnumerable<Object>
(或VB中的(Of Object)
)相当。我说“将是”,因为它们不是。泛型IEnumerable<T>
具有其他优势,正如我们将在本文后面看到的。
也许您想知道我为什么还要写一篇关于集合的文章。CP上有很多文章,MSDN上也有相当多的相关文档。我认为MSDN上的文档足以帮助我创建自己的自定义集合,但不足以让我深入理解.NET中集合的内部机制。在我以MSDN为例创建了第一个(非常简单的)自定义集合后,我被IList<T>
所实现的众多方法吓退了。我也在CP上查找了一些解释,但找到的文章评分不高,年代久远,让我怀疑它们是否值得阅读,或者只是没有涵盖我想要的所有内容(尽管我也找到了一些非常好的!)。在对.NET中的集合进行了一些研究之后,我觉得它一点也不吓人或困难了。我希望本文能帮助您理解**和**创建任何类型的集合。
我提供了两个示例项目。一个是VB(因为我日常生活中是VB程序员),另一个是C#(因为我想学习,而且我知道C#在CP用户中更受欢迎)。这两个项目(希望)功能完全相同,所以如果您懂两种语言,可以选择任何一个。我使用了.NET 4版本的Framework。文章的基础可以追溯到.NET 2.0版本,但我将使用一些来自更新版本的LINQ。如前所述,我将解释.NET中的集合是如何工作的以及如何从头开始创建自己的集合。我将一步一步来,所以希望它易于理解。对于用户交互(和我的自定义集合的测试),我创建了一个WinForms用户界面。我认为一切都相当自明,但我会尽量引导您完成代码。大部分解释是用VB和C#进行的。我还会看一些两者之间的区别。尽情享用!
"一件是小摆设,两件是偶然,三件就是一件有灵魂的收藏品。" - Vicente Wolf
为什么构建自定义集合?
说真的,你为什么要构建自己的集合?List<T>
、Dictionary<Tkey, TValue>
、ReadOnlyCollection<T>
、ObservableCollection<T>
等类,真的比你想要的拥有更多的功能!虽然.NET确实有一些非常先进的集合类型,几乎可以用于任何情况,但总有一些情况需要自定义集合。比如,假设您有一个Person
类。现在您想创建一个People
类。简单地继承List<Person>
就可以解决问题。但说实话。虽然List<T>
是一个很棒的类,它曾多次帮助过我,但它并不是为定制而设计的。它没有可重写成员这一点就说明了很多问题。所以,比如,您想要一个人的列表,但只允许年龄大于18岁的人进入列表。您将如何解决这个问题?List<T>
不在乎一个人的年龄是多少才能添加到列表中。您可以在将他们添加到列表之前检查一个人的年龄是否至少为18岁,但下一个开发人员不会知道您的意图,只会创建一个新的Person
类,并将一个孩子放入您的成人列表中!您可以创建一个继承自Person
的Adult
类,但您真的不想那样做。面对现实吧,您唯一的选择就是构建一个自定义集合。幸运的是,一旦您掌握了窍门,这很容易。在我告诉您需要实现哪些接口之前,您应该知道这一点……到目前为止,您将要创建的大部分自定义集合都只是对现有的List<T>
类进行封装!哇!这当然是个好消息。Microsoft在创建List<T>
和其他集合类型时已经为我们完成了所有艰苦的工作。让我们感激地使用它们!
精美的集合由这些组成!
IEnumerable
如果您正在阅读本文,您无疑已经使用过某种集合。无论是Array
、List<T>
,甚至是Dictionary<TKey, TValue>
(VB中为(Of TKey, TValue)
)。毫无疑问,在您的代码中的某个时候,您已经使用过foreach
(VB中为For Each
)关键字来迭代集合中的项目。您是否曾想过为什么您可以在Array
或List
等集合上使用foreach
关键字?这是因为.NET中的每个集合类型最终都是IEnumerable
(非泛型)。自己试试。创建一个新类并实现IEnumerable
。不必为它提供的方法编写实际代码(您将不运行此示例)。现在创建一个您刚创建的类的实例,并在您的实例上使用foreach
(VB中为For Each
)。是的,它实际上是有效的!
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
namespace MyTest
{
public class Test
{
public Test()
{
TestEnumerator te = new TestEnumerator();
foreach (var o in te)
{
// Do stuff.
}
}
}
public class TestEnumerator : System.Collections.IEnumerable
{
public System.Collections.IEnumerator GetEnumerator()
{
throw new NotImplementedException(
"No need to implement this for our example.");
}
}
}
Public Class Test
Public Sub New()
Dim te As New TestEnumerator
For Each o In te
' Do stuff.
Next
End Sub
End Class
Public Class TestEnumerator
Implements IEnumerable
Public Function GetEnumerator() As System.Collections.IEnumerator _
Implements System.Collections.IEnumerable.GetEnumerator
Throw New NotImplementedException(_
"No need to implement this for our example.")
End Function
End Class
如您所见,IEnumerable
是.NET中每个集合的关键。它为集合中非常重要的foreach
关键字提供了支持。然而,IEnumerable
和IEnumerable<T>
在可用性方面提供的很少。它不支持从集合中添加或删除。它实际上只是用于迭代集合的手段。它通过调用接口提供的唯一函数来实现:GetEnumerator
或GetEnumerator<T>
函数,它返回一个IEnumerator
或IEnumerator<T>
。返回的IEnumerator
负责遍历Object
集合并返回该索引处的项目。幸运的是,这并不难,正如您将在本文后面看到的。 .NET Framework不包含可以使用的泛型Enumerable
或Enumerator
基类。您必须自己实现它。当然,您可以创建自己的基类或泛型Enumerator
,我稍后也会在本文中向您展示。
当您完成我刚才给您的小练习后,您可能会注意到,当您在项目中键入“te
”时,IntelliSense会为您提供一些不继承自Object
或由IEnumerable
提供的其他方法。这里有一个好消息!Microsoft为IEnumerable
及其派生类创建了许多扩展方法。这正是IEnumerable
和IEnumerable<Object>
之间不同的地方。在上面的示例中,尝试使您的TestEnumerator
实现System.Collections.Generic.IEnumerable<int>
或System.Collections.Generic.IEnumerable(Of Integer)
。
public class TestEnumerator : System.Collections.Generic.IEnumerable<int>
{
public System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
System.Collections.Generic.IEnumerator<int> GetEnumerator()
{
throw new NotImplementedException(
"No need to implement this for our example.");
}
}
Public Class TestEnumerator _
Implements IEnumerable(Of Integer)
Public Function GetEnumerator() As _
System.Collections.Generic.IEnumerator(Of Integer) _
Implements System.Collections.Generic.IEnumerable(Of Integer).GetEnumerator
Throw New NotImplementedException(_
"No need to implement this for our example.")
End Function
Private Function GetEnumerator1() As System.Collections.IEnumerator _
Implements System.Collections.IEnumerable.GetEnumerator
Return GetEnumerator()
End Function
End Class
现在您应该注意到几件事。在C#中,我显式实现了非泛型GetEnumerator
。在VB中,我将从非泛型IEnumerable
实现的GetEnumerator
重命名为GetEnumerator1
(您可以将其命名为任何您喜欢的名称),并将其设为Private
。这意味着它不会暴露给使用TestEnumerator
的其他类(除非您将其转换为IEnumerable
)。此外,此非泛型GetEnumerator
现在调用由泛型IEnumerable(Of T)
提供的泛型GetEnumerator
。我这样做是为了只编写一个GetEnumerator
的代码,如果它发生变化,非泛型GetEnumerator
将自动调用更改后的代码。另一个有趣的改变是在使用TestEnumerator
的Test
类中。如果您在将IEnumerable
更改为IEnumerable(Of Integer)
之前,将鼠标悬停在C#中的var
或VB中的o
上,在以下代码行中
foreach(var o in te)
For Each o In te
您可以看到var
或o
是Object
。如果您现在将其悬停在上面,编译器将知道var
或o
在C#中是int
,在VB中是Integer
,因为在C#中te
是IEnumerable<int>
,在VB中是IEnumerable(Of Integer)
。泛型万岁!那么,是什么使IEnumerable
与IEnumerable<Object>
如此不同呢?对我来说未知的原因是,只有当您显式提及变量的IEnumerable
类型时,它才会变得清晰。
System.Collections.IEnumerable te = new TestEnumerable();
// te. returns few Extension Methods.
IEnumerable<int> te = new TestEnumerable();
// te. returns lots of Extension Methods!
Dim te As IEnumerable = New TestEnumerable
' te. returns few Extension Methods.
Dim te As IEnumerable(Of Integer) = New TestEnumerable
' te. returns lots of Extension Methods!
如果您在C#中看不到任何扩展方法,请确保您在文档的顶部有using System.Linq
。在VB中,如果您没有修改默认的项目引用,您应该默认看到扩展方法。也许现在您知道,例如List<T>
提供的许多方法都是扩展方法,您不必太担心创建自己的集合类型。毕竟,大部分功能已经为您实现了!
总而言之,IEnumerable
是.NET中所有集合类型的基接口。它通过使用GetEnumerator
函数提供迭代集合(使用foreach
或For Each
)的功能。IEnumerable<T>
是IEnumerable
的泛型版本(并继承自它)。此时,我应该为我糟糕的复制/粘贴/绘画技能道歉……
IEnumerator
现在您已经大致了解了集合是如何以及为什么可以迭代的,让我们仔细看看这个极其重要的IEnumerator
。IEnumerable
调用GetEnumerator
,它返回一个IEnumerator
,这个IEnumerator
负责遍历集合并返回当前索引处的项目。实际上,看看IEnumerator
提供的方法,这似乎是很有道理的。
如果您对IEnumerable
使用foreach
(VB中为For Each
)关键字,则会调用GetEnumerator
。返回的枚举器调用MoveNext
方法,该方法应递增一个表示索引值的Integer
,并返回一个Boolean
,指定是否可以再次调用MoveNext
(只要您没有达到集合的最后一项,就可以)。之后,调用Current
属性,该属性应返回集合当前索引处的项目。在此处需要知道的是,如果您使用foreach
,它将为第一项调用MoveNext
,为下一项调用两次,为下一项调用三次,依此类推。这意味着IEnumerable
不必“记住”IEnumerator
的索引。它甚至根本不必记住它的枚举器!相反,每次调用GetEnumerator
时,您可以返回一个新的枚举器。这也意味着Reset
方法永远不应被调用,因为每次获取枚举器时,它都会返回枚举器的新实例。实际上,Reset
方法用于与COM的兼容性,如果您打算不使用或不支持COM,则应抛出NotImplementedException
。所以让我们看看如何在最简单的形式中实现所有这些。在示例代码中,我包含了一个名为Alphabet的项目。在此项目中,您会找到两个类。一个是IEnumerable<String>
,另一个是IEnumerator<String>
。为了可读性,我省略了注释。
using System;
using System.Collections.Generic;
using System.Linq;
namespace Alphabet
{
public class WesternAlphabet : IEnumerable<String>
{
IEnumerable<String> _alphabet;
public WesternAlphabet()
{
_alphabet = new string[] { "A", "B", "C",
"D", "E", "F", "G",
"H", "I", "J", "K",
"L", "M", "N", "O",
"P", "Q", "R", "S",
"T", "U", "V", "W",
"X", "Y", "Z" };
}
public System.Collections.Generic.IEnumerator<String> GetEnumerator()
{
return new WesternAlphabetEnumerator(_alphabet);
}
System.Collections.IEnumerator
System.Collections.IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
internal class WesternAlphabetEnumerator : IEnumerator<String>
{
private IEnumerable<String> _alphabet;
private int _position;
private int _max;
public WesternAlphabetEnumerator(IEnumerable<String> alphabet)
{
_alphabet = alphabet;
_position = -1;
_max = _alphabet.Count() - 1;
}
public string Current
{
get { return _alphabet.ElementAt(_position); }
}
object System.Collections.IEnumerator.Current
{
get { return this.Current; }
}
public bool MoveNext()
{
if (_position < _max)
{
_position += 1;
return true;
}
return false;
}
void System.Collections.IEnumerator.Reset()
{
throw new NotImplementedException();
}
public void Dispose() { }
}
}
Public Class WesternAlphabet
Implements IEnumerable(Of String)
Private _alphabet As IEnumerable(Of String)
Public Sub New()
_alphabet = {"A", "B", "C", "D", "E", _
"F", "G", "H", "I", "J", _
"K", "L", "M", "N", "O", _
"P", "Q", "R", "S", "T", _
"U", "V", "W", "X", "Y", "Z"}
End Sub
Public Function GetEnumerator() As _
System.Collections.Generic.IEnumerator(Of String) _
Implements System.Collections.Generic.IEnumerable(Of String).GetEnumerator
Return New AlphabetEnumerator({})
End Function
Private Function GetEnumeratorNonGeneric() As _
System.Collections.IEnumerator Implements _
System.Collections.IEnumerable.GetEnumerator
Return GetEnumerator()
End Function
End Class
Friend Class AlphabetEnumerator
Implements IEnumerator(Of String)
Private _alphabet As IEnumerable(Of String)
Private _position As Integer
Private _max As Integer
Public Sub New(ByVal alphabet As IEnumerable(Of String))
_alphabet = alphabet
_position = -1
_max = _alphabet.Count - 1
End Sub
Public ReadOnly Property Current As String Implements _
System.Collections.Generic.IEnumerator(Of String).Current
Get
Return _alphabet(_position)
End Get
End Property
Private ReadOnly Property Current1 As Object _
Implements System.Collections.IEnumerator.Current
Get
Return Me.Current
End Get
End Property
Public Function MoveNext() As Boolean Implements _
System.Collections.IEnumerator.MoveNext
If _position < _max Then
_position += 1
Return True
End If
Return False
End Function
Private Sub Reset() Implements System.Collections.IEnumerator.Reset
Throw New NotImplementedException
End Sub
Public Sub Dispose() Implements IDisposable.Dispose
End Sub
End Class
现在我听到您在想,那个Dispose
方法是从哪里来的?我还没有讨论过这个,但这是IEnumerator
和IEnumerator<T>
之间的区别。IEnumerator<T>
继承自IDisposable
。IDisposable
是一个.NET接口,它支持(正如名称所示)释放资源(这超出了本文的范围)。如果一个类实现了这个接口,您就可以使用using
语句(VB中为Using
)声明它,就像IEnumerator
提供foreach
功能一样。那么为什么IEnumerator<T>
继承自IDisposable
呢?在极少数情况下,枚举器可能枚举文件或数据库记录,因此正确关闭连接或文件锁很重要。Microsoft选择让IEnumerator<T>
继承自IDisposable
,因为它比在foreach
循环外部打开和关闭连接要快一些,并且拥有一个空的Dispose
方法并不是什么大问题。枚举器中的Dispose
方法在循环结束时被调用(这意味着当MoveNext
返回false时)。在大多数情况下,Dispose
方法将是空的。
现在让我们看看frmAlphabet
。这个Form
使用WesternAlphabet
类。当您按下左上角的按钮时,代码将迭代WesternAlphabet
中的项目并将其放入TextBox
。
private void btnGetAlphabet_Click(object sender, EventArgs e)
{
txtAlphabet.Text = String.Empty;
int count = _alphabet.Count();
for (int i = 0; i <= count - 2; i++)
{
txtAlphabet.Text += _alphabet.ElementAt(i) + ", ";
}
// Omit the comma after the last character.
txtAlphabet.Text += _alphabet.ElementAt(count - 1);
}
Private Sub btnGetAlphabet_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnGetAlphabet.Click
txtAlphabet.Text = String.Empty
Dim count As Integer = _alphabet.Count
For i As Integer = 0 To count - 2
txtAlphabet.Text += _alphabet(i) & ", "
Next
' Omit the comma after the last character.
txtAlphabet.Text += _alphabet(count - 1)
End Sub
这是相当常规的东西,对吧?您可能已经每天都在这样做了。但这里到底发生了什么?如您所见,我调用了WesternAlphabet
类的Count
方法。我们没有实现这个,但请记住,这是一个扩展方法。Count
实际上所做的是调用IEnumerable.GetEnumerator
。它使用这个IEnumerator
调用MoveNext
直到它返回false。最后,它在返回它可以调用MoveNext
的次数之前调用IEnumerator.Dispose
。并不完全是您期望的?您可以想象多次调用大集合上的Count
方法会大大减慢速度。这就是为什么我只调用一次并将其结果存储在变量中的原因。虽然性能在这里不是真正的问题,但这是您应该注意的一点。
那么循环中的代码呢?这段代码在C#和VB之间差别很大。这是因为IEnumerable<T>
不是索引化的。在VB中,这无关紧要,您可以使用()
在任何集合上获取指定索引处的项目。然而,在C#中,我们被迫使用ElementAt
扩展方法。非泛型IEnumerable
也没有那个扩展方法,您被迫编写自己的项按索引获取的函数。IList
和IList<T>
是索引化的,我稍后会回到它们。
我将忽略txtIndex.Validating
事件,因为它与本文无关。它只是检查输入是否为有效的Integer
。相关的是txtChar.Click
事件。
private void btnGetChar_Click(object sender, EventArgs e)
{
if (txtIndex.Text != String.Empty)
{
txtChar.Text =
_alphabet.ElementAtOrDefault(Convert.ToInt32(txtIndex.Text) - 1);
}
}
Private Sub btnGetChar_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles btnGetChar.Click
If txtIndex.Text <> String.Empty Then
txtChar.Text = _alphabet(CInt(txtIndex.Text) - 1)
End If
End Sub
有趣的是,集合中某个索引处的项目被获取了。然而,与前面的示例不同,索引可能不存在!对VB来说,这仍然不是问题。它只会获取一个“默认”Object
。对于String
是String.Empty
,对于数值类型是0,等等。然而,我前面使用的C#ElementAt
扩展方法在索引越界时会抛出Exception
。要获得与VB相同的行为,您可以使用ElementAtOrDefault
扩展方法。除此之外,CInt
只是VB中Convert.ToInt32
的简写,它将String
转换为Integer
。我们知道这是可能的,因为我们检查了String.Empty
,如果它非空,则已由txtIndex.Validate
事件验证。
以上是基础知识。到目前为止,我们已经了解了任何集合类型的绝对基础IEnumerable
和IEnumerator
。现在我必须分享一个小秘密。实现我们自己的IEnumerator<T>
的所有工作实际上并非真正必要……正如您所见,WesternAlphabet
类有一个_alphabet
变量,它实际上已经是一个集合类型!我们本可以调用_alphabet.GetEnumerator
(这将返回一个枚举器,其功能与我们的相同,甚至可能更好!)。然而,这不会给我们带来我们刚刚获得的对IEnumerator
的理解。我确实不建议为每个自定义集合构建自己的枚举器。
一群狂热分子……
现在您已经了解了基础知识,是时候创建一个具有添加和删除功能的集合了。当然,您可以实现IEnumerable<T>
并编写自己的Add
和Remove
方法。一个更好的解决方案是实现ICollection<T>
。请注意,我跳过了非泛型ICollection
?这是因为ICollection
不支持添加和删除功能。ICollection
更像是一个线程安全的基类集合(或者,可以说,因为它不支持添加和删除功能,所以它根本算不上一个集合)。为了让事情更加混乱,ICollection<T>
不继承ICollection
(与IEnumerable<T>
和IEnumerator<T>
都继承它们的非泛型祖先不同)。要添加非泛型的添加和删除功能,您需要实现IList
。我们将在本文后面实现IList<T>
。
所以回到ICollection<T>
。在System.Collections.ObjectModel
命名空间中有一个这个接口的实现。Collection<T>
类也实现了IList<T>
,所以它不是一个“ICollection<T>
原型”,我猜。为什么这个Collection<T>
不在System.Collections.Generic
命名空间中?因为在Microsoft.VisualBasic
命名空间中已经有一个名为Collection
的类(C#没有这样的Collection
类)。由于VB项目默认都引用Microsoft.VisualBasic
和System.Collections.Generic
命名空间,Microsoft决定将Collection<T>
放在另一个命名空间以避免混淆。然而,ICollection<T>
在System.Collections.Generic
命名空间中,并且ICollection<T>
拥有构建支持添加和删除项目的自定义集合所需的一切。
如您所见,ICollection<T>
继承自IEnumerable<T>
。除了GetEnumerator
函数之外,ICollection<T>
还需要实现Add
、Remove
、Clear
和Contains
方法。另外请注意,Count
也应该实现,尽管这已经是扩展方法了(稍后您将了解原因)。IsReadOnly
属性用于支持ReadOnly
集合(例如System.Collections.ObjectModel.ReadOnlyCollection<T>
)。最后一个是CopyTo
方法。此方法由LINQ扩展方法(如ToList
)使用。
我以前经常玩角色扮演游戏,所以我创建了一个名为TheCult的项目。邪恶的邪教在RPG游戏中总是很受欢迎,所以我想把它们介绍给.NET。另外,加入邪教的人有些有趣的事情。基本上,我这样看待:一个人进入一个邪教,一个邪教徒就诞生了!出于这个原因,我创建了两个类,Person
和Cultist
。您可以在示例项目中找到它们。Person
类非常直接,所以我在这里不再赘述。Cultist
类继承自Person
。这意味着Cultist
仍然是Person
,但Cultist
定义了一些额外的属性。Cultist
有一种ReadOnly Mark
,证明他属于邪教。在这种情况下,Mark
只是邪教内的唯一Integer
。邪教徒还有一个具有internal
(VB中为Friend
)setter的Rank
。为什么无法在程序集外部设置Mark
和Rank
属性?仅仅是因为只有Cult
类才能决定一个新的邪教成员获得什么唯一的Mark
以及哪个Rank
。您的自定义Person
集合实际上是一个Cultist
工厂!这也意味着,虽然它是一个ICollection<Person>
,但GetEnumerator
函数实际上返回一个包含Cultist
的枚举器。那么这一切是如何工作的呢?让我们快速看一下代码。我已经省略了实现接口所需的所有方法,以及大量的注释。您可以自己研究这些。
public class Cult : ICollection<Person>, IListSource
{
private List<Cultist> _innerList;
public Cult()
{
_innerList = new List<Cultist>();
}
public Cult(IEnumerable<Person> people)
: this()
{
this.AddRange(people);
}
protected List<Cultist> InnerList
{
get { return _innerList; }
}
public void Add(Person item)
{
if (item == null)
throw new ArgumentNullException("Item cannot be nothing.");
// Check if the person is allowed to enter the cult.
if (CanJoin(item))
{
// Create a new cultist with the current person.
Cultist cultist = Cultist.CreateCultist(item, GetMark());
// Add the new recruit to the cult!
this.InnerList.Add(cultist);
// Check how many members the cult currently has and
// set the new recruits rank accordingly.
int count = this.InnerList.Count;
switch (count)
{
case 1:
// If the inner list has no items yet then the cultist that is
// to be added becomes the first cultist and thus gets leader status.
cultist.Rank = CultRanks.Leader;
break;
case 10:
// If the cult has 10 members then it is needed to get a second in command.
// The second person to join the cult becomes general.
this.InnerList[1].Rank = CultRanks.General;
// The to be added cultist becomes a soldier.
cultist.Rank = CultRanks.Soldier;
break;
default:
// Nothing special.
// The to be added cultist becomes a soldier.
cultist.Rank = CultRanks.Soldier;
break;
}
// If there are 20 cult members or any number dividable by 10 after that
// then it is necessary to get a new person that can keep the peace.
AssignNextCaptainIfNecessary();
}
}
public void Clear()
{
// Clears the list of cultists (perhaps the police raided the place?).
this.InnerList.Clear();
}
public bool Contains(Person item)
{
return (this.InnerList.Where(cultist => cultist.FullName ==
item.FullName).FirstOrDefault() != null);
}
public void CopyTo(Person[] array, int arrayIndex)
{
List<Person> tempList = new List<Person>();
tempList.AddRange(this.InnerList);
tempList.CopyTo(array, arrayIndex);
}
public int Count
{
get { return this.InnerList.Count; }
}
bool ICollection<Person>.IsReadOnly
{
get { return false; }
}
public bool Remove(Person item)
{
// First retrieve the cultist based on the persons name.
Cultist cultist = this.InnerList.Where(c =>
c.FullName == item.FullName).FirstOrDefault();
// Check if a cultist with the given name exists.
if (cultist != null)
{
if (this.InnerList.Remove(cultist))
{
// If the cultist was removed the ranks have to be recalculated.
// We do not know who was removed from the cult. Maybe it was the leader
// or general, so we need some promotions.
RecalculateRanksOnRemove(cultist);
cultist.Rank = CultRanks.None;
return true;
}
}
return false;
}
public System.Collections.Generic.IEnumerator<Person> GetEnumerator()
{
return new GenericEnumerator.GenericEnumerator<Cultist>(this.InnerList);
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
bool IListSource.ContainsListCollection
{
get { return true; }
}
System.Collections.IList IListSource.GetList()
{
return this.InnerList;
}
}
Public Class Cult
Implements ICollection(Of Person)
Implements IListSource
Private _innerList As List(Of Cultist)
Public Sub New()
MyBase.New()
_innerList = New List(Of Cultist)
End Sub
Public Sub New(ByVal people As IEnumerable(Of Person))
Me.New()
Me.AddRange(people)
End Sub
Protected ReadOnly Property InnerList As List(Of Cultist)
Get
Return _innerList
End Get
End Property
Public Sub Add(ByVal item As Person) Implements _
System.Collections.Generic.ICollection(Of Person).Add
If item Is Nothing Then
Throw New ArgumentNullException("Item cannot be nothing.")
' Check if the person is allowed to enter the cult.
If CanJoin(item) Then
' Create a new cultist with the current person.
Dim cultist As Cultist = cultist.CreateCultist(item, GetMark)
' Add the new recruit to the cult!
Me.InnerList.Add(cultist)
' Check how many members the cult currently has and
' set the new recruits rank accordingly.
Dim count = Me.InnerList.Count
Select Case count
Case 1
' If the inner list has no items yet then the cultist that is
' to be added becomes the first cultist and thus gets leader status.
cultist.Rank = CultRanks.Leader
Case 10
' If the cult has 10 members then it
' is needed to get a second in command.
' The second person to join the cult becomes general.
Me.InnerList(1).Rank = CultRanks.General
' The to be added cultist becomes a soldier.
cultist.Rank = CultRanks.Soldier
Case Else
' Nothing special.
' The to be added cultist becomes a soldier.
cultist.Rank = CultRanks.Soldier
End Select
' If there are 20 cult members or any number dividable by 10 after that
' then it is necessary to get a new person that can keep the peace.
AssignNextCaptainIfNecessary()
End If
End Sub
Public Sub Clear() Implements System.Collections.Generic.ICollection(Of Person).Clear
' Clears the list of cultists (perhaps the police raided the place?).
Me.InnerList.Clear()
End Sub
Public Function Contains(ByVal item As Person) As Boolean _
Implements System.Collections.Generic.ICollection(Of Person).Contains
Return Not Me.InnerList.Where(Function(cultist) cultist.FullName = _
item.FullName).FirstOrDefault Is Nothing
End Function
Public Sub CopyTo(ByVal array() As Person, ByVal arrayIndex As Integer) _
Implements System.Collections.Generic.ICollection(Of Person).CopyTo
Dim tempList As New List(Of Person)
tempList.AddRange(Me.InnerList)
tempList.CopyTo(array, arrayIndex)
End Sub
Public ReadOnly Property Count As Integer Implements _
System.Collections.Generic.ICollection(Of Person).Count
Get
Return Me.InnerList.Count
End Get
End Property
Private ReadOnly Property IsReadOnly As Boolean Implements _
System.Collections.Generic.ICollection(Of Person).IsReadOnly
Get
Return False
End Get
End Property
Public Function Remove(ByVal item As Person) As Boolean _
Implements System.Collections.Generic.ICollection(Of Person).Remove
' First retrieve the cultist based on the persons name.
Dim cultist = Me.InnerList.Where(Function(c) c.FullName = item.FullName).FirstOrDefault
' Check if a cultist with the given name exists.
If Not cultist Is Nothing Then
If Me.InnerList.Remove(cultist) Then
' If the cultist was removed the ranks have to be recalculated.
' We do not know who was removed from the cult. Maybe it was the leader
' or general, so we need some promotions.
RecalculateRanksOnRemove(cultist)
cultist.Rank = CultRanks.None
Return True
End If
End If
Return False
End Function
Public Function GetEnumerator() As System.Collections.Generic.IEnumerator(Of Person) _
Implements System.Collections.Generic.IEnumerable(Of Person).GetEnumerator
Return New GenericEnumerator.GenericEnumerator(Of Cultist)(Me.InnerList)
End Function
Private Function GetEnumerator1() As System.Collections.IEnumerator _
Implements System.Collections.IEnumerable.GetEnumerator
Return Me.GetEnumerator
End Function
Private ReadOnly Property ContainsListCollection As Boolean _
Implements System.ComponentModel.IListSource.ContainsListCollection
Get
Return True
End Get
End Property
Private Function GetList() As System.Collections.IList _
Implements System.ComponentModel.IListSource.GetList
Return Me.InnerList
End Function
End Class
首先,暂时忽略IListSource
。您可以看到,虽然我实现了ICollection<Person>
,但InnerList
实际上是一个List<Cultist>
。也许您也注意到Cultist
有一个私有构造函数和一个internal static
(VB中为Friend Shared
)CreateCultist
函数,它接受一个Person
作为参数并返回一个Cultist
。这正是我们的Cult
类在使用新的Person
添加到Cult
时使用的函数。外部用户也无法创建自己的Cultist
。一切都通过我们的Cult
类进行。Cult
类负责分配Mark
和Rank
。非常重要的是,这个类基本上只是将一个正常的List<T>
类进行了封装!当添加一个Person
时,我将其添加到InnerList
;当移除一个Person
时,我从InnerList
中移除,等等。
所以我们已经知道了GetEnumerator
函数。让我们看看ICollection<T>
提供的其他方法。在Add
方法中,我首先检查Person
是否允许加入(不允许同名的人进入)。之后,我使用Person
创建一个Cultist
并给他一个Mark
。然后我将新的Cultist
添加到InnerList
。之后,我可以检查有多少人已经加入了Cult
,并检查是否需要设置新的CultRank
。第一个成员始终是Leader
。当第十个成员加入时,第二个加入Cult
的Person
成为General
。之后每十个新邪教徒,下一个Cultist
就成为Captain
。每个新招募的成员自动成为Soldier
。
在Remove
函数中移除Cultist
需要一些额外的工作。Remove
函数接受一个Person
作为参数(毕竟我们实现的是ICollection<Person>
),但我的InnerList
只包含Cultist
。我使用LINQ查询来获取与想要离开的Person
同名的Cultist
(我们的Cult
中名字是唯一的,还记得吗?)。这可能不是最好的方法,但对于这个例子来说足够了。此时,我应该说,.NET中引用类型集合的默认Contains
行为是检查参数是否指向内存中某个元素也指向的位置。然而,这可以被覆盖,正如我们稍后将看到的。然后我从InnerList
中移除Cultist
,将他的Rank
设置为None
,并重新计算Rank
。如果Leader
刚刚被移除,General
会晋升,如果General
被移除或成为Leader
,第一个Captain
会成为General
,并且在必要时会指定一个新的Captain
。跟踪谁在何时获得哪个等级的部分实际上是最难的部分。这也是您可以让它变得尽可能难或易的部分。将项目添加到InnerList
以及从中移除项目实际上很简单,对吧?毕竟,Microsoft已经完成了困难的部分!
Contains
函数也值得一看。再次,我使用LINQ来获取一个名字与传入的Person
相同的Cultist
。如果找到Cultist
,我返回True
。我也可以创建一个新的List<Person>
并将我的InnerList
作为参数传递给构造函数。我可以检查传递给Contains
函数的Person
是否是一个实际指向内存中某个Person
的指针,该Person
也存在于新的临时List
中。我也可以使用InnerList<Person>
,并将Cultist
添加到其中?这样我也可以始终检查InnerList
是否实际包含传递给方法的Person
,而不是按名字检查。然而,我想让它非常清楚,我有一个ICollection<Person>
,它创建Cultist
。
Clear
方法只是清空InnerList
。如果集合是只读的,ReadOnly
应该返回True
。我们的不是,所以我们只返回False
。然后是CopyTo
方法。您可能不会自己实现这个,所以您只需调用InnerList.CopyTo
方法。这个方法用于将一个类型的集合复制到另一个类型(例如,从ICollection<T>
到IList<T>
)。有一些LINQ扩展方法使用这个方法。
Count
是一个有趣的属性。我们已经有了Count
扩展方法,对吧?那么为什么我们必须再次实现它呢?扩展方法需要遍历集合中的每个项目并保持一个计数器。然后它返回它循环了多少次。现在我们有了一个集合,我们可以保留自己的计数器,并在添加Person
时递增它,在移除Person
时递减它。现在当我调用Count
属性时,我将立即得到答案,而无需先可能循环遍历数千个项目。List<T>
已经有了一个像这样的智能机制,所以我真的不必重新发明轮子。只需返回InnerList.Count
。
上面代码中未列出的一个函数,但包含另一个每个程序员都应该知道的重要技巧是CanJoin
。
protected virtual bool CanJoin(Person person)
{
// A person is only able to join if no person
// with the same name is already in the cult.
return !this.Contains(person, new PersonNameEqualityComparer());
}
Protected Overridable Function CanJoin(ByVal person As Person) As Boolean
' A person is only able to join if no person
' with the same name is already in the cult.
Return Not Me.Contains(person, New PersonNameEqualityComparer)
End Function
这个重载的Contains
函数是什么?我们肯定没有实现它!它是IEnumerable<T>
接口上的另一个扩展方法。它接受一个IEqualityComparer<T>
作为参数,其中T
与您的IEnumerable<T>
类型相同。您可以看到PersonNameEqualityComparer
类是如何实现这个的。这个类实际上没有实现IEqualityComparer<T>
接口,但继承自EqualityComparer<T>
,它确实实现了接口。Microsoft建议我们继承基类而不是实现接口(阅读MSDN上的原因)。对我们的类来说,这无关紧要。
class PersonNameEqualityComparer : EqualityComparer<Person>
{
public override bool Equals(Person x, Person y)
{
if (x == null | y == null)
{
return false;
}
return x.FullName == y.FullName;
}
public override int GetHashCode(Person obj)
{
return obj.ToString().GetHashCode();
}
}
Public Class PersonNameEqualityComparer_
Inherits EqualityComparer(Of Person)
Public Overloads Overrides Function Equals(ByVal x As Person, _
ByVal y As Person) As Boolean
If x Is Nothing Or y Is Nothing Then
Return False
End If
Return x.FullName = y.FullName
End Function
Public Overloads Overrides Function _
GetHashCode(ByVal obj As Person) As Integer
Return obj.ToString.GetHashCode
End Function
End Class
这里最重要的是Equals
函数,它接受两个Person
作为参数,并返回一个Boolean
,表示这两个Person
是否相同。如您在代码中看到的,两个人相同当且仅当他们的FullName
属性匹配。我们为什么需要这个?好吧,通常如果您检查列表是否包含引用类型,列表只是检查两个项目是否指向内存中的相同位置。但我不想这样,我想确保两个人相等,即使它们是完全不同的Object
,只要它们名字相同!所以这可以通过Equals
函数来完成。您可以随心所欲地使其复杂化,例如,还可以检查Age
属性。在CanJoin
函数中,我调用了this.Contains
(VB中为Me.Contains
),所以它调用Cult
对象上的扩展方法,而不是InnerList
上的。这样您就可以在CanJoin
函数上设置一个断点,并查看它如何使用GetEnumerator
函数返回的IEnumerator
遍历Cult
中的每个对象(类似于Count
扩展方法),并检查每个项目是否相等。
那么IListSource
接口是怎么回事呢?在frmCult
中,我将Cult
绑定到了DataGridView
。然而,绑定需要一个IList
或IListSource
。ICollection<T>
不继承自IList
,所以我们有点被难住了。我们是否实现IList
并获得几乎所有我们已经实现的方法的泛型和非泛型版本?我们是否实现IList
并失去我们保证的类型安全性?Microsoft确实让我们面临艰难的选择……是的,如果我们确实想保留我们所有的功能,我们应该实现IList
。但我不会这样做。我向您展示了如何实现ICollection<T>
,我将向您展示如何实现IList<T>
。之后,您应该完全能够自己实现IList
。对于这个例子,我将使用一个更快的解决方案,即IListSource
。IListSource
只需要您实现一个属性,该属性应返回True
,以及一个函数。该函数实际上返回一个IList
。幸运的是,我们有那个IList
,因为IList<T>
确实实现了IList
。请注意,您必须直接返回InnerList
。人们可能会Add
或Remove
Cultist
(好吧,如果他们能实例化它们的话),然后造成混乱!请记住,我们的InnerList
对Rank
一无所知!所以这似乎是一个不错、快速的解决方案。在这种情况下,它只是快。唯一真正正确地使您的集合可绑定是实现IList
。请注意,我没有直接将Person
添加到DataGridView
。这实际上使IListSource
完成了它需要做的事情。在网格中显示我们的Cultist
列表。
您可以随意查看我在此文章中省略的代码。您会发现我将一些方法和属性设为了Protected
。这是因为我想在下一个IList<T>
示例中继承此类。不过,通常这样做不是一个好主意。继承您集合的任何人都不一定应该这样做。实际上,任何继承自Cult
的类都注定要失败,因为谁知道何时以及为何调用,例如,AssignNextCaptainIfNecessary
?如果人们真的想扩展Cult
类,他们可以创建自己的ICollection<Person>
并让我们的Cult
作为他们的InnerList
。
frmCult
来查看这一切实际是如何工作的。您可以通过填写名字、姓氏和年龄来添加成员,然后按“添加”按钮。要删除成员,请在DataGridView
中选择一行或多行,然后按“移除”按钮。Form
启动时有49个成员。您添加的第一个成员应该会分配一个新Captain
。尝试移除一个Captain
,看看下一个成员如何成为Captain
。尝试移除十个以上的Soldier
,然后移除一个Captain
。由于当前成员数量不多,因此不会分配新的Captain
。frmCult
中的代码非常直接。它并不使用我们Cult
类中的所有功能,但它很好地展示了Mark
和Rank
是如何工作的(这就是我们最初创建自定义集合的原因)。
从集合到列表
随着我们的Cult
越来越壮大,它变得不仅仅是一群快乐在一起的人。政治开始发挥作用,随之而来的是腐败。所以一个有侄子也想加入Cult
的船长就占了优势,因为他的叔叔实际上是一个高级成员。那么,如果这个新成员不仅仅是排队等候,而是悄悄地溜到船长们中间呢?他很快就会成为船长!由于我们的CultMember
的Rank
由它们在集合中的位置决定,因此我们必须有一种方法将那个侄子放在,比如说,第四个位置。这就是IList<T>
的用武之地。IList<T>
接口继承自ICollection<T>
。这很容易,因为这意味着我们已经讨论了IList<T>
接口的一半以上。那么IList<T>
为ICollection<T>
增加了什么呢?很简单,索引。现在,这有点令人困惑,因为我们已经有了所有这些扩展方法。即使在IEnumerable<T>
中,我们也可以获取特定索引处的项目,例如,我们Cult
中的第n个Person
。然而,IList<T>
接口还允许在特定索引处添加。IList<T>
只向ICollection<T>
接口添加了三个方法和一个属性:Insert
、IndexOf
、RemoveAt
和Item
(这是一个默认属性,稍后会详细介绍)。
我认为方法的名称实际上是自明的,不需要进一步解释。所以让我们看看我在OrganisedCult
中是如何实现它们的。在这个例子中,我省略了CanJoin
方法。在Cult
中,成员可以在没有同名成员加入的情况下加入。在OrganisedCult
中,新成员的年龄至少应为18岁。为了关注IList<T>
的方法,我没有在下面的代码中包含这一点。
public class OrganisedCult : Cult, IList<Person>
{
public OrganisedCult()
: base()
{
}
public OrganisedCult(IEnumerable<Person> people)
: base(people)
{
}
public int IndexOf(Person item)
{
Cultist cultist = base.InnerList.Where(c =>
c.FullName == item.FullName).FirstOrDefault();
return base.InnerList.IndexOf(cultist);
}
public void Insert(int index, Person item)
{
if (item == null)
throw new ArgumentNullException("Item cannot be nothing.");
// First check if the person can join.
if (base.CanJoin(item))
{
// Create a cultist and insert it to the inner list.
Cultist cultist = Cultist.CreateCultist(item, base.GetMark());
base.InnerList.Insert(index, cultist);
if (index < 2)
{
// If the just inserted person has an index lower than
// two (someone must like him or this is a hostile takeover!)
// he is either leader or general.
// Reset the first three ranks in the inner list.
base.InnerList[0].Rank = CultRanks.Leader;
base.InnerList[1].Rank = CultRanks.General;
base.InnerList[2].Rank = CultRanks.Captain;
}
else if (base.InnerList[index + 1] != null &&
base.InnerList[index + 1].Rank == CultRanks.Captain)
{
// If the person above the just inserted person
// is a captain then the just inserted cultist
// automatically becomes a captain too.
cultist.Rank = CultRanks.Captain;
}
else
{
// If none of the above the just
// inserted person simply becomes a soldier.
cultist.Rank = CultRanks.Soldier;
}
base.AssignNextCaptainIfNecessary();
}
}
public Person this[int index]
{
get { return base.InnerList[index]; }
set
{
// Cannot use the MyBase.InnerList.Contains,
// because it contains cultists, not persons.
if (!this.Contains(value) && this.CanJoin(value))
{
// Create a new cultist and replace the old cultist with the new one.
Cultist cultist = Cultist.CreateCultist(value, base.GetMark());
cultist.Rank = base.InnerList[index].Rank;
base.InnerList[index].Rank = CultRanks.None;
base.InnerList[index] = cultist;
}
}
}
public void RemoveAt(int index)
{
// Call the MyBase.Remove with the person
// at the specified index of the inner list.
// The MyBase.Remove handles recalculations for ranks etc.
base.Remove(base.InnerList[index]);
}
}
Public Class OrganisedCult _
Inherits Cult _
Implements IList(Of Person)
Public Sub New()
MyBase.New()
End Sub
Public Sub New(ByVal people As IEnumerable(Of Person))
MyBase.New(people)
End Sub
Public Function IndexOf(ByVal item As Person) As Integer _
Implements System.Collections.Generic.IList(Of Person).IndexOf
Dim cultist = MyBase.InnerList.Where(Function(c) _
c.FullName = item.FullName).FirstOrDefault
Return MyBase.InnerList.IndexOf(cultist)
End Function
Public Sub Insert(ByVal index As Integer, ByVal item As Person) _
Implements System.Collections.Generic.IList(Of Person).Insert
If item Is Nothing Then
Throw New ArgumentNullException("Item cannot be nothing.")
' First check if the person can join.
If MyBase.CanJoin(item) Then
' Create a cultist and insert it to the inner list.
Dim cultist As Cultist = cultist.CreateCultist(item, MyBase.GetMark)
MyBase.InnerList.Insert(index, cultist)
Select Case True
Case index < 2
' If the just inserted person has an index lower
' than two (someone must like him or this is a hostile takeover!)
' he is either leader or general.
' Reset the first three ranks in the inner list.
MyBase.InnerList(0).Rank = CultRanks.Leader
MyBase.InnerList(1).Rank = CultRanks.General
MyBase.InnerList(2).Rank = CultRanks.Captain
Case Not MyBase.InnerList(index + 1) Is Nothing AndAlso _
MyBase.InnerList(index + 1).Rank = CultRanks.Captain
' If the person above the just inserted person
' is a captain then the just inserted cultist
' automatically becomes a captain too.
cultist.Rank = CultRanks.Captain
Case Else
' If none of the above the just
' inserted person simply becomes a soldier.
cultist.Rank = CultRanks.Soldier
End Select
MyBase.AssignNextCaptainIfNecessary()
End If
End Sub
Default Public Property Item(ByVal index As Integer) As Person _
Implements System.Collections.Generic.IList(Of Person).Item
Get
Return MyBase.InnerList(index)
End Get
Set(ByVal value As Person)
' Cannot use the MyBase.InnerList.Contains,
' because it contains cultists, not persons.
If Not Me.Contains(value) AndAlso Me.CanJoin(value) Then
' Create a new cultist and replace the old cultist with the new one.
Dim cultist As Cultist = cultist.CreateCultist(value, MyBase.GetMark)
cultist.Rank = MyBase.InnerList(index).Rank
MyBase.InnerList(index).Rank = CultRanks.None
MyBase.InnerList(index) = cultist
End If
End Set
End Property
Public Sub RemoveAt(ByVal index As Integer) Implements _
System.Collections.Generic.IList(Of Person).RemoveAt
' Call the MyBase.Remove with the person at the specified index of the inner list.
' The MyBase.Remove handles recalculations for ranks etc.
MyBase.Remove(MyBase.InnerList(index))
End Sub
End Class
如您所见,IndexOf
函数只是返回列表中某个项目的索引。所以在这里我做的是在我的InnerList
中找到Person
(请记住,每个名字都是唯一的,所以我们只需要找到名字与Person
相同的Cultist
)。找到Cultist
后,我返回它的0基索引。
InsertAt
方法更复杂。基本上,您看到Person
被插入到Cult
的某个点。接下来发生的是,我检查他的索引并相应地设置他的Rank
。之后,所有新的Captain Ranks
都应重新计算。(如果Cult
有49个成员,并且我们将新成员插入到最后一个船长之后,那么Cult
的Count
将达到50,并会分配一个新的Captain
。这将是新成员。)所有这些都相当复杂。可能甚至过于复杂,但谁说Cult
的事情很容易?
现在,让我们先看看RemoveAt
。这实际上比InsertAt
容易得多。只需查找指定索引处的Person
,然后使用找到的Person
作为参数调用base.Remove
(VB中为MyBase.Remove
)。Cult
集合的Remove
将重新计算Rank
。
现在Item
属性有点奇怪。这是一个默认属性。在C#中,它由“this
”关键字指示。您现在可以在OrganisedCult
类的实例上调用此Property
,而无需显式调用它。我们在VB中已经看到了这一点,因为它是一个让我们使用VB的小礼物。然而,在C#中,我们不得不调用ElementAt
扩展方法。但有了这个默认属性,我们现在也可以在C#中这样做。
OrganisedCult cult = New OrganisedCult();
// Returns the Person at the 5th index.
Person p1 = cult[5];
// Instead of:
Person p2 = cult.ElementAt(5);
关于此属性的另一个相当奇怪之处是,getter需要一个参数。如果您要为此属性分配一个新值,您必须提供此参数。
// 5 is the parameter of the Getter.
cult[5] = new Person("John", "Doe", 18);
' 5 is the parameter of the Getter.
cult(5) = New Person("John", "Doe", 18)
这就完成了IList<T>
接口的实现!并不难,对吧?frmOrganisedCult
调用了OrganisedCult
中的所有新方法。它的工作方式与frmCult
非常相似。您可以在闲暇时查看代码,了解它是如何工作的。我承认,这个Form
中的代码质量和美观度有时不如我(希望)的其他代码。但它只是一个小的Form
来测试OrganisedCult
类。对此来说,它足够了。输入索引时,无论是插入、替换还是删除,请记住索引是0基的。这意味着索引0是Cult
中的第一个项目,Cultist
的数量减1是最后一个索引。
魔法炉
从前,有一个美丽的女孩名叫Elsa。Elsa非常漂亮,以至于王国里的每个男人都向她求婚。除了一个人。Elsa对所有想娶她的男人都不感兴趣。Elsa已经将心给了某人……那个没有向她求婚的人!这个人是英俊的Eatalot王子。Eatalot王子有一个爱好,那就是吃。王子非常爱吃,以至于他停不下来。他吃派、甜点、水果和蔬菜,以及您能想到的任何东西。而他的这个爱好恰恰是Elsa的问题。Elsa不会做饭……一天,Elsa梦见了王子,她决定去上一些烹饪课。她买了一台炉子和一本烹饪书。她去上烹饪课,并尝试烹饪烹饪书中的每一道菜。但无论她如何努力,她总是失败。练习了几周仍然无法煎一个鸡蛋后,Elsa非常沮丧,以至于她把烹饪书扔进了炉子里!“愚蠢的食物,我再也不想做饭了!”她喊道。就在她快要哭出来的时候,发生了令人惊奇的事情……炉子开始呼呼地冒着热气,突然,门打开了,各种食物都飞了出来!当炉子停止移动时,它已经做出了可以献给国王和皇帝的餐点。Elsa惊呆了,目不转睛地盯着炉子,炉子现在看起来就像一个普通的炉子。在她惊奇之后,Elsa赶紧去市场买了一些食材。当她回到家时,她把它们扔进炉子里,炉子又一次烤出了美味的派。然后Elsa抓起炉子去找王子。王子正准备吃晚饭时,Elsa来了。当王子看到Elsa时,他很生气。“你为什么要打断我的晚餐?”他问道。“哦,英俊的Eatalot王子,”Elsa说,“我来问您是否愿意娶我!”王子被Elsa的美貌打动了,但他知道他不能娶任何人。“给我做一顿比我吃过的任何一顿都好的饭,我就娶你,”王子说。Elsa很快匆忙来到厨房,拿到了她能找到的所有食材。她把它们扔进炉子里,炉子又一次烤出了她见过的最美味的食物。Elsa把这顿饭呈献给王子,王子咬了一口,几乎立刻跳了起来喊道:“我的天!这绝对是我吃过的最美味的一顿饭!”于是王子娶了Elsa,他们幸福地生活在一起。完。
使用IDictionary<TKey, TValue>构建炉子
我们不都想要这样的炉子吗?嗯,这是可能的!使用IDictionary<TKey, TValue>
,我们可以制作一个能做到这一点的炉子。顾名思义,Dictionary<TKey, TValue>
是一种集合类型,它可以保存可以通过唯一键值查找的值。IDictionary<TKey, TValue>
继承自ICollection<KeyValuePair<TKey, TValue>>
。这是一个泛型struct
(VB中为Structure
)在一个泛型接口中!所以ICollection<T>
中的<T>
被替换为KeyValuePair<TKey, TValue>
。KeyValuePair
有两个重要的属性,Key
和Value
。知道了这一点,您就可以像这样通过Dictionary<int, String>
迭代项目,这应该不足为奇
foreach (KeyValuePair<int, String> pair in myDictionary)
{
Console.WriteLine(pair.Key.ToString() + " - " + pair.Value);
}
For Each pair As KeyValuePair(Of Integer, String) in myDictionary
Console.WriteLine(pair.Key.ToString() & " - " & pair.Value)
Next
结果可能是这样的
1 - John the 1st Of Nottingham
2 - John Doe
3 - Naerling
4 - How is this for a String?
所以,如果一个Dictionary
有一个Key
和一个Value
,那么它一定与ICollection
和IList
非常不同,在那些我们只有Value
的情况下,对吧?部分正确。让我们先看看IDictionary<TKey, TValue>
的定义。
Add
、Contains
和Remove
方法具有“Dictionary版本”。可以单独添加键和值,因此Dictionary
的用户不必为要添加的每个条目创建一个KeyValuePair
。除此之外,您可能会注意到Value
是通过其键获取或删除的。有一个ContainsKey
函数和一个只接受Key
作为参数的Remove
函数。您可能还注意到默认属性Item
也回来了,并且它接受一个TKey
作为getter的参数(与IList
中的类似,只是它接受的是索引)。这也意味着Dictionary Value
不是通过索引获取的,而是通过Key
获取的。新的是TryGetValue
函数以及Keys
和Values
属性。
那么这如何融入魔法炉的故事呢?嗯,如果您仔细想想,炉子需要一定量的食材。所以我们需要一个Dictionary
,它以食材为Key
,以数量为Value
。现在我不会骗您。以下代码相当高级,我可能让它变得太复杂了,但我会一步一步地引导您。首先,我创建了三个接口。一个定义了食材,一个定义了餐点,一个定义了食谱。食谱是食材和餐点之间的关键联系,我还创建了一个名为BaseRecipe
的基类。接口如下所示:
public interface IIngredient
{
String Name { get; }
}
public interface IMeal
{
String Name { get; }
int Calories { get; }
}
public interface IRecipe
{
String Name { get; }
ReadOnlyIngredients NeededIngredients { get; }
IMeal Cook(Ingredients ingredients);
}
public abstract class BaseRecipe : IRecipe
{
public BaseRecipe()
: base() { }
public abstract string Name { get; }
public abstract ReadOnlyIngredients NeededIngredients { get; }
protected abstract IMeal CookMeal();
public IMeal Cook(Ingredients ingredients)
{
foreach (KeyValuePair<IIngredient, int> pair in NeededIngredients)
{
if (!ingredients.Contains(pair))
{
// If the ingredients are not sufficient throw an Exception.
throw new NotEnoughIngredientsException(this);
}
else
{
// Remove the used ingredients from the ingredients.
ingredients.Add(pair.Key, -pair.Value);
}
}
return CookMeal();
}
}
Public Interface IIngredient
ReadOnly Property Name As String
End Interface
Public Interface IMeal
ReadOnly Property Name As String
ReadOnly Property Calories As Integer
End Interface
Public Interface IRecipe
ReadOnly Property Name As String
ReadOnly Property NeededIngredients As ReadOnlyIngredients
Function Cook(ByVal ingredients As Ingredients) As IMeal
End Interface
Public MustInherit Class BaseRecipe
Implements IRecipe
Public Sub New()
MyBase.New()
End Sub
Public MustOverride ReadOnly Property Name As String Implements IRecipe.Name
Public MustOverride ReadOnly Property NeededIngredients _
As ReadOnlyIngredients Implements IRecipe.NeededIngredients
Protected MustOverride Function CookMeal() As IMeal
Public Function Cook(ByVal ingredients As Ingredients) As IMeal Implements IRecipe.Cook
For Each pair As KeyValuePair(Of IIngredient, Integer) In NeededIngredients
If Not ingredients.Contains(pair) Then
' If the ingredients are not sufficient throw an Exception.
Throw New NotEnoughIngredientsException(Me)
Else
' Remove the used ingredients from the ingredients.
ingredients.Add(pair.Key, -pair.Value)
End If
Next
Return CookMeal()
End Function
End Class
那么我们如何得到这些接口呢?我们通过Stove
类到达那里,该类继承自Ingredients
类。Ingredients
类是我们真正的IDictionary<IIngredient, int>
。由于这是一个相当大的类,我将一步一步地向您解释。让我们先看看IDictionary
接口提供的Add
和Remove
方法。
public virtual void Add(IIngredient key, int value)
{
// Cast to call the explicitly implemented Add Method.
(this as ICollection<KeyValuePair<IIngredient, int>>).Add(
new KeyValuePair<IIngredient, int>(key, value));
}
void ICollection<KeyValuePair<IIngredient, int>>.Add(
System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
// If the key already exists then do not add a new KeyValuePair.
// Add the specified amount to the already existing KeyValuePair instead.
if (this.ContainsKey(item.Key))
{
if (this[item.Key] + item.Value < 0)
{
throw new InvalidOperationException(ErrorMessage);
}
else
{
this[item.Key] += item.Value;
}
}
else
{
if (item.Value < 0)
{
throw new InvalidOperationException(ErrorMessage);
}
else
{
_innerDictionary.Add(item.Key,
new ChangeableDictionaryValue<int>(item.Value));
}
}
}
public virtual bool Remove(IIngredient key)
{
if (this.ContainsKey(key))
{
return _innerDictionary.Remove(GetPairByKeyType(key).Key);
}
else
{
return false;
}
}
bool ICollection<KeyValuePair<IIngredient, int>>.Remove(
System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
this.Add(item.Key, -item.Value);
return true;
}
Public Overridable Sub Add(ByVal key As IIngredient, ByVal value As Integer) _
Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Add
Add(New KeyValuePair(Of IIngredient, Integer)(key, value))
End Sub
Private Sub Add(ByVal item As System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)) _
Implements System.Collections.Generic.ICollection(-
Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Add
' If the key already exists then do not add a new KeyValuePair.
' Add the specified amount to the already existing KeyValuePair instead.
If Me.ContainsKey(item.Key) Then
If Me(item.Key) + item.Value < 0 Then
Throw New InvalidOperationException(ErrorMessage)
Else
Me(item.Key) += item.Value
End If
Else
If item.Value < 0 Then
Throw New InvalidOperationException(ErrorMessage)
Else
_innerDictionary.Add(item.Key, _
New ChangeableDictionaryValue(Of Integer)(item.Value))
End If
End If
End Sub
Public Overridable Function Remove(ByVal key As IIngredient) As Boolean _
Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Remove
If Me.ContainsKey(key) Then
Return _innerDictionary.Remove(GetPairByKeyType(key).Key)
Else
Return False
End If
End Function
Private Function Remove(ByVal item As System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)) _
As Boolean Implements System.Collections.Generic.ICollection(_
Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Remove
Me.Add(item.Key, -item.Value)
Return True
End Function
Add
方法相当直接。IDictionary
添加了一个接受key
和value
作为参数的Add
方法,它只是创建一个KeyValuePair
并将其传递给我们已经从ICollection
知道的Add
方法。现在这个Dictionary
有一个奇怪的小问题。如果Dictionary
已经包含key
,它不会添加新的KeyValuePair
,而是查找key
并将新的KeyValuePair
的value
添加到现有的value
中。正常情况下这会是个问题。KeyValuePair
是一个struct
(VB中为Structure
),是不可变的(key
和value
在设置后不能更改)。然而,我没有改变KeyValuePair
的value
,我改变的是value
的value
!请注意,我将一个ChangeableDictionaryValue<int>
添加到_innerDictionary
而不是一个int
。我这样做是为了能够改变value
!在这个Dictionary
中还需要注意的另一件事是,接受KeyValuePair
作为参数的Remove
函数实际上是将负值添加到指定的key
!
这就是真正的惊喜。这个Dictionary
(与我们的Cult
非常相似)不通过检查项目指针来检查一个项目是否已在Dictionary
中,而是通过检查key
的Type
来检查!所以,如果两个具有相同key
的完全不同的KeyValuePair
被添加到Dictionary
中,那么这意味着一个项目被添加,其值为两个KeyValuePair
的总和。
通过查看Contains
和ContainsKey
函数,可以清楚地了解这一点。
public bool Contains(IIngredient ingredient, int amount)
{
return this.Contains(new KeyValuePair<IIngredient, int>(ingredient, amount));
}
bool ICollection<KeyValuePair<IIngredient, int>>.Contains(
System.Collections.Generic.KeyValuePair<IIngredient, int> item)
{
if (this.ContainsKey(item.Key))
{
return GetPairByKeyType(item.Key).Value.Value >= item.Value;
}
else
{
return false;
}
}
public bool ContainsKey(IIngredient key)
{
KeyValuePair<IIngredient, ChangeableDictionaryValue<int>> pair =
_innerDictionary.Where(p => p.Key.GetType() == key.GetType()).FirstOrDefault();
if (pair.Key == null)
{
return false;
}
else
{
return true;
}
}
Public Function Contains(ByVal ingredient As IIngredient, ByVal amount As Integer) As Boolean
Return Me.Contains(New KeyValuePair(Of IIngredient, Integer)(ingredient, amount))
End Function
Private Function Contains(ByVal item As System.Collections.Generic.KeyValuePair(_
Of IIngredient, Integer)) As Boolean Implements System.Collections.Generic.ICollection(_
Of System.Collections.Generic.KeyValuePair(Of IIngredient, Integer)).Contains
If Me.ContainsKey(item.Key) Then
Return GetPairByKeyType(item.Key).Value.Value >= item.Value
Else
Return False
End If
End Function
Public Function ContainsKey(ByVal key As IIngredient) As Boolean Implements _
System.Collections.Generic.IDictionary(Of IIngredient, Integer).ContainsKey
Dim pair As KeyValuePair(Of IIngredient, ChangeableDictionaryValue(Of Integer)) = _
_innerDictionary.Where(Function(p) p.Key.GetType = key.GetType).FirstOrDefault
If pair.Key Is Nothing Then
Return False
Else
Return True
End If
End Function
如您所见,ContainsKey
检查提供的键的Type
是否与Dictionary
中任何KeyValuePair
的key
相同。GetType
函数要求两个类型完全相同(因此它不会看到基类或派生类)。更多关于这里。您可能已经猜到了,接受KeyValuePair
作为参数的Contains
不检查提供的KeyValuePair
是否指向Dictionary
中某个KeyValuePair
也指向的内存位置,而是检查key
的Type
是否存在于Dictionary
中并且其value
是否大于或等于指定KeyValuePair
的value
。所以它基本上检查Dictionary
中是否存在某种数量的某种配料。
IDictionary
的下一个函数是一个相当直接的函数,TryGetValue
。它所做的是尝试使用给定的key
获取一个value
。value
作为out
参数(VB中为ByRef
)传递,如果未找到key
(而不是引发Exception
!),则返回Nothing
或默认值(对于值类型)。TryGetValue
返回一个Boolean
,指定是否找到了value
。
public bool TryGetValue(IIngredient key, out int value)
{
if (this.ContainsKey(key))
{
value = GetPairByKeyType(key).Value.Value;
return true;
}
else
{
value = default(int);
return false;
}
}
Public Function TryGetValue(ByVal key As IIngredient, ByRef value As Integer) _
As Boolean Implements System.Collections.Generic.IDictionary(_
Of IIngredient, Integer).TryGetValue
If Me.ContainsKey(key) Then
value = GetPairByKeyType(key).Value.Value
Return True
Else
value = Nothing
Return False
End If
End Function
如您所见,如果找到了key
,我们将找到的值赋给value
参数并返回True
(找到了value
)。如果未找到value
,我们使用C#中的default
关键字将默认值赋给value
参数(我们本可以只说默认值是0,但我不知道default
关键字,它看起来很有趣)。对于VB,将其设为Nothing
就足够了(这将为值类型分配默认值)。我们已经在IList<T>
接口中看到了Item
属性,但让我们快速回顾一下IDictionary
。
public virtual int this[IIngredient key]
{
get { return GetPairByKeyType(key).Value.Value; }
set
{
if (value < 0)
{
throw new InvalidOperationException(ErrorMessage);
}
else
{
GetPairByKeyType(key).Value.Value = value;
}
}
}
Default Public Overridable Property Item(ByVal key As IIngredient) _
As Integer Implements _
System.Collections.Generic.IDictionary(Of IIngredient, Integer).Item
Get
Return GetPairByKeyType(key).Value.Value
End Get
Set(ByVal value As Integer)
If value < 0 Then
Throw New InvalidOperationException(ErrorMessage)
Else
GetPairByKeyType(key).Value.Value = value
End If
End Set
End Property
这里没什么惊喜,与IList<T>
唯一的区别是getter不接受索引(Integer
)作为参数,而是接受一个key
。在这种情况下,是一个IIngredient
。然后只有两个属性我们还没有讨论过。Keys
和Values
属性。正如您可能猜到的,它们只是返回一个包含Dictionary
中每个key
或每个value
的集合(实际上是ICollection<T>
)。大多数时候,您可以只返回_innerDictionary.Keys
和_innerDictionary.Values
。在这种情况下,_innerDictionary
的类型与我们接口实现的TValue
的类型不同。所以我们首先需要创建一个新列表并将我们的Integer
值放入其中。我再次使用了这个技巧,用于我们已经在ICollection<T>
中看到的GetEnumerator
和CopyTo
方法。
public System.Collections.Generic.ICollection<IIngredient> Keys
{
get { return _innerDictionary.Keys; }
}
public System.Collections.Generic.ICollection<int> Values
{
get
{
return InnerDictToIntValue().Values;
}
}
private Dictionary<IIngredient, int> InnerDictToIntValue()
{
Dictionary<IIngredient, int> dict = new Dictionary<IIngredient, int>();
// Put all KeyValuePairs of the _innerDictionary
// in the new dictionary using a one-line LINQ query.
_innerDictionary.ToList().ForEach(pair => dict.Add(pair.Key, pair.Value.Value));
return dict;
}
Public ReadOnly Property Keys As System.Collections.Generic.ICollection(Of IIngredient) _
Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Keys
Get
Return _innerDictionary.Keys
End Get
End Property
Public ReadOnly Property Values As System.Collections.Generic.ICollection(Of Integer) _
Implements System.Collections.Generic.IDictionary(Of IIngredient, Integer).Values
Get
Return InnerDictToIntValue.Values
End Get
End Property
Private Function InnerDictToIntValue() As Dictionary(Of IIngredient, Integer)
Dim dict As New Dictionary(Of IIngredient, Integer)
' Put all KeyValuePairs of the _innerDictionary
' in the new dictionary using a one-line LINQ query.
_innerDictionary.ToList.ForEach(Sub(pair) dict.Add(pair.Key, pair.Value.Value))
Return dict
End Function
真正令人兴奋的代码是InnerDictToIntValue
函数。在这个函数中,我调用了ToList
扩展方法。对于Dictionary
,它返回一个IList<KeyValuePair<TKey, TValue>>
)。然后我对返回值调用ForEach
。ForEach
是IList<T>
上的一个扩展方法,接受一个委托作为参数。这超出了本文的范围,我使用了一个匿名方法将我们_innerDictionary
的每个key
和value
放入一个包含Integer
作为值的新Dictionary
中。然后我返回与我们接口实现一致的新Dictionary
。
就是这样!这并不容易,但可能比您想象的要容易得多!所以现在我们有一个Dictionary
可以存储特定数量的食材,但是……关于Stove
呢?注意到我让Dictionary
中的一些方法成为virtual
(VB中为Overridable
)了吗?好吧,看看Stove
类。它继承了我们的Dictionary
并重写了这些方法,并且它不仅将食材添加到其_innerDictionary
中,还检查它是否已经组装了足够的食材来烹饪它所知道的任何餐点。其学习的食谱。
public class Stove : Ingredients
{
public event MealCookedEventHandler MealCooked;
public delegate void MealCookedEventHandler(
object sender, MealCookedEventArgs e);
private List<IRecipe> _recipes;
public Stove()
: base()
{
_recipes = new List<IRecipe>();
}
public void AddRecipes(IEnumerable<IRecipe> recipes)
{
_recipes.AddRange(recipes);
CookMeals();
}
Public void AddRecipes(IRecipe recipe)
{
_recipes.Add(recipe);
CookMeals();
}
private void CookMeals()
{
foreach (IRecipe recipe in _recipes)
{
bool canCook = true;
foreach (KeyValuePair<IIngredient, int> ingredient
in recipe.NeededIngredients)
{
if (!this.Contains(ingredient.Key, ingredient.Value))
{
canCook = false;
}
}
if (canCook)
{
if (MealCooked != null)
{
MealCooked(this, new MealCookedEventArgs(recipe.Cook(this)));
}
CookMeals();
}
}
}
public override void Add(IIngredient key, int value)
{
base.Add(key, value);
// Only cook when ingredients were added!
if (value > 0)
{
CookMeals();
}
}
public override int this[IIngredient key]
{
get { return base[key]; }
set
{
// Check if the current value is smaller than the new value.
// If it is not smaller then ingredients are removed.
bool mustCook = (base[key] < value);
base[key] = value;
// Only cook if ingredients were added!
if (mustCook)
{
CookMeals();
}
}
}
}
Public Class Stove
Inherits Ingredients
Public Event MealCooked(ByVal sender As Object, ByVal e As MealCookedEventArgs)
Private _recipes As List(Of IRecipe)
Public Sub New()
MyBase.New()
_recipes = New List(Of IRecipe)
End Sub
Public Sub AddRecipes(ByVal recipes As IEnumerable(Of IRecipe))
_recipes.AddRange(recipes)
CookMeals()
End Sub
Public Sub AddRecipes(ByVal recipe As IRecipe)
_recipes.Add(recipe)
CookMeals()
End Sub
Private Sub CookMeals()
For Each recipe As IRecipe In _recipes
Dim canCook As Boolean = True
For Each ingredient As KeyValuePair(Of IIngredient, _
Integer) In recipe.NeededIngredients
If Not Me.Contains(ingredient.Key, ingredient.Value) Then
canCook = False
End If
Next
If canCook Then
RaiseEvent MealCooked(Me, New MealCookedEventArgs(recipe.Cook(Me)))
CookMeals()
End If
Next
End Sub
Public Overrides Sub Add(ByVal key As IIngredient, ByVal value As Integer)
MyBase.Add(key, value)
' Only cook when ingredients were added!
If value > 0 Then
CookMeals()
End If
End Sub
Default Public Overrides Property Item(ByVal key As IIngredient) As Integer
Get
Return MyBase.Item(key)
End Get
Set(ByVal value As Integer)
' Check if the current value is smaller than the new value.
' If it is not smaller then ingredients are removed.
Dim mustCook As Boolean = (MyBase.Item(key) < value)
MyBase.Item(key) = value
' Only cook if ingredients were added!
If mustCook Then
CookMeals()
End If
End Set
End Property
End Class
所以随便看看。代码量不大,而且也不难理解。您可以将IRecipe
添加到Stove
中,一旦食材被添加到Stove
中,就会调用CookMeals
。在这个方法中,我迭代IRecipe
并检查Stove
是否拥有足够的食材,方法是将其与IRecipe.NeededIngredients
进行比较。如果有足够的食材,我调用IRecipe.Cook
并将Stove
作为Ingredients
传递。Cook
方法然后会消耗所需的食材(从Stove
的食材计数中减去)并返回烹饪好的IMeal
。BaseRecipe
确保此模式被正确实现。您可能已经注意到NeededIngredients
是一个ReadOnlyIngredients
类。这是一个您无法添加或删除任何Ingredient
的类。这个类继承自System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>
。这个类非常简单。
public class ReadOnlyIngredients :
System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>>
{
public ReadOnlyIngredients(Ingredients ingredients)
: base(ingredients.ToList()) { }
}
Public Class ReadOnlyIngredients _
Inherits ObjectModel.ReadOnlyCollection(Of KeyValuePair(Of IIngredient, Integer))
Public Sub New(ByVal ingredients As Ingredients)
MyBase.New(ingredients.ToList)
End Sub
End Class
我没有讨论ReadOnlyCollection
,但根据它的名字,我认为它们不需要进一步解释。它只是一个不允许添加或删除项目的IList<T>
。
我通过实现IIngredient
创建了许多食材。我还实现了三个IRecipe
和三个相应的IMeal
。您可以自己检查它们,了解Ingredients Dictionary
和ReadOnlyIngredients
集合的用法。有关Stove
类示例和用法的示例,请查看frmMagicStove
。您可以从顶部的ComboBox
中选择任何食材,并指定要添加到Stove
的数量。您将看到食材被添加到Stove
中,并且只要Stove
有足够的食材,它就会烹饪找到的第一个食谱,直到它不再有足够的食材为止,然后它将烹饪另一个餐点或停止烹饪。每次Stove
烹饪一顿饭时,都会引发一个事件。这个事件包含烹饪的IMeal
(这是您做任何事情的唯一机会,用它或失去它!)。我将其添加到IMeal
的List
中,并将其绑定到底部网格,该网格显示所有已烹饪的餐点。
关注点
对我而言,所有这些都非常有趣。事实上,说实话,在开始写这篇文章之前,我从未创建过自定义集合。所以我从中学到了很多。而且我只能说,我对集合总体上非常热情!在撰写本文时,我开始更频繁地使用LINQ,并且发现System.Linq
命名空间本身就包含八个(!)继承自IEnumerable
的接口!其中许多是Framework 4.0的新增功能。集合是处理任何类型数据的强大工具,您会发现它实际上是每个应用程序的基础。本文只讨论了基础知识,甚至不是所有基础知识。例如,我没有讨论IComparable<T>
、IComparer<T>
、Comparer<T>
(都用于排序)和IEquatable<T>
(它相对于IEqualityComparer<T>
的作用就像IComparable<T>
相对于IComparer<T>
的作用一样)。我也没讨论一些更常见的泛型List
,如Queue<T>
和Stack<T>
。不过,这些类在MSDN上都有很好的文档。所以,如果您想了解更多关于.NET集合的信息,我建议您从那里开始。或者在这里CodeProject上,有一些关于各种集合的非常好的文章。此外,如果您是C#程序员,您应该查看yield
关键字,它可以在迭代器中使用。VB中的Yield和Iterator函数在新版Visual Studio ASync CTP (SP1 Refresh)中可用,但我还没有试过。对我学习集合非常有帮助的是浏览System.Collections
命名空间及其子命名空间,并在互联网上查看找到的集合,以及使用Reflector等工具和免费的JustDecompile查看它们的源代码。我也挑战您查看我的示例项目,并思考我可以在哪些方面做得更好(我能想到一些地方)。我希望本文能让您对.NET集合世界有所了解。它们是如何工作的,如何使用它们,以及如何创建它们。我们在本文中创建了一些非常疯狂和愚蠢的集合,我希望它能传播开来,因为它很有趣。这里有一些链接供您进一步探索集合的可能性。
- 迭代器设计模式
- yield(仅限C#)和迭代器
- 使用C# yield提高可读性和性能
- System.Collections vs. System.Collections.Generics and System.Collections.ObjectModel
- 秩序中的混乱:.NET集合
- 您的集合中有什么?第1部分(共3部分):接口
- 您的集合中有什么?第2部分(共3部分):具体类
- 您的集合中有什么?第3部分(共3部分):自定义集合
- .NET 4.0的并发集合
- 用于集合项的泛型比较类
- 增强的 ObservableCollection,支持延迟或禁用通知
- 可观察的字典
祝您编码愉快!:)