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

58 阅读47分钟

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

原文:C++ 2013 for C# Developers

协议:CC BY-NC-SA 4.0

九、指针和不安全代码

为了你的工作可以增加人类的幸福,你应该了解应用科学是不够的。对人类自身及其命运的关注必须始终构成所有技术努力的主要兴趣;关心劳动组织和商品分配等尚未解决的重大问题,以使我们头脑中的创造成为人类的幸福而不是诅咒。在你的图表和方程式中,永远不要忘记这一点。—阿尔伯特·爱因斯坦

从一开始,指针的使用就是 C 和最终 C++ 中最好和最差的(尽管许多人会认为 C++ 模板的深奥使用给了指针一个机会)。没有任何其他特性能够让您剥去编程语言的外衣,直达核心,也没有任何其他特性容易产生隐藏的错误,而这些错误可能会在几年内不被发现。

C# 视图:是福是祸

C# 和。NET Framework 已经做了大量工作,使得在日常编程中不必使用指针。大多数常见的任务现在可以写成安全、可验证的代码。整个垃圾收集系统的设计考虑到了指针的废弃。引用类型,实现为分配的数据和指向。NET 中,在 C# 中显示为单个实体。句柄上的类型安全也被强制以避免指针问题。

可验证的代码更可取,因为它允许执行系统随时知道所有分配项的类型;它保证了所有类型的通用异常处理系统;它还提供了一个无泄漏、公共内存、托管内存分配系统。

尽管如此,C# 的设计者还是克制了自己,没有完全省略 C# 中的指针。

在使用unsafe关键字并使用/unsafe编译器选项编译的代码中,支持在 C# 中使用指针。另一方面,C++ 支持指针作为其主要编程模型的一部分。一个有趣的事实是,C# 默认情况下是可验证的,否则需要关键字或编译器选项来编译,而 C++ 默认情况下是不可验证的,使用编译器选项来编译可验证的。

指针:定义和警告

那么,到底什么是指针呢?指针是一种包含另一种数据类型地址的数据类型。这似乎没有什么害处,但是有一个问题:假设你设置一个指针指向一个类型的实例。这个想法是,你可以单独使用指针来引用实例。您可以将这个指针传递给其他方法,每个方法都在处理它认为是指向数据类型实例的指针;这通常很好。当数据超出范围,或者某个指针持有者以其他人意想不到的方式更改了它所指向的数据时,事情就开始出错了,因此,它不是指向堆栈上的数据实例,而是从一个完全不同的方法指向堆栈数据。那么,使用指针的方法实际上并没有写入数据实例;是写垃圾。

指针错误最常见的发生方式是指针和数据不同步;然后,指针和数据之间的契约,也就是指针指向有效数据的契约就被打破了。也许指针是旧的,或者数据不再存在,指针不指向任何地方。或者,指针可以作为访问数据的唯一方式开始,这在使用new时很常见,后来指针超出范围,对数据的访问就丢失了。如果没有垃圾收集机制,这将导致不可引用或孤立的数据对象以及内存泄漏。

我分享一个朋友的战争故事作为例子。

HOW TO BRING A 5,000,000-FAULT–TOLERANT SERVER TO ITS KNEES BY PAUL CAYLEY

我假设所有阅读这本书的人都知道什么是内存泄漏,并且可能实际上已经造成了一些。尽管聪明人尽了最大努力,内存泄漏仍然司空见惯。当我听说 Java 将终结内存泄漏时,我不知道是应该欣喜若狂,还是应该持怀疑态度。“怀疑”似乎是正确的答案,因为贸易出版物充斥着消除 Java 内存泄漏工具的广告。

为什么内存泄漏如此难以消除?有很多原因。我认为最大的一个问题是设计时的决策对下游是不透明的。内存分配和释放发生在不同的时间和不同的地方。架构师和设计师没有充分地预见到他们的代码将如何被使用。更糟糕的是,有些人甚至认为其他人会阅读他们的文档和行内注释,并正确理解其中隐含的智慧和警告。(当然,这假设程序员记录了他们的代码,这可能是也可能不是。)

当然,愚蠢和草率也可能是原因之一。程序员可能会忘记他们正在做什么或已经做了什么。因为有些人写的代码很差——最终可能会链接到您的代码,调用您的位,甚至成为您的位——内存可能会一字节一字节地泄漏!我们来看一些例子。

还记得微软 Outlook 吗?Outlook 的早期版本每小时会泄漏大约 100KB 的内存,就在那里。因此,如果你习惯于连续几周打开电脑,你必须每隔几天退出并重新启动 Outlook。如果你不这样做,大约一周后,你的系统会变得缓慢和不可预测。

不幸的是,大多数程序员都希望事情能够正常运行,错误检查有时是不完整的或者完全被忽略了。在malloc()空手返回后,一些程序可能继续前进,丢弃位并产生错误。该场景中的步骤如下:

Outlook leaks memory to the point that other programs and/or the OS become starved.   Calls to malloc() start failing in new and unusual places.   Even though memory is freed by exiting Outlook, things are no longer stable.   You bring up the Task Manager to kill the lingering Outlook Messaging Application Programming Interface (MAPI) pump, but things are iffy still.   You rename outlook.exe to lookout.exe as a reminder and reboot.  

没什么大不了的,您必须重新启动您的工作站——当然,除非您处于一个终端服务器环境中,在这个环境中,一个单独的机器托管许多用户会话。然后,您还可以享受 Outlook 的多个实例和谐地一起工作来泄漏大量内存的乐趣。当你重新启动时,每个人都必须重新开始——快乐,快乐,快乐,快乐!

让我们看看长时间运行流程的另一个场景——系统状态监视器。在这个场景中,您有一个支持关键任务应用程序的大型容错服务器。你不仅要为一个装有多个电源、RAID 驱动器、冗余网络等的盒子支付 50 万美元,还要为一些系统监控工具支付 5 万美元。这个系统将在未来十年全天候运行。然后,发布了操作系统的服务包,并公开了新的代码路径。监控应用程序运行良好,不会泄漏任何内存。不幸的是,检索 OS 状态更新的 OS API 确实会泄漏内存,所以每次通过这个 API 调用时,都会返回一个记录集,并且会丢失 50 字节或更多的内存。大约 3 个月后,监控软件将完成它的工作,重新启动防弹镀金服务器。

简而言之,内存泄漏是邪恶的。电脑不错。但是没有恶就没有善吗?

有效的目标和语法

在 C# 中,指针可以设置为值类型的地址或另一个指针。此外,指针可以自由地转换为其他指针。

由于托管堆中的对象被垃圾收集器以看似任意的时间间隔移动。NET 语言必须限制指向托管堆上对象的指针。C# 指针主要设计用于指针或值类型,它们驻留在堆栈上。C++/CLI 和 C# 都允许您临时固定对象在托管堆上的位置,尽管它们使用不同的语法。阻塞垃圾收集机制,即使是短暂的阻塞,看起来也很危险,但是对于调用本机 API(称为 InterOp)来说,这是非常有用的。我们将在第十九章和第二十章中再次讨论这些话题。

常见指针运算符

表 9-1 中列出了 C# 指针操作符。所有这些运算符都存在,并且在 C++ 中具有相同的定义和用法。

表 9-1。

Common Pointer Operators in C++ and C#

| 操作员 | 意义 | | --- | --- | | `&` | 取一个值的地址。这是 C++ 命名法中运算符的地址。 | | `*` | 获取指针或引用指向的值。这是 C++ 中的解引用运算符。当你使用它的时候,你正在解引用一个指针。 | | `->` | `ptr->`是`(*ptr)`的别名。`ptr->`是会员接入运营商。当`ptr`指向一个 C# `struct`或者值类型的实例,并且你想要访问该结构的一个成员时,这是一个方便的快捷方式。 |

指针用法示例

下面是一些在 C# 和 C++/CLI 中使用指针的简短例子。

声明一个指向整数的指针:

int *ptr;

将整数的地址分配给整数指针:

int i;

ptr = &i

通过取消对指针的引用,为原始整数赋值:

*ptr = 3;

此时,整数i被赋予3的值。现在让我们将这个功能包装在一个程序中并尝试一下。

C# 中可验证的代码和指针用法

由于指针的使用是不可验证的,C# 将指针操作符的使用限制在标有unsafe关键字的块中。此外,编译时必须指定/unsafe命令行选项。这可以直接从命令行完成,也可以通过在 Visual Studio IDE 的“项目属性”对话框的“生成”选项卡中选中相应的框来完成。例如,看看下面这个程序,叫做test.cs:

class R

{

static void Main()

{

int i;

unsafe

{

int *ptr = &i;

*ptr = 3;

}

System.Console.WriteLine(i);

}

}

要编译和运行该程序,请执行以下操作:/nologo选项取消版权信息:

csc /unsafe /nologo test.cs

test

您应该会收到以下输出:

3

如您所见,/unsafe命令选项用于指示编译器接受unsafe关键字的用法。如果您忽略了使用/unsafe选项进行编译,您会看到下面的诊断,如果编译器正在工作的话:

test.cs(12,9): error CS0227: Unsafe code may only appear if compiling with /unsafe

编写不安全代码的副作用

编写不安全的代码有一些有趣的副作用。因为可以使用指针间接初始化变量,所以使用不安全代码会影响编译器检测未初始化变量的能力。例如,考虑下面的 C# 片段:

int i;

System.Console.WriteLine(i);

如果您在程序的上下文中编译它,您会看到以下诊断信息:

test.cs(9,29): error CS0165: Use of unassigned local variable 'i'

如果您将此代码包装在一个不安全的块中,并添加了一个指针引用,如下所示:

unsafe

{

int i;

int *p = &i;

System.Console.WriteLine(i);

}

即使变量i仍未初始化,编译器也不会进行诊断。

如果您随后执行这个块,您会看到一个未初始化变量的默认值:

C:\>test

0

在这种情况下,这没什么大不了的,但是您可以看到使用不安全的块和指针是如何限制编译器帮助您编写可靠代码的能力的。

C++ 中的指针用法

C++ 指针类似于 C# 指针,有许多相同的限制。基本语法是相同的,考虑到语言的历史和发展,这并不奇怪。有一些重要的区别,所有这些都证明了 C++ 的强大:

  • C++ 不要求在源代码中使用unsafe关键字。
  • C++ 有四个编译选项;C# 有安全和不安全之分。
  • C++ 允许指针指向数组。
  • C++ 允许指向本地函数的指针。
  • C++ 允许指针指向独立于实例的成员。
  • C++ 允许在同一个声明中混合和组合指向数组的指针、指向指针的指针、指向成员的指针和指向本机函数的指针。

因为本章的目标是介绍指针和相关概念,所以我们将高级方面的讨论推迟到第十八章和第十九章进行。毕竟,在你准备好之前,你不想花太多时间去尝试解码一个像下面这样的有效 C++ 声明!

void (**(*(*p)(int, char))[])(int);

C++ 中的可验证代码

/clr开关决定了如何编译 C++ 代码以适应 CLR。它规定了编译器在禁止指针和非托管类型等构造时需要有多严格,以及确定您的代码是否被编译为在中的 CLR 下运行。或者作为独立的本机可执行文件。

以下是用 C++ 编译代码的几种方法:

  • 产生一个仅可验证的 IL 输出文件,并且只能用于托管类型和托管代码。
  • /clr:pure生成一个仅包含 IL 的输出文件(没有本机可执行代码),并且只能用于托管和本机类型以及托管代码。
  • 产生本地和 IL 文件的混合。允许托管和本机类型以及托管代码和本机代码。
  • <default>表示没有指定选项。该程序为本机执行而编译。

此外,为了与 Visual C++ 2002 和 2003 兼容,还提供了另外两个选项:

  • /clr:oldSyntax接受 Visual C++ 2002 和 2003 中的托管扩展语法。
  • /clr:initialAppDomain表示使用 Visual C++ 2002 的初始AppDomain 1 行为。

一般来说,C# 默认生成可验证的代码,但是可以使用指针和使用unsafe关键字或命令行选项的不可验证的代码。C++ 希望您在命令行上定义目标可执行文件,因为 C# 和 C++ 倾向于反映不同的范例,而不是像 C# 那样,通过可选的优化或调整来反映可验证的代码。在 C++ 中,与 C# 编译直接对应的是/clr:safe,但是这种模型不允许您利用 C++ 的大部分真正功能。出于这个原因,我一般更喜欢使用/clr:pure,并根据需要切换到/clr:safe/clr

现在,让我们采用与 C# 示例完全相同的指针用法,并将其转换为 C++/CLI:

ref struct R

{

static void Main()

{

int i;

int *ptr = &i;

*ptr = 3;

System::Console::WriteLine(i);

}

};

void main() {R::Main();}

如您所见,程序中 C++ 指针的用法与 C# 版本相同。因为指针是不可验证的,或者是不安全的,我们应该把它编译成pure。输入以下行:

cl /clr:pure test.cpp

这编译成一个 CLR 可执行文件,将3写入控制台,就像 C# 版本一样。

另一方面,如果您试图将其编译为safe,您会看到以下内容(/nologo选项隐藏了版权信息):

cl /clr:safe /nologo test.cpp

test.cpp

test.cpp(6) : error C4956: 'int *' : this type is not verifiable

如果您试图在没有任何形式的clr标志的情况下编译这段代码,您会收到一条更加深奥的消息:

cl /nologo test.cpp

test.cpp

test.cpp(2) : error C2143: syntax error : missing ';' before '<class-head>'

test.cpp(2) : error C4430: missing type specifier - int assumed. Note: C++ does not

support default-int

test.cpp(8) : error C2653: 'System' : is not a class or namespace name

test.cpp(8) : error C3861: 'WriteLine': identifier not found

如果不指定/clr选项,编译器会将这段代码编译为本机 C++。在原生 C++ 中,ref不是一个关键字,所以编译器不知道它指示了什么特殊的东西,你得到的东西遵循旧的计算机科学规则,即编译器垃圾进,垃圾出。当编译器似乎抛出看似无意义的诊断时,在 IDE 或 makefile 中仔细检查clr标志的设置总是一个好主意。

BEHIND THE SCENES

在前面的例子中,理解解析器在做什么是很有启发性的。当它看到关键字class时,它意识到,因为类只能在某些地方声明,所以之前的类应该已经完成了,所以它输出下面的诊断:

test.cpp(2) : error C2143: syntax error : missing ';' before '<class-head>'

注意,这发生在第 2 行,因为解析器在对表达式作出判断之前继续到左花括号。

error C4430: missing type specifier - int assumed. Note: C++ does not

support default-int

下一个错误消息是传统 C 语言的遗留问题,它允许您在不指定类型的情况下声明变量或函数。默认情况下,该类型将被标识为int。毫不奇怪,这种行为被称为“??”。

因此,在传统的 C # 中,以下声明是有效的:

ref;

这一行声明ref是一个int类型的全局变量。

C++ 句柄

在 CLR 中,引用类型分为两个实体:托管堆上的对象和这些对象的句柄。C# 模糊了这种划分,并提供了允许您像处理对象本身一样处理句柄的语法。另一方面,C++/CLI 将句柄视为指向托管堆上的对象的指针。这样做时,它使用了本章中概述的指针语法。

让我们在这里回顾一下语法。首先让我们声明一个引用类型和一个值类型:

//declare a reference type

ref struct R { int i; };

//declare a value type

value struct V { int j; };

现在让我们实例化它们(在方法的上下文中):

V v;

R ^ r = gcnew R();

变量v已经被分配到堆栈上。变量r也被分配在堆栈上,它是一个句柄,或者托管指针,指向一个分配在托管堆上的R对象。

假设现在我们想要访问V中的字段变量j。我们将使用.字段访问操作符并编写如下代码:

v.j = 3;

在第三章的中,我展示了对于引用类型,你需要使用->成员访问操作符来代替。之前我也提到过ptr->(*ptr)的别名。让我们看看这些是如何组合在一起的。

使用指针语法将字段i设置为3:

r->i = 3;

常规语法中的等效语法如下:

(*r).i = 3;

在这种情况下,变量r位于堆栈上,是托管堆上的R对象的句柄或指针:

(*r)

这个表达式表明指针应该被解引用,这意味着“转到由变量r指向的对象。”换句话说,这个表达式返回托管堆上的实际对象。添加.i访问字段本身,这与 C# 或 C++ 中的堆栈分配值类型相同。

当然,使用->操作符更简单,但是了解语法很重要。

C++/CLI 地址运算符

我们已经知道了&操作符用于获取值类型或指针类型的地址。具体来说,在 C# 中,这意味着它可以获取堆栈上变量的地址。如果在 C++ 中使用这个操作符获取一个不在堆栈上的变量的地址,就会得到一个诊断。为了测试这一点,我们需要使用表达式(*r)来获取托管堆上的一个对象。让我们用 operator 的地址来确定它的地址,看看会发生什么:

&(*r);

如果您尝试编译它,您会看到以下诊断信息:

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

test.cpp

test.cpp(12) : error C3072: operator '&' cannot be applied to an instance of a

ref class

use the unary '%' operator to convert an instance of a ref class to a

handle type

错误 3072 是这里的重要诊断,因为它指出操作符的地址不能用于托管堆中的对象。

为了允许您获取托管堆上对象的地址,C++/CLI 引入了%操作符。这个操作符相当于托管堆上对象的&操作符。

总之,表 9-2 显示了 C++/CLI 中本机指针和托管指针之间的对应关系。

表 9-2。

Native to Managed-Pointer Operator Correspondence in C++/CLI

| 操作员 | 堆栈/本机堆 | 托管堆 | | --- | --- | --- | | 地址 | `&` | `%` | | 指针声明 | `*` | `^` | | 解除…的关联 | `*` | `*` | | 例子 | `value struct V{ int i; };` | `ref struct R{ int i; };` | |   | `V v;` | `R ^r1 = gcnew R();` | |   | `V * pV = &v;` | `R ^r2 = %(*r1);` | |   | `v.i;` | `R r;` | |   | `pV->i;` | `R ^r3 = %r;` | |   |   | `r1->i;` | |   |   | `(*r2).i;` |

如您所见,根据目标是在托管堆、本机堆还是堆栈上,有不同的地址和指针声明操作符语法。另一方面,只有一个解引用操作符,因为解引用指针是明确的。在任何情况下,您最终都会得到一个表示堆栈或托管堆上的对象的表达式。

复杂的例子

下面是一个更复杂的例子,展示了如何获取指针的地址。注意,由于指针和句柄是在堆栈上分配的,我们在两种情况下都使用了&地址操作符:

static void Main()

{

V v;

V *pV = &v;

R ^ r;

R ^ * phR = &r;

}

这个例子为phR提供了以下有趣且更高级的声明:

R ^ * phR

在这种情况下,phR是一个指针,指向一个R对象的句柄。这一开始可能有点奇怪,但是你会慢慢习惯的。请注意以下详细信息:

  • *phR是一个R^指针。
  • **phR是一个R对象。

这是另一个有趣的构想:

V ^hV = %v;

这是正确的吗?它说,“给我一个驻留在托管堆上的值类型的句柄。”但是值类型不是活在栈上吗?这应该不会编译吧?事实证明,这可以很好地编译。为什么呢?原来,值类型V的装箱版本是在托管堆上自动创建的。然后,表达式用该对象的句柄设置变量hV

噩梦

由于在 C++ 中类型是递归定义的,所以除了 CLR 的限制之外,对您可以创建的噩梦般的表达式的类型没有什么限制。

例如,下面的有效表达式将变量p声明为一个指针,该指针指向一个采用intchar的函数,并返回一个指针数组,该数组指向采用int并将句柄返回给R的函数的指针:

R ^ (**(*(*p)(int, char))[])(int);

一般来说,这是你阅读复杂声明的方式:从中间的变量名开始。在这种情况下,变量名是p。从那里,向右看。如果你看到一个左括号,它就是一个函数。如果你看到一个方括号,它就是一个数组。如果您看到分号、右括号或什么都没有,请向左看。如果你看到一个^,那就是手柄。如果你看到一个*,那就是一个指针。从这里开始,继续向外扩展,注意圆括号,跳过已经使用过的标记。显然,这是一个说起来容易做起来难的算法。

复杂声明的好处是唯一的限制是你的想象力。

摘要

在这一章中,我向你介绍了指针和不安全代码,希望不会吓到你,让你放弃编程,搬到南极洲去。如果我失败了,一定要给我寄张明信片——最好是有企鹅或狗的明信片。

你现在可能不是指针方面的专家,但是考虑到本章中的贯穿和例子,你至少会知道如何识别它们以及在简单的情况下应用它们。在下一章,我们将看看 C++/CLI 中的属性和事件。

Footnotes 1

有关clr:initialAppDomain的详细信息,请参考 Visual C++ 文档。

十、属性和事件

年收入 20 英镑,年支出 1996 英镑,结果幸福。年收入 20 英镑,年支出 20 英镑应该和 6,导致痛苦。—查尔斯·狄更斯、大卫·科波菲尔

创建属性是为了在一个上下文中为数据提供类似字段的功能,该上下文允许程序员以与类型系统完全不同的方式访问数据或抽象数据。

普通字段是类中的简单类型声明。属性包含一个检索数据的方法(称为 getter ),一个存储数据的方法(称为 setter ),或者两者都包含在一个统一的语法中,该语法允许属性看起来和行为起来像一个字段。getter 和 setter 通常也被称为get访问器和set访问器。

属性是 C# 和 C++/CLI 的共同元素,尽管它们的语法有很大不同。属性目前还不是标准 C++ 的一个元素,尽管没有理由不在将来的某一天将它们添加到语言中并标准化。

C# 中使用属性的基本示例

假设我们想写一个Clock类,在这个类中我们将小时存储为 0 到 11 之间的一个数字,但是我们仍然希望调用者能够将小时作为 1 到 12 之间的一个数字来使用。我们可以使用 C# 中的属性以任何我们想要的方式存储数据,并使用 getters 和 setters 在格式之间转换数据。我不会在这里详述这种表述的优点;只要说它对于执行各种计算是有用的就够了。

这是一个用 C# 编写的示例:

using System;

class Clock

{

public int Hour

{

get

{

if(hour == 0)

{

return 12;

}

else

{

return hour;

}

}

set

{

hour = value % 12;

}

}

private int hour;

public static void Main()

{

DateTime t = DateTime.Now;

Clock c = new Clock();

c.Hour = t.Hour;

Console.WriteLine("The little hand is on the {0}", c.Hour);

c.Hour = 12;

Console.WriteLine("at midnight it will be {0} o'clock", c.Hour);

}

}

在本例中,Hour是一个属性。让我们来看看二传手:

set

{

hour = value % 12;

}

在这种情况下,变量value是 setter 的隐含输入。然后,我们的私有变量hour被设置为value12,将其转换为 0 到 11 之间的数字。

getter 处理另一个方向的交互。如果hour0,则返回12,其他值返回时钟小时:

get

{

if(hour == 0)

{

return 12;

}

else

{

return hour;

}

}

结果如下:

C:\>csc /nologo test.cs

C:\>test

The little hand is on the 7

at midnight it will be 12 o'clock

在 C++/CLI 中使用属性的基本示例

C++/CLI 中的类似程序如下:

using namespace System;

private ref class Clock

{

public:

property int Hour

{

int get()

{

if(hour == 0)

{

return 12;

}

else

{

return hour;

}

}

void set(int value)

{

hour = value % 12;

}

}

private:

int hour;

public:

static void Main()

{

DateTime t = DateTime::Now;

Clock ^c = gcnew Clock();

c->Hour = t.Hour;

Console::WriteLine("The little hand is on the {0}", c->Hour);

c->Hour = 12;

Console::WriteLine("at midnight it will be {0} o'clock", c->Hour);

}

};

void main() { Clock::Main();}

让我们看看 C++/CLI 中的结果:

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

C:\>test

The little hand is on the 7

at midnight it will be 12 o'clock

虽然这个例子产生相同的结果,但是语法完全不同。设置器编写如下:

void set(int value)

{

hour = value % 12;

}

C++/CLI 访问器不具有独特的语法,而是像方法一样编写。在这种情况下,C# 隐式参数value被显式声明为函数参数。事实上,在 C++/CLI 中它可以被命名为任何名称,而不仅仅是value。注意,set访问器返回void;标准要求返回void

吸气剂也有类似的区别:

int get()

{

if(hour == 0)

{

return 12;

}

else

{

return hour;

}

}

它具有方法样式的语法,并有一个与属性类型相同的返回值int。尽管 C++/CLI 语法与 C# 语法完全不同,但它也非常直观,因为它反映了 getter 和 setter 实际上是方法的事实。

语法内部的观察

在 C++/CLI 中解析属性时,根据属性是被读取还是被写入,访问被转换为set()get()方法调用,例如:

c->Hour = t.Hour

该表达式将被转换为以下形式:

c->Hour::set(t.Hour);

现在以下面一行为例:

int i = c->Hour;

该表达式将被转换为

int i = c->Hour::get();

事实上,语言本身接受这种显式语法和隐式语法。我们可以如下重写前面的Main()方法,并看到相同的结果:

static void Main()

{

DateTime t = DateTime::Now;

Clock ^c = gcnew Clock();

c->Hour::set(t.Hour);

Console::WriteLine("The little hand is on the {0}", c->Hour::get());

c->Hour::set(12);

Console::WriteLine("at midnight it will be {0} o'clock",c->Hour::get());

}

当然,这首先违背了属性的一些目的。尽管如此,这种转换的知识有助于理解我们将在本章后面遇到的一些棘手的诊断。同样重要的是要注意属性名的范围是getset方法。换句话说,它可以被看作是一个包含类内部方法的名称空间,这使得用于显式访问方法的语法非常直观。

琐碎的属性

平凡属性是指没有显式 getter 或 setter 的属性;编译器根据属性声明创建默认的 getter 和 setter,以及保存信息的数据占位符。属性只是类中的另一个字段,在编译器术语中称为后备存储。这显然是最基本的一种属性,与字段声明仅略有不同,但也有优点。对于最终将被重写为完整属性的项目,平凡属性在开发阶段作为占位符非常有用。同时,它们作为属性的存在可以防止您无意中编写出只适用于字段而不适用于属性的表达式;请参见下面标题为“注意事项”的部分。

语法

要声明一个小属性,可以声明一个不带花括号的 getter 或 setter 的属性,如下所示:

property int Hour;

这创建了一个名为Hour的属性,带有隐式创建的方法Hour::get()Hour::set(),以及一个类型为int的后备存储来存储属性数据。编译器生成的 getter 和 setter 方法有以下声明:

int get();

void set(int);

换句话说,普通属性的基本语法是

property-type``property

你可能会问自己,“如果这被称为一个微不足道的属性,我们怎么称呼我们在前面的例子中明确定义的属性?”嗯,你猜对了。我们直觉地称它为非平凡的性质。

例子

下面是一个使用平凡属性的简单示例:

using namespace System;

ref struct Test

{

property int Item;

int UseItem()

{

Item = 3;

return Item;

}

};

方法UseItem()既读取又写入普通属性项。

索引属性

索引属性类似于属性数组。索引属性使用其他参数(称为索引)来确定getset操作的结果。这些参数不必是整数;事实上,财产指数可以是任何类型的。

语法

若要声明索引属性,请用方括号将逗号分隔的参数列表括起来。您还需要将这个列表复制到 getter 和 setter 的声明中,对于 setter,从左到右从索引开始,到属性类型结束。这种语法使得某些复杂指针类型(类似于第九章中遇到的噩梦)的属性声明成问题。你可以用一个typedef来解决这个问题,这是一个 C++ 语言的特性,将在第二十章的中介绍。此外,索引属性不能是无关紧要的,因为编译器会不知所措,弄不清要创建哪种隐式访问器。

例子

下面是一个使用索引属性的示例:

using namespace System;

ref struct R

{

String ^m_Key;

int m_Value;

property int Hash[String ^]

{

int get(String^Key)

{

if(Key == m_Key)

{

return m_Value;

}

else

{

return -1;

}

}

void set(String^Key, int Value)

{

m_Key = Key;

m_Value = Value;

}

}

R()

{

Hash["dog"]=3;

}

static void Main()

{

R ^ r = gcnew R();

r->Hash["first"]=42;

Console::WriteLine(r->Hash["first"]);

Console::WriteLine(r->Hash["second"]);

}

};

void main() { R::Main(); }

在这个例子中,我们创建了一个非常简单的散列。它只能存储单个值;尝试读取任何其他值都会返回–1。使用一个字符串来索引这个散列。在main()中,我们首先通过将索引为"first"Hash属性设置为42来初始化散列。如果使用了索引first,所有后续的散列读取都返回42,否则返回–1表示错误。暂时忽略构造器;本章后面将使用它来演示 C# 和 C++/CLI 之间的区别。

让我们检查结果:

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

C:\>test

42

-1

默认索引属性

对于索引属性,可以用关键字default代替属性标识符。这对于标量或非索引属性是不允许的。使用default允许您将类本身视为属性的容器,因为属性没有唯一的标识符。使用一个例子可能更容易理解这个概念。

我们可以将前面的示例转换为默认索引属性,如下所示:

using namespace System;

ref struct R

{

String ^m_Key;

int m_Value;

property int default[String ^]

{

int get(String^Key)

{

if(Key == m_Key)

{

return m_Value;

}

else

{

return -1;

}

}

void set(String^Key, int Value)

{

m_Key = Key;

m_Value = Value;

}

}

R()

{

default["dog"]=3;

}

static void Main()

{

R ^ r = gcnew R();

r["first"]=42;

Console::WriteLine(r["first"]);

Console::WriteLine(r["second"]);    }

};

void main() { R::Main(); }

如您所见,这些代码示例之间的唯一区别是标识符Hash不用于默认索引的属性。

C# 属性

因为在 C++/CLI 中,属性是作为方法透明地实现的,所以我觉得直接解释 C++/CLI 索引的属性比将讨论建立在从 C# 翻译过来的基础上更有启发性。就我个人而言,我认为 C# 属性语法有些特别。

然而,仍然有一些有趣的 C# 特性值得在这一部分占有一席之地。

C# 中的标量属性

下面是 C# 中标量或非索引属性的一个示例:

class R

{

int savedValue;

public int BasicProperty

{

get

{

return savedValue;

}

set

{

savedValue = value;

}

}

}

在 C# 中,set访问器的值参数是隐式声明的,并且有一个标识符value。使用。NET Reflector,我们可以看到这些访问器转换为以下方法:

public int get_BasicProperty()

{

return this.savedValue;

}

public void set_BasicProperty(int value)

{

this.savedValue = value;

}

如您所见,C# 语法生成的代码类似于 C++/CLI 语法。

C# 中的索引属性

C# 中的索引属性和 C++/CLI 中的索引属性之间的主要区别在于,在 C# 中,所有索引属性都是默认索引属性。在 C++/CLI 中,一个类中可以有多个索引属性,而在 C# 类中只能有一个。由于这个限制,C++/CLI 和 C# 都允许您避免使用类似于default的关键字来访问属性。因为一个类中最多有一个默认属性,所以标识符是隐式的。下面是我们转换为 C# 的默认索引属性示例:

using System;

class R

{

string m_Key;

int m_Value;

public int this[string Key]

{

get

{

if (Key == this.m_Key)

{

return this.m_Value;

}

return -1;

}

set

{

this.m_Key = Key;

this.m_Value = value;

}

}

R()

{

this["dog"]=3;

}

public static void Main()

{

R r = new R();

r["first"]=42;

Console.WriteLine(r["first"]);

Console.WriteLine(r["second"]);

}

}

Main()函数中,使用以下语法访问属性:

r["first"]=42;

在 C++/CLI 中,等效的语法是相同的:

r["first"]=42;

C# 构造器使用以下语法,使用关键字this来访问属性:

this["dog"]=3;

在 C++/CLI 中,等效的语法是

default["dog"]=3;

C++/CLI 版本使用关键字default,尽管对于引用类型,您也可以使用关键字this1

属性的高级属性

在实现面向对象的范例和抽象时,选择属性而不是字段有很多好处,因为您可以用属性做几乎所有可以用方法做的事情。因为它们在框架中得到支持,所以您几乎可以拥有两个世界的精华。

只读和只写属性

将属性设为只读或只写非常简单。为此,不要分别提供 setter 或 getter。

只读属性

一个只读属性跟在后面;只读属性是指缺少set访问器的属性:

using namespace System;

ref struct R

{

property DateTime Time

{

DateTime get()

{

return DateTime::Now;

}

}

static void Main()

{

R ^ r = gcnew R();

Console::WriteLine(r->Time);

}

};

void main() { R::Main(); }

这个属性允许我们读取当前时间。以下是我运行时得到的结果:

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

C:\>test

2/18/2006 12:16:12 PM

您的结果可能会有所不同。

只写属性

创建只写属性类似于创建只读属性,只是在这种情况下缺少 getter。我无论如何都要包括下面的例子,向您展示只写是如何有用的;使用属性并不是访问数据的唯一方式:

using namespace System;

ref struct R

{

int SavedValue;

property int SetOptions

{

void set(int Value)

{

SavedValue = Value;

}

}

static void Main()

{

R ^ r = gcnew R();

r->SetOptions = 3;

Console::WriteLine(r->SavedValue);

}

};

void main() { R::Main(); }

结果如下:

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

C:\>test

3

静态属性

就像字段和方法一样,属性也可以是静态的,因此它们不需要实例化就可以使用。事实上,让我们使用一个静态属性来重写我们的时间示例,这样做更有意义:

using namespace System;

ref struct R

{

static property DateTime Time

{

DateTime get()

{

return DateTime::Now;

}

}

};

void main()

{

Console::WriteLine(R::Time);

}

虚拟财产

属性不仅可以是虚拟的,还可以用来覆盖其他方法,就像常规方法一样。考虑下面的例子,其中get访问器在基类中被覆盖,就像第八章中的例子一样:

using namespace System;

ref struct Base

{

property int Prop

{

virtual int get()

{

return 1;

}

}

void Test()

{

Console::WriteLine(Prop);

}

};

ref struct Derived : Base

{

int value;

property int Prop

{

virtual int get() override

{

return 3;

}

}

};

void main()

{

Derived ^d = gcnew Derived();

Base ^b = gcnew Base();

b->Test();

Console::WriteLine(d->Prop);

d->Test();

}

结果如下:

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

C:\>test

1

3

3

在这个例子中,基类的get()方法返回1,而被覆盖的方法返回3。从输出中可以看出,Derived::Prop::get()不仅返回3,而且在派生类的实例上被调用时还会影响Base::Prop::get()

抽象和密封属性

属性访问器也可以声明为abstractsealed。下面的示例显示了在派生类中实现和密封的基类中的抽象 getter:

using namespace System;

ref struct Base abstract

{

property int Prop

{

virtual int get() abstract;

}

};

ref struct Derived : Base

{

property int Prop

{

virtual int get() override sealed

{

return 1;

}

}

};

void main()  {}

根据定义,接口中声明的属性也是abstract。没有必要明确说明这一点。

命名覆盖

属性显式重写虚函数也是可能的。考虑以下示例:

using namespace System;

ref struct Base

{

virtual String ^GetProp()

{

return "Base";

}

};

ref struct Derived : Base

{

property String ^ Prop

{

virtual String ^ get() = Base::GetProp

{

return "Derived";

}

}

};

void main()

{

Derived ^d = gcnew Derived();

Base ^b = d;

Console::WriteLine(b->GetProp());

}

结果如下:

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

C:\>test

Derived

在第八章中,你看到了一个第三方库版本强迫你显式覆盖一个不同名字的虚函数的例子。这是同样的情况。在这种情况下,您在基类中有一个现有的函数GetProp(),您希望通过派生类中的属性get访问器来覆盖它。这允许您用一个更抽象的范式替换一个过时的范式,而对先前存在的代码影响最小。

虽然不太常见,但也可以在相反的方向进行命名重写,例如:

using namespace System;

ref struct Base

{

property String^ Prop

{

virtual String^ get()

{

return "Base";

}

}

};

ref struct Derived : Base

{

virtual String ^ GetProp() = Base::Prop::get

{

return "Derived";

}

};

void main()

{

Derived ^d = gcnew Derived();

Base ^b = d;

Console::WriteLine(b->Prop);

}

在这个例子中,派生的方法覆盖了基类中的get访问器。以下是朝此方向覆盖时的结果:

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

C:\>test

Derived

如您所见,无论哪种方式,我们总是显示来自具有覆盖方法的Derived类的结果。

财产保护机制

您可以限制属性、其 getter、其 setter 或其 getter 和 setter 的可访问性。除非另外指定,否则应用于属性的可访问性将延续到 getter 和 setter 的可访问性。唯一的限制是 getter 和 setter 的可访问性不能比属性本身的限制更少。

例如,考虑以下代码:

using namespace System;

ref struct R

{

private:

static property DateTime Time

{

public:

DateTime get()

{

return DateTime::Now;

}

}

};

void main()

{

Console::WriteLine(R::Time);

}

当我们尝试编译它时,我们会遇到以下诊断:

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

test.cpp(7) : error C3908: access level less restrictive than that of 'R::Time'

test.cpp(5) : see declaration of 'R::Time'

发出该诊断是因为属性访问级别是private,这将 getter 和 setter 限制为最多private

另一方面,考虑下面的代码:

using namespace System;

ref struct R

{

public:

static property DateTime Time

{

DateTime get()

{

return DateTime::Now;

}

private:

void set(DateTime t)

{

}

}

};

void main()

{

Console::WriteLine(R::Time);

}

在这种情况下,setter 的保护级别比属性的保护级别更严格,此程序可以很好地编译。剩下要做的就是填充set访问器!

财产警告

因为属性遵循字段语法,所以在复杂的表达式中使用它们很有吸引力,就像使用常规字段一样。这并不总是可行的,因为限制是set访问器返回void而不是属性的类型。 2 考虑以下例子:

ref struct Test

{

property int PropInt;

int RealInt;

};

void main()

{

Test ^a = gcnew Test();

Test ^b = gcnew Test();

Test ^c = gcnew Test();

a->RealInt = b->RealInt = c->RealInt;

a->PropInt = b->PropInt = c->PropInt;

}

让我们试着编译一下:

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

test.cpp(12) : error C2664: 'Test::PropInt::set' : cannot convert parameter 1 from

void' to 'int'

Expressions of type void cannot be converted to other types

第一个构造a->RealInt = b->RealInt = c->RealInt工作正常,使用 C++/CLI 的评估规则顺序从右到左进行解析。第二个例子使用了一个int类型的property,由于 setters 返回了void,所以没有编译。

编译器将属性表达式转换为以下形式:

a->PropInt::set(b->PropInt::set(c->PropInt::get()));

事实上,如果用这个表达式替换属性表达式,就会得到完全相同的错误代码。如您所见,a->PropInt::set()正试图对类型为void的项目进行操作,这是由b->PropInt::set()返回的。

这样设计是出于优化和设计的考虑。要求从set()方法返回值会给程序员带来困惑。如果该属性模拟或访问一个硬件设备,其中的set()方法指示设备编程信息,而get()方法返回状态,那该怎么办?每次出现这种语法时,编译器应该总是自动插入对get()的额外调用吗?有定义良好的规则来处理定义可接受的优化的编程语言。这些规则决定了编译器是否可以尝试伪读地址等等,因为这些操作通常会产生真实的结果,尽管 CLR 当然是一个仿真环境,或者至少现在是这样。有一天可能会有直接实现 CLR 的硬件。

我认为这种担心影响了属性的设计,尽管不能保证编译器的未来版本会支持返回值不是 ?? 的 ?? 函数。

杂项属性详细信息

属性类型不能是constvolatilemutable。您可以将const关键字添加到变量中,使其成为只读的。类可以是常量,这使得类实例是只读的;只有const方法可以被const类调用。附加到const类变量的mutable关键字使该变量免于成为constvolatile关键字表示不应该执行假设变量永远不变的优化。这对内存映射变量很有用。随着本书的展开,我们将在上下文中重新审视这些关键词。

事件和代表

C++/CLI 事件和委托在形式和功能上类似于相应的 C# 形式。本节主要关注语法差异,但是仍然提供了一些说明性的例子。

代表

委托是指向函数的指针的类型安全版本。英寸 NET 中,一个委托可以包含对几个方法的调用,这是封装来自外部或异步事件的回调的理想方式。

整理

在这一节中,我将给出一个简单排序的例子,并使用委托来指导它的发展。假设您有一个名为MyObject的类数组,每个类都有一个名为Value的字段,并且您想通过这个Value对它们进行排序。冒泡排序的简单实现可能如下所示:

using namespace System;

ref struct MyObject

{

int Value;

MyObject(int Value)

{

this->Value = Value;

}

virtual String ^ToString() override

{

return Value.ToString();

}

};

void main()

{

array<MyObject^> ^myObjectArray = gcnew array<MyObject^>

{

gcnew MyObject(5),

gcnew MyObject(3),

gcnew MyObject(1),

gcnew MyObject(4),

gcnew MyObject(2),

};

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

{

for (int j=0; j<i; j++)

{

if(myObjectArray[i]->Value < myObjectArray[j]->Value)

{

MyObject ^tempObject;

tempObject = myObjectArray[i];

myObjectArray[i]=myObjectArray[j];

myObjectArray[j]=tempObject;

}

}

}

for each(MyObject^ o in myObjectArray)

{

Console::Write(o);

}

Console::WriteLine();

}

这都是非常标准的:你在数组上通过几次,如果邻居的顺序错了,就交换邻居,较大的对象冒泡到顶部。

关于代码,有几件有趣的事情需要注意。我有时候有点懒,喜欢用ToString()打印类型。在这种情况下,我是这样做的:

int Value;

return Value.ToString();

这将在托管堆上分配一个字符串,该字符串具有整数Value值的 Unicode 表示形式。

构造器的另一个有趣部分如下:

int Value;

MyObject(int Value)

{

this->Value = Value;

}

在这种情况下,有两个不同的整数被命名为Value;的确,我不必给他们俩起同一个名字。事实上,人们通常不这样做,对成员变量使用类似于m_Value的东西。在这种情况下,我想展示如何使用this来区分它们;this指向的变量是实例变量,缺少this的变量是输入参数。

我们可以通过以下方式使用委托使代码更加通用:

  • 将排序过程从数组类型中分离出来。创建一个通用的sort类,它对任意类型的对象进行排序。
  • 将排序算法与排序过程分离。排序算法根据数组的顺序执行不同的操作。一些算法擅长对随机数组进行排序;当数组中只有几个元素没有按顺序排列时,其他的就更好了。如果允许用户根据数据选择算法就好了。

我们将使用委托来完成这两项工作。

  • 我们将首先进行强制转换,以便排序过程可以对一个数组Object^进行排序,并使用特定于类的比较过程对实际对象进行排序。将使用委托来访问比较过程。
  • 接下来,我们将为排序算法本身实现一个委托。在这种情况下,委托将指向冒泡排序算法的实现。
确定代表

要创建一个委托,我们首先要决定我们想要调用哪种方法。有几种处理任意类型对象的基本方法。一种方法是执行参数的多态,这将在第十四章中讨论,当我讨论泛型和模板的时候。另一种是执行类型的多态,将任意类型强制转换为公共基类,并对基类执行操作。让我们采用后一种策略,将任意对象投射到Object^

这种策略将对象的类型与排序过程分离开来。我们的调用算法为排序程序传递一个可以比较两个项目的方法的地址。我们用一个委托来表示它。我们方法的模型如下:

bool Compare(Object ^, Object ^);

如果第一个Object小于第二个,Compare()方法返回true。委托是一种类型,因此它出现在类级别的范围内,并且具有可见性说明符。它是通过在类似的方法声明前添加关键字delegate来创建的。为了实施类型安全,输入参数和返回类型必须与目标方法完全匹配:

public delegate bool DelCompare(Object ^o1, Object^ o2);

这里的语法有点微妙。假设您忘记了返回类型:

public delegate dog();

如果是这样,您可能会看到一个有些误导性的诊断:

test.cpp(1) : error C2059: syntax error : 'public'

如果您得到一个误导性的诊断,不要认为它是由编译器错误引起的。而是再三检查你的代码,以确保它是有效的 C++。

带有比较方法和委托声明的新MyObject类如下:

using namespace System;

public delegate bool DelCompare(Object^, Object^);

ref struct MyObject

{

int Value;

static DelCompare ^dCompare = gcnew DelCompare(Compare);

MyObject(int Value)

{

this->Value = Value;

}

static bool Compare(Object ^o1, Object ^o2)

{

MyObject ^m1 = (MyObject^) o1;

MyObject ^m2 = (MyObject^) o2;

return (m1->Value < m2->Value);

}

virtual String ^ToString() override

{

return Value.ToString();

}

};

现在我们需要为任意类型创建一个排序类。这使我们有机会看到委托类型的方法将委托作为参数。我们希望我们的排序算法有一个委托,为它提供排序数组所需的准确信息——在本例中,有一个用于比较过程的委托和一个对数组的引用。因此,在这种情况下,我们有以下内容:

public delegate void DelAlgorithm(DelCompare ^dCompare, array<Object^> ^a);

接下来,我们添加排序类和冒泡排序算法本身:

ref struct Sorter abstract sealed

{

static DelAlgorithm ^dAlgorithm = gcnew DelAlgorithm(Bubble);

static void Bubble(DelCompare ^dCompare, array<Object^> ^a)

{

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

{

for (int j=0; j<i; j++)

{

if(dCompare(a[i], a[j]))

{

Object ^tempObject;

tempObject = a[i];

a[i]=a[j];

a[j]=tempObject;

}

}

}

}

static void Sort(array<Object^> ^a, DelCompare ^dCompare)

{

dAlgorithm(dCompare, a);

}

};

注意这个类是abstract sealed。我们希望确保没有人实例化这个类,无论是作为它自己还是作为一个派生类。它被设计成Sort()方法的容器,就像System::ConsoleWrite()的容器一样。

最后,下面是修改后的main()程序:

void main()

{

array<MyObject^> ^myObjectArray = gcnew array<MyObject^>

{

gcnew MyObject(5),

gcnew MyObject(3),

gcnew MyObject(1),

gcnew MyObject(4),

gcnew MyObject(2),

};

Sorter::Sort(myObjectArray, MyObject::dCompare);

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

{

Console::Write(myObjectArray[i]);

}

Console::WriteLine();

}

就这些吗?也许我们可以把它提高一个档次。

下一关

这个实现相当巧妙,但是仍然有一点不必要的繁琐。我们需要将dCompare委托传递给Sort()例程。也许我们对MyObject太过于圆滑了;很高兴还记得MyObject是一个拥有所需委托的类。界面可以帮助我们解决这个问题。我们创建了一个接口ICompare,它告诉编译器我们的特殊对象能够返回一个指示数组中元素排序的委托。完整的程序如下,供您自己学习:

using namespace System;

public delegate bool DelCompare(Object^, Object^);

interface class ICompare

{

virtual DelCompare ^getCompareDelegate();

};

ref struct MyObject : ICompare

{

int Value;

static DelCompare ^dCompare = gcnew DelCompare(Compare);

MyObject(int Value)

{

this->Value = Value;

}

static bool Compare(Object ^o1, Object ^o2)

{

MyObject ^m1 = (MyObject^) o1;

MyObject ^m2 = (MyObject^) o2;

return (m1->Value < m2->Value);

}

virtual String ^ToString() override

{

return Value.ToString();

}

virtual DelCompare ^getCompareDelegate()

{

return dCompare;

}

};

public delegate void DelAlgorithm(DelCompare ^dCompare, array<Object^> ^a);

ref struct Sorter abstract sealed

{

static DelAlgorithm ^dAlgorithm = gcnew DelAlgorithm(Bubble);

static void Bubble(DelCompare ^dCompare, array<Object^> ^a)

{

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

{

for (int j=0; j<i; j++)

{

if(dCompare(a[i], a[j]))

{

Object ^tempObject;

tempObject = a[i];

a[i]=a[j];

a[j]=tempObject;

}

}

}

}

static void Sort(array<Object^> ^a)

{

ICompare ^ic = (ICompare^)a[0];

dAlgorithm(ic->getCompareDelegate(), a);

}

};

void main()

{

array<MyObject^> ^myObjectArray = gcnew array<MyObject^>

{

gcnew MyObject(5),

gcnew MyObject(3),

gcnew MyObject(1),

gcnew MyObject(4),

gcnew MyObject(2),

};

Sorter::Sort(myObjectArray);

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

{

Console::Write(myObjectArray[i]);

}

Console::WriteLine();

}

当我们运行该例程时,我们得到以下结果:

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

C:\>test

12345

多播代理

多播委托是调用多个方法的委托。若要创建多播委托,请向委托本身添加更多方法。申报没有区别。可以使用+=操作符将方法添加到委托中,同样也可以使用-=将其减去。为了完成这项工作,我们需要对每个方法使用gcnew操作符,如下所示:

using namespace System;

public delegate void Handler(String^);

ref struct Class1

{

static void News(String^s)

{

Console::WriteLine("Class1 : {0}",s);

}

};

ref struct Class2

{

static void News(String^s)

{

Console::WriteLine("Class2 : {0}",s);

}

};

ref struct Class3

{

static void News(String^s)

{

Console::WriteLine("Class3 : {0}",s);

}

};

void main()

{

Handler ^dNews1 = gcnew Handler(Class1::News);

Handler ^dNews2 = gcnew Handler(Class2::News);

Handler ^dNews3 = gcnew Handler(Class3::News);

Handler ^dNews;

dNews = dNews1 + dNews2 + dNews3;

dNews("News has arrived!");

dNews -= (dNews2+dNews3);

dNews("We lost subscribers");

dNews += dNews3;

dNews("A subscriber has returned");

}

在这个例子中,我们有三个类,Class1Class2Class3,每个类都对接收新闻感兴趣。它们各有一个静态方法,News(),应该是在我们有新闻要播的时候调用。在main()中,我们创建了一个委托dNews,使用+操作符将消息发送给所有三个类。然后我们使用+=-=操作符来改变谁接收新闻。这样,许多类可以使用委托订阅同一个新闻提要。

实例委托

到目前为止,所有委托的例子都在一个类中使用了一个static方法来接收通知。不幸的是,static方法不能是virtual,这限制了我们如何覆盖它们。我们可以将实例方法传递给委托。而不是写以下内容:

Handler ^dNews1 = gcnew Handler(Class1::News);

写:

Handler ^dNews1 = gcnew Handler(gcnew(Class1),&Class1::News);

这样,我们将一个句柄传递给类的实例以及方法的地址,如操作符&所示。我们不必把这些写在一行上;以下内容也适用:

Class1 ^ pClass1 = gcnew(Class1);

Handler ^dNews1 = gcnew Handler(pClass1,&Class1::News);

既然我们正在使用实例,我们可以重构前面的示例:

using namespace System;

public delegate void Handler(String^);

ref struct Base

{

virtual void News(String^s)

{

Console::WriteLine("{0} : {1}",ToString(),s);

}

};

ref struct Class1 : Base {};

ref struct Class2 : Base {};

ref struct Class3 : Base {};

void main()

{

Handler ^dNews1 = gcnew Handler(gcnew(Class1),&Class1::News);

Handler ^dNews2 = gcnew Handler(gcnew(Class2),&Class2::News);

Handler ^dNews3 = gcnew Handler(gcnew(Class3),&Class3::News);

Handler ^dNews;

dNews = dNews1 + dNews2 + dNews3;

dNews("News has arrived!");

dNews -= (dNews2+dNews3);

dNews("We lost subscribers");

dNews += dNews3;

dNews("A subscriber has returned");

}

这个版本干净多了。基类现在有了通用消息,如果需要的话,派生类可以自由地修改它。

事件

事件是。NET 通知机制。事件为委托提供保护,并允许通过添加和删除订阅者以及激活另一个事件(如前面示例中的发送新闻)来进行定制。

事件包含三种方法:addremoveraise。这些方法都有自己的可访问性。像属性一样,这些方法可以由编译器显式或隐式地声明和实现。带有隐式addremoveraise方法的事件被称为琐碎事件。

琐碎的事件

下面是前面的示例,修改后使用了一个小事件:

using namespace System;

public delegate void Handler(String^);

ref struct Base

{

virtual void News(String^s)

{

Console::WriteLine("{0} : {1}",ToString(),s);

}

};

ref struct Class1 : Base {};

ref struct Class2 : Base {};

ref struct Class3 : Base {};

ref struct Holder

{

void Deliver(String ^s)

{

News(s);

}

event Handler ^News;

};

void main()

{

Holder ^h = gcnew Holder();

h->News += gcnew Handler(gcnew(Class1),&Class1::News);

h->Deliver("News has arrived!");

}

我们可以编译并运行这个:

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

C:\>test

Class1 : News has arrived!

这是一个只有一条新闻的简单例子。

非同寻常的事件

在前面的示例中,我们使用了以下结构:

event Handler ^News;

宣布一件小事。我们可以声明一个显式事件并实现addremoveraise方法。下面的代码是前面转换成同等重要事件的普通事件代码;注意addremoveraise的不同保护等级:

using namespace System;

public delegate void Handler(String^);

ref struct Base

{

virtual void News(String^s)

{

Console::WriteLine("{0} : {1}",ToString(),s);

}

};

ref struct Class1 : Base {};

ref struct Class2 : Base {};

ref struct Class3 : Base {};

ref struct Holder

{

void Deliver(String ^s)

{

News(s);

}

event Handler ^News

{

public:

void add( Handler^ d )

{

this->_News += d;

}

protected:

void remove( Handler^ d )

{

this->_News -= d;

}

private:

void raise( String ^s)

{

this->_News(s);

}

}

private:

Handler ^_News;

};

void main()

{

Holder ^h = gcnew Holder();

h->News += gcnew Handler(gcnew(Class1),&Class1::News);

h->Deliver("News has arrived!");

}

如果我们编译并运行它,我们会得到相同的结果:

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

C:\>test

Class1 : News has arrived!

摘要

本章中的示例应该为您提供使用属性、委托和事件的基础。如果一开始觉得力不从心也不用担心。尽管这是一个需要花点时间来适应的领域,但它无疑为您的编码库添加了一些强大的工具。

在下一章,我们将看看 C# 和 C++ 中的表达式和运算符。

Footnotes 1

当这个类是value type的时候,有趣的事情发生了。在值类型中,this被认为是值类型的内部指针,而不是跟踪句柄。因此,当编译器看到this["dog"]时,它试图像解引用原生数组一样解引用该指针,并抱怨"dog"不是有效的数组下标。请记住,在 C++ 中,本机数组的行为与指针完全一样,只是它们被绑定到一个特定的长度。

  2

在 C++ 语言的当前版本最终确定之前,关于 setters 需要返回 c++ 语言设计者分发列表上的void有一场大讨论。我和本书的技术评论家都支持允许用户灵活使用set方法返回代码;我们输了。