可变与不可变描述了系统、基础设施或数据在创建后是否可以更改。可变资源可以就地修改。不可变资源无法更改,任何修改都会创建新的实例。
可变与不可变是推动软件开发和基础设施管理现代方法的原则。
这种区别可以比作在白板上写字。如果您可以添加单词、擦除某些部分或更改编写的内容,那么它们就像可变资源一样但若在书写完毕后立即用玻璃封存白板,且需使用新白板进行后续书写,这便是不可变资源。
虽然这个概念广泛适用于计算领域,但最常见于编程。在编程中,理解哪些数据类型可直接修改、哪些必须创建新副本,对完成常见任务至关重要。这些任务包括编写算法、构建应用程序编程接口(API) 和设计面向对象编程 (OOP) 中的类。
选择使用可变或不可变对象将影响数据在内存中的管理方式、数据共享与修改的安全性,以及是否可能产生意外副作用。这就是为什么可变与不可变是初学者和经验丰富的程序员都必须掌握的基础概念。
例如,在 Python 编程语言中,列表和字典是可变类型。可以在这些对象中添加、删除或修改各项。相比之下,布尔值(真/假值)或元组(如 (1,2,3) 这类有序集合)等对象属于不可变类型。若需更改其内容,必须创建全新的对象。
在可变数据和不可变数据之间进行选择通常取决于三个关键因素:数据是否需要频繁更新,是否需要在线程之间共享,或者是否需要版本历史记录。
当数据需要频繁更新,且程序的多个部分需修改同一对象时,可变类型通常最为适用。
可变对象可就地修改数据,从而避免创建新对象,减少内存使用。由于需要创建和收集的临时对象较少,它可以降低垃圾回收(删除未使用数据以释放内存的过程)对处理器的使用率。
例如,应用程序中的购物车使用可变列表直接添加或删除商品,而无需为每次更改创建新的对象。
可变类型在处理频繁变化的数据(如不断增长的列表或实时计数器)时性能更优,因为它们直接更新现有对象而非创建新对象。这种效率优势可加速依赖快速修改的数据结构操作。
例如,音乐应用程序的播放列表可以使用可变列表进行快速更新。当增删歌曲时,可变列表可在微秒级完成更新,而无需为每个变动重新创建千首歌曲的播放列表。
可变对象允许程序的多个部分访问和更改同一对象。这一过程使它们能够通过共享状态(即多个组件读取和写入以协调行动的数据)进行协同运作。当多个组件需要通过公共数据协调或通信时,这种机制尤为实用。
例如,项目管理应用程序使用可变对象来分享任务列表、日历和通知。当一名团队成员更新任务时,所有人都能立即看到变化。
当数据在创建后不应更改时,不可变类型通常最为适用。在具有并发性的应用程序中,这一点尤为重要,因为程序的多个部分会访问相同的数据。
由于不可变对象的状态是固定的,因此不会被其他代码更改。这一特性使程序更具可预测性且更易于理解,因为它消除了由意外变更引发的程序错误。
例如,银行应用常将交易记录存储为不可变对象,以确保后续代码无法篡改。这对于确保符合监管要求和维护可证明交易未被篡改的审计追踪至关重要。
不可变对象通常是线程安全的,因为它们的状态在创建后无法更改。多线程可安全地同时读取这些不可变对象而不会产生冲突,不过在并发系统中开发者仍需谨慎管理对象引用。这一特性使其成为多线程程序的理想选择,因为多个线程需访问相同数据而不会引发冲突。
例如,天气应用可并行运行当前实况、预报和警报的并发线程。将天气数据存储为不可变对象意味着每个线程都可以读取相同的信息,而不会存在信息意外更改的风险。
不可变对象能简化调试过程,因为其在程序执行期间不会发生值意外变更。这一特性有助于减少由副作用引发的程序错误,并加快问题排查速度。
例如,电子游戏经常将玩家的运行状况和统计数据存储为不可变对象。由于这些数值不会意外改变,开发人员可以轻松追踪错误,并确信无关代码不会篡改统计数据。
两种最主流的编程范式:面向对象编程 (OOP) 与函数式编程,对可变性采取了不同的处理方式。
OOP 通常采用可变性,围绕同时持有数据和行为的对象来构建程序。这些对象可通过称为 setter 的特殊函数随时间改变,例如更新属性值(如修改人员年龄或商品价格)。
相比之下,函数式编程倾向于不可变性。每当需要变更时,它都会创建并返回新值,这使得程序更具可预测性且更易于测试。
编程语言在处理可变与不可变类型时也采用不同策略。
在 Python 中,可变类型和不可变类型都很常见。
一个示例就是字符串(如姓名或句子等字符序列)。Python 中的字符串是不可变的。附加新文本会创建一个新的字符串对象。相比之下,列表是可变的。这些有序集合具有可迭代特性,您可以在列表对象中添加、删除或修改项目。
Python 并非使用编译器(在执行前将代码转换为机器语言的程序)在运行前检查代码,而是在运行时进行类型检查。这意味着只有在程序运行时才能捕获错误。涉及可变性的错误(例如尝试修改不可变字符串)会触发 TypeError。
如果错误没有得到处理,程序会立即停止,并防止进一步运行任何代码。这种过程能够加速开发,但需要对类型处理保持高度注意。
理解 Python 中的可变性有助于在函数间共享数据或操作共享模块时避免错误。GitHub 上的教程和代码示例提供了使用 Python 内置类型的最佳实践。
JavaScript 既使用可变类型,也使用不可变类型。和 Python 一样,字符串也是不可变的。但是,与 Python 不同,默认情况下所有对象都是可变的。
JavaScript 灵活的语法同时支持面向对象和函数式风格,从而允许开发人员根据需要管理可变性。
与 Python 类似,Java 字符串是不可变的。一旦创建,字符串的值就无法更改。对于经常构建或修改文本的程序来说,这种特性可能会降低效率。
为了解决这个问题,Java 提供了 StringBuilder,这是一个可变字符串类,允许直接修改文本而无需创建新对象。它可以提高性能并减少内存使用,在不可变性的安全性与可变性的性能优势之间取得平衡。
C++ 使用 const 关键字将变量、函数甚至整个对象标记为只读。其能够为开发人员提供对可变性的精细化控制,通过防止更改来有效地将可变对象转变为不可变对象。
与 Java 一样,C++ 字符串可以是可变的,也可以是不可变的,具体取决于其实现。
C++ 支持面向对象和函数式编程风格。在 OOP 中,开发人员会随着时间的推移修改现有对象,而函数式编程会创建新值,而不是更改现有数据。
可变性与不变性的原则不仅限于编程,还延伸到基础设施和系统。现代软件工程师在设计云架构与部署管道时,同样运用了这些概念。
可变基础设施是指部署后可以更改的服务器或其他 IT 资源。例如,您可能登录服务器并手动更新软件、更改配置或安装补丁。虽然这种方法提供了灵活性,但它可能会导致配置漂移,使服务器变成独特的“雪花”,并且更改将无法跟踪或重现。
不可变基础设施意味着服务器或 IT 资源在部署后无法更改。团队不再更新运行中的系统,而是直接部署内置变更的新实例,随后淘汰旧实例。这种方法减少了配置漂移,简化了回滚,并有助于确保部署的一致性。
可变性和不变性原则也可以应用于软件和系统设计的其他领域。
某些数据库采用仅追加日志,这意味着每次变更都会被永久记录且不可篡改。另一些则是可变的,允许直接更新或删除数据,比如编辑文档。
某些云存储系统可配置为不可变存储,以保留旧版本并锁定防止修改。这有助于防止数据被意外更改或删除。可变存储允许随时编辑或替换文件。
许多版本控制工具(如 Git)都遵循不可变模型,其中每次提交都保存为单独的、不可更改的快照。它有助于确保可靠的版本历史记录,即使添加了新的更改也是如此。