我没有停止恐惧,但我不再让恐惧控制我。我已经接受了恐惧是生活的一部分,特别是对变化的恐惧,对未知的恐惧,尽管心里怦怦直跳,说着:回头,回头;如果你走得太远,你会死的。—埃里卡·琼
在这一章中,我们将介绍 C++ 的互操作性特性,并向您展示一种结合 C# 和 C++ 的快速方法。我们首先用 C# 开发一个洗牌类。接下来,我们添加一个使用 C# 类的 C++ 存根。在第 4 章中,我们更进一步,将整个应用程序迁移到 C++。我们将在第 19 章中更详细地讨论语言集成和互操作性。
假设你有一个非常好的 C# 类,你想把它和你的 C++ 代码一起使用。如果不得不抛弃这一切并用 C++ 重写,那就太可惜了,不是吗?
当我在开发。NET Reflector add-in for C++/CLI 时,我发现自己正处于这种情况。在我的开发过程中。NET Reflector,正处于改进反射器接口的过程中,结果删除了我需要的一个类。为了帮我,他给我发了一个 C# 文件,里面有被删除的代码。我没有被迫用 C++ 重写他的代码,而是在我的项目中添加了对他的类的引用,然后回去继续编写插件。
似乎面试的问题总是相关的,不管你在这个行业已经多少年了。它们可以发人深省并富有娱乐性。我最喜欢的一个游戏,洗牌,应该是有教育意义的。
从表面上看,这似乎是一个简单的问题,但是在您开始编码之前,有几种方法会让您陷入困境。
面试开始出错的第一次是当你在洗牌之前试图找出如何表现这副牌的时候。噩梦会像这样展开:
- 你:套牌是什么样子的?
- 面试官:随机的。
- 你:我如何表示随机输入?你会给我一个输入状态的卡片列表吗?
让我说,在这一点上,面试官会退到洞穴里,以某种形式重复这个问题:
- 记者:给你一副任意的牌,你需要洗一副牌。我就说这么多。
他是这么说的,但他想的是“不雇佣”你需要在这里停下来想一想目标。目标是产生一副完全随机的洗牌牌。开始时牌的顺序并不重要。你可以选择任何你喜欢的顺序。
面试中的下一个障碍是通过四种不同的花色来表现王牌中的王牌。有一个更简单的方法:用一个从1
到52
的数字来标识每张牌。如果卡片从0
到51
编号,那么用 C++ 和 C# 编程就更容易了,因为在这些语言中数组是零索引的。
给花色分配一个任意的顺序,例如 0 到 3 之间的一个数。Bridge 采用字母顺序,为什么不效仿呢?
namespace CSharp
{
class Deck
{
enum Suit
{
Clubs = 0, Diamonds, Hearts, Spades
}
}
}
你可以对卡片本身使用同样的技巧:
namespace CSharp
{
class Deck
{
enum Card
{
Ace=0, Deuce, Trey, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King
}
}
}
因此,我们有两种类型的信息来分别表示:0
和12
之间的Card
号,以及0
和3
之间的Suit
号。这个问题的一个常见解决方案是使用以下公式将它们映射到一个数字:
Number = Suit*13+Card
由于Card
小于13
,很明显(int)(Card/13) ==0
,所以两边除以 13 得到Suit
,余数为Card
。因此,我们已经导出了用于逆变换的以下方程:
Suit = Number/13
Card = Number%13
Number
在Card
和Suit
都为0
时达到最小值,在Card=12
和Suit=3
时达到最大值。
min(Number) = 0 * 13 + 0 = 0
max(Number) = 3 * 13 + 12 = 51
因此,我们将任意一张卡片(Suit
,Card
)映射到 0 到 51 之间的唯一数字。实际上,这个问题可以归结为 0 到 51 之间的随机数的随机化问题。你可能会认为这是一件容易的事情,但事实证明这并不简单,而且很容易出错。鉴于在线赌博的激增,这尤其令人不安。
Note
这里有一个诱人的算法,只是不工作。将卡片放在一个数组中,遍历它们,用随机位置的一张卡片交换每张卡片。事实上,这确实非常壮观地混淆了牌,但是它有利于某些牌的顺序并产生不均匀的分布。你能看出为什么吗?
每一次交换都有 52 分之一的机会与自己交换——一次微不足道的交换。你可能会想,如果洗牌的结果是一副未洗牌的牌,比如说,{0 1 2 3 4… .
51},那么一定有偶数的非平凡交换。现在这副牌{2 1 3 4… .
51}需要奇数个非平凡交换。这应该是一个危险信号,因为我们的算法总是精确地执行 52 次交换,这是偶数,所以这两副牌以相等的可能性生成似乎是可疑的。
一个声音算法模仿你发牌时的动作。首先,你从 52 张牌中随机抽取一张,然后从剩下的 51 张中抽取一张,以此类推。在这个算法中,你得到一个均匀的分布,直到随机数发生器的随机性:
namespace CSharp
{
class Deck
{
void Shuffle()
{
for (uint u = 52; u > 0; --u)
{
Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
}
}
}
}
这个实现将一副牌洗牌,并“分发”出前五张牌供观看。我们可以断定这个游戏的名字是五牌梭哈。
using System;
namespace CSharp
{
public class Deck
{
uint[] Cards;
Random randomGenerator;
public enum Suit
{
Clubs = 0, Diamonds, Hearts, Spades
}
public enum Card
{
Ace = 0, Deuce, Trey, Four, Five, Six, Seven,
Eight, Nine, Ten, Jack, Queen, King
}
Deck()
{
randomGenerator = new Random();
Cards = new uint[52];
for (uint u = 0; u < 52; ++u)
{
Cards[u] = u;
}
}
void Swap(ref uint u, ref uint v)
{
uint tmp;
tmp = u;
u = v;
v = tmp;
}
void Shuffle()
{
for (uint u = 52; u > 0; --u)
{
Swap(ref Cards[u - 1], ref Cards[RandomCard(u)]);
}
}
uint RandomCard(uint Max)
{
return (uint)((double)Max * randomGenerator.NextDouble());
}
string CardToString(uint u)
{
Suit s = (Suit)(Cards[u] / 13);
Card c = (Card)(Cards[u] % 13);
return c.ToString() + " of " + s.ToString();
}
public static void Main()
{
Deck deck = new Deck();
deck.Shuffle();
for (uint u = 0; u < 5; ++u)
{
Console.WriteLine(deck.CardToString(u));
}
Console.ReadLine();
}
}
}
如同在每个 C# 应用程序中一样,代码以static Main()
开始。在那里,我们创建一个新的Deck
,在上面调用Shuffle()
,然后显示前五张卡。因为WriteLine()
不熟悉如何打印卡片,我们创建了一个将卡片转换成字符串的函数,然后用它的结果调用WriteLine()
。函数CardToString(uint cardnumber)
完成了这个任务。
首先让我们创建一个简单的 C# shuffle 项目。这个 C# 项目没有什么特别独特的地方。要创建它,请选择文件➤新➤项目。浏览新的项目树视图,创建一个名为 Shuffle 的 Visual C# 控制台应用程序。如果你的系统设置和我的一样,控制台应用程序会出现如图 2-1 所示。
图 2-1。
The C# Shuffle console application
C# 和 C++ 编译器都将元数据打包成模块和程序集。模块是程序集的构建块。程序集由一个或多个模块组成,是部署单元。程序集被部署为可执行文件或类库。在第一个版本中,Shuffle 项目是一个独立的可执行文件。在本章的后面,我们将把这个可执行文件变成一个类库,而不需要修改任何一行 C# 代码。
选择编辑➤概述➤折叠到定义。这给了你一个代码的鸟瞰图,如图 2-2 所示。
图 2-2。
A bird’s-eye view of the code
将光标放在任何包含省略号的框上都会弹出一个窗口,显示代码的折叠部分。
选择“生成➤生成解决方案”来生成项目。对于 Visual C++ 键绑定,这是 F7 键。对于 Visual C# 键绑定,这是 F6 键。在任一情况下,您都可以用 F5 键执行它。
您会看到类似如下的输出—您的手牌可能会有所不同:
Ten of Diamonds
Deuce of Clubs
Trey of Clubs
Jack of Hearts
Deuce of Spades
由于调用了Console.ReadLine()
,命令窗口现在暂停,等待您按回车键。
嗯。一对 2——还不错,但还没好到可以打开。
现在我们要从 C++ 中调用这个 C# 类。我们将利用 C++/CLI 程序以名为main()
的全局函数开始的事实,而 C# 程序以名为Main()
的静态函数开始。因为这些名字是截然不同的,所以它们并不冲突,我们可以将它们无缝地绑定在一起。
首先,我们将 C# 程序与 C++/CLI 合并。要创建一个 C++ 项目,选择文件➤添加➤新项目。在模板下,依次选择 Visual C++、CLR 和 CLR 控制台应用程序。将项目命名为 CardsCpp,从解决方案下拉列表中选择添加到解决方案,如图 2-3 所示。然后单击确定。
Note
您也可以使用解决方案资源管理器中的“添加项目”。这样,您就不会冒意外创建新解决方案的风险。
图 2-3。
Creating the C++/CLI project
您应该有一个名为 CardsCpp 的新项目。在解决方案资源管理器中按照下列步骤操作:
Right-click the CardsCpp project, and select Build Dependencies ➤ Project Dependencies. Check the box so that CardsCpp depends on Shuffle. This ensures that the C# project Shuffle is built before the C++ project CardsCpp. We want a dependency in this direction, because we will bring in the completed C# project as a class library DLL and the C++ project will be the master project. See Figure 2-4.
图 2-4。
Project Dependencies dialog boxRight-click the CardsCpp project again, and select Set as Startup Project.
现在,我们将变一点魔法,修改 C# 应用程序,以便它可以作为类库被 C++ 应用程序引用。在解决方案资源管理器中右击 Shuffle,然后选择 Properties。在应用选项卡中,将输出类型改为类库,如图 2-5 所示。
图 2-5。
Convert the C# project to a class library
右键单击 CardsCpp 项目,并选择“添加➤引用”。然后单击“添加新引用”按钮。单击“项目”选项卡;洗牌项目应该已经被选中,如图 2-6 所示。单击 OK 向 C++ 项目添加对 Shuffle 的引用。
图 2-6。
Add a reference to the C# project
对 C++ 源文件CardsCpp.cpp
有一个小的改动。替换以下行:
Console::WriteLine(L"Hello World");
随着
CSharp::Deck::Main();
请注意,当您键入时,Visual C++ IntelliSense 会弹出一个窗口来帮助您。就像 C# IntelliSense 一样,它是一个上下文敏感的代码引擎,可以帮助您在键入时发现类成员和参数信息。如图 2-7 所示,智能感知揭示了CSharp::Deck
类的方法和字段。它们是什么以及如何访问它们由名称左侧的小图标决定。较小的框添加了关于所选项的更多信息,以及 XML 文档注释(如果有的话)。
图 2-7。
IntelliSense helps you code
您的代码现在应该如图 2-8 所示,准备好使用 F5 执行。
图 2-8。
The finished C++/CLI stub
在没有 IDE 的情况下,组合 C++ 和 C# 程序也很容易,尽管它不容易扩展到大型项目。IDE 为您提供了强大的处理能力,但也增加了一层复杂性。使用 IDE,您可以获得以下内容:
- 使用智能感知和浏览功能编辑帮助和代码信息
- 项目管理
- 构建管理
- 集成调试
因为这是一个小而简单的项目,所以我们不需要通过完整的 IDE 设置来展示我们的演示。
使用以下去掉预编译头文件的基本 C++ 程序。在与Program.cs:
相同的目录中创建一个名为cardscpp1.cpp
的文件
#using "shuffle.dll"
void main()
{
CSharp::Deck::Main();
}
打开 Visual Studio 2013 命令提示符并导航到此目录。编译并执行该程序,如下所示:
csc /target:library /out:shuffle.dll program.cs
cl /clr cardscpp1.cpp
cardscpp1
King of Diamonds
Trey of Clubs
Jack of Hearts
Deuce of Diamonds
Four of Hearts
看来这次我们该弃牌了!
模块是比 DLL 更小的编译单元。使用模块,可以将几个模块组合成一个 DLL。下面是一个使用模块而不是 DLL 的例子。在这种情况下,使用模块和 DLL 没有什么区别。
在与shuffle.cs
相同的目录下创建一个名为cardscpp2.cpp
的文件:
#using "shuffle.netmodule"
void main()
{
CSharp::Deck::Main();
}
将 C# 编译成一个模块,使用 C++ 制作一个可执行文件,并运行它:
csc /target:module /out:shuffle.netmodule program.cs
cl /clr cardscpp2.cpp
cardscpp2
King of Clubs
Queen of Diamonds
Queen of Spades
Ten of Spades
Ace of Clubs
这是一手好牌!
在这一章中,我们开发了一个简单的 C# 程序。首先,我们从 IDE 中编译并独立运行它。然后,我们将它的输出类型更改为库,以便创建一个供 C++ 可执行文件使用的 DLL,既可以从 IDE 也可以从命令行使用。最后,我们给出了一个使用模块的例子。这应该给你一个很好的介绍,让你知道在. NET 下使用 C# 和 C++ 的各种方法。在第 19 章的中,我们将重温这些主题,并讨论与本地代码的互操作性。但是我们不要想得太多;首先要涵盖许多基础知识,我们将在下一章探讨语法差异。