我将演示模板和泛型在何种情况下能配合使用,在何种情况下不能配合使用,并指明在 Visual C++® 2005 下当前模板实现方式的缺陷,以此来结束这一系列有关在 Microsoft® .NET Framework 中进行泛型编程的专栏。我选择通过讨论将标准模板库 (STL) 引入 .NET 的过程中进行的工作,来介绍这份材料。首先,我将回顾 STL 的基本元素,这样所有人都将站在同一起跑线上。
有三个主要元素可用于标准模板库的设计:容器、算法和迭代器 (CAI)。STL 矢量和 List 类表示顺序容器。顺序容器保存第一个元素、第二个元素等等,直到最后一个元素。用程序来表示的函数参数列表通常作为包含字符串类型的元素的矢量来实现。例如:
vectorparamlist;
Map 和 Set 类表示关联容器。关联容器支持快速查找。例如,Map 表示键/值对:键用于查找,而值表示存储和检索的数据。要表示电话号码簿,您需要声明一个带有字符串键和整数值的 Map:
mapphonedir;
多重映射可使单一键支持多个电话条目。
STL 还提供一个算法集,其中包含用于查找、排序、替换和合并的算法(可以对容器进行运算)。这些算法称为泛型算法,因为它们独立于正在其上进行运算的元素类型(例如整型、双精度类型或字符串类型)和包含元素的容器类型(例如无论容器是矢量、列表还是内置数组)。
泛型算法通过直接在容器上进行运算来实现容器独立性。程序不向算法传递容器,而是向它们传递标记了要通过其进行迭代的元素范围的迭代器对 (第一个, 最后一个],其中最后一个元素充当终结标志或一个标记,以表明元素集之后的元素并将停止算法:
sort( paramlist.begin(), paramlist.end() );
此处,begin() 和 end() 是所有 STL 容器提供的方法,会将迭代器返回到元素集第一个元素和位于末尾元素之后的元素。例如,看一下以下声明序列:
void f() { int ia[4] = {21, 8, 5, 13 }; vectorivec( ia, ia+4 ); //将 ivec 初始化为 ia...(请注意:示例程序文件中的程序员注释使用的是英文,本文中将其译为中文是为了便于参考) list ilist( ia, ia+4); //将 ilist 初始化为 ia ... // ... }
注意:ia+4 实际指向最后一个元素 (13) 之后的地址。使用 STL 的人们最初可能被这种区别所蒙蔽,例如,传递截止到 ia+3 处,这将使元素 13 不能包括在一系列值中。
迭代器提供了一种统一且独立于类型的方式来访问、遍历和比较容器的元素。迭代器是一种类抽象,可以提供统一的指针操作(例如 ++、--、*、==、!=),无论容器是内置数组、列表还是其他任何一致的 STL 容器:
void f() { // ... 同上... // 每次调用相同的泛型算法... sort( ia, ia+4 ); sort( ivec.begin(), ivec.end() ); sort( ilist.begin(), ilist.end() ); }
在每个排序调用中(共三个),结果序列理所当然是:5、8、13、21(Fibonacci 序列的第四个到第七个元素)。
现在,这只是一种对 STL 的理想化观点,并不能证明在正式约束和实际约束下实际可行。正式约束是指:不是所有的容器类型均支持所有算法运算。例如,Map 或 Set 不支持 random_shuffle,因为对元素进行的任何重新排序均违反容器类型,这就像将索引编入堆栈中将违反堆栈的语义特征一样。
更实际地说,通过泛型算法,使用排序或查找来获得 List 容器中的元素,比在矢量上进行同一运算更加费力。因此,List 容器类型提供了自己的比泛型算法更高效的类方法。同样,使用 List 容器类型的查找方法来查找 Map 元素,比使用查找算法(通过向算法传递 Map 的开始和结束迭代器及其键)更快捷。
大家可能会希望算法在所有容器上的执行效果都相同,但实际上,算法更适合在 Block 容器和内置数组,而不是在 List 容器和关联容器类型上使用。实际上,当我在 Bell 实验室与 Alex Stepanov 一起工作时,他就把泛型算法称为 Block 算法。
要将 STL 合并到 .NET 中,首先要将容器作为公共语言运行库 (CLR) 类型重新实现。出于多种原因,我在本文中不会进行深入的讨论。总之,最好使容器成为引用类而不是值类。例如:
// 暂时简化声明... templateref class vector { ... }; template ref class map { ... };
在本机 STL 中,所有容器都是非多态的。矢量的声明将给定实际矢量对象。示例如下:
// 本机 STL 矢量 // ivec.empty() == true // ivec.size() == 0 vector< int > ivec; // ivec2.empty() == false // ivec2.size() == 10 vector< int > ivec2( 10 );
但是在 C++/CLI 下声明引用类型时,将定义一个跟踪句柄(矢量本身位于托管的堆中)。默认情况下,句柄将设置为 nullptr。请看:
public ref class sequence { protected: vector^elems; }; public ref class fibonacci : sequence { ... }; // equivalent test if ( elems == nullptr ) ... if ( ! elems ) ... } // ivec == nullptr; vector< int >^ ivec; // ivec2 != nullptr ... // ivec2->empty() == true // ivec2->size == 0 vector< int > ^ivec2 = gcnew vector ; // ivec3 != nullptr ... // ivec3->empty() != true // ivec3->size() == 10 ... vector< int > ^ivec3 = gcnew vector ( 10 );
下一个设计要求是:使不支持模板的其他语言(例如 C# 和 Visual Basic®)能够使用容器。最简单的策略是:使模板容器实现一个或多个系统容器接口(分到两个命名空间中),如下所示:
System::Collections:: | System::Collections::Generic:: |
---|---|
ICollection | ICollection |
IList | IList |
IDictionary | IDictionary |
templateref class vector : System::Collections::ICollection, System::Collections::Generic::ICollection { ... };
要实现系统收集命名空间的容器接口,还必须实现 IEnumerator 和 IEnumerator
genericref class vector_enumerator : System::Collections::IEnumerator, System::Collections::Generic::IEnumerator { ... };
实现系统容器接口的弊端是:虽然使得元素可用,但是无法操作 STL/CLR 类型的容器。因此,额外的设计支持还应该提供泛型阴影容器类型,以使其他语言可以使用实际的容器类型。有两个常规策略可以实现这种支持:Plauger 方式和 Tsao 方式(以两个主要设计师 P. J. Plauger 和 Anson Tsao 的姓名来命名)。
可以认为 Plauger 方式提供泛型阴影类型。也就是说,您将创建阴影泛型类(可以将其称为 generic_vector)。它包含矢量模板的副本。示例如下:
genericpublic ref class vector_generic { vector ^ m_templ; // 哎呀... public: vector_generic( vector ^ ); };
m_templ 声明行上的“哎呀”注释表示在 .NET 下对模板使用的约束。由于存在这种约束,您不能以泛型类型存储要求实例化的模板。这是因为,两个参数化类型功能的实例化时间不同。泛型由运行时来实例化;而模板由编译器来实例化。因此,模板可以包含泛型,而泛型不能包含模板。
Plauger 方式下的解决方案为:创建一个公共泛型接口,模板和泛型均通过该接口派生。有关示例,请看:
genericinterface class vector_interface {...}; template ref class vector : vector_interface{...}; generic public ref class vector_generic : vector_interface { vector_interface ^ m_templ; public: vector_generic( vector_interface ^ ); // ... };
Tsao 方式下的解决方案是根据以下事实得出的:接口始终为模板容器(在特定程序集中实例化)的引用。因此,您只需提供一个接口并实现模板即可。泛型阴影类型将被消除。
genericinterface class vector_interface :ICollection {...}; template ref class vector :vector_interface, ICollection {...};
在任何情况下,除了那些使用程序集的人们以外,所有人都可以执行泛型实例而不是 STL/CLR 容器。这是因为,C++/CLR 下的模板不能成为公共程序集成员。下一部分中讨论了此问题。
要使 .NET 识别类型,它需要两个元素:程序代码(表示要转换为公共中间语言 (CIL) 的类型)和元数据(描述类型的详细信息)。不幸的是,模板此时还不能通过任何一个元素提供给 .NET。与析构函数一样,模板并不是为 .NET 而存在的。
.NET 仅能识别模板的具体实例;而不能识别出它们是一种模板。例如,.NET 可以识别 vector 和 vector 的 CIL 和元数据,但是不能识别共享的模板矢量,CIL 和元数据都是该模板矢量的实例。这种不可识别性的副作用是,您不能辨别模板。也就是说,您不能询问 vector 实例:“您是模板吗?如果是,请把您的参数列表和类定义传递给我好吗?”
另一方面,泛型在 CLR 2.0 中直接支持 CIL,并且具有完全支持泛型反射的扩展反射命名空间。这是您在选择设计方式(在应用程序中使用模板还是泛型类型)时,应该考虑的一个方面。还应考虑是否需要跨程序集共享。模板不能跨程序集共享,而泛型可以。因此,如果跨程序集共享对您的设计非常重要,则应选择泛型类而不是模板类。否则,您将需要提供某种形式的公共接口,来实现跨程序集的共享,正如我所介绍的有关 STL/CLR 设计的内容。
模板不能跨程序集识别的原因是:.NET 具有一个包含其源的扩展类型概念。也就是说,在 .NET 下与在本机 C++ 中不同,类型具有一个位置,在此位置名称可在共享的全局空间中自由浮动。这种全局名称混乱使得将各种组件合并到一个工作应用程序中非常困难。进一步的区别在于:命名空间将提供程序级别的解决方案,而为类型添加位置将提供程序集级别的解决方案。
也就是说,全局公共名称实际上由其程序集分配,从而避免在合并程序集时发生名称冲突。在 .NET 下,类型具有一个位置。这意味着,一个程序集的 vector 不会被识别为另一个程序集的相同 vector,因为类型已由单独的程序集名称标记。由于 CLR 提供的运行时实例化,泛型不会出现这种问题。
那么,既然存在这些约束,为什么我们还选择同时提供模板和 STL/CLR 呢?执行工作的 C++ 程序员已建立起该库和现有代码体的专业知识。我们不仅希望提供现有代码的迁移路径,还希望提供现有专业知识的迁移路径。如果您以前在 C++ 编程过程中依靠 STL,则会感到在 .NET 下缺少 STL 是一种损失。而使用 STL/CLR 则不会这样。我后来听说,计划在 STL 完成后使其可以下载使用。请继续关注!我知道我会的。
请将您的疑问和意见通过 purecpp@microsoft.com 发送给 Stanley。
Stanley B. Lippman 从 1984 年开始在 Bell 实验室与 C++ 的发明者 Bjarne Stroustrup 一起使用 C++。后来,Stan 在 Disney 和 DreamWorks 的动画部门工作过,还担任过 Fantasia 2000 的软件技术主管。此后他一直担任 JPL 的杰出顾问,以及与 Microsoft 公司 Visual C++ 团队合作的设计师。
欢迎访问最专业的网吧论坛,无盘论坛,网吧经营,网咖管理,网吧专业论坛
https://bbs.txwb.com
关注天下网吧微信/下载天下网吧APP/天下网吧小程序,一起来超精彩
|
本文来源:vczx 作者:佚名