65.9K
CodeProject 正在变化。 阅读更多。
Home

自定义集合的乐趣!

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (71投票s)

2011年10月11日

CPOL

44分钟阅读

viewsIcon

203009

downloadIcon

2930

从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不是类型安全的,并且需要装箱/拆箱和/或转换为特定类型。泛型解决了这些问题。例如,一个集合可以是StringIntegerForm...的集合,您自己命名。<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类,并将一个孩子放入您的成人列表中!您可以创建一个继承自PersonAdult类,但您真的不想那样做。面对现实吧,您唯一的选择就是构建一个自定义集合。幸运的是,一旦您掌握了窍门,这很容易。在我告诉您需要实现哪些接口之前,您应该知道这一点……到目前为止,您将要创建的大部分自定义集合都只是对现有的List<T>类进行封装!哇!这当然是个好消息。Microsoft在创建List<T>和其他集合类型时已经为我们完成了所有艰苦的工作。让我们感激地使用它们!

精美的集合由这些组成!

IEnumerable

如果您正在阅读本文,您无疑已经使用过某种集合。无论是ArrayList<T>,甚至是Dictionary<TKey, TValue>(VB中为(Of TKey, TValue))。毫无疑问,在您的代码中的某个时候,您已经使用过foreach(VB中为For Each)关键字来迭代集合中的项目。您是否曾想过为什么您可以在ArrayList等集合上使用foreach关键字?这是因为.NET中的每个集合类型最终都是IEnumerable(非泛型)。自己试试。创建一个新类并实现IEnumerable。不必为它提供的方法编写实际代码(您将不运行此示例)。现在创建一个您刚创建的类的实例,并在您的实例上使用foreach(VB中为For Each)。是的,它实际上是有效的!

C#
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.");
        }
    }
}
VB
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关键字提供了支持。然而,IEnumerableIEnumerable<T>在可用性方面提供的很少。它不支持从集合中添加或删除。它实际上只是用于迭代集合的手段。它通过调用接口提供的唯一函数来实现:GetEnumeratorGetEnumerator<T>函数,它返回一个IEnumeratorIEnumerator<T>。返回的IEnumerator负责遍历Object集合并返回该索引处的项目。幸运的是,这并不难,正如您将在本文后面看到的。 .NET Framework不包含可以使用的泛型EnumerableEnumerator基类。您必须自己实现它。当然,您可以创建自己的基类或泛型Enumerator,我稍后也会在本文中向您展示。

当您完成我刚才给您的小练习后,您可能会注意到,当您在项目中键入“te”时,IntelliSense会为您提供一些不继承自Object或由IEnumerable提供的其他方法。这里有一个好消息!Microsoft为IEnumerable及其派生类创建了许多扩展方法。这正是IEnumerableIEnumerable<Object>之间不同的地方。在上面的示例中,尝试使您的TestEnumerator实现System.Collections.Generic.IEnumerable<int>System.Collections.Generic.IEnumerable(Of Integer)

C#
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.");
    }
}
VB
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将自动调用更改后的代码。另一个有趣的改变是在使用TestEnumeratorTest类中。如果您在将IEnumerable更改为IEnumerable(Of Integer)之前,将鼠标悬停在C#中的var或VB中的o上,在以下代码行中

C#
foreach(var o in te)
VB
For Each o In te

您可以看到varoObject。如果您现在将其悬停在上面,编译器将知道varo在C#中是int,在VB中是Integer,因为在C#中teIEnumerable<int>,在VB中是IEnumerable(Of Integer)。泛型万岁!那么,是什么使IEnumerableIEnumerable<Object>如此不同呢?对我来说未知的原因是,只有当您显式提及变量的IEnumerable类型时,它才会变得清晰。

C#
System.Collections.IEnumerable te = new TestEnumerable();
// te. returns few Extension Methods.

IEnumerable<int> te = new TestEnumerable();
// te. returns lots of Extension Methods!
VB
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函数提供迭代集合(使用foreachFor Each)的功能。IEnumerable<T>IEnumerable的泛型版本(并继承自它)。此时,我应该为我糟糕的复制/粘贴/绘画技能道歉……

IEnumerable.png

IEnumerable_Of_T_.png

IEnumerator

现在您已经大致了解了集合是如何以及为什么可以迭代的,让我们仔细看看这个极其重要的IEnumeratorIEnumerable调用GetEnumerator,它返回一个IEnumerator,这个IEnumerator负责遍历集合并返回当前索引处的项目。实际上,看看IEnumerator提供的方法,这似乎是很有道理的。

IEnumerator.png

IEnumerator_Of_T_.png

如果您对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>。为了可读性,我省略了注释。

C#
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() { }
    }
}
VB
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方法是从哪里来的?我还没有讨论过这个,但这是IEnumeratorIEnumerator<T>之间的区别。IEnumerator<T>继承自IDisposableIDisposable是一个.NET接口,它支持(正如名称所示)释放资源(这超出了本文的范围)。如果一个类实现了这个接口,您就可以使用using语句(VB中为Using)声明它,就像IEnumerator提供foreach功能一样。那么为什么IEnumerator<T>继承自IDisposable呢?在极少数情况下,枚举器可能枚举文件或数据库记录,因此正确关闭连接或文件锁很重要。Microsoft选择让IEnumerator<T>继承自IDisposable,因为它比在foreach循环外部打开和关闭连接要快一些,并且拥有一个空的Dispose方法并不是什么大问题。枚举器中的Dispose方法在循环结束时被调用(这意味着当MoveNext返回false时)。在大多数情况下,Dispose方法将是空的。

现在让我们看看frmAlphabet。这个Form使用WesternAlphabet类。当您按下左上角的按钮时,代码将迭代WesternAlphabet中的项目并将其放入TextBox

C#
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);
}
VB
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也没有那个扩展方法,您被迫编写自己的项按索引获取的函数。IListIList<T>是索引化的,我稍后会回到它们。

我将忽略txtIndex.Validating事件,因为它与本文无关。它只是检查输入是否为有效的Integer。相关的是txtChar.Click事件。

C#
private void btnGetChar_Click(object sender, EventArgs e)
{
    if (txtIndex.Text != String.Empty)
    {
        txtChar.Text = 
          _alphabet.ElementAtOrDefault(Convert.ToInt32(txtIndex.Text) - 1);
    }
}
VB
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。对于StringString.Empty,对于数值类型是0,等等。然而,我前面使用的C#ElementAt扩展方法在索引越界时会抛出Exception。要获得与VB相同的行为,您可以使用ElementAtOrDefault扩展方法。除此之外,CInt只是VB中Convert.ToInt32的简写,它将String转换为Integer。我们知道这是可能的,因为我们检查了String.Empty,如果它非空,则已由txtIndex.Validate事件验证。

以上是基础知识。到目前为止,我们已经了解了任何集合类型的绝对基础IEnumerableIEnumerator。现在我必须分享一个小秘密。实现我们自己的IEnumerator<T>的所有工作实际上并非真正必要……正如您所见,WesternAlphabet类有一个_alphabet变量,它实际上已经是一个集合类型!我们本可以调用_alphabet.GetEnumerator(这将返回一个枚举器,其功能与我们的相同,甚至可能更好!)。然而,这不会给我们带来我们刚刚获得的对IEnumerator的理解。我确实不建议为每个自定义集合构建自己的枚举器。

一群狂热分子……

现在您已经了解了基础知识,是时候创建一个具有添加和删除功能的集合了。当然,您可以实现IEnumerable<T>并编写自己的AddRemove方法。一个更好的解决方案是实现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.VisualBasicSystem.Collections.Generic命名空间,Microsoft决定将Collection<T>放在另一个命名空间以避免混淆。然而,ICollection<T>System.Collections.Generic命名空间中,并且ICollection<T>拥有构建支持添加和删除项目的自定义集合所需的一切。

ICollection_Of_T_.png

如您所见,ICollection<T>继承自IEnumerable<T>。除了GetEnumerator函数之外,ICollection<T>还需要实现AddRemoveClearContains方法。另外请注意,Count也应该实现,尽管这已经是扩展方法了(稍后您将了解原因)。IsReadOnly属性用于支持ReadOnly集合(例如System.Collections.ObjectModel.ReadOnlyCollection<T>)。最后一个是CopyTo方法。此方法由LINQ扩展方法(如ToList)使用。

我以前经常玩角色扮演游戏,所以我创建了一个名为TheCult的项目。邪恶的邪教在RPG游戏中总是很受欢迎,所以我想把它们介绍给.NET。另外,加入邪教的人有些有趣的事情。基本上,我这样看待:一个人进入一个邪教,一个邪教徒就诞生了!出于这个原因,我创建了两个类,PersonCultist。您可以在示例项目中找到它们。Person类非常直接,所以我在这里不再赘述。Cultist类继承自Person。这意味着Cultist仍然是Person,但Cultist定义了一些额外的属性。Cultist有一种ReadOnly Mark,证明他属于邪教。在这种情况下,Mark只是邪教内的唯一Integer。邪教徒还有一个具有internal(VB中为Friend)setter的Rank。为什么无法在程序集外部设置MarkRank属性?仅仅是因为只有Cult类才能决定一个新的邪教成员获得什么唯一的Mark以及哪个Rank。您的自定义Person集合实际上是一个Cultist工厂!这也意味着,虽然它是一个ICollection<Person>,但GetEnumerator函数实际上返回一个包含Cultist的枚举器。那么这一切是如何工作的呢?让我们快速看一下代码。我已经省略了实现接口所需的所有方法,以及大量的注释。您可以自己研究这些。

C#
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;
    }

}
VB
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 SharedCreateCultist函数,它接受一个Person作为参数并返回一个Cultist。这正是我们的Cult类在使用新的Person添加到Cult时使用的函数。外部用户也无法创建自己的Cultist。一切都通过我们的Cult类进行。Cult类负责分配MarkRank。非常重要的是,这个类基本上只是将一个正常的List<T>类进行了封装!当添加一个Person时,我将其添加到InnerList;当移除一个Person时,我从InnerList中移除,等等。

所以我们已经知道了GetEnumerator函数。让我们看看ICollection<T>提供的其他方法。在Add方法中,我首先检查Person是否允许加入(不允许同名的人进入)。之后,我使用Person创建一个Cultist并给他一个Mark。然后我将新的Cultist添加到InnerList。之后,我可以检查有多少人已经加入了Cult,并检查是否需要设置新的CultRank。第一个成员始终是Leader。当第十个成员加入时,第二个加入CultPerson成为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

C#
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());
}
VB
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上的原因)。对我们的类来说,这无关紧要。

C#
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();
    }
}
VB
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。然而,绑定需要一个IListIListSourceICollection<T>不继承自IList,所以我们有点被难住了。我们是否实现IList并获得几乎所有我们已经实现的方法的泛型和非泛型版本?我们是否实现IList并失去我们保证的类型安全性?Microsoft确实让我们面临艰难的选择……是的,如果我们确实想保留我们所有的功能,我们应该实现IList。但我不会这样做。我向您展示了如何实现ICollection<T>,我将向您展示如何实现IList<T>。之后,您应该完全能够自己实现IList。对于这个例子,我将使用一个更快的解决方案,即IListSourceIListSource只需要您实现一个属性,该属性应返回True,以及一个函数。该函数实际上返回一个IList。幸运的是,我们有那个IList,因为IList<T>确实实现了IList。请注意,您必须直接返回InnerList。人们可能会AddRemove Cultist(好吧,如果他们能实例化它们的话),然后造成混乱!请记住,我们的InnerListRank一无所知!所以这似乎是一个不错、快速的解决方案。在这种情况下,它只是快。唯一真正正确地使您的集合可绑定是实现IList。请注意,我没有直接将Person添加到DataGridView。这实际上使IListSource完成了它需要做的事情。在网格中显示我们的Cultist列表。

您可以随意查看我在此文章中省略的代码。您会发现我将一些方法和属性设为了Protected。这是因为我想在下一个IList<T>示例中继承此类。不过,通常这样做不是一个好主意。继承您集合的任何人都不一定应该这样做。实际上,任何继承自Cult的类都注定要失败,因为谁知道何时以及为何调用,例如,AssignNextCaptainIfNecessary?如果人们真的想扩展Cult类,他们可以创建自己的ICollection<Person>并让我们的Cult作为他们的InnerList

另外,别忘了通过启动frmCult来查看这一切实际是如何工作的。您可以通过填写名字、姓氏和年龄来添加成员,然后按“添加”按钮。要删除成员,请在DataGridView中选择一行或多行,然后按“移除”按钮。Form启动时有49个成员。您添加的第一个成员应该会分配一个新Captain。尝试移除一个Captain,看看下一个成员如何成为Captain。尝试移除十个以上的Soldier,然后移除一个Captain。由于当前成员数量不多,因此不会分配新的CaptainfrmCult中的代码非常直接。它并不使用我们Cult类中的所有功能,但它很好地展示了MarkRank是如何工作的(这就是我们最初创建自定义集合的原因)。

从集合到列表

随着我们的Cult越来越壮大,它变得不仅仅是一群快乐在一起的人。政治开始发挥作用,随之而来的是腐败。所以一个有侄子也想加入Cult的船长就占了优势,因为他的叔叔实际上是一个高级成员。那么,如果这个新成员不仅仅是排队等候,而是悄悄地溜到船长们中间呢?他很快就会成为船长!由于我们的CultMemberRank由它们在集合中的位置决定,因此我们必须有一种方法将那个侄子放在,比如说,第四个位置。这就是IList<T>的用武之地。IList<T>接口继承自ICollection<T>。这很容易,因为这意味着我们已经讨论了IList<T>接口的一半以上。那么IList<T>ICollection<T>增加了什么呢?很简单,索引。现在,这有点令人困惑,因为我们已经有了所有这些扩展方法。即使在IEnumerable<T>中,我们也可以获取特定索引处的项目,例如,我们Cult中的第n个Person。然而,IList<T>接口还允许在特定索引处添加。IList<T>只向ICollection<T>接口添加了三个方法和一个属性:InsertIndexOfRemoveAtItem(这是一个默认属性,稍后会详细介绍)。

IList_Of_T_.png

我认为方法的名称实际上是自明的,不需要进一步解释。所以让我们看看我在OrganisedCult中是如何实现它们的。在这个例子中,我省略了CanJoin方法。在Cult中,成员可以在没有同名成员加入的情况下加入。在OrganisedCult中,新成员的年龄至少应为18岁。为了关注IList<T>方法,我没有在下面的代码中包含这一点。

C#
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]);
    }
}
VB
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个成员,并且我们将新成员插入到最后一个船长之后,那么CultCount将达到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#中这样做。

C#
OrganisedCult cult = New OrganisedCult();
// Returns the Person at the 5th index.
Person p1 = cult[5];
// Instead of:
Person p2 = cult.ElementAt(5);

关于此属性的另一个相当奇怪之处是,getter需要一个参数。如果您要为此属性分配一个新值,您必须提供此参数。

C#
// 5 is the parameter of the Getter.
cult[5] = new Person("John", "Doe", 18);
VB
' 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有两个重要的属性,KeyValue。知道了这一点,您就可以像这样通过Dictionary<int, String>迭代项目,这应该不足为奇

C#
foreach (KeyValuePair<int, String> pair in myDictionary)
{
    Console.WriteLine(pair.Key.ToString() + " - " + pair.Value);
}
VB
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,那么它一定与ICollectionIList非常不同,在那些我们只有Value的情况下,对吧?部分正确。让我们先看看IDictionary<TKey, TValue>的定义。

IDictionary_Of_TKey__TValue_.png

AddContainsRemove方法具有“Dictionary版本”。可以单独添加键和值,因此Dictionary的用户不必为要添加的每个条目创建一个KeyValuePair。除此之外,您可能会注意到Value是通过其键获取或删除的。有一个ContainsKey函数和一个只接受Key作为参数的Remove函数。您可能还注意到默认属性Item也回来了,并且它接受一个TKey作为getter的参数(与IList中的类似,只是它接受的是索引)。这也意味着Dictionary Value不是通过索引获取的,而是通过Key获取的。新的是TryGetValue函数以及KeysValues属性。

那么这如何融入魔法炉的故事呢?嗯,如果您仔细想想,炉子需要一定量的食材。所以我们需要一个Dictionary,它以食材为Key,以数量为Value。现在我不会骗您。以下代码相当高级,我可能让它变得太复杂了,但我会一步一步地引导您。首先,我创建了三个接口。一个定义了食材,一个定义了餐点,一个定义了食谱。食谱是食材和餐点之间的关键联系,我还创建了一个名为BaseRecipe的基类。接口如下所示:

C#
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();
    }
}
VB
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接口提供的AddRemove方法

C#
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;
}
VB
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添加了一个接受keyvalue作为参数的Add方法,它只是创建一个KeyValuePair并将其传递给我们已经从ICollection知道的Add方法。现在这个Dictionary有一个奇怪的小问题。如果Dictionary已经包含key,它不会添加新的KeyValuePair,而是查找key并将新的KeyValuePairvalue添加到现有的value中。正常情况下这会是个问题。KeyValuePair是一个struct(VB中为Structure),是不可变的(keyvalue在设置后不能更改)。然而,我没有改变KeyValuePairvalue,我改变的是valuevalue!请注意,我将一个ChangeableDictionaryValue<int>添加到_innerDictionary而不是一个int。我这样做是为了能够改变value!在这个Dictionary中还需要注意的另一件事是,接受KeyValuePair作为参数的Remove函数实际上是将负值添加到指定的key

这就是真正的惊喜。这个Dictionary(与我们的Cult非常相似)不通过检查项目指针来检查一个项目是否已在Dictionary中,而是通过检查keyType来检查!所以,如果两个具有相同key的完全不同的KeyValuePair被添加到Dictionary中,那么这意味着一个项目被添加,其值为两个KeyValuePair的总和。

通过查看ContainsContainsKey函数,可以清楚地了解这一点。

C#
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;
    }
}
VB
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中任何KeyValuePairkey相同。GetType函数要求两个类型完全相同(因此它不会看到基类或派生类)。更多关于这里。您可能已经猜到了,接受KeyValuePair作为参数的Contains不检查提供的KeyValuePair是否指向Dictionary中某个KeyValuePair也指向的内存位置,而是检查keyType是否存在于Dictionary并且value是否大于或等于指定KeyValuePairvalue。所以它基本上检查Dictionary中是否存在某种数量的某种配料。

IDictionary的下一个函数是一个相当直接的函数,TryGetValue。它所做的是尝试使用给定的key获取一个valuevalue作为out参数(VB中为ByRef)传递,如果未找到key(而不是引发Exception!),则返回Nothing或默认值(对于值类型)。TryGetValue返回一个Boolean,指定是否找到了value

C#;
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;
    }
}
VB
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

C#
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;
        }
    }
}
VB
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。然后只有两个属性我们还没有讨论过。KeysValues属性。正如您可能猜到的,它们只是返回一个包含Dictionary中每个key或每个value的集合(实际上是ICollection<T>)。大多数时候,您可以只返回_innerDictionary.Keys_innerDictionary.Values。在这种情况下,_innerDictionary的类型与我们接口实现的TValue的类型不同。所以我们首先需要创建一个新列表并将我们的Integer值放入其中。我再次使用了这个技巧,用于我们已经在ICollection<T>中看到的GetEnumeratorCopyTo方法

C#
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;
}
VB
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>>)。然后我对返回值调用ForEachForEachIList<T>上的一个扩展方法,接受一个委托作为参数。这超出了本文的范围,我使用了一个匿名方法将我们_innerDictionary的每个keyvalue放入一个包含Integer作为值的新Dictionary中。然后我返回与我们接口实现一致的新Dictionary

就是这样!这并不容易,但可能比您想象的要容易得多!所以现在我们有一个Dictionary可以存储特定数量的食材,但是……关于Stove呢?注意到我让Dictionary中的一些方法成为virtual(VB中为Overridable)了吗?好吧,看看Stove类。它继承了我们的Dictionary并重写了这些方法,并且它不仅将食材添加到其_innerDictionary中,还检查它是否已经组装了足够的食材来烹饪它所知道的任何餐点。其学习的食谱。

C#
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();
            }
        }
    }
}
VB
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的食材计数中减去)并返回烹饪好的IMealBaseRecipe确保此模式被正确实现。您可能已经注意到NeededIngredients是一个ReadOnlyIngredients类。这是一个您无法添加或删除任何Ingredient的类。这个类继承自System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>。这个类非常简单。

C#
public class ReadOnlyIngredients : 
  System.Collections.ObjectModel.ReadOnlyCollection<KeyValuePair<IIngredient, int>>
{
    public ReadOnlyIngredients(Ingredients ingredients)
        : base(ingredients.ToList()) { }
}
VB
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 DictionaryReadOnlyIngredients集合的用法。有关Stove类示例和用法的示例,请查看frmMagicStove。您可以从顶部的ComboBox中选择任何食材,并指定要添加到Stove的数量。您将看到食材被添加到Stove中,并且只要Stove有足够的食材,它就会烹饪找到的第一个食谱,直到它不再有足够的食材为止,然后它将烹饪另一个餐点或停止烹饪。每次Stove烹饪一顿饭时,都会引发一个事件。这个事件包含烹饪的IMeal(这是您做任何事情的唯一机会,用它或失去它!)。我将其添加到IMealList中,并将其绑定到底部网格,该网格显示所有已烹饪的餐点。

关注点

对我而言,所有这些都非常有趣。事实上,说实话,在开始写这篇文章之前,我从未创建过自定义集合。所以我从中学到了很多。而且我只能说,我对集合总体上非常热情!在撰写本文时,我开始更频繁地使用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集合世界有所了解。它们是如何工作的,如何使用它们,以及如何创建它们。我们在本文中创建了一些非常疯狂和愚蠢的集合,我希望它能传播开来,因为它很有趣。这里有一些链接供您进一步探索集合的可能性。

祝您编码愉快!:)

© . All rights reserved.