跳到主要内容

编程概念

可以常来看看

面向

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

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

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

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

泛型编程引入这两个概念是为了解决泛型类型的继承关系问题
比如AppleFruit的子类,但是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是逆变

协变很好理解,AppleFruit的子类,所以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>
这样限制就会对这个类型的所有对象生效,而且类型定义也会更严格