懂得编程语言的通用结构,随便哪个语言都是手拿把掐
编程语言核心结构体系:从相似性到本质理解
前言
在接触过多个编程语言的学习之后,观察到一些通用的范式结构,编程语言虽然表面差异巨大,但底层存在一套不可简化的最小完备集——这是所有语言都必须包含的基本元素,否则无法表达任意算法。
而把握住这一点之后,对任意编程语言的学习都有一种脉络极其明晰的感觉,一旦了解到这种通用范式的结构,那么对于入门编程语言就会有一个系统性的学习认知框架,知道该学什么,从哪里开始学。
这种通用的范式结构就是所有编程语言共有的基础元素,这种相似性源于计算机科学的基本原理——所有编程语言本质上都是人与计算机沟通的抽象工具,需要遵循计算机底层执行逻辑的约束。
正是这样的约束,导致了编程语言在设计时所共同遵守的某种规则,也就是那些隐藏在各种语法表面之下的共性规律。也因为是讲解共性的内容,所以只会涉及到有哪些共性,不会描述这些共性在具体语言中是怎么表示的内容。
基础要素
任何编程语言都像一座建筑,需要最基础的材料和结构。这些基础元素是表达程序逻辑的基本单元,它们共同构成了编程语言的基础框架,包括如下五个部分:
数据表示表达式与运算控制结构抽象机制输入输出机制
下面按顺序进行介绍。
数据表示
任何计算都涉及数据,必须有表示数据的方式
就像是在草稿本上求解数学题一样,特别是代数内容,有字母、计算符号以及数值,这些写在本子上的字符是表达这些内容的具体形式,并且可以保证每个学习过代数的人都可以看懂和理解,因为是一套相同的机制。
变量
首先要介绍的是变量,那么为什么要有变量呢?想象一下,在代数中,如果没有变量,只有常量,也就是具体的数值,那么所有的问题都只是数值计算问题,而且是必须一次性完成的计算,不可能分步骤,迭代式的计算。
同时在实际情况中,就是有求解未知量的需求,也有某些量在动态变化的情况,所以单纯的常量无法建构一个复杂且动态的数学世界,对编程而言也同样如此。
在程序中,变量的作用如下所示:
- 临时保存数据,用于分步计算,避免一次性大量计算
- 避免直接使用常量,因为在一个表达式中,常量是无法修改的
- 假设定义一个穿了增高鞋的人的身高函数为
f(x)=x+2,其中变量x表示这个人的实际身高,而整数常量2就表示增高鞋的高度,是固定的数值 - 如果它只穿同一个增高鞋的话,这个函数没有问题,但是哪天他换了其他高度的鞋子,这个2就不适用了
- 难道要为每个鞋子定义一个专属的函数吗,这显然不可能。但是又不知道鞋子具体能给他提供多少身高
- 所以这时候就换用变量
a来描述,f(x)=x+a,此时这个变量a就代指了增高鞋的高度,根据实际鞋子的增高功能同步变化,灵活度就更高了。
- 假设定义一个穿了增高鞋的人的身高函数为
- 记录程序运行状态,不同于临时保存数据只是某一计算的中间过程,此处的运行状态可以调控程序的流程和效果
- 根据输入变化行为,提供了与外界交互的可能,因为输入是不确定的,只有变量才能描述这种不确定性
- 隐藏内存细节,因为变量本质上是内存中存储数据的位置代称,否则需要直接操作内存地址,可读性非常差
下面给一个关于代数中的变量与程序中的变量的对比:
| 代数 | 编程 | 相同点 | |
|---|---|---|---|
| 变量 | 表示未知数或可变的量 | 本质上是内存中存储数据的位置代称,或者说一个容器的名字 | 1. 两者都是"符号代表值" 2. 都可以被重新赋值 3. 都遵循"先定义后使用"的原则 |
有一个常见的混淆点,就是符号=,在数学中,=表示的是一种等价关系,只是一种逻辑关系、比如x=5,表示的是x等于5这样一个关系或事实表述;而在程序中,表示的是一个动作,即赋值,也可以形象的表述为把一个值放入进一个容器中,具体来说就是把数值5放入到名为x的容器中,这个x也称为变量。
作为对比,现在有表达式x=x+1,如果从数学的角度来看,这个等价关系是不成立的,但是从程序的角度来看,就是取出容器x的值,加一后再放回去的意思。
小结一下,可以认为变量就是一种容器(当然也有其它类型的容器),既然是容器那么就是可重复利用的,同时所有语言都必须提供将数据存储在内存中并可通过名称引用的机制,这是计算的前提,后面的内容表述中,容器就是变量的意思。
标识符
上一小节介绍了变量,也提到变量就是容器,但这些都是抽象的概念,也就是说,给你一些看起来一模一样的容器,然后拿一个小球随机放进一个容器中,并打乱容器的摆放顺序,你还能找到小球在哪个容器中吗?
很难对吧,但是如果给每个容器标识一个唯一的名字,那么只要记住小球放入哪个名字标识的容器就可以了,因为此时容器是可识别的。
实际上只要标识符能唯一确定某个容器,并不会关心标识符由什么组成,但现实是程序的标识符需要遵循一些规范,比如不能以数字开头、不能包含特殊字符等。
此外还有一类编程语言独有的预定义标识符,也称为关键字,这些标识符是不可使用的,比如python中的input、print内置函数名。
数据/值
既然有了容器(变量),那么总要往容器里面放入一些东西,对于程序而言,就是数据,也可以称为值,而把数据放入变量这种容器的动作就是赋值操作。
数据有很多种类型,比如日常在excel中有文本类型的数据、有数值类型的数据,还有一些复合类型的数据,这是因为数据来源于多种形式的活动中。
其中文本类型的数据可能是公司的员工姓名、数值类型的数据可能是员工的薪资、复合类型的数据可能是员工其他信息的组合。
在数学中,数字有整数、小数,复数等类型,同样在程序中的数据类型也有多种,比如数值类型(整数,浮点数)、布尔类型(真/假)和字符串类型等。
之所以有这些数据类型,就是要定义数据的性质以及不同类型的处理方式,既可以是同类型之间的运算,比如3+2是两个整数之间的运算;也可以是不同类型之间的运算,比如3+2.5中一个是整数,另一个是小数,定义它们之间的运算方式为:把整数转换为小数之后再与另一个小数进行计算。
表达式与运算
没有运算就无法计算
所有语言都支持将值通过运算符组合成新值,而这种由变量、常量和运算符组合的形式就是表达式,比如x + 5 * 3,和数学中的形式很像,并且一般情况下运算符的语义也是相通的。
这样的表达式称为算术表达式,可以包含变量和常量,也可以通过小括号改变运算顺序,但是不同于数学中这样的表达式只表示关系,在程序中,这样的表达式会实际计算值,也就是有一个算术结果,并且乘号不能省略。
接下来是比较与逻辑运算,比如数学中x > 5,表示变量与数值的关系,是这样一个事实陈述:x大于5;在程序中,这样的表达式称为布尔表达式,会产生一个布尔值(真/假)。
要记住只要是值就可以赋值给变量,比如is_greater = x > 5,那么如果x>5,则变量is_greater保存的结果就为一个逻辑真值,在python中就是true,否则为一个逻辑假值false,一般布尔表达式用于条件判断,比如if条件判断。
控制结构
控制流就是逻辑的表达
基本控制结构有下表所示:
| 元素 | 本质作用 | 说明 |
|---|---|---|
| 顺序执行 | 默认执行方式 | 语句按顺序依次执行 |
| 条件分支 | 根据条件选择路径 | 必须支持if或等价机制 |
| 循环/迭代 | 重复执行代码块 | 必须支持while或等价机制 |
| 跳转/返回 | 改变执行位置,体现思维的跳跃 | 如return、break |
其中顺序执行图示如下:
按照规定好的工序一步步顺序执行,不完成步骤1,就不会执行到步骤2,就比如洗完澡穿衣服,正常的顺序应该是先穿内衣再穿外衣,也就是步骤1是穿内衣,步骤2是穿外衣,排除不穿内衣的情况,那么应该没人会先执行步骤2穿外衣,再执行步骤1穿内衣吧(不会吧不会吧😲😲)
条件分支图示如下:
一个逻辑判断只会产生两个结果,不是真就是假,比如你今天下班买菜了吗这个逻辑判断,要么买了,要么没买,所以对应到图上,只会产生两条支路,如果买菜了,那么就执行操作A,可以是自己做晚饭,如果没买菜,那么执行操作B,可以是点外卖
循环结构图示如下:
循环,可以理解为就是重复,比如你计划一个长达1年的早起习惯养成目标,那么对应到图上,循环条件就是不满365天,也就是还在习惯养成过程中。
然后循环体中就是要执行早起这个动作并且累计早起天数,从开始早起的第一天算起,后面每天早起都增加天数,直到超过365天,这时候就恭喜完成1年的目标啦
对于跳转和返回这类非顺序的控制流,关键在于打破默认的从上至下的执行顺序,但一般用在循环的结束条件和函数上下文切换中,其他地方不推荐使用,比如goto语句,因为属于逻辑跳跃,不利于理解。
其次是所有图灵完备语言都必须支持条件判断和循环(或等价的递归),这是表达任意算法的必要条件
抽象机制
没有抽象就无法管理复杂度
从本质上讲,抽象就是信息隐藏——将复杂的内部实现封装起来,只对外提供必要的操作接口。这就像驾驶汽车:你只需要知道油门、刹车、方向盘,而不需要了解发动机如何工作、变速箱如何换挡。在程序中,基础的抽象的形式主要有:函数/过程,类与对象。
函数/过程
什么是函数呢,本质上是将一段完成特定任务的代码封装成一个独立的、可复用的单元,通过定义输入(参数)和输出(返回值)来隐藏内部实现细节。
它是最基础、最核心的编程抽象机制。通过这种方式我们可以将复杂系统分解为易于理解和管理的小模块,所有实用语言都提供将代码组织成可复用单元的机制,否则无法编写大型程序。
形象的理解函数,可以认为函数就是一台机器,排除额外的改造之外,每台机器都有各自的功能,这是在机器诞生之日起就固定下来了,比如吸尘器,顾名思义,就是吸入灰尘的。
也就是说吸尘器的输入是灰尘,灰尘经过吸尘器之后会有一个输出,一团聚集的灰尘,这是机器的理想工作状态,那如果输入纸巾呢,好像也勉强能接收吧,可能有的机器处理的不是很好吧,但如果输入砖头呢,应该没有哪个吸尘器能干这个事情吧。
所以一台机器的输入是有规定的,虽然它的输入是不受机器自身控制,而来源于外部,但是如果想要正常使用机器的功能,那么就不能由着自己的性子,想输入什么就输入什么
同样的对于输出而言,这是机器本身固定的部分,吸尘器不能把灰尘变成黄金,同时对于输入是砖头时,吸尘器也不知道该输出什么,它直接罢工不干了。
总结下来,函数的核心作用如下所示:
- 代码复用,一次定义,多次使用
- 可维护性,修改一处即可
- 可读性,函数名表达意图
- 错误排查,错误集中在函数内
那么该怎么使用函数呢,首先就是定义的问题,也就是确定函数的作用、函数的输入以及函数的输出,即函数名、参数以及返回值。作为对比,可以和数学中的函数定义进行比较,
| 数学思维 | 编程思维 |
|---|---|
| 函数是一种映射关系, 从定义域映射到值域 |
函数是一个可执行过程, 会实际计算出结果 |
| 函数的输入来自于定义域 | 函数的参数有类型约束 |
| 函数的输出来自于值域 | 函数的返回值也有类型约束 |
需要注意的是,需要区分函数的定义与调用,也就是先有定义才可以调用,比如先有函数f(x)=2x,才会有f(2)=2*x=4这个计算过程。
输入输出机制
没有I/O的程序无法与外界交互
日常中,我们能感知到的输入输出机制就是在使用手机的过程中,比如刷手机时,手指滑动屏幕,会切换到下一个视频。
此时输入就是手指滑动这个动作,输出就是手机屏幕做切换视频的动作,这就是一种与用户交互的方式,可以很直观的被用户感知到。
所有实用语言都提供与外部世界交互的途径,否则程序的作用就被限制在计算机内部。
除了这种显式的交互方式,还有就是隐藏在系统内部发生的输入输出过程,这里涉及到多层级的过程,对于用户而言,输入和输出都在最外层。
这个过程就像食品加工过程一样,比如水果罐头的加工过程,如下图所示:
输入: 水果原料
输出: 洁净果块] --> 装罐[装罐与糖水灌注
输入: 洁净果块 + 糖水
输出: 半成品罐头] end %% 第二层:密封杀菌 subgraph 第二层_密封杀菌 装罐 --> 密封杀菌[密封与杀菌
输入: 半成品罐头
输出: 杀菌罐头] end %% 第三层:成品处理 subgraph 第三层_成品处理 密封杀菌 --> 冷却包装[冷却与包装
输入: 杀菌罐头
输出: 包装成品] end %% 最终输出 冷却包装 --> 成品[🍑 整果罐头成品] %% 样式设置 style 第一层_原料准备 fill:#fff3e0,stroke:#ef6c00,stroke-width:2px style 第二层_密封杀菌 fill:#f3e5f5,stroke:#8e24aa,stroke-width:2px style 第三层_成品处理 fill:#e8f5e8,stroke:#2e7d32,stroke-width:2px style 水果原料 fill:#ffccbc,stroke:#d84315,stroke-width:2px style 成品 fill:#c8e6c9,stroke:#388e3c,stroke-width:3px
输入输出关系标注规则如下所示:
- 输入:当前工序接收的物料/半成品
- 输出:当前工序处理后产生的物料/半成品
- 每个工序框内都明确标注了输入和输出内容
- 箭头方向表示物料流向,即上一工序的输出是下一工序的输入
对比到app的登陆验证过程中,用户输入密码->app内部做验证->返回验证结果,对于用户而言内部怎么做验证是不需要关心的,只要密码正确就应该登录进账户,密码错误就应该无法进入账户。
而对于app而言,它不会直接接收到用户的输入,用户的输入直接给到了键盘,这中间还有操作系统的工作,app只是接收了某一过程的输出作为输入。
同样对于验证结果而言,用户能看到的只有屏幕,但是屏幕绝不会做验证密码的事情,实际上app输出的验证结果也不是直接给到屏幕作为输入的,这个验证结果也要经过操作系统的底层操作,输出给屏幕,然后屏幕再输出给用户显示结果。
在学习编程时,都会想要看到反馈的结果,一个普遍的做法是把结果或其他信息输出到屏幕上,比如python的print函数,就可以输出信息到屏幕,比如print("读取文件失败")这一语句就表示程序想要读取某个文件,但是读取失败了。
那么接下来可以就为啥会读取失败这个问题,有针对性的调试代码了,这种信息就是调试信息。而这个print函数其实并不能直接驱动屏幕,它只是触发了驱动屏幕的开关,给了下一层级一个输入,也就是哪个调试信息文本,整个流程就和上面介绍的水果罐头加工过程一样,涉及到底层I/O操作。
总结
经过以上内容的介绍,我们知道在学习新语言时,就应该先把握住如下重点:
- 它如何表示数据(变量、值),如何声明变量和赋值,有哪些数据类型
- 它如何进行计算(运算符、表达式),有哪些基本运算符(算术、比较、逻辑)
- 它如何控制流程(条件、循环),如何写条件语句(if或等价形式),如何写循环(while/for或等价形式)
- 它如何封装代码(函数、作用域),如何定义和调用函数
- 它如何与外界交互(I/O),如何读取输入和输出结果
一旦对编程语言的认知框架形成,基本就掌握了使用这个编程语言的能力,处理一些简单问题是完全足够的。
在理解语言共性时,应关注功能等价性而非语法相似性。例如,Python的缩进和C++的花括号都是表达代码块的方式,本质相同。
这种"透过语法看语义"的视角,才能真正把握编程语言的共性规律。
加油吧,每个初学编程的朋友。
微信公众号:软趴趴的工程师

原文地址: https://www.cveoy.top/t/topic/qFS1 著作权归作者所有。请勿转载和采集!