编写类库

  对于本例,假定读者对 IDE 比较熟悉,所以不再使用标准的 “试一试” 方式明确列出各个步骤(这些步骤已经在前面多次用过),重要的是详细讨论代码。不过,这里要包含一些提示以确保不出问题。

  类和枚举都包含在一个类库项目 Ch10CardLib 中。这个项目将包含 4 个 .cs 文件:Card.cs 包含 Card 类的定义,Deck.cs 包含 Deck 类的定义, Suit.csRank.cs 文件包含枚举。

  可以使用VS的类图工具把许多代码组合在一起。

  如果不愿意使用类图工具,也不必担心。下面各节都包含了类图生成的代码,所以读者完全可以理解这些内容。

  首先需要完成以下操作:

  (1)在 C:\BegVCSharp\Chapter10 目录中创建一个新类库项目 Ch10CardLib

  (2)从项目中删除 Class1.cs

  (3)使用 解决方案资源管理器 窗口打开项目的类图(右击项目,然后单击 类详细信息)。类图开始时应为空白,因为项目不包含类。

  1. Suit 和 Rank 枚举

  把一个 Enum 从工具箱拖动到类图中,再在显示的 New Enum 对话框中填写信息,就可以在类图中添加一个枚举。例如,对于 Suit 枚举,应在对话框中添加 如图 10-11 所示 的信息。

图 10-11图 10-11

  接着使用 类详细信息 窗口添加枚举的成员。需要添加的值 如图 10-12 所示

图 10-12图 10-12

  以相同的方式利用工具箱添加 Rank 枚举。需要的值 如图 10-13 所示

图 10-13图 10-13

  第一个成员 Ace 的输入值为1,它会使枚举的底层存储匹配扑克牌的大小,例如 Six 就存储为 6。

  为这个两个枚举生成的代码位于 Suit.csRank.cs 文件中。在 Ch10CardLib 文件夹的 Suit.cs 文件中可以找到 Suit 枚举的完整代码,如下所示:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace Ch10CardLib
  6. {
  7. public enum Suit
  8. {
  9. Club,
  10. Diamond,
  11. Heart,
  12. Spade,
  13. }
  14. }

  在 Ch10CardLib 文件夹的 Rank.cs 文件中可以找到 Rank 枚举的完整代码,如下所示:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. namespace Ch10CardLib
  6. {
  7. public enem Rank
  8. {
  9. Ace = 1,
  10. Deuce,
  11. Three,
  12. Four,
  13. Five,
  14. Six,
  15. Seven,
  16. Eight,
  17. Nine,
  18. Ten,
  19. Jack,
  20. Queen,
  21. King,
  22. }
  23. }

  另外,也可以添加 Suit.csRank.cs 代码文件,再手工输入这些代码。注意 ⚠️,代码生成器在最后一个枚举成员后添加的逗号不会妨碍编译,不会创建一个额外的空成员,但它们可能会带来一些混乱。

  2. 添加 Card 类

  本节将结合使用类设计器和代码编辑器来添加 Card 类。使用类设计器添加类与添加枚举十分类似,也是把相应的项从工具箱拖动到类图中。这里要把 Class 拖动到类图中,并把新类命名为 Card

  使用 类详细信息 窗口添加字段 ranksuit,再使用 属性 窗口把字段的 常量类型 设置为 readonly。还需要添加两个构造函数,一个是默认构造函数(私有),另一个构造函数(公共)带有两个参数:newSuitnewRank,其类型分别是 SuitRank。最后重写 ToString(),这需要在 属性 窗口中修改 继承修饰符,将它设置为 override

  图 10-14 显示了 类详细信息 窗口和已输入所有信息的 Card 类(可在 Ch10CardLib\Card.cs 中找到其代码)。

图 10-14图 10-14

  然后需要修改 Card.cs 中类的代码(或者把这些代码添加到名称空间 Ch10CardLib 的新类 Card 中),如下所示:

  1. public class Card
  2. {
  3. public readonly Suit suit;
  4. public readonly Rank rank;
  5. }
  6. private Card() { }
  7. public Card(Suit newSuit, Rank, newRank)
  8. {
  9. suit = newSuit;
  10. rank = newRank;
  11. }
  12. public override string ToString()
  13. {
  14. return "The " + rank + " of " + suit + "s";
  15. }

  重写的 ToString() 方法将已存储的枚举值的字符串表示写入到返回的字符串中,非默认的构造函数初始化 suitrank 字段的值。

  3. 添加 Deck 类

  Deck 类需要使用类图定义以下成员:

  ● Card[] 类型的私有字段 cards

  ● 公共方法 GetCard(),它带有一个 int 参数 cardNum,并返回一个 Card 类型的对象。

  ● 公共方法 Shuffle(),它不带参数,返回 void

  添加了这些成员后,Deck 类的 类详细信息 窗口就 如图 10-15 所示

图 10-15图 10-15

  为使类图更加清晰,可以显示所添加的成员和类型之间的关系。在类图依次右击下面的项,从菜单中选择 显示为关联 选项:

  ● Deck 中的 cards

  ● Card 中的 suit

  ● Card 中的 rank

  完成后的类图 如图 10-16 所示

图 10-16图 10-16

  接着修改 Deck.cs 中的代码(如果不使用类设计器,就必须首先使用下面的代码添加这个类)。这些代码包含子在 Ch10CardLib\Deck.cs 中。首先实现构造函数,它在 cards 字段中创建 52 张牌,并给它们赋值。对两个枚举的所有组合进行迭代,每次迭代都创建一张牌。这将使 cards 最初包含一个有序的扑克牌列表:

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq; using System.Text;
  4. using System.Threading.Tasks;
  5. namespace Ch10CardLib
  6. {
  7. public class Deck
  8. {
  9. private Card[] cards;
  10. public Deck()
  11. {
  12. cards = new Card[52];
  13. for (int suitVal = 0; suitVal < 4; suitVal++)
  14. {
  15. for (int rankVal = 1; rankVal < 14; rankVal++)
  16. {
  17. cards[suitVal * 13 + rankVal - 1] = new Card((Suit)suitVal, (Rank)rankVal);
  18. }
  19. }
  20. }
  21. }
  22. }

  然后实现 GetCard() 方法,为指定的索引返回 Card 对象,或者以与前面相同的方式抛出一个异常:

  1. public Card GetCard(int cardNum)
  2. {
  3. if (cardNum >= 0 && cardnum <= 51)
  4. return cards[cardNum];
  5. else
  6. throw (new System.ArgumentOutOfRangeException("cardNum", cardNum, "Value must be between 0 and 51."));
  7. }

  最后实现 Shuffle() 方法。这个方法创建一个临时的扑克牌数组,并把扑克牌从现在的 cards 数组随机复制到这个数组中。这个函数的主体是一个从 0~51 的循环,在每次循环时,都会使用 .NET Framework 中 System.Random 类的实例生成一个 0~51 之间的随机数。进行了实例化后,这个类的对象使用方法 Next(X) 生成一个介于 0~X 之间的随机数。有了一个随机数后,就可以使用它作为临时数组中 Card 对象的索引,以便复制 cards 数组中的扑克牌。

  为了记录已赋值的扑克牌,我们还有一个 bool 变量的数组,在复制每张牌时,把该数组中的值指定为 true。在生成随机数时,检查这个数组,看看时否已经把一张牌复制到临时数组中由随机数指定的位置上了,如果已经复制,将生成另一个随机数。

  这不是完成该任务的最高效的方式,因为生成的许多随机数都可能找不到空位置以复制扑克牌。但是,它仍能完成任务,而且很简单,因为 C#代码的执行速度很快,我们几乎觉察不到延迟。代码如下:

  1. public void Shuffle()
  2. {
  3. Card[] newDeck = new Card[52];
  4. bool[] assigned = new bool[52];
  5. Random sourceGen = new Random();
  6. for (int i = 0; i < 52; i++)
  7. {
  8. int destCard = 0;
  9. bool foundCard = false;
  10. while (foundCard == false)
  11. {
  12. destCard = sourceGen.Next(52);
  13. if (assigned[destCard] == false)
  14. foundCard = true;
  15. }
  16. assigned[destCard] = true;
  17. newDeck[destCard] = cards[i];
  18. }
  19. newDeck.CopyTo(cards, 0);
  20. }

  这个方法的最后一行使用 System.Array 类的 CopyTo() 方法(在创建数组时使用),把 newDeck 中每张扑克牌复制回 cards 中。也就是说,我们使用同一个 cards 对象中的同一组 Card 对象,而不是创建新的实例。如果改用 cards=newDeck,就会用另一个对象替代 cards 引用的对象实例。如果其他地方的代码仍保留对原 cards 实例的引用,就会出问题—不会洗牌。

  至此,就完成了类库代码。