跳到主要内容

编程概念

可以常来看看

面向​

Oriented
以...为核心或方向,进行思考、设计或操作

Anyway,面向过程,面向对象,这种语法结构是很西化很英语语境的东西
在中文语境中确实理解起来很神秘

当我们说“面向A”,其实意思就是用“A”这种方式或方法来完成目标

  • 面向过程
    • 想象我们有一个玩具制作套件,里面有一本说明书告诉你每一步如何制作玩具。我们从第一步开始,一步接一步,直到玩具完成
    • 就像是按照说明书的步骤来制作玩具
  • 面向对象
    • 现在想象我们有很多小玩具部件,每个部件都有自己的功能
      • 都可以自己动
      • 有的可以转圈
      • 有的可以叫
      • 有的可以发光
    • 就像是用这些有功能的小部件组合起来制作新的玩具

如此类推,面向函数,面向服务,面向……

然而,面向各种东西作为思想来讲并不是相互隔离的
如果解决一个问题,通常需要很多思想通力协作,交叉使用
在编程语言中,我们说某某语言是面向对象的,或者同时有面向对象和面向过程的
其实是在说它被设计出来是主要支持这一种或多种设计思想的
这个支持的意思是它有一些强制或非强制的特性(函数、继承、封装、多态,库、组件、事件)
你当然可以在面向对象的编程语言里面写面向过程,这是所有图灵完备的编程语言的基础
但是,我们通常使用Java来写面向对象的程序,用C来写面向过程的程序

现在流行面向对象,是因为人习惯把事物分解成一个个对象去思考,比较符合人的思维逻辑
但是面向对象也不一定是最好,我们需要具体情况具体分析

序列化​

Serialize
将一个 数据结构 的状态信息转换为 可以存储或传输的形式的过程

序列化常用于数据持久化、网络通信等场景

在序列化期间,数据结构将其当前状态写入到临时或持久性存储区
以后,可以通过从存储区中 反序列化 数据结构的状态,重新创建该数据结构

序列化几乎一定与反序列化成对出现

为什么叫“序列化”?​

“序列”在许多语言中都与“一系列”或“按顺序”这样的概念相关

Serialize v. 序列化,使连续,使串行
Serialization n. 序列化过程
Deserialize v. 去连续,使并行

数学上,序列是被排成一列的对象(或事件)
这样每个元素不是在其他元素之前,就是在其他元素之后

将一个离散的数据结构按照某种顺序转换成一个连续的数据格式(如字节流)
可以说把该数据结构转化为了一个序列

“序列化”就是将事件或动作连续地、按照某种顺序表示出来
“反序列化”就是相反的这个过程

最佳实践​

Best Practice

某个领域中,经过长期实践和验证后形成的、被广泛认可的、能够有效提高效率、减少错误、提升质量或安全性的操作方法、策略、标准或习惯

"约定优于配置"​

基于约定优于配置(Convention over Configuration)是一种软件设计理念
“约定”是指在工程结构、配置方式、默认行为等方面事先定义好一套标准、规则
基于约定的设计意味着,用户只要遵循这些预定的约定,就不需要做过多的配置

以Maven为例,它定义了一套标准的项目结构,例如:

  • 源码都放在src/main/java
  • 资源文件都放在src/main/resources
  • 测试代码都放在src/test/java

并且它有默认的构建流程,例如执行mvn package时,会按照编译、测试、打包的顺序执行
用户只要按照这些约定组织项目的结构和文件,就可以直接使用Maven进行构建,不需要太多配置

相比之下,不基于约定的工具则需要用户大量的配置来定制行为,这被称为“配置优于约定”
基于约定的设计简化了使用,用户只要遵守约定就可以,减少了学习成本和配置工作
但也降低了灵活性,如果项目与约定不符,就需要更多配置来覆盖默认约定

所以综上,“基于约定”指利用预定义的标准结构和默认行为来减少配置
用户只要遵守这些预设的约定就可以直接使用

副作用​

Side Effect

在计算机科学中是指函数或表达式执行时除了计算其主要结果之外,还对程序状态或其他外部系统产生的非预期的、不可预测的变化。

这一概念借用自医学术语,在药物学中,副作用是指药物治疗目的之外所引起的其他生理反应。

在编程领域里,纯函数被认为是无副作用的,因为它们只依赖于输入参数,并且每次给定相同输入时总是产生相同的输出,同时不改变任何全局状态或对外部环境造成影响。
相反,带有副作用的函数或操作可能修改全局变量、文件内容、数据库记录、屏幕输出等,这些变化不仅会影响当前函数的结果,还可能影响到其他代码的执行结果和系统的整体行为。

协变/逆变​

Covariance/Contravariance

这个原本是张量分析里面的概念

  • 协变:自变量增大,因变量增大
  • 逆变:自变量增大,因变量减小

可以类比于正相关和负相关

泛型编程引入这两个概念是为了解决泛型类型的继承关系问题
比如Apple是Fruit的子类,但是Plate<Apple>不是Plate<Fruit>的子类
协变和逆变可以让泛型类型Gene<T>根据泛型参数T的一些规则来确定继承关系

有些地方叫法不同,比如Java里面叫上下界
以Kotlin举例:

open class Fruit

class Apple : Fruit()
class Banana : Fruit()

class Plate<T> {
val stack = mutableListOf<T>()

fun add(item: T) : Boolean = stack.add(item)
fun get() : T = stack.removeAt(stack.size - 1)
}

fun main() {
val p1: Plate<out Fruit> = Plate<Apple>()
val p2: Plate<in Apple> = Plate<Fruit>()
}

这里out是协变,in是逆变

协变很好理解,Apple是Fruit的子类,所以Plate<Apple>是Plate<out Fruit>的子类
然后p1就可以使用get() : Fruit获取一个Fruit(实际获得的是Apple)
但是p1无法使用add(Fruit),因为p1实际上是Plate<Apple>,如果add(Banana)就会类型错误

逆变不是很好理解,但其实就是反过来,即Plate<Fruit>是Plate<in Apple>的子类
p2可以使用add(Apple)来添加一个Apple(实际添加进去的是Fruit)
但是p2不能使用get() : Apple,可以使用get() : Any?
泛型类型Gene<T>的泛型参数T是仅编译期的,泛型参数在运行时会被擦除Gene<*>(无法获得)
当函数作为一个泛型类的成员,并且使用了泛型类的泛型参数,那么函数的泛型参数也会被擦除
即get() : T会被擦除为get() : Any?
所以即使一个逆变类型只能输入T类型的对象,它的函数返回也没法自动转换成T,因为它在运行时压根不知道这个函数的返回类型

协变和逆变仅能用在类型声明上面,不能用在函数参数和构造函数上
上面举的例子是对单个对象加限制,其实还可以写在类型的定义声明上面,比如class Plate<out T>
这样限制就会对这个类型的所有对象生效,而且类型定义也会更严格