面向-C--开发者的-C---2013-教程-五-

35 阅读43分钟

面向 C# 开发者的 C++ 2013 教程(五)

原文:C++ 2013 for C# Developers

协议:CC BY-NC-SA 4.0

十三、有趣,有趣,更有趣

一个项目是如何迟到一年的?一天一天来。—小佛瑞德·P·布鲁克斯,神话中的人月

当我们结束对 C++ 的基本介绍并准备处理更高级的主题时,也许稍微转移一下注意力来奖励我们的努力是合适的。我们将通过编程 C++ 解决方案来看一些有趣的面试问题。

掉落的灯泡

网上流传着一个面试问题,让你判断一个超强的灯泡能跌落多远而不破碎。我们将从数学上分解这个程序,并实现一个很好的递归算法来展示解决方案。

LIGHT BULBS

你有两个几乎打不坏的灯泡和一座 100 层的大楼。使用尽可能少的跌落次数,确定这种类型的灯泡可以承受多大的冲击(例如,它可以承受从 17 楼跌落,但从 18 楼跌落时会破碎)。注意,一直流行的二分搜索法给你的最坏情况是 50 滴。你应该能在 20 分钟内完成。

最初的想法

这个问题首先要注意的是,你只有两个灯泡。一旦第一个被打破,你就不能再冒险了。你需要遵循最保守的策略来护理你的灯泡,直到你确定当它坏掉的时候你已经找到了正确的地板。这意味着你必须一次爬一层楼,从一层楼开始,你要确定灯泡从一层楼掉下来还能存活。你不能冒第二个灯泡破裂的风险,除非这能确切地告诉你最大允许冲击力是多少。

换句话说,如果第二个灯泡从 n 层坠落时破碎了,你应该已经确定这个灯泡从 n–1 层坠落时不会损坏。

我们最初的直觉是将楼层数除以 2,然后从那里开始,所以让我们来看看当我们从第 50 层放下灯泡时会发生什么。

好吧,如果它在第一次落下时就断了会怎么样?因为只剩下一个灯泡了,我们不能冒险跳过一层楼。剩下的唯一策略就是极端保守,从尽可能低的楼层开始,一次升一层。然后我们被迫从一楼扔下第二个灯泡,然后是第二个,以此类推。最糟糕的情况是,我们被迫一次一层地一路爬到 49 楼。这将意味着总共 50 滴(1+49 滴)。

所以把楼层数除以 2 作为初始落差是非常低效的。如果我们把楼层数除以一个更大的数作为初始落差,会发生什么?

粗略的估计

假设我们将楼层数除以五。那么我们的第一次空投会从 20 楼开始。如果最初的灯泡没有坏掉,我们可以尝试从 40 楼,60 楼,等等直到 100 楼。如果灯泡坏了,我们最多只剩下两次下降之间的楼层数来确定灯泡坏了。在这种情况下,会有 19 次下降(20–1 次下降)。现在我们可以计算最坏的情况。第一个灯泡对于每层楼最多掉落一次,每层楼是 20 的倍数,或者{20,40,60,80,100}。这就产生了五种可能的下落,也等于 100 除以 20,或 100/20。在任何中断之后,第二个灯泡最多会掉落这些 20 的倍数之间的差值,或 20-1 倍。因此,我们看到最差情况下的跌落次数如下:

A978-1-4302-6707-2_13_Fig1_HTML.jpg

当然,24 次跌落比我们之前算法的最坏情况 50 次要好得多。

一点代数

或许,用一点点代数,我们可以看到我们能把这个想法推进多远。假设我们从第 n 层扔下第一个灯泡,对于 n 的所有可能值,按照前面导出的模式,我们得到

A978-1-4302-6707-2_13_Fig2_HTML.jpg

100 可能不能被 n 整除,这一事实会造成轻微的不准确,并且在之前从 80 跌落后从地板 100 跌落灯泡不会给你提供与之前从 60 跌落后从 80 跌落一样多的新信息。不过,这个公式很容易让我们计算出最佳算法的上限。

假设 k 是给定 n 的算法的最差情况,则我们有以下公式:

A978-1-4302-6707-2_13_Fig3_HTML.jpg

本质上,

A978-1-4302-6707-2_13_Fig4_HTML.jpg

现在我们有一个 n 中的二次方程,可以用一点点初等代数来粗略计算一下。我们希望在 n 的所有可能值上使 k 最小,这将给出最好的最坏情况。

一个二次因素为:

A978-1-4302-6707-2_13_Fig5_HTML.jpg

因为算术平均值总是至少与几何平均值一样大,

A978-1-4302-6707-2_13_Fig6_HTML.jpg

当 a=b 时,这实现了等式,并且我们得到如下:

A978-1-4302-6707-2_13_Fig7_HTML.jpg

A978-1-4302-6707-2_13_Fig8_HTML.jpg

还有这里

A978-1-4302-6707-2_13_Fig9_HTML.jpg

A978-1-4302-6707-2_13_Fig10_HTML.jpg

A978-1-4302-6707-2_13_Fig11_HTML.jpg

还有这里。

A978-1-4302-6707-2_13_Fig12_HTML.jpg

我们应该每隔十层放下第一个灯泡,以便从这个算法中获得最佳结果。在这种情况下,即 k=19 滴。 2

面试问题的目标是看你是否能发现 19 降算法,即使你不用代数来揭示它。

不要误会,我并不是说这是最好的算法。第一次从十楼掉下第一个灯泡似乎是有道理的,但是如果灯泡没有坏,我们能不能不用这个信息来重新评估这个问题?现在不是新问题了吗?现在是两个灯泡和 90 层楼的问题。出于这个原因,如果第一个灯泡从下落中幸存下来,那么减少下落之间的距离似乎是一个明显的优化。这可能会随着每次下降而继续被重新评估。我们不应该仅仅因为十楼是第一次空投的最佳地点,就被束缚于每十楼空投一次灯泡。

发现递归算法

这样,这个问题的递归性就出现了。与其用 100 个故事来思考这个问题,不如用 N 个故事来思考问题,递归考虑。那么也许我们可以找到一种更有效的算法。

为此,我们需要定义一个函数:

int Drop(int floors);

函数Drop()返回给定楼层数的最小下降次数。当然,这并没有告诉我们从哪个楼层往下掉,但是如果我们愿意,我们可以修改程序来做到这一点。

假设我们在一个楼层数等于floors的建筑里。如果我们从i楼层掉下第一个灯泡,灯泡坏了,我们需要用另一个灯泡来确定它从1, 2, 3...(i-1)楼层掉下时是否会坏。这最多需要(i-1)滴。如果灯泡没有坏,那么Drop(floors-i)计算出我们能做的最高高度floors-i的建筑。只要floors-i总是严格小于floors,并且我们注意到Drop(1)==1这个事实,我们就不会被锁定在一个无限循环中。 3

在这种情况下,最差的掉线次数是

max(i, Drop(floors-i)+1)

如果我们循环合理的i值,我们可以找到最小的最坏情况。i的合理值是多少?尝试i的所有可能值,从1floors都是矫枉过正。一个简单的优化是认识到我们不应该从前面显示的二进制例子下降到中间以上。从1floors的平方根的i的尝试值似乎直观地满足了之前提出的论点。在下面的代码中,我使用了一系列的[1, floors/2]:

using namespace System;

ref struct Drops

{

array<int>^floordata;

Drops()

{

floordata = gcnew array<int>(300);

floordata[1] = 1;

}

int Drop(int floors)

{

if(floordata[floors])

{

return floordata[floors];

}

int best = Int32::MaxValue;

if(floors == 1)

{

best = 1;

}

else

{

int i;

for(i=1;i<floors/2+1;i++)

{

int drops = Drop(floors-i) + 1;

int thisone = (drops>i) ? drops : i;

best = thisone<best ? thisone : best;

}

}

floordata[floors]=best;

return best;

}

};

void main()

{

Drops ^d = gcnew Drops();

Console::WriteLine("For {0} floors, the minimum is {1}", 100, d->Drop(100));

}

输出如下:

For 100 floors, the minimum is 14

履行

代码本身非常简单,尽管需要一次优化。

我写的最初版本运行得太慢了,以至于我想边喝咖啡边等它完成。因为代码是递归的,Drop(n)被称为Drop(n-1)Drop(n-2),等等,并导致所有这些被重新计算。Drop(n+1)除了调用Drop(n)之外,做更多相同的事情,所以有如此多的重复计算,我们必须非常小心如何度过我们的周期。

我们需要确保对于floors的每个值,最多只计算一次Drop(floors),否则我们最终会一次又一次地重复同样的工作。为此,我们创建了floordata数组来保存之前对Drop()调用的结果。这个数组是Drops类的成员,由构造器Drops()分配。对于楼层的每个值,我们检查数组以查看是否已经计算了一个值。如果没有,我们继续执行Drop()函数中的工作,并在退出时将结果保存在floordata数组中。

该程序确实使用了?:三元条件运算符:

expression ? value1 : value2

如果expression为真,则该运算符计算为value1,否则计算为value2。它允许我们避免简单结构的if else语句,并以更紧凑的形式编写。

SHOW THE WAY: PART 1

程序计算出我们的算法至少需要 14 次投放。发现灯泡强度,并显示导致 100 层建筑跌落 14 次的最坏情况的跌落顺序。

第一滴

这个程序不会告诉你放置灯泡的最佳位置,尽管它可以被修改来做到这一点。结果是,你应该从哪一层放下灯泡,这很大程度上取决于还剩几层,因为这个问题是离散的。因此,不可能打印出一份简短的清单,说明如何扔掉灯泡;相反,解决方案是一个图表。如果灯泡在这一层坏了,就这样做,否则就那样做。然而,对于任何给定的灯泡强度,打印这种最佳算法将遵循的液滴序列是简单的。

不过,你可能会注意到一件事,那就是你最好从 14 楼第一次往下掉。这样做的理由很简单:我们希望从尽可能高的楼层进行第一次坠落,这样我们可以最快地解决问题。如果灯泡坏了,我们最多有 13 秒钟的灯泡跌落时间来确定灯泡强度,因此我们不会比 14 次跌落的最佳最坏情况更糟。使用一个程序,你可以确定从 11 楼到 14 楼的初始下落仍然允许你实现最多 14 次下落的最佳顺序。

跨线桥

另一个在互联网上流传的常见问题是,四个人拿着一个手电筒走过一座黑暗的桥。

FOUR MEN ON A BRIDGE

四个人必须在晚上过桥。这座桥破旧不堪,一次最多只能容纳两个人。没有栏杆,人们只有一个手电筒。任何时候有人过,不管是一个人还是两个人去,手电筒都必须带着。手电筒必须来回走;不能扔。每个人走路的速度不同。一个需要 1 分钟穿越,另一个 2 分钟,另一个 5 分钟,最后 10 分钟。如果两个人一起过马路,他们必须以较慢的人的速度行走。这四个人穿越最少需要多少时间?

背景

现在这个问题如此流行的原因是,有一个明显的、逻辑上不正确的结论,所有人都跳到了这个结论上。只有经过更深入的思考和一点洞察力,你才能得出正确的答案。 4

在你继续读下去之前,想一想这个问题。你在桥上,来回走着。几个跑得快的人,一个非常快,还有几个比较慢的人,坐在桥上。你甚至可以放纵自己;想象你是一个能在一分钟内穿越的步行者。

最初不正确的结论是,走得更快的人应该来回引导大家。1 分钟步行者和 10 分钟步行者一起走过,需要 10 分钟。她回来了,花了 1 分钟。她去找下一个男人,等等。这个解决方案给你 19 分钟的最佳时间让所有人过桥。你自豪地向面试官宣布这一点,面试官沾沾自喜地说有更好的解决方案。惊恐之余,你跌跌撞撞地回到白板前,试着写些有智慧的东西。

更好的解决方案依赖于经典的调度算法。你试图平衡你的任务,所以任务是同时完成的。在 19 分钟的解决方案中,10 分钟和 5 分钟的步行者分别走过,这样你的基本时间是 15 分钟,不管谁和他们一起走。这是一件很难优化的事情。如果你把 5 分钟和 10 分钟的步行者放在一起,那么慢的人只用 10 分钟就可以走完,再用一点手电筒,你计算出你可以在总共 17 分钟内让所有人都走完。我会把如何解决这个问题留给你。

这当然给我们带来了另一个问题:这是最好的算法吗?

我们也很想知道,如果我们改变穿越者的速度,会得到什么样的结果。那么最好的旅行时间是什么时候?

我们可以通过编写一个简短的计算机程序来回答这些问题。为了实现它,我们做如下假设:

  • 从起点到终点的每个路口正好有两个行人。
  • 从终点到起点的每一个交叉点都恰好包含一个行走者。

考虑到问题的性质,这些都是相当合理的假设。我确信有可能证明没有最优解会违反这些假设,但证明不是这里的目标,所以我们将继续。

算法和实现

一个显而易见的算法是,在给定这些假设的情况下,尝试所有可能的步行者过桥。为了做到这一点,我们必须跟踪谁在桥的哪一边以及手电筒的位置。因为这些假设,我们知道每一次有两个步行者的过桥,后面都会有一个步行者的返回,直到我们完成。那么手电筒的位置是隐含的。

然后我们可以写一个函数crossover(State, Direction),我们可以用它在任何方向上过桥。我最初是这样写程序的,它确实工作了,但是事实证明,将交叉点分成一个出站start(State)函数和一个入站end(State)函数更简洁。

对于我们如何跟踪穿越的State,有几种可能性,但是一个简单的位字段表明谁在桥的起始端就足够了,而且是干净的。我们用一个unsigned int来表示谁在桥的起始端。对于start(unsigned here)函数,这个数据表明谁在桥的起始端,或者“这里”对于end(unsigned there)函数,该数据指示谁在另一边,或者“在那里”在这两种情况下,数据总是指示谁在桥的起始端。我们可以通过对带有一个位字段的数据连续应用异或来传递谁在手电筒的那一边,但是我发现总是传递起始端的状态是用于调试目的的最简单的方式。

《守则》

我们使用一个名为Crossingref struct,它包含我们不希望递归传递的常量信息。在这种情况下,我们有步行者的穿越速度,存储在times数组中,还有一个Mask,它允许我们通过简单的异或运算来计算谁在桥的对面。我们不在构造器中初始化它们;相反,我们从输入参数中保存times,并在每次开始穿越时重新计算Mask。这与其说是追求准确,不如说是品味和风格的问题。代码本身很有趣,也很有教育意义;花点时间浏览一下,继续熟悉 C++/CLI:

using namespace System;

ref struct Crossing

{

array<unsigned>^ times;

unsigned int Mask;

int cross(...array<unsigned>^ times)

{

this->times = times;

Mask = (1u<<times->Length)-1;

return start(Mask);

}

int end(unsigned there)

{

if(there==0)

{

return 0;

}

unsigned here = Mask^there;

unsigned best = 0xffff;

for(int i=0;i<times->Length; i++)

{

if(here & (1<<i))

{

unsigned thistrip;

thistrip = times[i] + start(there^(1<<i));

if(thistrip<best)

{

best = thistrip;

}

}

}

return best;

}

int start(unsigned here)

{

if(here==0)

{

return 0;

}

unsigned best = 0xffff;

for(int i=0;i<times->Length; i++)

{

if(here & (1<<i))

{

unsigned thistrip;

for(int j=i+1;j<times->Length; j++)

{

if(here & (1<<j))

{

thistrip =

(times[i]>times[j]? times[i] : times[j])

+ end(here^((1<<i)|(1<<j)));

if(thistrip<best)

{

best = thistrip;

}

}

}

}

}

return best;

}

};

void main()

{

Crossing ^c = gcnew Crossing();

int time = c->cross(1,2,5,10);

Console::WriteLine("It takes at least {0} minutes", time);

}

如果您执行这个程序,您会得到预期的结果:

It takes at least 17 minutes

SHOW THE WAY: PART 2

增强程序,以便显示实际的最小交叉顺序。这需要递归地传递更多的数据。

带着算法兜一圈

当然,因为我们用接受可变数量参数的param数组实现了这一点,所以很容易用不同数量的 walkers 尝试不同的情况。如果我们改变下面一行:

int time = c->cross(1,2,5,10);

int time = c->cross(1,2,5,10,7);

那么凭直觉,我们预计这可能需要额外的 7 分钟。真相就在那里:

It takes at least 23 minutes

这有点让人吃惊。增加一个 7 分钟的步行器只会让整个过程慢 6 分钟。结果是,7 分钟的步行者最终与 10 分钟的步行者交叉,剩下 5 分钟的步行者来处理。5 分钟步行器与 1 分钟步行器进行一次往返,总共需要额外的 6 分钟。好吧,有道理。

如果我们再增加一个 7 分钟步行机呢?

int time = c->cross(1,2,5,10,7,7);

既然他不能和 10 分钟步行者一起过马路,他必须和 5 分钟步行者一起过马路,这样就多了 7 分钟。让我们试一试:

It takes at least 29 minutes

等等,只多了 6 分钟——这又是一个反直觉的答案。我肯定有一个简单的解释。也许 1 分钟步行机的回程可以避免?

这绝对是一个好玩的程序。

对付食人族

这个问题似乎不像其他两个问题那样经常在面试中出现,但它有一个有趣而有启发性的转折。

RIVER CROSSING

三个食人族和三个人类学家必须过河。他们的船只够两个人坐。如果在任何时候,河这边的食人族比人类学家还多,食人族就会吃掉他们。人类学家可以用什么计划过河,这样他们就不会被吃掉?

假设

乍一看,这个问题似乎不像桥梁问题那样困难。我们有一些离散的食人族和人类学家,没有附带的穿越时间(或者说进食时间)的概念。).

我们能够通过对谁在过河做出几个明确的假设来解决前一个问题。我们很乐意在这里做类似的假设,例如:

  • 两个人总是从发射侧穿越到着陆侧。
  • 总有一个人会回来。

不幸的是,这些假设是无效的。有非常清楚的理由,为什么你可能想在回程中派两个人去维持河两边的人的安全组合。事实上,11 个交叉点的最优解就是这样做的。

怎样才能想出一个保证在所有情况下都终止的算法?如果我们尝试一条路径,在这条路径上,我们正好来回发送两个人,我们的递归算法不起作用。

这里的技巧是根据可能性的深度优先遍历来考虑这个算法。我们不是让算法在一条潜在的无限路径中越来越深地寻找一个解,而是构造算法,让它迭代地问:“有没有一个有iteration个交叉点的解?”我们从1iteration开始,直到得到肯定的回答。如果有解决方案,这个算法一定会成功。如果没有解决方案,这个循环将继续下去,直到资源耗尽;在这种情况下,资源就是堆栈。

《守则》

在这个程序中,我创建了一个单独的子程序,用于任意方向的交叉,因为这些操作是完全对称的,因为缺少假设。变量dir在集合{1,–1}中,它决定了我们要穿越的方向。

因为船上的人只有五种可能的组合,所以我用逻辑“或”把它们分开列出来。就像在 C# 中一样,一旦调用返回true,短路评估会阻止对crossover()的进一步调用。

代码的其余部分相当简单,可能就是将来某一天您需要在白板上生成的内容:

using namespace System;

ref struct Crossing

{

int MaxA;

int MaxC;

int cross(int MaxA, int MaxC)

{

this->MaxA=MaxA;

this->MaxC=MaxC;

int iterations;

for(iterations=1;  ;iterations++)

{

if(crossover(MaxA, MaxC, iterations, -1))

{

break;

}

}

return iterations;

}

bool crossover(int A, int C, int iterations, int dir)

{

if(iterations--<0)

{

return false;

}

if(A==0 && C==0)

{

return true;

}

if(A<0 || C<0)

{

return false;

}

if(A>0 && C>A)

{

return false;

}

int Ap = MaxA-A;

int Cp = MaxC-C;

if(Ap>0 && Cp>Ap)

{

return false;

}

return (

crossover(A+dir,C,iterations,-dir) ||

crossover(A,C+dir,iterations,-dir) ||

crossover(A+dir,C+dir,iterations,-dir) ||

crossover(A+dir+dir,C,iterations,-dir) ||

crossover(A,C+dir+dir,iterations,-dir)

);

}

};

void main()

{

Crossing ^c = gcnew Crossing();

int Count = c->cross(3,3);

Console::WriteLine("It takes at least {0} crossings", Count);

}

在我们用三个食人族和三个人类学家运行这个程序后,我们得到了预期的结果:

It takes at least 11 crossings

如果我们尝试更多或更少会发生什么?如果只有两个食人族和两个人类学家,我们得到以下结果:

It takes at least 5 crossings

如果每种都有 4 个呢?事实证明是无解的。程序会一直运行,直到它的存储容量溢出。

摘要

所有这些例子都展示了棘手问题的优雅的递归解决方案。这是一个很好的练习,用这些类型的问题挑战你的假设,不仅寻找逻辑的解决方案,也寻找程序的解决方案。

在下一章中,我们将通过学习基本泛型上下文中的参数多态来开始对 C++ 有更深的理解。

Footnotes 1

你可以在 http://www . com 找到这一章中的问题以及其他类似的问题。技术访谈。org 。这个网站转而引用了威廉·庞德斯通的《你会如何移动富士山》作为资料来源。庞德斯通没有声称发明了这些问题,但他将它们收集在一个有趣的文本中。

  2

这个方法可以推广。对于一栋 N 层高的建筑,如果我们选择,我们可以进行同样的分解:

A978-1-4302-6707-2_13_Fig13_HTML.jpg

那么我们可以看到

A978-1-4302-6707-2_13_Fig14_HTML.jpg

  3

你注意到递归算法的实现和数学归纳法的证明之间有什么相似之处吗?

  4

或者我们应该说,当你坐在面试官的办公室里,汗流浃背,头脑被恐惧冻结的时候,你有一点运气?

十四、泛型

当人们试图设计完全万无一失的东西时,一个常见的错误是低估了十足傻瓜的聪明才智。—道格拉斯·亚当斯

在这一章中,我将介绍泛型。此功能是在的 2.0 版中引入的。NET Framework,并且受所有 Microsoft Visual Studio 2013 语言支持。您将了解泛型如何帮助我们解决各种问题,这些问题是在我们希望构建处理不同类型数据而又不牺牲类型安全性的类时出现的。在本章中,我们还将开始使用各种。NET Framework 类来创建更有趣、更有指导意义的示例。

一系列任务

让我们从一个简单的例子开始。我们有一个名为Task的定制类队列。我们用主线程将Task项添加到队列的末尾,并用第二个线程从队列的开头读取它们。然后,我们以先进先出(FIFO)的方式执行我们的类。

下的 C# 实现。NET 1.0

英寸 NET 1.0 版,我们将使用System.Collections.Queue类在 C# 中实现它。让我们使用 Visual Studio 2013 IDE 中的对象浏览器来看看这一点。

从“视图”菜单中选择“对象浏览器”,主框架中会添加一个“对象浏览器”选项卡。我们可以展开mscorlibSystem.CollectionsQueue上的加号,然后我们可以查看System.Collections.Queue类的方法、字段、属性和事件。Enqueue(object o)Dequeue()方法提供了我们需要的功能。

使…入队

System.Void Enqueue(System.Object obj)

  • obj:添加到System.Collections.Queue的对象。值可以是null

  • 成员:System.Collections.Queue的成员。

  • 描述:在System.Collections.Queue的末尾添加一个对象。

  • 参数:

出列

System.Object Dequeue()

  • System.InvalidOperationException:队列System.Collections.Queue为空。

  • 例外:

  • System.Collections.Queue开始处移除的对象。

  • 成员:System.Collections.Queue的成员。

  • 描述:移除并返回System.Collections.Queue开头的对象。

  • 返回值:

履行

为了实现我们的Task实例队列,我们需要将Task对象转换为System. Objectobject,因为我们使用的是 C#,所以它可以被Enqueue()例程使用。这是由编译器自动执行的,因为object是一个基类。不过,出队带来了更多的问题。出列返回一个object,它必须被重新转换为Task的一个实例。这种重铸带来了一种可能性,即某个人可能会Enqueue()一个非Task类型的对象,从而生成一个异常,使这个实现不是类型安全的。

我们的应用程序的核心如下所示:

using System.Collections;

namespace Task

{

class Task

{

public void Execute()

{

}

}

class Program

{

static Queue q = new Queue();

static void ExecutionThread()

{

Task t = (Task)q.Dequeue();

t.Execute();

}

static void Main(string[] args)

{

q.Enqueue((object) new Program());

ExecutionThread();

}

}

}

注意,编译器自动将Task的实例转换为Enqueue()object,因为objectTask的基类。然而,来自Dequeue()的重铸需要显式的强制转换,因为从表面上看,编译器无法知道队列包含一个Task对象。事实上,您可以将Enqueue()行改为如下所示:

q.Enqueue(new Program());

该代码片段编译无误。现在尝试执行它:

Unhandled Exception: System.InvalidCastException: Unable to cast

object of type 'Task.Program' to type 'Task.Task'.

at Task.Program.ExecutionThread()

at Task.Program.Main(String[] args)

您的程序编译时没有任何错误,但在执行时会产生一个错误。如果它在您的主代码路径中,您肯定会在测试时发现它,但是如果它只存在于一些很少使用的、只在特定情况下调用的例程中,会发生什么呢?这可能会变得很难看。当我们切换到泛型时,我们将回头解决这个问题。同时,让我们回到样本。

线

为了让我们的示例更有意义,我们希望将Enqueue()Dequeue()任务放在不同的线程上。为此,我们利用了System.Threading名称空间。

线程类

让我们转向 Visual Studio 对象浏览器。展开[mscorlib] System.Threading下的Thread类,找到Thread类。我们对这个构造器重载感兴趣。

构造器

public Thread(System.Threading.ThreadStart start)

  • System.ArgumentNullException:start的说法是null

  • 例外:

  • start:一个System.Threading.ThreadStart委托,代表当这个线程开始执行时要调用的方法。

  • 成员:System.Threading.Thread的成员。

  • 描述:初始化System.Threading.Thread类的新实例。

  • 参数:

方法

我们将使用Start()Sleep()方法。

开始

public void Start()

  • System.Threading.ThreadStateException:线程已经启动。

  • System.Security.SecurityException:呼叫者没有合适的System.Security.Permissions.SecurityPermission

  • 没有足够的内存来启动这个线程。

  • 成员:System.Threading.Thread的成员。

  • 描述:使操作系统将当前实例的状态更改为System.Threading.ThreadState.Running

  • 例外情况:

睡眠

public static void Sleep(int millisecondsTimeout)

  • System.ArgumentOutOfRangeException:超时值为负,不等于System.Threading.Timeout.Infinite

  • 例外:

  • millisecondsTimeout:线程被阻塞的毫秒数。指定零(0)表示该线程应该被挂起,以允许其他等待线程执行。指定System.Threading.Timeout.Infinite无限期阻塞线程。

  • 成员:System.Threading.Thread的成员。

  • 描述:挂起当前线程一段指定的时间。

  • 参数:

ThreadStart 类

英寸 NET 中,线程是通过使用包含新线程的 main 方法的委托来初始化的。如您所见,Thread类接受了一个ThreadStart委托。

我们来看看描述。

构造器

public delegate void ThreadStart()

  • 成员:System.Threading的成员。
  • 描述:表示在System.Threading.Thread上执行的方法。
让线程工作

为了使用第二个线程,我们声明了一个不带参数并返回void的方法,以便它匹配ThreadStart委托的签名。接下来,我们创建一个ThreadStart的实例,并将其传递给Thread构造器,以便创建一个新线程。然后我们可以使用Thread类的Start()方法来启动这个线程。

把它放在一起

我们现在有两个线程在我们的队列上操作:主线程添加任务,后台线程执行它们。因为它们都在访问同一个数据对象,即队列,所以我们需要尽力确保它们轮流使用这个对象。如果我们不这样做,线程切换机制可能会在更新队列状态的过程中挂起一个线程,而恢复的线程可能会被传递一个无效状态的队列。为了确保线程轮流,我们在 C# 中使用了lock关键字:

static Queue q = new Queue();

lock(q)

{

//exclusive access to q here

}

使用。NET Reflector 在 IL 视图中,我们可以看到lock关键字在lock块的开头调用了System.Threading.Monitor.Enter(object),在结尾调用了System.Threading.Monitor.Exit(object)。通过这种方式,我们可以确保对q对象的访问被保留,直到该块完成,即使线程在lock块的中间被切换。

因此,我们得出以下结论:

using System;

using System.Collections;

using System.Threading;

namespace Task

{

class Task

{

private string taskname;

public Task(string s)

{

taskname = s;

}

public void Execute()

{

Console.WriteLine(taskname);

}

}

class Program

{

static Queue q = new Queue();

static Thread executionThread =

new Thread(new ThreadStart(ExecutionThread));

static void ExecutionThread()

{

while (true)

{

Task t;

lock (q)

{

if (q.Count == 0)

{

continue;

}

t = (Task)q.Dequeue();

}

if (t == null)

{

return;

}

t.Execute();

}

}

static void Main(string[] args)

{

executionThread.Start();

lock(q)

{

q.Enqueue(new Task("task #1"));

q.Enqueue(new Task("task #2"));

q.Enqueue(null);

}

while (true)

{

Thread.Sleep(10);

lock (q)

{

if (q.Count == 0)

{

break;

}

}

}

}

}

}

在这个示例中,我们生成并启动执行线程。接下来,我们对各种任务使用Enqueue()方法,将一个 final null放入队列以指示所有的工作都已完成。然后,我们进入一个循环,等待任务完成。

需要注意的一件重要事情是,我们尽量减少了持有队列锁的时间,以便不干扰其他任务。在执行线程中,这采取的形式是将任务的执行推迟到锁被释放之后。

执行该程序会产生以下输出:

C:\>csc /nologo task.cs

C:\>task

task #1

task #2

转向泛型

让我们转到 C++/CLI 和。NET 2.0 中使用了一个更复杂的示例。为了便于转换,我们将先用 C# 来解释一下。

在洗车场工作

假设我们在模拟洗车。首先,每辆车都要用真空吸尘器清扫内部。接下来,它被移动到传送带上,传送带拖着它通过一台清洗其外部的机器。我们可以利用流水线作业:汽车可以在清洗外部的同时清洗内部。为了实现这一点,我们为每个内部和外部工作站维护一个单独的队列和专用的执行线程。流水线式洗车让我们能够在不影响等待时间的情况下增加洗车的吞吐量。

让我们看看 C# 中的代码:

using System;

using System.Collections.Generic;

using System.Threading;

namespace CarWash

{

class Car

{

private string CarName;

public override string ToString()

{

return CarName;

}

public Car(string s)

{

CarName = s;

}

}

class Program

{

static Queue<Car> washQueue = new Queue<Car>();

static Queue<Car> vacuumQueue = new Queue<Car>();

static Thread WashThread = new Thread(new ThreadStart(Wash));

static Thread VacuumThread = new Thread(new ThreadStart(Vacuum));

static void Wash()

{

for (; true; Thread.Sleep(10))

{

Car c;

lock (washQueue)

{

if (washQueue.Count == 0)

{

continue;

}

c = washQueue.Dequeue();

}

if (c == null)

{

break;

}

Console.WriteLine("-Starting Wash of {0}", c);

Thread.Sleep(1300);

Console.WriteLine("-Completing Wash of {0}", c);

}

}

static void Vacuum()

{

for(;true;Thread.Sleep(10))

{

Car c;

lock(vacuumQueue)

{

if(vacuumQueue.Count == 0)

{

continue;

}

c = vacuumQueue.Dequeue();

}

if (c != null)

{

Console.WriteLine("+Starting Vacuum of {0}", c);

Thread.Sleep(1000);

Console.WriteLine("+Completing Vacuum of {0}", c);

}

lock (washQueue)

{

washQueue.Enqueue(c);

}

if (c == null)

{

break;

}

}

}

static void Main(string[] args)

{

VacuumThread.Start();

WashThread.Start();

lock (vacuumQueue)

{

vacuumQueue.Enqueue(new Car("Volvo"));

vacuumQueue.Enqueue(new Car("VW"));

vacuumQueue.Enqueue(new Car("Jeep"));

vacuumQueue.Enqueue(null);

}

while (VacuumThread.IsAlive || WashThread.IsAlive)

{

Thread.Sleep(10);

}

}

}

}

现在让我们运行它:

C:\>csc /nologo carwash.cs

C:\>carwash

+Starting Vacuum of Volvo

+Completing Vacuum of Volvo

-Starting Wash of Volvo

+Starting Vacuum of VW

+Completing Vacuum of VW

+Starting Vacuum of Jeep

-Completing Wash of Volvo

-Starting Wash of VW

+Completing Vacuum of Jeep

-Completing Wash of VW

-Starting Wash of Jeep

-Completing Wash of Jeep

请注意,从输出中任务开始和完成的顺序可以看出任务的重叠以及依赖延迟。

C# 代码的回顾

这段代码的元素与前面的示例或多或少有些相同,但是有一些主要的区别。

在第一个例子中,我们使用来自System.CollectionsQueue类来管理任务队列。由于Queue类对类型System.Object的元素进行操作,我们必须将Task转换为基类System.Object,以便让它入队。这是由编译器自动完成的。当我们想让它出列时,我们必须显式地将其重新转换回Task。编译器在编译时不会捕捉到对错误类型的重新转换,这可能会导致运行时错误。

队列的通用版本

在这个例子中,我们使用了通用版本的Queue,称为Queue<T>。使用对象浏览器,我们可以发现这个类是System.dll而不是mscorlib的一部分,并且存在于System.Collections.Generic名称空间中。现在,不要被这些类有相似名字的事实所误导。QueueQueue<T>是完全不同的档次。它们各自代表不同的类型。Queue<T>具有更复杂的类型,因为它依赖于类型参数T。由于这些类型完全不同,编译器在它们之间没有定义任何隐式或显式的转换。此外,在指定类型参数T之前,Queue<T>是早期类型。当使用带有定义的类型参数的Queue<T>的代码被执行时,运行时使用适合类型参数T的中间语言专门处理Queue<T>。这可能因T是引用类型还是值类型而异。

让我们检查方法。

使…入队

public void Enqueue(T item)

  • item:添加到System.Collections.Generic.Queue<T>的对象。引用类型的值可以是null

  • 成员:System.Collections.Generic.Queue<T>的成员。

  • 描述:在System.Collections.Generic.Queue<T>的末尾添加一个对象。

  • 参数:

出列

public T Dequeue()

  • System.InvalidOperationException:System.Collections.Generic.Queue<T>为空。

  • 例外:

  • System.Collections.Generic.Queue<T>开始处移除的对象。

  • 成员:System.Collections.Generic.Queue<T>的成员。

  • 描述:移除并返回System.Collections. Generic.Queue<T>开头的对象。

  • 返回值:

分析

如你所见,Queue<T>Enqueue()Dequeue()的方法与Queue中的方法相似。在这种情况下,“差异万岁”似乎是一个合适的短语。Enqueue()方法接受类型为T的项目,而Dequeue()返回类型为T的项目。在我们的示例代码中,我们通过管道移动类Car的实例。因为我们想要一个Car的队列,所以我们首先创建一个Queue<T>的实例,其中TCar:

static Queue<Car> washQueue = new Queue<Car>();

如果我们把Queue<Car>看作一个单独的标识符,我们会发现这是一个对构造器的简单调用。接下来,我们可以让类Car的实例入队和出队,编译器会为我们做所有的类型检查:

Car c;

washQueue.Enqueue(c);

//

c = washQueue.Dequeue();

这就是全部了。不管怎样,Queue是一个行为与Queue<object>非常相似的类,当然,它们并不完全相同。但是它们完成相同的事情——它们各自管理一个类型为objectSystem.Object的元素队列。

我们还使用IsAlive属性来管理洗车处的线程。我们使用这个属性来等待清空和清洗线程完成。

伊萨维

public bool IsAlive { get; }

  • true如果该线程已经启动并且没有正常终止或中止。

  • false否则。如您所见,IsAlive是一个只读属性。它有一个get访问器,但没有set访问器。

  • 成员:System.Threading.Thread的成员。

  • 描述:获取一个值,该值指示当前线程的执行状态。

  • 返回值:

转移到 C++/CLI

在这一节中,我们将回顾泛型的 C++ 语法。在描述名称空间之类的东西时,我也会切换到 C++ 语法。

在 C# 中创建和定义泛型类的语法与 C++ 非常不同。然而,使用和消费它们的语法几乎是相同的。这类似于托管数组在 C# 和 C++ 中声明方式的不同,但它们在这两种语言中的用法是相似的。

由于语法上的差异,我们首先将 CarWash 程序转换为 C++/CLI,并检查 C++/CLI 中泛型的定义和用法。接下来,我们将开始创建自己的泛型类。

现在让我们将洗车程序示例翻译成 C++/CLI:

#using <System.dll>

#include <msclr\lock.h>

using namespace msclr;

using namespace System;

using namespace System::Collections::Generic;

using namespace System::Threading;

namespace CarWash

{

ref class Car

{

private:

String ^CarName;

public:

virtual String ^ ToString() override

{

return CarName;

}

Car(String ^s)

{

CarName = s;

}

};

ref class Program

{

static Queue<Car^> ^washQueue = gcnew Queue<Car^>();

static Queue<Car^> ^vacuumQueue = gcnew Queue<Car^>();

static Thread ^washThread =

gcnew Thread(gcnew ThreadStart(wash));

static Thread ^vacuumThread =

gcnew Thread(gcnew ThreadStart(vacuum));

static void wash()

{

for (; true; Thread::Sleep(10))

{

Car ^c;

{

lock l(washQueue);

if (washQueue->Count == 0)

{

continue;

}

c = washQueue->Dequeue();

}

if (c == nullptr)

{

break;

}

Console::WriteLine("-Starting wash of {0}", c);

Thread::Sleep(1300);

Console::WriteLine("-Completing wash of {0}", c);

}

}

static void vacuum()

{

for(;true;Thread::Sleep(10))

{

Car ^c;

{

lock l(vacuumQueue);

if(vacuumQueue->Count == 0)

{

continue;

}

c = vacuumQueue->Dequeue();

}

if (c != nullptr)

{

Console::WriteLine("+Starting vacuum of {0}", c);

Thread::Sleep(1000);

Console::WriteLine(

"+Completing vacuum of {0}", c);

}

{

lock l(washQueue);

washQueue->Enqueue(c);

}

if (c == nullptr)

{

break;

}

}

}

public:

static void Main(...array<String^> ^ args)

{

vacuumThread->Start();

washThread->Start();

{

lock l(vacuumQueue);

vacuumQueue->Enqueue(gcnew Car("Volvo"));

vacuumQueue->Enqueue(gcnew Car("VW"));

vacuumQueue->Enqueue(gcnew Car("Jeep"));

vacuumQueue->Enqueue(nullptr);

}

while (vacuumThread->IsAlive || washThread->IsAlive)

{

Thread::Sleep(10);

}

}

};

}

void main()

{

CarWash::Program::Main();

}

事实证明,这个示例在翻译成 C++/CLI 时有些困难。除了一般的翻译,我想在下面的部分指出一些。

推翻

要覆盖一个虚方法,我们需要在 C++/CLI 中同时使用virtualoverride关键字。还有,关键词的顺序不一样。

在 C# 中,我们可以使用下面一行来覆盖一个虚方法:

public override string ToString()

在 C++/CLI 中,我们需要这个:

public:

virtual String ^ ToString() override

添加对 System.dll 的引用

当我们使用Queue<T>时,我们不仅需要告诉编译器哪个名称空间包含该类,还需要告诉编译器哪个汇编 DLL。这可以通过 C# 或 C++/CLI 命令行或者通过代码中的显式#using引用来完成。您也可以使用 IDE 指定对此 DLL 的引用。在代码中指定引用,如下所示:

#using <System.dll>

如果您忽略添加这一行,编译器会发出一个语法错误,因为它没有发现标识符Queue被定义,或者它将它与mscorlib.dll中定义的System::Collections::Queue类混淆,后者不是泛型。

翻译 lock 关键字

C# 中的lock关键字使用System::Threading::Monitor类生成一组块。在 C# 中,下面的代码:

class Program

{

public static void Main()

{

string s = "hello";

lock(s)

{

//inside the lock

}

}

}

相当于以下代码:

using System.Threading;

class Program

{

public static void Main()

{

string s = "hello";

Monitor.Enter(s);

try

{

//inside the lock

}

finally

{

Monitor.Exit(s);

}

}

}

Monitor.Enter()声明在try块期间独占访问对象。当try程序块完成时,无论是否出现异常,都使用Monitor.Exit()释放对象。

进入

public static void Enter(object obj)

  • System.ArgumentNullException:obj的说法是null

  • 例外:

  • obj:获取监视器锁的对象。

  • 成员:System.Threading.Monitor的成员。

  • 描述:获取指定对象的独占锁。

  • 参数:

出口

public static void Exit(object obj)

  • System.Threading.SynchronizationLockException:当前线程不拥有指定对象的锁。

  • System.ArgumentNullException:obj的说法是null

  • 例外情况:

  • obj:要解除锁定的对象。

  • 成员:System.Threading.Monitor的成员。

  • 描述:释放指定对象上的独占锁。

  • 参数:

锁定 onc++/cli

在 C++/CLI 中可以使用完全相同的结构:

using namespace System;

using namespace System::Threading;

ref class Program

{

public:

static void Main()

{

String ^s = "hello";

Monitor::Enter(s);

try

{

//inside the lock

}

finally

{

Monitor::Exit(s);

}

}

};

此外,在 C++ 中还有其他创建锁的方法,因为它具有对象的确定性销毁。其中 C# 将析构函数视为。NET 终结器,C++/CLI 有显式终结器和析构器,一旦对象超出范围,它们就能够释放资源。使用IDisposable接口和。NET Dispose()模式,但是要繁琐的多。在 C++ 中,这是自动的。分配一个类的实例,当块结束时它被销毁。因此,可以使用构造器来声明锁,使用析构函数来释放锁。

实现这一点的 C++ 实现如下所示:

using namespace System;

using namespace System::Threading;

ref struct Locker

{

Object ^o;

Locker(Object ^s)

{

o = s;

Monitor::Enter(o);

}

∼Locker()

{

Monitor::Exit(o);

}

};

ref class Program

{

public:

static void Main()

{

String ^s = "hello";

{

Locker lk(s);

//inside the lock

}

}

};

我们首先创建一个名为Locker的类,它是System::Threading:: Monitor类的容器。当进入锁定节时调用构造器,当退出锁定节时调用析构函数。

下面的代码行创建了一个Locker的实例,并使用参数s调用构造器:

Locker lk(s);

当实例变量lk在下一个右花括号处超出范围时,Locker的析构函数会和Monitor::Exit()一起被调用。

注意,即使锁内的代码抛出异常,析构函数也会被调用。例如,让我们修改前面的代码以显示状态并抛出异常:

using namespace System;

using namespace System::Threading;

ref struct Locker

{

Object ^o;

Locker(Object ^s)

{

o = s;

Console::WriteLine("Lock acquired");

Monitor::Enter(o);

}

∼Locker()

{

Console::WriteLine("Lock released");

Monitor::Exit(o);

}

};

ref class Program

{

public:

static void Main()

{

String ^s = "hello";

{

Locker lk(s);

Console::WriteLine("throw exception");

throw;

}

}

};

void main()

{

try

{

Program::Main();

}

catch(Exception ^e)

{

Console::WriteLine("catch exception");

}

}

如果我们运行它,我们会看到以下内容:

C:\>cl /nologo /clr:pure test.cpp

C:\>test

Lock acquired

throw exception

Lock released

catch exception

这个输出表明,没有留下锁的危险,在 C++/CLI 中,确定性类构造和销毁是对try finally块的可行替代。

要在 C# 中做同样的事情,您可以利用using的变体将实例的范围限制在一个具有实现System::IDisposable的类的块中。C# 中此方法的原型如下:

using System;

class Locker : IDisposable

{

public void Dispose()

{

}

public Locker()

{

}

∼Locker()

{

}

}

class Program

{

public static void Main()

{

using(Locker l = new Locker())

{

}

}

}

预定义的锁类

包含文件msclr\lock.h定义了一个能够锁定资源的复杂类。它使用System::Threading::ReaderWriterLock以及模板编程。

为了利用它,我们只需添加以下几行:

#include <msclr\lock.h>

using namespace msclr;

然后我们可以像实例化Locker类一样实例化msclr::lock类:

{

lock l(vacuumQueue);

// locked code

}

// destructor has been called and lock is released.

编译并运行

让我们编译 CarWash,并检查结果:

C:\>cl /clr:pure /nologo carwash.cpp

C:\>carwash.exe

+Starting vacuum of Volvo

+Completing vacuum of Volvo

-Starting wash of Volvo

+Starting vacuum of VW

+Completing vacuum of VW

+Starting vacuum of Jeep

-Completing wash of Volvo

-Starting wash of VW

+Completing vacuum of Jeep

-Completing wash of VW

-Starting wash of Jeep

-Completing wash of Jeep

在 C++/CLI 中创建泛型类

在 C++/CLI 中,泛型类型的语法是根据模板的语法建模的。模板与泛型相似,都接受类型参数。它们之间的主要区别在于模板是在编译时处理的,而泛型是在运行时处理的。由于这种区别,模板变得更加复杂和强大,我们将在第十五章中详细介绍这一点。现在,重要的是要认识到泛型的 C++/CLI 语法是基于 C++ 模板的语法。

C# 中声明了一个带有单个类型参数T的基本泛型类,如下所示:

class R<T> {}

同一个类在 C++/CLI 中声明如下:

generic <typename T> ref class R {};

在 C++/CLI 中,有必要告诉编译器T是一个类型参数,而这在 C# 中是隐式的。原因是 C++ 有几种不同类型的模板参数,包括以下几种:

  • 类型模板参数
  • 非类型模板参数
  • 模板模板参数

下的泛型。NET 2.0 中,唯一支持的场景是T表示一个类型,但是为了语法的一致性和在当前 C++ 编译器语法下实现的容易性,需要长形式的声明。

在类的定义中,字母T被自由地用作类型名的替代,但有一些注意事项。

当你写一个泛型类时,编译器不知道你打算用什么类型作为泛型参数。因此,该类必须针对所有类型进行编译,而不仅仅是您所想的类型。Queue<T>类是一个很好的例子,它对项目的句柄进行操作,而不对项目本身做任何事情。编译器不需要知道任何关于类型T的信息,只需要知道它是从System::Object派生出来的。所有的泛型类型都被认为是从System::Object派生的。可以使用模板、泛型约束或 cast 运算符创建更多可以处理数据实例本身的专用类。下两章关于模板和高级泛型和类将帮助你编写更强大的类。

下面是一个简单的泛型类:

using namespace System;

generic <typename T>

ref struct Test

{

static void Print(T t)

{

Console::WriteLine(t->ToString());

}

};

int main()

{

Test<int>::Print(3);

}

编译和运行之后,我们得到

C:\>cl /nologo /clr:pure test.cpp

C:\>test

3

在这段代码中,我们创建了一个名为Test的泛型类。在它的内部,我们有一个名为Print()的静态方法,它调用ToString()。我们的类型TSystem::Object继承了方法ToString()。注意,唯一可以在T实例上使用的方法是那些来自System:Object的方法。

例如,考虑以下代码:

using namespace System;

ref struct Hello

{

void Function()

{

Console::WriteLine("Hello!");

}

};

generic <typename T>

ref struct Test

{

static void Run(T t)

{

t->Function();

}

};

void main()

{

Hello ^ hello = gcnew Hello();

Test<Hello^>::Run(hello);

}

这个会编译吗?我们要担心的片段就在这里:

static void Run(T t)

{

t->Function();

}

为了编译这段代码,类型T需要有一个可以调用的方法Function(),对吗?在这种情况下,当调用Run()时,我们传递Hello^作为我们的类型T。这似乎是合理的,因为当Run()执行时,它发现Hello^有一个方法叫做Function()并且这段代码编译了,对吗?

让我们试一试:

C:\>cl /nologo /clr:pure test.cpp

test.cpp

test.cpp(13) : error C2039: 'Function' : is not a member of 'System::Object'

c:\windows\microsoft.net\framework\v2.0.50727\mscorlib.dll :

see declaration of 'System::Object'

泛型类(或函数)中的代码只有在每一个可能为T插入的类型对代码都有效时才编译。在这种情况下,编译器做出的唯一假设是T派生自System::Object;因此,您会得到所述的错误消息。有几种方法可以做到这一点,我们将在第十五章和第十六章中详细介绍,但我想在这里介绍一下。

使用约束

约束告诉编译器泛型类型总是具有某些特征。一种可能是说类型THello作为基类。为了声明这一点,我们将Test的通用声明改为如下:

generic <typename T>

where T : Hello

ref struct Test

{

static void Run(T t)

{

t->Function();

}

};

我们添加了这一行:

where T : Hello

这一行指示编译器假设HelloT的基类。现在让我们编译它:

C:\>cl /nologo /clr:pure test.cpp

C:\>test

Hello!

当然,如果我们修改main()来传递一个不是从Hello派生的类型,我们会得到一个编译错误。尝试以下几行:

void main()

{

Test<int>::Run(0);

}

并编译代码:

C:\>cl /nologo /clr:pure test.cpp

test.cpp

test.cpp(21) : error C3214: 'int' : invalid type argument for generic

parameter 'T' of generic 'Test', does not meet constraint 'Hello ^'

test.cpp(12) : see declaration of 'Test'

有关约束的更多信息,参见第十六章。

使用模板

泛型是运行时机制,其中的代码必须适用于所有类型。模板是编译时机制,其中的代码可以针对特定类型进行特殊化。如果在我介绍约束之前,我们仅仅将原始示例代码中的单词generic改为template,我们将得到如下结果:

C:\>cl /nologo /clr:pure test.cpp

C:\>test

Hello!

一切正常。如果我们使用经过修改的传递一个int而不是一个Hello^main(),我们得到

C:\>cl /nologo /clr:pure test.cpp

test.cpp(14) : error C2227: left of '->Function' must point to

class/struct/union/generic type

type is 'int'

test.cpp(13) : while compiling class template member function

'void Test<T>::Run(T)'

with

[

T=int

]

test.cpp(20) : see reference to class template instantiation

'Test<T>' being compiled

with

[

T=int

]

关于模板的更多信息,参见第十五章。

使用石膏

处理这个问题的第三种方法是让编译器假设泛型类型参数只从System::Object派生,并在运行时将其转换为所需的类型。因为这不是类型安全的,所以我们想多添加一点代码来以智能的方式实现这一点。使用 C# isas关键字的等价物,我们如下构造代码,检查类型是否确实是从

Hello^:

static void Run(T t)

{

if(t->GetType() == Hello::typeid)

{

Hello ^hello = safe_cast<Hello^>(t);

hello->Function();

}

}

有关铸造机制的更多信息,参见第十六章。

通用函数

当您只需要特定函数的类型参数而不是整个类时,可以使用泛型函数。泛型函数比泛型类简单,其优点是编译器通常可以从函数的参数中自动推导出函数的类型参数。要声明泛型函数,可以使用与泛型类相似的语法。

下面是一个简单的通用函数,它只打印出类型参数的类型:

using namespace System;

generic <typename T>

void Function(T t)

{

Console::WriteLine(t->GetType());

}

ref class Test {};

int main()

{

Function(0);

Function<short>(0);

Test ^t = gcnew Test();

Function(t);

}

我们第一次调用Function()的时候,使用了类型演绎,编译器确定0的类型是int。在第二个调用中,我们明确声明我们正在传递一个short 0。在最后一个调用中,我们回到了类型演绎,因为编译器确定Function()被传递了一个类型为Test^的实例。

现在让我们编译并执行它:

C:\>cl /nologo /clr:pure test.cpp

C:\>test

System.Int32

System.Int16

Test

摘要

现在,您应该对泛型有了基本的了解,并且对。NET 框架。现在让我们进入下一个阶段。我们将在此基础上构建并解决第十五章中的模板和第十六章中的高级泛型。