当前位置: 首页 > news >正文

编程语言的本质是什么?从“动态性“三维模型重新理解你写的每一行代码

写了十几年代码,用过不下十种语言,我们天天讨论"该学哪门语言"“哪个语言性能好”“哪个语言有前途”,却很少有人退后一步问:编程语言的复杂性,到底从哪来?

不是语法多不多的问题,不是关键字记不记得住的问题。真正让代码变得难写、难读、难调试的,是一个更底层的东西。

最近在读一本叫《代码的文明:编程语言如何改变历史》的书,里面提出了一个我觉得非常锐利的框架——编程语言的动态性三维模型。读完之后有种"原来如此"的通透感,很多以前模模糊糊的直觉,一下子被结构化了。

今天把这个模型拆开聊聊。


一切复杂性的根源:代码不是静态的

先说一个反直觉的事实:如果代码是完全静态的——每一行干什么、执行顺序是什么、数据类型是什么,在你写下它的那一刻就全部确定——那代码几乎不存在"难理解"的问题。

你从第一行读到最后一行,完事。

但现实不是这样。代码之所以难,是因为它有动态性——你光看源码,没法完全确定它运行时会干什么。

这个"动态性"不是指"动态类型语言"那个狭义概念。书里把它拉到了一个更高的层面:编程语言的核心设计,本质上就是在围绕"动态性"做文章——引入多少动态性、在哪个阶段引入、如何控制它带来的复杂度。

书中提出,代码的动态性存在三个维度,维度越高,抽象能力越强,理解难度也越大。

这三个维度是:执行流的动态性、编译时的动态性、运行时的动态性。


第一维:执行流的动态性——你以为最简单,其实藏着暗流

这是最基础的一层。if-elseforwhile、函数调用——代码不再是一条直线往下走,而是会分叉、会循环、会跳转。

看起来平平无奇对吧?每个入门教程第二课就教。

但别小看它。书里举了两个很好的例子:

构造函数和函数重载,是隐藏的执行流。你调用一个构造函数,表面上看不到"函数调用"的语法特征,但实际上一段代码被执行了。重载更甚——同一个函数名,根据参数类型不同,实际跑的是完全不同的代码。你读源码时,如果不去确认参数类型,根本不知道走的是哪条路。

异常机制,是反人类直觉的执行流。正常代码从上往下读,你的大脑天然就是这么建模的。但throw一出来,执行流直接跳到不知道哪层的catch块里去了。这种跳跃不像goto那么直白(goto至少有个标签告诉你往哪跳),异常的跳转目标取决于调用栈的运行时状态。

所以书里说得很直接:异常机制带来了"额外的阅读和理解成本"。这不是异常机制设计得烂,而是它在第一维度上就引入了比较高的动态性。

单靠第一维度能写出复杂软件吗?能,但很吃力。纯粹靠条件分支和函数调用来组织几十万行代码,抽象能力是不够的。于是人们开始往第二维走。


第二维:编译时的动态性——代码在写代码

这一层的核心思想是:让编译器在编译阶段生成新的代码。

最粗暴的实现是C/C++的宏。本质就是文本替换——你写一个#define,编译器在编译前把它展开成实际代码。书里给了个单例模式的例子,我觉得很能说明问题:

// 没有宏,每个单例类都要写一遍几乎一样的代码classSingletonOne{public:staticSingletonOne&Instance(){staticSingletonOne instance;returninstance;}};// 有了宏,一行搞定#defineSINGLETON_INSTANCE(Class)\public:staticClass&Instance(){\staticClass instance;\returninstance;\}classSingletonOne{SINGLETON_INSTANCE(SingletonOne)};

代码量减少了,可重用性提高了。但代价是什么?你看到的源码和编译器实际编译的代码不一样了。宏展开后是什么样子,出了错怎么定位,嵌套宏怎么展开——这些问题让C++的宏成了公认的"能不用就别用"的特性。

宏的问题在于它太粗暴了:纯文本替换,没有类型安全,没有作用域,报错信息一塌糊涂。

于是C++搞了一个更精密的编译时动态性工具:模板元编程。

模板元编程最初只是为了解决"类型泛化"——我写一个排序函数,希望它能排int也能排double,不用写两份代码。但当模板特例化引入之后,事情起了质变。

书里给了一个编译期计算Fibonacci数列的例子:

template<intn>structFibonacci{staticconstintvalue=Fibonacci<n-1>::value+Fibonacci<n-2>::value;};template<>structFibonacci<0>{staticconstintvalue=0;};template<>structFibonacci<1>{staticconstintvalue=1;};// Fibonacci<10>::value 在编译期就算出了55// 运行时零开销,汇编里直接就是一个常量

Fibonacci<10>::value等于55,这个55在编译阶段就算好了,运行时不需要做任何计算。编译后的汇编指令里,直接就是把55输出。

这为什么重要?因为模板特例化让C++的泛型编程变成了图灵完备的。图灵完备意味着理论上你可以在编译期做任何计算。Andrei Alexandrescu在2001年的《C++设计新思维》里把这个能力推到了极致,把大量原本只能在运行时做的事情提前到了编译期。

但书里也指出了一个很容易被忽略的区分:C++的泛型编程和C#/TypeScript/Dart的泛型编程,是完全不同的两种东西。后者本质上是"类型约束"——告诉编译器"这个参数必须是某种类型",它不生成新代码,不是图灵完备的,玩不出C++模板元编程那些花样。它们只是恰好都叫"泛型"。

这个区分太关键了。我以前混用这两个概念,读完才意识到自己一直搞混了。

编译时动态性的上限在哪?书里说得很清楚:它只能处理编译期已知的数据。用户运行时输入一个数字,你没法在编译期算它的Fibonacci值。所以编译时动态性是对编程体系的补充,不是替代。

而且它有另一个现实代价:编译时间。模板元编程越复杂,编译越慢。在程序员时薪越来越贵的今天,编译慢了几分钟,就是真金白银的损失。


第三维:运行时的动态性——最强大,也最危险

第三维是三层里抽象能力最强的,也是理解难度最高的。它有两条路线:多态元编程

多态:传入不同的"可执行单元"

书里对"多态"做了一个很漂亮的泛化定义:在运行时,通过传入不同的"可执行单元",让同一段代码产生不同的行为。

如果传入的是C++虚表——就是经典的面向对象多态。
如果传入的对象刚好有你需要的方法——就是鸭子类型。
如果传入的是一个函数——就是高阶函数。

本质上是同一件事。

书里的calculator例子很直观:

voidcalculator(doublea,doubleb,double(*op)(double,double)){doubleresult=op(a,b);cout<<"Result: "<<result<<endl;}calculator(10,5,&add);// 输出15calculator(10,5,&subtract);// 输出5calculator(10,5,&multiply);// 输出50

你单看calculator的实现,完全不知道它要算什么。加减乘除,甚至取模、幂运算,都行。运行时传什么函数进来,它就干什么。

这就是运行时动态性的核心特征:光读源码,你无法确定运行时的行为。

在真实项目里,情况远比这个例子复杂。一个多态单元里可能传入多个可执行单元,每个可执行单元本身又是多态的——层层嵌套下去,代码的运行时行为就像一棵不断分叉的树,静态阅读源码能覆盖的路径极其有限。

元编程:在运行时改写游戏规则

如果说多态是"运行时选择执行哪条路",元编程就是"运行时修路、拆路、建新路"。

书里把元编程分成了"读"和"写"两个方向:

读——反射。程序在运行时查看自己的结构:这个对象是什么类型?有哪些方法?参数列表是什么?Java、C#、Python都支持。反射最常见的用途是序列化——你扔一个对象进去,框架通过反射读出它所有字段,自动生成JSON/XML。

写——运行时改代码。给一个类动态添加方法、修改方法实现、甚至创建一个全新的类。

书里给了一个Python元类的例子:

classMyMeta(type):def__init__(cls,name,bases,attrs):super().__init__(name,bases,attrs)if"length"inattrs:setattr(cls,"len",cls.length)# 动态添加len方法classMyClass(metaclass=MyMeta):deflength(self):print("length call")obj=MyClass()obj.len()# 输出:length call

MyClass的源码里根本没有len这个方法。它是元类在运行时"凭空"加上去的。

这种能力强大得可怕,但书里的态度很明确:工程实践中,大规模使用元编程"写"特性的场景非常少见。更多时候用的是"读"和"加"(往现有类上追加方法),因为"加"相比"改"和"删"是无副作用的。Objective-C的Category、Ruby的Open Class,都是朝这个方向走的。

为什么对"写"持谨慎态度?因为编译器帮不了你。代码是运行时生成或修改的,静态分析工具看不到它,代码扫描器抓不住它,出了bug你面对的是一个在运行时被动态改过的程序——调试难度直接起飞。

书里引用了Brian Kernighan那句经典的话,我觉得说得极其到位:

“调试代码的难度是编写代码难度的两倍。因此,如果你在编写代码时用尽了聪明,那么你根本就不够聪明去调试它。”

调试的本质,就是理解运行时动态性的过程。这句话一旦理解了,你对"写清晰的代码"这件事的态度会发生根本性转变。

最极端的运行时动态性:创造方言

书里还提到了一种更激进的玩法——在运行时创建新的语法规则(方言)

运行时系统解析你定义的方言规则,在内存里构建语法树,接到现有的语法分析器上。后续代码直接用新语法写。

Lisp、Rebol、Red这些语言支持这种技术。用得好,能让语言完美适配某个特定领域的表达需求;用得不好,你的同事打开代码文件会以为自己在看一种从未见过的语言。


三个维度拼在一起,就是编程语言的全景

把三个维度摆在一起看:

维度动态性发生时机抽象能力理解难度典型技术
第一维:执行流运行时if/else、循环、函数调用、异常
第二维:编译时编译期宏、模板元编程、代码生成
第三维:运行时运行时多态、反射、元编程、方言

每种编程语言,本质上就是在这三个维度上做不同的取舍。

C语言基本只在第一维度活动,几乎没有编译时和运行时的动态性,所以C代码(单个函数层面)往往很容易读懂,但大规模抽象能力有限。

C++在三个维度上都有深入的投入——模板元编程把第二维推到了极致,虚函数和多态覆盖了第三维的多态方向。所以C++表达力极强,也极难精通。

Python、Ruby、JavaScript在第三维的元编程方向走得很远,运行时能做的事情非常多,灵活性拉满,代价是静态分析几乎无能为力,大型项目的可维护性压力巨大。

Java取了一个中间位置:有反射(第三维的"读"),有泛型(但只是类型约束,不是C++那种编译时代码生成),运行时动态性适度。这也是Java能在企业级大型项目里占据统治地位的原因之一——动态性够用,但不会失控。

Rust的选择更有意思:它用trait和泛型覆盖了编译时动态性,用所有权系统把运行时的很多不确定性提前到了编译期检查,刻意限制了运行时动态性的范围。牺牲灵活性,换取安全性。这不是技术落后,是设计哲学。


这个框架给我的启发

过去评价一门语言,我习惯看"语法好不好看"“生态大不大”“性能快不快”。这些当然重要,但都是表层。

用动态性三维模型去看,视角完全不一样了:

学一门新语言时,先搞清楚它在三个维度上各走了多远。这门语言的宏系统怎样?泛型是代码生成还是类型约束?支不支持反射?元编程能做到什么程度?搞清楚这些,你对这门语言的能力边界和适用场景就有了七八成的判断。

写代码时,有意识地控制动态性的引入。能用第一维解决的问题,别用第三维。不是因为第三维不好,而是因为维度越高,未来维护和调试的成本越高。只在真正需要高抽象能力的地方才升维。

读别人代码时,先识别动态性发生在哪一层。读不懂一段代码,往往不是因为算法复杂,而是因为你没搞清楚动态性藏在哪里——是编译时宏展开了?是运行时多态了?还是元编程动态修改了类结构?定位到维度,就找到了理解的入口。


最后

"编程语言的动态性三维模型"这个理论框架出自《代码的文明:编程语言如何改变历史》第一章。说实话这本书让我意外——我本来以为是讲编程语言历史故事的科普书,结果第一章就抛出了这种有理论深度的原创框架。

后面几章按十年一个阶段,从1950年代的FORTRAN一直讲到2020年代的Rust、Zig,40多种语言的诞生背景、设计哲学、工程得失,每种语言还附有作者基于十多年一线大厂开发经验的独立点评。

作者是腾讯的客户端Tech Leader,深度掌握超过10种编程语言。这种背景写出来的东西,跟学术圈或自媒体写的"编程语言排行榜"完全不是一回事。里面很多判断很尖锐——哪些语言被高估了,哪些设计决策后来被证明是错的,说得相当直接。

如果你写了几年代码,开始想搞明白"语言为什么是这样设计的"“我用的这些特性从哪来的”“下一个十年该押注什么方向”,这本书值得认真读一遍。


📚《代码的文明:编程语言如何改变世界》
ISBN:9787121523595 各大平台均可找到。

http://www.jsqmd.com/news/616390/

相关文章:

  • 品牌设计没头绪?专业公司来帮你!
  • 2026年怎么部署OpenClaw?阿里云超简单5步喂奶级教程
  • Phi-3-mini-4k-instruct-gguf完整指南:GGUF模型加载机制与llama-cpp内存优化原理
  • Entity Framework Core 10向量搜索深度实践(从NuGet包冲突到ANN精度调优全链路拆解)
  • 前端学习笔记-vue3基础
  • TOP3化妆学校,究竟哪家强?
  • 20230709直播实录
  • 基于STM32的多功能温室大棚环境监测系统:实时显示、远程监控与智能调节温湿度
  • Gradio UI定制化:修改SenseVoice-Small webui.py实现多语言切换+结果导出功能
  • 20230908直播录播回放
  • VOOHU沃虎单对以太网(SPE)技术白皮书:原理、标准、应用与选型
  • 2026化工行业高仓(6~12 米)条码采集方案:海雅达HDT500“12米中远距扫描”的5寸手持终端PDA
  • GTE-Base-ZH镜像体验:可视化界面+API,双模式交互更便捷
  • 2026企业用工数字化:如何选择适合自己的企业培训系统?
  • PHP 8.9 JIT开启后反而变慢?深度剖析opcache.jit_buffer_size与CPU缓存行对齐的隐性冲突
  • 直播带货系统源码开发需要哪些功能?电商直播平台搭建详解
  • 20230930直播实录
  • sp-html2canvas-render在iOS中跨域问题
  • 长沙这个酒吧好玩到让你不想回家!
  • Vue 2 与 Vue 3 的区别
  • Windows Terminal 文本出现黑色背景问题解决方法
  • java项目(附资料)-基于SpringBoot+Vue前后端分离的在线商城系统设计与实现
  • Windows 11系统优化完全指南:从卡顿到流畅的专业解决方案
  • AdMergeX 斩获信通院铸基计划双项权威认可
  • 20231022探讨赚钱直播实录
  • 根据渠道来实现不通逻辑的方法
  • 从零构建统一大模型应用平台:对话、代码、任务代理全解析!
  • html怎么用inert属性禁用_HTML如何通过Inert暂停交互区域
  • OpenClaw个人搜索引擎:Qwen3-14b_int4_awq构建本地文件语义检索系统
  • UI 动效设计:让界面活起来的艺术