通用编程
通用编程是一种计算机编程样式,其中编写了算法,该算法是按照指定词组的数据类型来编写的,然后在需要的情况下对提供的特定类型作为参数进行实例化。这种方法是由ML编程语言在1973年开创的,允许编写仅在使用时其操作类型的类型集合中差异的常见功能或类型,从而减少了重复的代码。
通用编程于1977年与ADA一起引入了主流。在C ++中,通用编程成为专业图书馆设计曲目的一部分。这些技术得到了进一步的改进,并在1994年有影响力的书籍设计模式中引入了参数化类型。
Andrei Alexandrescu在其2001年的《现代C ++设计:通用编程和设计模式》中引入了新技术。随后, D实施了相同的想法。
此类软件实体在ADA , C# , Delphi , Eiffel , F# , Java , Nim , Python , Go , Rust ,Swift, Swift , Typescript和Visual Basic .NET中被称为仿制药。它们被称为ML , Scala , Julia和Haskell中的参数多态性。 (Haskell术语还将术语“通用”用于相关但有些不同的概念。)
通用编程术语最初是由大卫·穆瑟(David Musser)和亚历山大·斯蒂芬诺夫(Alexander Stepanov)更具体的,比上述更具体的,以描述一个编程范式,其中从算法和数据结构的具体示例中抽象了对数据类型的基本要求,并正式为概念,,概念正式,,,,,概念正式,,,,,,,概念正式化,,,,,,,概念,正式化的数据类型通过根据这些概念实现的通用功能,通常使用上述语言通用机制。
Stepanov – Musser和其他通用编程范例
通用编程在Musser&Stepanov(1989)中定义,如下所示,
通用编程围绕从混凝土,有效算法抽像以获得通用算法的想法,这些算法可以与不同的数据表示结合,以生成各种有用的软件。
- Musser,David R。; Stepanov,Alexander A.,通用编程
“通用编程”范式是一种软件分解的方法,从算法和数据结构的具体示例中抽象了类型的基本要求,并正式为概念,类似于抽像在抽象代数中的抽象。这种编程方法的早期示例是在方案和ADA中实现的,尽管最著名的示例是标准模板库(STL),该案例开发了一种迭代器理论,该理论用于解除序列数据结构和在其上运行的算法。
例如,给定n个序列数据结构,例如单个链接列表,向量等和M算法,例如find
,sort
等等,直接方法将专门针对每个数据结构实现每种算法,从而为实现N × M组合提供。但是,在通用编程方法中,每个数据结构都返回一个迭代概念的模型(一种可以指定为检索当前值的简单值类型,或更改为序列中的另一个值),而每种算法都是编写的通常,具有此类迭代器的参数,例如,一对迭代器指向子序列或过程的开始和结束。因此,仅需要实现N + M数据结构 - 合理组合。 STL中指定了几个迭代概念,每个迭代概念的改进更加限制性概念,例如向前迭代器仅提供顺序中的下一个值的运动(例如适合单链接列表或输入数据流),而随机访问迭代器还提供对序列任何元素的恒定时间访问(例如适合向量)。一个重要的一点是,数据结构将返回可以有效实现的最通用概念的模型 -计算复杂性要求明确是概念定义的一部分。这限制了可以应用给定算法的数据结构,并且这种复杂性要求是数据结构选择的主要决定因素。通用编程类似地也应用于其他域,例如图形算法。
尽管这种方法通常使用编译时通用性和模板的语言特征,但它独立于特定的语言技术细节。通用编程先驱亚历山大·史蒂芬诺夫(Alexander Stepanov)写道,
通用编程是关于抽象和分类算法和数据结构。它从诺斯(Knuth)而不是从类型理论中获得灵感。它的目标是有用,高效和抽象算法和数据结构的系统目录的增量构建。这样的事业仍然是一个梦想。
-亚历山大·史蒂芬诺夫(Alexander Stepanov),STL的简短历史
我认为,迭代器理论对于计算机科学至关重要,就像戒指或Banach空间的理论是数学的核心。
-亚历山大·史蒂芬诺夫(Alexander Stepanov),接受A. Stepanov的采访
在Stepanov之后,我们可以在不提及语言功能的情况下定义通用编程:提升算法和数据结构从具体示例到其最一般和最抽象的形式。
- Bjarne Stroustrup,在现实世界中不断发展一种语言:C ++ 1991-2006
被描述为通用编程的其他编程范例包括“通用编程 - 简介”中所述的数据类型通用编程。您的样板方法是Haskell的轻便通用编程方法。
在本文中,我们将上述通用编程的高级编程范式与用于实施它们的较低级别的编程语言通用机制(请参阅“通用性”的编程语言支持)。有关通用编程范式的进一步讨论和比较,请参见。
编程语言支持通用性
自1970年代以来,通用设施就以高级语言存在于ML , Clu和Ada等语言中,随后被许多基于对象的和对象的语言(包括Beta , C ++ , D , Eiffel ,Java,Java,, Java ,)采用。和dec的现在已倒闭的格子。
在各种编程语言中,实施和支持仿制药。 “通用”一词在各种编程环境中也有所不同。例如,在第四章中,编译器可以在编译时执行代码,并且可以为这些单词创建新的编译器关键字和新的实现。它几乎没有示意编译器行为的单词,因此自然而然地提供了通用能力,但是在大多数文本中都不称呼这些能力。同样,动态键入的语言,尤其是解释的语言通常会默认提供通用性,因为将值传递给函数和值分配都是类型的诱因,并且这种行为通常用于抽像或代码较长性,但是这通常不是标记为通用性,因为它是一个通用性该语言采用的动态打字系统的直接结果。该术语已用于功能编程中,特别是在类似Haskell的语言中,该语言使用结构类型系统,其中类型始终是参数,并且这些类型上的实际代码是通用的。这些用途仍然有类似的避免代码和渲染抽象的目的。
数组和结构可以视为预定义的通用类型。数组或结构类型的每种用法都会实例化新的混凝土类型,或重用以前的实例化类型。数组元素类型和结构元素类型是参数化类型,用于实例化相应的通用类型。所有这些通常是编译器中内置的,语法与其他通用构造不同。一些可扩展的编程语言试图统一内置和用户定义的通用类型。
随后是对编程语言中通用机制的广泛调查。有关比较通用编程机制适用性的特定调查,请参见。
在面向对象的语言中
在静态键入语言中创建容器类时,为每个数据类型编写特定的实现是不方便的,尤其是如果每个数据类型的代码实际上相同。例如,在C ++中,可以通过定义类模板来规避这种代码的重复:
template<typename T>
class List {
// Class contents.
};
List<Animal> list_of_animals;
List<Car> list_of_cars;
多于,T
是创建列表时指定的任何类型的占位符。这些“类型的容器T”通常称为模板,只要保留某些合同(例如子类型和签名) ,就可以使用不同的数据类型重复使用类。这种通用机制不应与包含多态性混淆,这是可交换子类的算法用法:例如,类型的对象列表Moving_Object
包含类型的对象Animal
和Car
。模板也可以用于类型无关的函数Swap
下面的示例:
// "&" denotes a reference
template<typename T>
void Swap(T& a, T& b) { // A similar, but safer and potentially faster function
// is defined in the standard library header <utility>
T temp = b;
b = a;
a = temp;
}
std::string world = "World!";
std::string hello = "Hello, ";
Swap(world, hello);
std::cout << world << hello << ‘\n’; // Output is "Hello, World!".
C ++template
上面使用的构造被广泛引用为通电程序员和语言设计师的概念的通用构造,并支持许多通用编程成语。 D编程语言还基于C ++先例提供了完全通用的能力模板,但具有简化的语法。自从引入Java平台,标准版(J2SE)5.0以来,Java编程语言根据C ++的句法提供了通用设施。
C# 2.0, Oxygene 1.5 (以前为Chrome)和Visual Basic .NET 2005具有自2.0版以来Microsoft .NET框架中存在的仿制药的支持。
ADA中的仿制药
自1977 - 1980年首次设计以来, Ada就有仿制药。标准库使用仿制药来提供许多服务。 ADA 2005向标准库增加了一个全面的通用容器库,该库的灵感来自C ++的标准模板库。
通用单元是采用一个或多个通用形式参数的软件包或子程序。
通用形式参数是一个值,变量,常数,类型,子程序,甚至是另一个指定的通用单元的实例。对于通用形式类型,语法将离散,浮点,定点,访问(指针)类型等区分。某些正式参数可以具有默认值。
要实例化通用单元,程序员将通过每个正式的实际参数。然后,通用实例的行为就像其他任何单元一样。可以在运行时实例化通用单元,例如在循环中。
例子
通用软件包的规范:
generic
Max_Size : Natural; -- a generic formal value
type Element_Type is private; -- a generic formal type; accepts any nonlimited type
package Stacks is
type Size_Type is range 0 .. Max_Size;
type Stack is limited private;
procedure Create (S : out Stack;
Initial_Size : in Size_Type := Max_Size);
procedure Push (Into : in out Stack; Element : in Element_Type);
procedure Pop (From : in out Stack; Element : out Element_Type);
Overflow : exception;
Underflow : exception;
private
subtype Index_Type is Size_Type range 1 .. Max_Size;
type Vector is array (Index_Type range <>) of Element_Type;
type Stack (Allocated_Size : Size_Type := 0) is record
Top : Index_Type;
Storage : Vector (1 .. Allocated_Size);
end record;
end Stacks;
实例化通用软件包:
type Bookmark_Type is new Natural;
-- records a location in the text document we are editing
package Bookmark_Stacks is new Stacks (Max_Size => 20,
Element_Type => Bookmark_Type);
-- Allows the user to jump between recorded locations in a document
使用通用软件包的实例:
type Document_Type is record
Contents : Ada.Strings.Unbounded.Unbounded_String;
Bookmarks : Bookmark_Stacks.Stack;
end record;
procedure Edit (Document_Name : in String) is
Document : Document_Type;
begin
-- Initialise the stack of bookmarks:
Bookmark_Stacks.Create (S => Document.Bookmarks, Initial_Size => 10);
-- Now, open the file Document_Name and read it in...
end Edit;
优点和限制
语言语法允许精确规范通用形式参数的约束。例如,可以指定通用形式类型只能接受模块化类型作为实际。也可以在通用形式参数之间表达约束;例如:
generic
type Index_Type is (<>); -- must be a discrete type
type Element_Type is private; -- can be any nonlimited type
type Array_Type is array (Index_Type range <>) of Element_Type;
在此示例中,array_type受index_type和element_type的约束。实例化设备时,程序员必须通过满足这些约束的实际数组类型。
这种细粒度控制的缺点是一个复杂的语法,但是,由于所有通用形式参数在规范中都完全定义,因此编译器可以在不查看通用物体的情况下实例化通用。
与C ++不同,ADA不允许使用专门的通用实例,并且要求明确对所有通用物进行实例化。这些规则有几个后果:
- 编译器可以实现共享的仿制药:可以在所有实例之间共享通用单元的对象代码(除非程序员请求在子程序中插入子程序)。作为进一步的后果:
- 不可能发生代码膨胀(Code Bloat在C ++中很常见,需要特殊护理,如下所述)。
- 由于新实例不需要新的对象代码,因此可以在运行时和编译时实例化仿制药。
- 对应于通用形式对象的实际对象始终被认为是通用内部的非静态对象。有关详细信息和后果,请参见Wikibook中的通用形式对象。
- 通用的所有实例都是完全相同的,更容易审查和理解他人编写的程序。没有“特殊情况”要考虑。
- 所有实例化都是显式的,没有隐藏的实例化可能使得很难理解该程序。
- ADA在编译时不允许任意计算,因为在运行时执行通用参数的操作。
C ++中的模板
C ++使用模板启用通用编程技术。 C ++标准库包括标准模板库或STL,该库为通用数据结构和算法提供模板框架。 C ++中的模板也可以用于模板元编程,这是在编译时预先评估某些代码的一种方法,而不是运行时。使用模板专业化,C ++模板已完成。
技术概述
有多种模板,最常见的是功能模板和类模板。功能模板是一种基于实例化时提供的参数化类型创建普通函数的模式。例如,C ++标准模板库包含功能模板max(x, y)
这会创建返回X或Y的功能,以较大者为准。max()
可以这样定义:
template<typename T>
T max(T x, T y) {
return x < y ? y : x;
}
此功能模板的专业知识,具有特定类型的实例化,可以像普通函数一样称为:
std::cout << max(3, 7); // Outputs 7.
编译器检查用于调用的参数max
并确定这是一个呼吁max(int, int)
。然后,它实例化参数化类型的函数版本T
是int
,使以下功能等效:
int max(int x, int y) {
return x < y ? y : x;
}
这是否有效x
和y
是整数,字符串或表达式的任何其他类型x < y
对于任何类型的operator<
被定义为。对于可以使用的一组类型,不需要常见的继承,因此它与鸭打字非常相似。定义自定义数据类型的程序可以使用操作员过载来定义<
对于这种类型,因此允许其与max()
功能模板。尽管在这个孤立的示例中,这似乎是一个较小的好处,但在像STL这样的综合库的背景下,它允许程序员通过为其定义一些操作员来获得新的数据类型的广泛功能。只是定义<
允许与标准一起使用类型sort()
,stable_sort()
, 和binary_search()
算法或将其放入数据结构(例如set
S,堆和关联阵列。
C ++模板在编译时完全可以安全。作为示范,标准类型complex
不定义<
操作员,因为复数上没有严格的顺序。所以,max(x, y)
如果x和y为x,将因编译错误而失败complex
值。同样,其他依赖的模板<
不能应用于complex
除非提供比较(以函数或函数的形式进行比较)。例如:complex
不能用作关键map
除非提供比较。不幸的是,编译器历史上为这种错误产生了一些深奥的,长而无助的错误消息。确保某个对象遵守方法协议可以减轻此问题。使用的语言compare
代替<
也可以使用complex
值为钥匙。
另一种类型的类模板将相同的概念扩展到类。类模板专业化是一类。类模板通常用于制造通用容器。例如,STL具有链接的列表容器。为了列出整数列表,一个人写list<int>
。字符串列表表示list<string>
。 Alist
具有与之相关的一组标准功能,可用于任何兼容的参数化类型。
模板专业化
C ++模板的强大功能是模板专业化。这允许根据正在实例化的参数化类型的某些特征提供替代实现。模板专业化有两个目的:允许某些形式的优化,并减少代码膨胀。
例如,考虑一个sort()
模板功能。该功能所做的主要活动之一是在容器的两个位置中交换或交换值。如果这些值很大(就存储每个值所需的字节数而言),则通常会更快地将单独的指针列表构建到对象,对这些指针进行排序,然后构建最终排序序列。如果值很小,但是通常只需按需要交换就位的值最快即可。此外,如果参数化类型已经是某种指针类型的,则无需构建单独的指针数组。模板专业化允许模板创建者编写不同的实现,并指定参数化类型必须具有每个实现的特征。
与功能模板不同,类模板可以部分专业化。这意味着,当已知某些模板参数时,可以提供类模板代码的替代版本,而留下其他模板参数通用。例如,可以使用这来创建一个默认实现(主要专业化),该实现假设复制参数化类型是昂贵的,然后为便宜的类型创建部分专业,从而提高了整体效率。这种类模板的客户只需使用它的专业知识,而无需知道编译器在每种情况下是使用主要专业化还是某些部分专业化。类模板也可以完全专业化,这意味着当所有参数化类型都已知道时,可以提供替代实现。
的优点和缺点
模板的一些用途,例如max()
函数以前是由函数样的预处理器宏( C语言的遗产)填充的。例如,这是这种宏的可能实现:
#define max(a,b) ((a) < (b) ? (b) : (a))
在编译正确之前,预处理器扩展了宏(复制粘贴);模板是实际的实际功能。宏总是在线扩展;当编译器认为合适时,模板也可以是内联函数。
但是,对于这些目的,模板通常被认为是对宏的改进。模板是类型安全。模板避免了代码中发现的一些常见错误,这些错误大量使用了函数样宏,例如评估具有两次副作用的参数。也许最重要的是,模板被设计为比宏更大的问题。
使用模板的使用有四个主要缺点:支持的功能,编译器支持,错误消息(通常使用C ++ 20 Sfinae )和代码膨胀:
- C ++中的模板缺乏许多功能,这使得实现它们并以直接的方式使用它们通常是不可能的。相反,程序员必须依靠复杂的技巧,这会导致肿胀,难以理解并且难以维护代码。 C ++标准中的当前发展通过大量利用这些技巧并为模板上的模板或牢记它们构建许多新功能,加剧了这个问题。
- 历史上,许多编译器对模板的支持很差,因此模板的使用可能会使代码的便携程度降低。当C ++编译器与不知道C ++的链接器一起使用,或者在尝试在共享库边界上使用模板时,支持也可能很差。
- 当使用SFINAE的代码中检测到错误时,编译器可能会产生令人困惑的,长,有时是无助的错误消息。这可能会使模板难以开发。
- 最后,模板的使用要求编译器为与之使用的每个类型参数生成模板类别或函数的单独实例。 (这是必要的,因为C ++中的类型并非全部相同,并且数据字段的大小对于类的工作方式很重要。)因此,模板的不加区分使用可能会导致代码膨胀,从而导致过多的可执行文件。但是,在某些情况下,明智地使用模板专业化和推导可以大大减少此类代码膨胀:
因此,由于使用了模板,因此是否可以使用推导来减少复制的代码问题?这将涉及从普通类得出模板。这种技术在真正使用方面被证明成功地遏制了代码膨胀。不使用这样的技术的人发现即使在适度的程序中,复制的代码也可能花费大量的代码空间。
- Bjarne Stroustrup ,C ++的设计和演变,1994 - 模板类或功能可能需要对模板类的明确专业化,这将需要重写整个类,以进行特定的模板参数。
模板产生的额外实例也可能导致某些辩论者在与模板上优雅地工作困难。例如,从源文件中设置模板中的调试断点可能会错过所需的实际实例化中的断点,或者可能会在每个位置设置一个断点,以实例化模板。
此外,使用它必须完全可用(例如,标题中包含在标题中)到翻译单元(源文件)的实现源代码。无法编译模板,包括大部分标准库(如果未包含),则无法编译。 (这与非策略的代码形成对比,该代码可以编译到二进制文件,仅提供使用该代码的声明标题文件。)这可能是通过公开实现代码来删除某些抽象的情况,这可能是一个缺点用于封闭源项目。
d中的模板
D语言支持基于C ++设计的模板。大多数C ++模板成语在D中无需更改,但D添加了一些功能:
- D中的模板参数不仅限于类型和原始值(就像C ++ 20之前的C ++一样),而且还允许任意编译时值(例如字符串和结构文字),以及对任意标识符的别名,包括其他模板或模板实例化。
- 模板约束和
static if
声明为C ++的C ++概念提供了替代方案和if constexpr
. - 这
is(...)
表达允许投机实例化在编译时验证对象的性状。 - 这
auto
关键字和typeof
表达式允许对可变声明和函数返回值进行类型推断,从而允许“伏尔德摩特类型”(没有全局名称的类型)。
D中的模板使用与C ++中不同的语法:而在C ++模板参数中,则包裹在角括号中(Template<param1, param2>
),D使用感叹号和括号:Template!(param1, param2)
。这避免了由于比较操作员的歧义而引起的C ++解析困难。如果只有一个参数,则可以省略括号。
通常,D结合上述功能,使用基于特质的通用编程提供编译时多态性。例如,输入范围定义为满足由支票执行的任何类型isInputRange
,定义如下:
template isInputRange(R)
{
enum bool isInputRange = is(typeof(
(inout int = 0)
{
R r = R.init; // can define a range object
if (r.empty) {} // can test for empty
r.popFront(); // can invoke popFront()
auto h = r.front; // can get the front of the range
}));
}
仅接受输入范围的函数然后可以在模板约束中使用上述模板:
auto fun(Range)(Range range)
if (isInputRange!Range)
{
// ...
}
代码生成
除模板元编程外,D还提供了几个功能来启用编译时代码生成:
- 这
import
表达式允许从磁盘读取文件,并将其内容用作字符串表达式。 - 编译时反射允许在编译过程中列举和检查声明及其成员。
- 用户定义的属性允许用户将任意标识符附加到声明中,然后可以使用编译时间反射来枚举。
- 编译时函数执行(CTFE)允许在编译过程中解释D(限于安全操作)的子集。
- 字符串Mixins允许评估并将字符串表达式的内容作为D代码,该代码成为程序的一部分。
结合上述允许根据现有声明生成代码。例如,d序列化框架可以枚举类型的成员,并为每种序列化类型生成专门功能,以执行序列化和挑选化。用户定义的属性可以进一步指示序列化规则。
这import
表达式和编译时函数执行还允许有效地实现特定于域的语言。例如,给定一个函数,该函数采用包含HTML模板并返回等效d源代码的字符串,可以以以下方式使用它:
// Import the contents of example.htt as a string manifest constant.
enum htmlTemplate = import("example.htt");
// Transpile the HTML template to D code.
enum htmlDCode = htmlTemplateToD(htmlTemplate);
// Paste the contents of htmlDCode as D code.
mixin(htmlDCode);
埃菲尔的通用性
自从原始方法和语言设计以来,通用类一直是Eiffel的一部分。 Eiffel的基础出版物使用术语一词来描述创建和使用通用类。
基本的,不受约束的通用性
通用类以其类名称和一个或多个正式的通用参数列表声明。在以下代码中,类LIST
有一个正式的通用参数G
class
LIST [G]
...
feature -- Access
item: G
-- The item currently pointed to by cursor
...
feature -- Element change
put (new_item: G)
-- Add `new_item' at the end of the list
...
正式的通用参数是任意类名称的占位符,如下两个通用派生所示,将提供通用类声明时提供的占位符。ACCOUNT
和DEPOSIT
是其他班级名称。ACCOUNT
和DEPOSIT
被认为是实际的通用参数,因为它们提供了真实的类名称以替代G
在实际使用中。
list_of_accounts: LIST [ACCOUNT]
-- Account list
list_of_deposits: LIST [DEPOSIT]
-- Deposit list
在Eiffel类型系统中,尽管类LIST [G]
被认为是类,不被视为一种类型。但是,LIST [G]
例如LIST [ACCOUNT]
被认为是一种类型。
限制性
对于上面显示的列表类,实际的通用参数代替G
可以是其他任何可用课程。为了限制可以从中选择有效的实际通用参数的类集,可以指定一个通用约束。在班级声明中SORTED_LIST
下面,通用约束表明,任何有效的实际通用参数都将是从类继承的类COMPARABLE
。通用约束确保了SORTED_LIST
实际上可以分类。
class
SORTED_LIST [G -> COMPARABLE]
Java中的仿制药
作为J2SE 5.0的一部分,对仿制药或“类型的容器”的支持被添加到Java编程语言中。在Java中,仅在编译时间检查仿制药以获得正确性。然后,通过称为类型擦除的过程删除通用类型信息,以保持与旧的JVM实现的兼容性,从而使其在运行时不可用。例如,List<String>
转换为原始类型List
。编译器插入类型铸件以将元素转换为String
与其他实现(例如C ++模板)相比,从列表中检索出来时会降低性能。
.net [C#,vb.net]中的仿制处
根据Microsoft Research的研究原型,添加了仿制药2.0的一部分,该原型于1999年开始。尽管类似于Java中的仿制药,但.NET Generics不应用类型的擦除,而是实现仿制药作为第一类机制在运行时使用重新化。这种设计选择提供了其他功能,例如允许对通用类型的保存进行反思,并减轻了一些擦除限制(例如无法创建通用数组)。这也意味着,运行时演员和通常昂贵的拳击转换不会受到性能的打击。当将原始和价值类型用作通用参数时,它们将获得专门的实现,从而获得有效的通用收集和方法。与C ++和Java一样,嵌套的通用类型,例如字典<string,list <int>>有效类型,但是,建议在代码分析设计规则中为成员签名提供建议。
.NET允许使用六种通用类型约束。where
关键字包括将通用类型限制为价值类型,是类,具有构造函数和实现接口。以下是一个接口约束的示例:
using System;
class Sample
{
static void Main()
{
int[] array = { 0, 1, 2, 3 };
MakeAtLeast<int>(array, 2); // Change array to { 2, 2, 2, 3 }
foreach (int i in array)
Console.WriteLine(i); // Print results.
Console.ReadKey(true);
}
static void MakeAtLeast<T>(T[] list, T lowest) where T : IComparable<T>
{
for (int i = 0; i < list.Length; i++)
if (list[i].CompareTo(lowest) < 0)
list[i] = lowest;
}
}
这MakeAtLeast()
方法允许在数组上操作,并具有通用类型的元素T
。该方法的类型约束表明该方法适用于任何类型T
这实现了通用IComparable<T>
介面.如果类型不支持比较,则可以确保调整时间错误,如果调用该方法。该界面提供通用方法CompareTo(T)
.
以上方法也可以在没有通用类型的情况下编写,只需使用非生成Array
类型。但是,由于阵列是违反的,因此铸件不会安全,并且编译器将无法找到某些可能的错误,这些错误否则在使用通用类型时会捕获。此外,该方法需要访问数组项目object
S相反,需要铸造以比较两个元素。 (对于等值类型,例如int
这需要拳击转换,尽管可以使用Comparer<T>
类,就像标准集合课中所做的那样。)
通用.NET类中静态成员的显著行为是每个运行时类型的静态成员实例(请参见下面的示例)。
//A generic class
public class GenTest<T>
{
//A static variable - will be created for each type on reflection
static CountedInstances OnePerType = new CountedInstances();
//a data member
private T mT;
//simple constructor
public GenTest(T pT)
{
mT = pT;
}
}
//a class
public class CountedInstances
{
//Static variable - this will be incremented once per instance
public static int Counter;
//simple constructor
public CountedInstances()
{
//increase counter by one during object instantiation
CountedInstances.Counter++;
}
}
//main code entry point
//at the end of execution, CountedInstances.Counter = 2
GenTest<int> g1 = new GenTest<int>(1);
GenTest<int> g11 = new GenTest<int>(11);
GenTest<int> g111 = new GenTest<int>(111);
GenTest<double> g2 = new GenTest<double>(1.0);
Delphi的通用性
Delphi的Object Pascal方言在Delphi 2007版本中获取了仿制药,最初仅使用(现在已停止).NET编译器,然后在Delphi 2009版本中添加到本机代码之前。 Delphi Generics的语义和功能在很大程度上是以.NET 2.0中的仿制药为基础建模的,尽管该实现的必要性截然不同。这是上面显示的第一个C#示例的或多或少直接翻译:
program Sample;
{$APPTYPE CONSOLE}
uses
Generics.Defaults; //for IComparer<>
type
TUtils = class
class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
Comparer: IComparer<T>); overload;
class procedure MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T); overload;
end;
class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T;
Comparer: IComparer<T>);
var
I: Integer;
begin
if Comparer = nil then Comparer := TComparer<T>.Default;
for I := Low(Arr) to High(Arr) do
if Comparer.Compare(Arr[I], Lowest) < 0 then
Arr[I] := Lowest;
end;
class procedure TUtils.MakeAtLeast<T>(Arr: TArray<T>; const Lowest: T);
begin
MakeAtLeast<T>(Arr, Lowest, nil);
end;
var
Ints: TArray<Integer>;
Value: Integer;
begin
Ints := TArray<Integer>.Create(0, 1, 2, 3);
TUtils.MakeAtLeast<Integer>(Ints, 2);
for Value in Ints do
WriteLn(Value);
ReadLn;
end.
与C#一样,方法和整个类型可以具有一个或多个类型的参数。在示例中,Tarray是一种通用类型(由语言定义),而MakeatLeast是通用方法。可用约束与C#:任何值类型,任何类,特定类或接口以及具有无参数构造函数的类非常相似。多个约束是增材。
免费pascal的通用性
免费的Pascal在Delphi之前实施了仿制药,并具有不同的语法和语义。但是,由于FPC版本2.6.0,使用{$ mode delphi}语言模式时可用Delphi风格的语法。因此,免费的Pascal代码以任何一种样式支持仿制药。
Delphi和免费的Pascal示例:
// Delphi style
unit A;
{$ifdef fpc}
{$mode delphi}
{$endif}
interface
type
TGenericClass<T> = class
function Foo(const AValue: T): T;
end;
implementation
function TGenericClass<T>.Foo(const AValue: T): T;
begin
Result := AValue + AValue;
end;
end.
// Free Pascal's ObjFPC style
unit B;
{$ifdef fpc}
{$mode objfpc}
{$endif}
interface
type
generic TGenericClass<T> = class
function Foo(const AValue: T): T;
end;
implementation
function TGenericClass.Foo(const AValue: T): T;
begin
Result := AValue + AValue;
end;
end.
// example usage, Delphi style
program TestGenDelphi;
{$ifdef fpc}
{$mode delphi}
{$endif}
uses
A,B;
var
GC1: A.TGenericClass<Integer>;
GC2: B.TGenericClass<String>;
begin
GC1 := A.TGenericClass<Integer>.Create;
GC2 := B.TGenericClass<String>.Create;
WriteLn(GC1.Foo(100)); // 200
WriteLn(GC2.Foo('hello')); // hellohello
GC1.Free;
GC2.Free;
end.
// example usage, ObjFPC style
program TestGenDelphi;
{$ifdef fpc}
{$mode objfpc}
{$endif}
uses
A,B;
// required in ObjFPC
type
TAGenericClassInt = specialize A.TGenericClass<Integer>;
TBGenericClassString = specialize B.TGenericClass<String>;
var
GC1: TAGenericClassInt;
GC2: TBGenericClassString;
begin
GC1 := TAGenericClassInt.Create;
GC2 := TBGenericClassString.Create;
WriteLn(GC1.Foo(100)); // 200
WriteLn(GC2.Foo('hello')); // hellohello
GC1.Free;
GC2.Free;
end.
功能性语言
Haskell中的通用性
Haskell的类型类机制支持通用编程。 haskell中的六个预定义类型类(包括Eq
,可以比较平等的类型,Show
,其值可以呈现为字符串的类型具有支持派生实例的特殊属性。这意味着定义新类型的程序员可以声明这种类型是这些特殊类型类之一的实例,而无需在声明类实例时提供类方法的实现。所有必要的方法将“得出”,即基于类型的结构自动构造。例如,以下二进制树的声明指出,它是一个类的实例Eq
和Show
:
data BinTree a = Leaf a | Node (BinTree a) a (BinTree a)
deriving (Eq, Show)
这会导致平等函数(==
)和字符串表示函数(show
)自动为任何类型的表单定义BinTree T
提供了T
本身支持这些操作。
对派生实例的支持Eq
和Show
制作他们的方法==
和show
以质性不同的方式具有多态性函数的通用方式:这些“函数”(更准确地说,函数的类型索引家族)可以应用于各种类型的值,尽管它们对每种参数类型的行为都不同,但几乎不需要工作,但是很少需要工作来添加对新类型的支持。 Ralf Hinze(2004)表明,通过某些编程技术,可以针对用户定义的类型类实现类似的效果。在Haskell和Haskell扩展的背景下,其他研究人员提出了针对这种和其他类型的通用方法(下面讨论)。
息肉
息肉是Haskell的第一个通用编程语言扩展。在息肉中,通用函数称为多型。该语言引入了一种特殊的结构,其中可以通过结构诱导对常规数据类型的模式函数的结构来定义这种多型函数。息肉中的常规数据类型是Haskell数据类型的子集。常规数据类型t必须是友好的*→ * ,如果A是定义中的正式类型参数,则所有递归调用t都必须具有ta形式。这些限制排除了较高的数据类型和嵌套数据类型,其中递归调用的形式不同。这里提供了息肉中的平坦功能:
flatten :: Regular d => d a -> [a]
flatten = cata fl
polytypic fl :: f a [a] -> [a]
case f of
g+h -> either fl fl
g*h -> \(x,y) -> fl x ++ fl y
() -> \x -> []
Par -> \x -> [x]
Rec -> \x -> x
d@g -> concat . flatten . pmap fl
Con t -> \x -> []
cata :: Regular d => (FunctorOf d a b -> b) -> d a -> b
通用Haskell
通用Haskell是荷兰乌得勒支大学(Utrecht University)开发的Haskell的另一个扩展。它提供的扩展是:
- 类型索引值定义为对各种Haskell类型构造函数(单位,原始类型,总和,产品和用户定义的类型构造函数)的值。此外,我们还可以使用constructor案例指定特定构造函数的类型索引值的行为,并使用默认情况在另一个情况下重复使用一个通用定义。
所得类型索引值可以专门针对任何类型。
- 类型类型是类型的类型,在类型上索引,通过给出*和K→K'的情况来定义。实例是通过将类型类型应用于一种来获得的。
- 通用定义可以通过将其应用于类型或类型来使用。这称为通用应用程序。结果是一种类型或值,具体取决于应用哪种通用定义。
- 通用抽象启用通用定义可以通过将类型参数(以给定类型的类型)提取来定义。
- 类型索引类型是类型构造函数索引的类型。这些可用于将类型提供给更多涉及的通用值。所得类型指数类型可以专门针对任何类型。
例如,通用haskell中的平等函数:
type Eq {[ * ]} t1 t2 = t1 -> t2 -> Bool
type Eq {[ k -> l ]} t1 t2 = forall u1 u2. Eq {[ k ]} u1 u2 -> Eq {[ l ]} (t1 u1) (t2 u2)
eq {| t :: k |} :: Eq {[ k ]} t t
eq {| Unit |} _ _ = True
eq {| :+: |} eqA eqB (Inl a1) (Inl a2) = eqA a1 a2
eq {| :+: |} eqA eqB (Inr b1) (Inr b2) = eqB b1 b2
eq {| :+: |} eqA eqB _ _ = False
eq {| :*: |} eqA eqB (a1 :*: b1) (a2 :*: b2) = eqA a1 a2 && eqB b1 b2
eq {| Int |} = (==)
eq {| Char |} = (==)
eq {| Bool |} = (==)
干净的
Clean提供基于通用编程的息肉和GHC≥6.0支持的通用Haskell 。它以这些方式参数为参数,但提供了过载。
其他语言
ML家族中的语言通过参数多态性和称为函子的通用模块来支持通用编程。标准ML和OCAML都提供了类似于类模板以及ADA的通用软件包的函数。方案句法抽像也与通用性有联系 - 实际上,这些是C ++模板的超集。
Verilog模块可以采用一个或多个参数,其实际值将在模块的实例化时分配给其实际值。一个示例是一个通用寄存器数组,其中通过参数给出数组宽度。这样的数组与通用线向量相结合,可以从单个模块实现中制造具有任意位宽度的通用缓冲区或内存模块。
源自ADA的VHDL也具有通用能力。
C使用_Generic
关键词:
#define cbrt(x) _Generic((x), long double: cbrtl, \
default: cbrt, \
float: cbrtf)(x)