长期以来,编程语言一直被按照它们的范式分类;函数式、面向对象或过程式编程。我觉得这样分类语言越来越没有意义。今天的热门编程语言已经很好地结合了多种编程范式,使得传统的分类方法不再适用。
内存管理从根本上区分了各种编程语言。内存管理是编程语言的基础——它影响性能特性、安全保证以及程序员的使用体验和认知负担。在选择最适合应用的语言时,该语言的内存管理模型比是否支持对象或函数更重要。
本文提出了一个新的基于该语言内存管理方式的分类体系方案。
关于范式分类法的挑战通常,编程语言按照以下抽象模型来分类:
- 面向对象:程序由具有数据字段和方法的对象构成,这些方法定义了对象的状态和行为特征
- 函数式:程序由不含副作用的纯函数构成
- 过程式:程序由一系列指令操作全局状态构成
历史上,以这种方式划分语言是有帮助的。这些方法对编程来说截然不同,知道某种语言提供了哪些工具是有帮助的。我们发现,几乎所有流行的现代编程语言都包含了各种范式的混合,不再局限于这些孤立的范式框架里。
- C++ 支持面向对象、过程化和函数式编程
- Rust 融合了函数式概念和系统级控制
- Python 和 JavaScript 根据需要愉快地混合使用各种编程风格
- 甚至面向对象编程的代表性语言 Java 也通过 lambda 表达式和流式处理拥抱了函数式编程
此外,即便这些语言被归为同一分类体系,它们在实际运用中却有很大不同,这表明传统分类存在局限。
- C++ 和 JavaScript 都是面向对象的语言,但它们的运行时特性和性能表现有很大差异。
- Python 和 Rust 都提供了对函数式编程模式的强大支持,但这两门语言对开发者的认知负担有很大差异。
- C 和 Zig 都可以被认为是过程化的语言,但它们内存模型的细微差异对其安全保证有着很大的影响。
与其被视为定义性特征,这些模式应被视为语言提供的特性集。我们更需要一个根本性的特征来更有意义和实际地对编程语言进行分类。
内存管理:真正的差异性,实际上,内存管理——语言如何分配、追踪和释放内存——造成了性能、效率或稳定性上的最根本差异。它会影响程序的性能、效率和稳定性。
- 性能特性及开销 — 运行时性能的潜力
- 安全保障 — 防止常见内存问题
- 开发者认知负荷 — 开发者需要承担的责任列表
- 适用的应用领域 — 特定语言是否适合特定的应用场景
- 生态系统中的常用模式和做法
最重要的是,对于语言分类方式,语言通常只采用一种主要的内存管理模式。也就是说,即使一种编程语言采用了多种策略,通常也只有一种内存管理模式是该语言的惯常方式。(即,尽管 Rust 支持不安全代码,但这显然不是该语言的主要内存管理模式)
内存管理分类系统我根据它们的内存管理策略提出了四大主要语言类别,并根据执行方法和安全级别的保障进行了次一级分类。
主要分类:我们将主要根据它们的内存管理方式来分类这些语言。
类 1:👑 君主语言课具有完全直接控制内存分配能力的语言我们将归类为 主权型 语言。这些语言不提供自动内存管理——内存安全由开发者全权负责。主权型语言通常提供最高的性能潜力,但要求程序员对资源的处理必须格外小心。
主要特征: 手动内存管理,直接控制
示例: C, Assembly, Zig, C++(早期版本)
特点:
- 显式的
malloc
/free
调用或等效方法 - 指针操作
- 没有运行时的安全保障
- 性能潜力最大
- 内存错误的风险最大
- 最接近硬件层面的指令
// C 语言示例:手动内存管理(例如,使用 malloc 和 free 函数)
char* buffer = malloc(100);
// 使用 buffer...
free(buffer); // 忘记这一步就会造成内存泄漏
// 再次使用 buffer 就会导致未定义行为
第2类:🛡️ 守护类语言
使用所有权或 RAII(资源获取即初始化)模式自动管理内存的语言,我们将称其为 守护者。内存与程序的运行值绑定,并且内存清理是确定的。守护者语言旨在防止内存错误,同时尽量减少运行时开销,但对开发人员有着严格的要求。
核心特征: 资源获取即初始化(RAII)和基于所有权的内存管理 例如: Rust, 现代C++
特征:
- 破坏是确定的
- 编译时内存安全检查
- 强所有权语义
- 没有运行时内存管理开销
- 经常使用转移语义
- 通常是静态类型
// Rust 示例代码:基于所有权的内存管理机制
fn main() {
let s1 = String::from("hello");
let s2 = s1; // 所有权从 s1 移动到 s2
// println!("{}", s1); // 会导致编译时错误
println!("{}", s2); // 这不会出错
} // s2 在这里被自动释放,内存也被释放了
// C++ 示例:基于所有权的内存管理方式
int main() {
std::string s1 = "hello";
std::string s2 = std::move(s1); // 将 s1 的所有权显式地移交给 s2
// 未定义行为:s1 处于有效的但未定义的状态
// std::cout << s1 << std::endl; // 可能正常工作,也可能崩溃或产生乱码
std::cout << s2 << std::endl; // 这样做是安全的
return 0;
} // s2 在这里自动销毁,释放内存
第3课:🤖 机器人语言
将内存管理交给垃圾收集器(GC)的语言,我们称之为 托管 语言。程序员从大部分内存管理任务中解脱出来,运行时系统自动识别并释放未使用的内存。托管语言优先考虑开发者的效率和内存安全,但代价是可能的非确定性清理和运行时暂停。
主要特点: 垃圾收集:
示例: Java, C#, Python, JavaScript, Go, Haskell, Elixir, Clojure, F#
特性:
- 运行时内存管理器
- 可能的暂停
- 非确定性的内存回收
- 降低内存管理的认知负担
- 大多数内存操作默认安全
- 通常更高的内存消耗
// Java 示例:垃圾收集
void createObjects() { // 创建对象的方法
List<Data> items = new ArrayList<>();
for (int i = 0; i < 10000,; i++) {
items.add(new Data()); // 创建许多数据对象
}
// 当此方法返回时,这些对象都可以被垃圾收集器回收
}
第四课:会计相关的语言符号
使用自动引用计数(ARC)来追踪每个对象有多少引用的语言我们将归类为 记账员。当引用计数达到零时,对象会被立即释放。与守护语言不同,所有权是共享的而不是独占的。虽然这些语言提供确定性的清理,容易陷入引用循环,并且在并发环境中会遇到挑战。
核心特点: 自动引用计数(ARC),
比如: Swift, Objective-C (ARC), PHP, Perl
特点:
- 确定性的清理过程
- 在处理并发更新时可能会遇到麻烦
- 可能存在引用循环
- 通常资源使用较为稳定
- 通常包含弱引用功能
// Swift 示例:自动引用计数
class Person {
deinit {
print("Person 对象被销毁")
}
}
do {
let person = Person()
// 操作 person ...
} // 引用计数归零,person 立刻被释放
📄 语言分类简介
👑 主权者
核心思想: 手动内存管理
典型语言: C、Zig、汇编
内存释放: 手动(程序员需要手动调用 free() 函数)
核心理念: 所有权 / RAII
典型语言: Rust,现代 C++ 等
内存清理: 确定性(基于作用域)
核心概念: 垃圾回收
典型语言: Java, Go, Python, C#
内存管理: 非确定性(运行时自动回收)
核心思想: 自动引用计数
典型语言: Swift,Objective-C
内存清理: 确定(引用计数归零)
我们还可以根据它们在内存安全性方面的不同处理方法进一步对语言进行分类。
安全级别是?- 不安全的: 语言本身缺乏安全性保障(C,汇编)
- 静态: 受保护免受内存访问违规、缓冲区溢出和释放后使用错误。在编译时强制执行(Rust)
- 动态: 受保护免受内存访问违规、缓冲区溢出和释放后使用错误。通过动态运行时检查强制执行(Python,Java,JavaScript)
- 条件性: 在特定使用场景下是安全的。(C++,Zig)
真实世界的语言实例一些语言如 C# 和 Swift 提供了动态和运行时检查。在这样的分类模型中,任何使用运行时检查的语言都归为动态安全类别。运行时安全机制的存在是该类别的特征。
这种分类更关注内存安全,而不是类型安全。类型安全虽然重要,但仍被视为一种功能,而不是语言分类。实践中,许多流行语言都提供了静态和动态类型检查的机制。
借助我们的分类体系,我们可以更准确地描述不同语言。
C: 不安全的主权语言。
Rust: 静态安全的守护语言。
Java: 动态安全的监护语言。
Python: 动态安全的监护语言。
Swift: 动态安全的监护语言。
C++: 有条件安全的监护语言。
Go: 动态安全的监护语言。
Haskell: 静态安全的监护语言。
Zig: 有条件安全的主权语言。
Objective-C: 动态安全的监护语言。
C#(C Sharp): 动态安全的监护语言。
Erlang: 动态安全的监护语言。
这类分类的实际用法不安全的 近期在语言讨论中变成了一种贬义词。这不应该如此。不安全的仅仅表示内存安全保证的责任完全落在了开发者身上。不安全的应该仅仅被视为在使用语言时所需认知负担的增加。
这一分类系统不仅局限于学术研究——它还为选择和设计语言提供了实际的指导。
系统编程在系统编程领域,Sovereign 或 Guardian 语言来说,通常表现优异:
- C 因为它可以直接控制硬件,仍然在操作系统内核领域占据主导地位
- Rust 在需要安全性和控制力的场景中变得越来越流行
- C++ 跨越了这两个类别,提供了在需要时使用 RAII(资源获取即初始化,Resource Acquisition Is Initialization)的灵活性
- Zig 虽然较新,但根据其分类,有可能在这一领域找到自己的位置
这些语言的确定性特征对于需要实时响应或资源有限的系统来说至关重要。
应用的开发在应用程序开发中,Custodian 或 Guardian 语言常常很出色:
- Java 和 C# 由于其安全性和工具链,在企业应用中占主导地位
- Python 和 JavaScript 在快速开发中表现出色
- Rust 在性能和安全性要求极高的领域正在兴起
当需要快速原型制作时,看门狗语言提供的较低的认知负担可能更受青睐。
网页开发在 web 开发中,Custodian 语言占主导地位:
- JavaScript 因为浏览器兼容性,在客户端JavaScript占主导地位
- Python 、 Ruby 和 PHP 在服务器端开发上仍然很受欢迎
- TypeScript 增加了静态类型特性,并且保持了JavaScript的内存管理特性
web应用程序的动态性使得自动内存管理功能特别有价值。
嵌入式技术:在嵌入式系统中,主权 或严格的 守护 语言是必要的:
- C 仍然在资源受限的环境中称霸
- Rust 在安全至上的环境中越来越受欢迎
在内存有限的环境下,这些语言的资源消耗是可预测的,这一点很重要。
案例研究:C# vs Erlang在传统的范式分类中,C# 和 Erlang 看起来完全不相干。
C#:
- 以面向对象为主
- 命令式编程
- 基于类的继承
- 静态强类型
- 用于通用应用程序开发的设计
Erlang:
- 主要功能性的
- 声明式编程风格
- 不采用继承;基于模式匹配的
- 动态类型系统
- 专为并发和分布式系统设计
尽管C#和Erlang看起来不同,但它们在内存管理分类中都被视为动态安全型的托管语言,这意味着它们在内存管理方面提供了额外的安全保障。
- 两者都优先考虑开发者的效率而不是纯粹的性能
- 两者都使开发者免去手动内存管理的烦恼
- 两者都有非确定性的资源清理过程
- 两者都可能偶尔出现垃圾回收暂停的情况
- 两者都运行在一个运行时环境中(C# 使用 CLR,Erlang 使用 BEAM)
- 两者都以一定性能开销换取安全保证
// C# 示例
public void 处理数据() {
var 大数据集 = 加载大数据集();
分块处理(大数据集);
// 内存回收无需显式清理,垃圾回收器会自动处理
}
%% Erlang 示例
process_data() ->
LargeDataSet = 加载大数据集(),
分批处理(LargeDataSet).
%% 不需要显式的清理;垃圾回收器最终会自动回收内存
在这两种语言中,开发人员不需要直接处理内存管理。运行时环境会自动跟踪对象引用,并在对象不再可达时回收相应的内存空间。
两种语言都做了类似的取舍。
- 相比君主或守护语言,内存占用更大
- 垃圾回收导致的暂停可能影响实时性能保证
- 预测确切的内存使用同样具有挑战性
使用这两种语言工作的程序员:
- 主要关注业务逻辑,而不是内存管理
- 信任运行时来处理清理
- 用生产力和安全性来换取原始性能的提升
- 会面临类似的内存泄漏调试难题(通常是引用循环或长时间未被释放的对象)
尽管 C# 和 Erlang 的设计理念和所采用的编程范式大相径庭,但它们在内存管理方式上却具有基本相似之处。这种基于内存的分类揭示了传统范式分类可能忽视的重要相似性,为开发人员在为特定应用领域选择语言时提供了更实用的参考。
基于内存管理选择语言:在选择编程语言时,与其问“这种语言是面向对象的还是函数式的?”,不如考虑这些问题及其带来的影响:
决定论的标准问题:你的资源管理需要有多可预测?
当你需要高确定性要求(例如实时系统、嵌入式设备、音频处理或安全关键的应用)时,可以考虑保障型语言,如Rust和C++,或严谨型语言,如Swift。
这些语言为你提供了可预测的清理时间——当作用域结束或引用计数归零时,资源会立即释放。
当你能接受一些不确定性时(比如网页应用、业务软件、数据分析或原型设计),像 Java、Python 和 C# 这样的 管家 语言在提高开发效率的同时,这些语言还能自动帮你管理内存。
安全须知问题:对您的应用程序来说,内存安全有多重要?
当安全是最重要的时候:
- 编译时保证选择 静态安全语言(Rust)或 静态安全语言(Haskell)
- 如果能接受运行时检查的话,考虑 动态安全语言(Java、C#、Python)
当性能优先于安全,且你的团队有手动管理内存的专业技能时,Sovereign语言(C,Zig)提供了最大的控制——但谨慎的编码实践必不可少。
性能限制条件问题:你需要什么样的性能概况?
当最小开销最关键时(比如处理大规模数据集、进行系统编程或开发实时应用时),Sovereign 或 Guardian 这样的语言提供最低的开销(如Rust和C++)。
当开发速度比执行效率更重要(比如在快速原型制作、商业软件或脚本开发中),像Custodian这样的语言,如Python或JavaScript,可以显著加快您的开发进度。
资源限制情况问题:你的运行时环境受到多大的约束?
对于高度受限的环境(如嵌入式系统和移动设备),Sovereign 或 Guardian 语言(Sovereign 或 Guardian 语言)以最小化内存占用和运行时需求。
对于中等受限的环境,像Swift这样的受限型语言提供了一个安全性和资源使用之间的良好平衡。
在这样的环境中,任何内存管理类都能正常工作,而Custodian语言通常能显著提升开发效率。
实用决策模式首先明确你的底线要求:
- 如果确定性的清理是必不可少的,排除 Custodian 语言
- 如果不可妥协的内存安全是必需的,排除 Unsafe Sovereign 语言
- 如果资源极其有限,排除较耗费资源的 Custodian 语言
考虑一下你的团队的专长:
- 手动内存管理的经验使Sovereign或Guardian语言变得可行
- 开始接触系统编程的团队可能会发现Custodian或Accountant语言更加安全
考虑你的生态系统和工具链需求。
每个记忆类别都有在特定领域支持丰富生态系统的语言
这个框架直接映射到我们的内存管理分类,帮助您选择适合特定需求的语言。
结论部分基于范式的传统分类方法在过去很有用,但无法捕捉现代编程语言之间的真正差异。内存管理——语言如何分配、跟踪和释放内存——已成为塑造语言性能、安全性和开发体验的关键因素。
我们的分类系统将编程语言分为主权、守护、看守和会计四大类,提供了一个更实际的分类体系,直接关联到实际决策过程。当与(静态、动态、条件、不安全)这几个维度结合时,这个框架提供了一种细腻且实用的方式来理解编程语言的整体情况。
这种方法的优势在对比看起来截然不同的语言如C#和Erlang时显得尤为突出。尽管它们的编程范式不同,但它们共享基本的内存管理特性,这导致了相似的运行时行为和权衡,这些细节通常会被传统的分类方式所忽略。
随着编程的不断发展,内存管理将继续成为语言的重要标志。即使新的语言特性不断涌现,语言的内存管理方式构成了其他特性基础。下次在选择编程语言时,首先看看它的内存管理——这可能就是使用该语言和应用成败的关键。
感谢您的阅读!我叫Alex Gilbert——我写关于编程、数学和数独等谜题的文章。如果您想看到更多这样的内容,请在Medium平台上关注我!
共同學習,寫下你的評論
評論加載中...
作者其他優質文章