Perl 5 引擎概述

一位开发者解释了使 Perl 成为其首选语言的特性。
366 位读者喜欢这篇文章。
30 years on, Perl and its community continue to thrive

Internet Archive Book Images。由 Opensource.com 修改。CC BY-SA 4.0

正如我在“我的 DeLorean 运行 Perl”一文中所描述的那样,切换到 Perl 极大地提高了我的开发速度和可能性。在这里,我将深入探讨 Perl 5 的设计,讨论对系统编程重要的方面。

几年前,我写了“Bash 的 OpenGL 绑定”,有点像一个玩笑。其实现只是一个用 C 语言编写的 X11 程序,它从 stdin(是的,以文本形式)读取 OpenGL 调用,并在 stdout 上发出用户输入。然后我有一个小的 bash include 文件,它会将所有 OpenGL 函数声明为 Bash 函数,这些函数会将函数名称回显到一个管道中,如果 GL 解释器进程尚未运行,则启动它。这个练习的目的是展示 OpenGL(1.4 API,而不是较新的着色器内容)只需每帧几次调用就可以使用 GL 显示列表渲染大量图形。OpenGL 库完成了所有繁重的工作,而 Bash 每帧只打印几十行文本。

但最终,Bash 是一种非常糟糕的 胶水语言,无论是从高开销还是有限的可用操作和语法来看。另一方面,Perl 是一种很棒的胶水语言。

语法先放一边...

如果您不是 Perl 的常用用户,您可能首先注意到的是语法。

Perl 5 构建于冗长且笨拙的语法之上,但较新版本已删除了许多标点符号的需要。通过选择为您提供特定领域“语法糖”的模块,可以避免剩余的缺点,这些模块甚至会在解析时更改 Perl 语法。这与大多数其他语言形成鲜明对比,在这些语言中,您会被给定的语法所束缚,并且比 C 的宏更灵活得多。结合 Perl 强大的稀疏语法运算符(如 mapgrepsort 和类似的用户定义运算符),我几乎总是可以使用 Perl 比使用 JavaScript、PHP 或任何编译语言更清晰且更少打字地编写复杂的算法。

因此,由于语法取决于您的使用方式,我认为底层引擎是语言最重要的方面。Perl 5 具有非常强大的引擎,并且与其他语言相比,它在有趣且有用的方面有所不同。

C 之上的一个层

我不建议任何人通过查看解释器的内部 API 来开始使用 Perl,但快速描述是有用的。我们在 C 语言世界中处理的主要问题之一是获取和释放内存,同时还支持通过函数调用链的控制流。C 语言粗略地具有使用 longjmp 抛出异常的能力,但它不会为您进行任何清理,因此在没有框架来管理资源的情况下几乎毫无用处。Perl 解释器正是这种框架。

Perl 提供了一个独立于 C 函数调用堆栈的变量堆栈,您可以在其上标记 Perl 作用域的逻辑边界。还有一些 API 调用可用于分配内存、Perl 变量等,并告诉 Perl 在 Perl 作用域结束时自动释放它们。现在您可以进行任何您喜欢的 C 调用,从中途“die”出来,并让 Perl 为您清理一切。

尽管这是一个非常规的视角,但我提出它来强调 Perl 位于 C 之上,并允许您根据需要使用尽可能多或尽可能少的解释开销。Perl 的内部 API 当然不如 C++ 那样适合通用编程,但当您完成工作后,C++ 不会为您提供位于您工作之上的解释型语言。我已经记不清多少次我想要反射能力来检查或更改我的 C++ 对象,而追随这个兔子洞已经使我的多个个人项目脱轨。

类似 Lisp 的函数

Perl 函数接受参数列表。缺点是您必须在运行时执行参数计数和类型检查。优点是您最终不会做那么多,因为您可以让解释器自己的运行时检查来捕获这些错误。您还可以通过检查给您的参数并相应地表现来创建 C++ 重载函数的效果。

因为参数是一个列表,并且返回值是一个列表,所以这鼓励了 Lisp 风格的编程,您可以在其中使用一系列函数来过滤数据元素列表。这种“管道”或“流式”效果可以使一些非常复杂的循环变成一行代码。

每个函数都作为 coderef 提供给语言,可以在变量中传递,包括匿名闭包函数。此外,我发现 sub {} 比 JavaScript 的 function(){} 或 C++11 的 [&](){} 更方便键入。

通用数据结构

Perl 中的变量可以是“标量”、引用、数组或“哈希”... 或其他一些我将跳过的东西。

标量充当字符串/整数/浮点混合体,并根据您使用的目的自动进行类型转换。换句话说,操作类型不是由变量类型决定的,而是由运算符类型决定变量应如何解释。这不如语言预先知道类型有效,但不如例如 shell 脚本效率低,因为 Perl 缓存了类型转换。

Perl 标量可能包含空字符,因此它们完全可以用作二进制数据的缓冲区。标量是可变的并且按值复制,但通过写时复制进行了优化,并且子字符串操作也得到了优化。字符串支持 unicode 字符,但在您附加高于 255 的代码点之前,它们以普通字节形式高效存储。

引用(也被认为是标量)保存对任何其他变量的引用;hashrefsarrayrefs 最常见,以及上面描述的 coderefs

数组只是标量(或引用)的动态长度数组。

哈希(即字典、映射或您想称呼它们的任何名称)是一种性能优化的哈希表实现,其中每个键都是字符串,每个值都是标量(或引用)。哈希在 Perl 中的使用方式与 C 中结构体的使用方式相同。显然,哈希的效率低于结构体,但它保持了通用性,因此在其他语言中需要数十行代码的任务在 Perl 中可以变成单行代码。例如,您可以将哈希的内容转储到(键,值)对列表中,或从这样的列表中重建哈希,作为 Perl 语法的自然组成部分。

对象模型

任何引用都可以被“blessed”以使其成为对象,从而授予它一个多重继承方法调度表。“blessing”只是包(命名空间)的名称,并且该命名空间中的任何函数都成为对象的可用方法。继承树由包中的变量定义。因此,您可以使用简单的数据编辑来修改类或类层次结构,或动态创建新类,而不是使用特殊的关键字或内置的反射 API。通过将此与 Perl 的 local 关键字(其中对全局变量的更改在当前作用域结束时自动撤消)相结合,您甚至可以临时更改类方法或继承!

Perl 对象只有方法,因此属性通过访问器访问,例如规范的 Java get_set_ 方法。Perl 作者通常将它们组合成一个仅包含属性名称的单个方法,并通过是否给定参数来区分 getset

您还可以将对象从一个类“重新 blessed”到另一个类,这使一些有趣技巧在大多数其他语言中不可用。考虑状态机,其中每个方法通常从检查对象的当前状态开始;您可以在 Perl 中通过将方法表交换为与对象状态匹配的方法表来避免这种情况。

可见性

虽然其他语言在类之间的访问规则上花费了大量精力,但 Perl 采用了一个简单的“如果名称以下划线开头,则除非是您的,否则不要碰它”的约定。虽然我可以看到这对于纪律松散的软件团队来说可能是一个问题,但在我的经验中它效果很好。C++ 的 private 关键字对我所做的唯一一件事是损害了我的调试工作,但将所有内容都设为 public 感觉很脏。Perl 消除了我的内疚感。

同样,对象提供方法,但您可以忽略它们,而只需访问底层的 Perl 数据结构。这是调试的另一个巨大推动力。

通过引用计数进行垃圾回收

虽然 引用计数 是一种相当容易泄漏的内存管理形式(它不检测循环),但它有一些优点。它为您提供对象的确定性销毁,就像在 C++ 中一样,并且永远不会通过意外的垃圾回收来中断您的程序。它强烈鼓励模块作者使用对象树模式,我非常喜欢这种模式,而不是在 Java 和 JavaScript 中经常看到的对象缠结模式。(我发现树更容易通过单元测试进行测试。)但是,如果您需要对象缠结,Perl 确实提供“弱”引用,在决定是否需要垃圾回收某些内容时,这些引用将不被考虑在内。

总的来说,唯一一次让我感到困扰的是在大量使用闭包进行事件驱动的回调时。很容易让一个对象持有对事件句柄的引用,该事件句柄持有对回调的引用,而回调又引用包含对象。同样,弱引用可以解决这个问题,但这又是一件需要注意的事情,而 JavaScript 或 Python 不会让您担心。

并行性

Perl 解释器是单线程的,尽管用 C 语言编写的模块可以在内部使用自己的线程,并且 Perl 通常包含对同一进程中多个解释器的支持。

虽然这是一个很大的限制,但知道数据结构将永远只被一个线程访问是很不错的,这意味着从 C 代码访问它们时不需要锁。即使在 Java 中,锁定以方便的方式内置于语法中,但要弄清楚线程可以交互的所有方式可能真的很耗时(尤其令人恼火的是,它们迫使您在编写的每个 GUI 程序中都处理这个问题)。

有几个事件库可用于帮助编写 Node.js 风格的事件驱动回调程序,以避免对线程的需求。

访问 C 库

除了通过 Perl 的 XS 系统直接编写自己的 C 扩展之外,Perl 的 CPAN 存储库中已经为您封装并提供了许多常用的 C 库。还有一个很棒的模块 Inline::C,它消除了 Perl 和 C 之间桥接的大部分痛苦,以至于您只需将 C 代码粘贴到 Perl 模块的中间。(它会在您第一次运行时编译,并缓存 .so 共享对象文件以供后续运行。)如果您想操作 Perl 堆栈或打包/解包 Perl 的变量(而不是您的 C 函数参数和返回值),您仍然需要学习一些 Perl 解释器 API。

内存使用

Perl 可能会使用惊人的内存量,特别是如果您使用重量级库并创建数千个对象,但在当今系统的规模下,这通常无关紧要。它也不比其他解释型系统差多少。我个人的偏好是只使用轻量级库,这通常也会提高性能。

启动速度

Perl 解释器在现代硬件上启动时间不到五毫秒。如果您注意仅使用轻量级模块,则可以将 Perl 用于您可能曾经使用 Bash 做过的任何事情,例如 hotplug 脚本。

正则表达式实现

Perl 提供了所有正则表达式实现的鼻祖... 但您可能已经知道了。正则表达式内置于 Perl 的语法中,而不是基于对象的或基于函数的 API;这有助于鼓励将它们用于您可能需要进行的任何文本处理。

普遍性和稳定性

Perl 5 安装在几乎所有现代 Unix 系统上,并且 CPAN 模块集合非常广泛且易于安装。几乎任何任务都有生产质量的模块,具有可靠的测试覆盖率和良好的文档。

Perl 5 在二十年的发布版本中具有近乎完整的向后兼容性。社区也接受了这一点,因此 CPAN 的大部分内容都非常稳定。甚至有一组测试人员定期在 CPAN 的所有内容上运行单元测试,以帮助检测损坏。

工具链也非常可靠。文档语法 (POD) 比我想要的要冗长一些,但它比 doxygenJavadoc 产生更有用的结果。您可以运行 perldoc FILENAME 以立即查看您正在编写的模块的文档。perldoc Module::Name 向您显示您将从 include 路径加载的模块版本的特定文档,并且同样可以向您显示该模块的源代码,而无需深入浏览您的文件系统。

测试用例系统(prove 命令和 Test Anything Protocol 或 TAP)并非 Perl 特有,并且非常易于使用(与基于特定于语言的面向对象结构或 XML 的单元测试相反)。像 Test::More 这样的模块使编写测试用例变得非常容易,以至于您可以在手动测试模块一次所需的时间内编写一个测试套件。测试工作的障碍非常低,以至于我也开始将 TAP 和 POD 文档样式用于我的非 Perl 项目。

总结

尽管有大量较新的语言与之竞争,但 Perl 5 仍然有很多优势。前端语法并没有停止发展,您可以使用自定义模块随意改进它。Perl 5 引擎能够处理您可以抛给它的大多数编程问题,它甚至适用于作为 C 库之上的“胶水”层的底层工作。一旦您真正熟悉它,它甚至可以成为开发 C 代码的环境。

标签
User profile image.
IntelliTree Solutions 的软件工程师。Michael 专注于 Web 应用程序、嵌入式系统和性能调优。

评论已关闭。

 

每周在您的收件箱中获取亮点。

© . All rights reserved.