0%

2020年3月19日 上午11:01

注:这部分知识要联系编译原理的知识进行组合学习 目录(编译原理)

Java 虚拟机中的即时编译

  1. 分层编译的
    • 从 Java 8 开始,Java 虚拟机默认采用分层编译的方式。它将执行分为五个层次,分为为 0 层解释执行,1 层执行没有 profiling 的 C1 代码,2 层执行部分 profiling 的 C1 代码,3 层执行全部 profiling 的 C1 代码,和 4 层执行 C2 代码。
    • 通常情况下,方法会首先被解释执行,然后被 3 层的 C1 编译,最后被 4 层的 C2 编译。
  2. 计数器对即时编译的触发
    • 即时编译是由方法调用计数器和循环回边计数器触发的。在使用分层编译的情况下,触发编译的阈值是根据当前待编译的方法数目动态调整的。
  3. OSR
    • 实际上,除了以方法为单位的即时编译之外,Java 虚拟机还存在着另一种以循环为单位的即时编译,叫做 On-Stack-Replacement(OSR)编译。循环回边计数器便是用来触发这种类型的编译的。
    • OSR 实际上是一种技术,它指的是在程序执行过程中,动态地替换掉 Java 方法栈桢,从而使得程序能够在非方法入口处进行解释执行和编译后的代码之间的切换。
    • OSR 编译可以用来解决单次调用方法包含热循环的性能优化问题。

Java 虚拟机的 profiling 以及基于所收集的数据的优化和去优化。

  1. profile
    1. 分层编译中的 0 层、2 层和 3 层都会进行 profiling,收集能够反映程序执行状态的数据。其中,最为基础的便是方法的调用次数以及循环回边的执行次数。它们被用于触发即时编译
    2. 通常情况下,解释执行过程中仅收集方法的调用次数以及循环回边的执行次数。
    3. 当方法被 3 层 C1 所编译时,生成的 C1 代码将收集条件跳转指令的分支 profile,以及类型相关指令的类型 profile。在部分极端情况下,Java 虚拟机也会在解释执行过程中收集这些 profile。
  2. profile 的优化
    1. 基于分支 profile 的优化以及基于类型 profile 的优化都将对程序今后的执行作出假设。这些假设将精简所要编译的代码的控制流以及数据流。
    2. 在假设失败的情况下,Java 虚拟机将采取去优化,退回至解释执行并重新收集相关的 profile。

2020年3月18日 下午12:38

C++ 很大程度就是用来解决 C 语言中的各种问题和各种不方便的。比如:

  • 用引用来解决指针的问题。
  • 用 namespace 来解决名字空间冲突的问题。
  • 通过 try-catch 来解决检查返回值编程的问题。
  • 用 class 来解决对象的创建、复制、销毁的问题,从而可以达到在结构体嵌套时可以深度复制的内存安全问题。
  • 通过重载操作符来达到操作上的泛型。(比如,消除《01 | 编程范式游记:起源》中提到的比较函数cmpFn,再比如用>>操作符消除printf()的数据类型不够泛型的问题。)
  • 通过模板 template 和虚函数的多态以及运行时识别来达到更高层次的泛型和多态。
  • 用 RAII、智能指针的方式,解决了 C 语言中因为需要释放资源而出现的那些非常 ugly 也很容易出错的代码的问题。
  • 用 STL 解决了 C 语言中算法和数据结构的 N 多种坑。

2020年3月18日 下午12:20

基础概念:

C++ 是如何有效解决程序泛型问题的,有三点

  1. 第一,它通过类的方式来解决。
    • 类里面会有构造函数、析构函数表示这个类的分配和释放。
    • 还有它的拷贝构造函数,表示了对内存的复制。
    • 还有重载操作符,像我们要去比较大于、等于、不等于。
    • 这样可以让一个用户自定义的数据类型和内建的那些数据类型就很一致了。
  2. 第二,通过模板达到类型和算法的妥协。
    • 模板有点像 DSL,模板的特化会根据使用者的类型在编译时期生成那个模板的代码。
    • 模板可以通过一个虚拟类型来做类型绑定,这样不会导致类型转换时的问题。
    • 模板很好地取代了 C 时代宏定义带来的问题。
  3. 第三,通过虚函数和运行时类型识别。
    • 虚函数带来的多态在语义上可以支持“同一类”的类型泛型。
    • 运行时类型识别技术可以做到在泛型时对具体类型的特殊处理。

一个良好的泛型编程需要解决如下几个泛型编程的问题:

  • 算法的泛型:算法与类型解耦
  • 数据结构(数据容器)的泛型:不区分顺序型的数据结构 、非顺序型的数据结构
  • 类型的泛型:我这里理解是SUM中的问题:那个T假设了 Iter 中出来的类型是T

实例详解:

第一步:算法的泛型:算法与类型解耦

  • void*指针
  • 函数指针
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    int search(void* a, size_t size, void* target, 
    size_t elem_size, int(*cmpFn)(void*, void*) )
    {
    for(int i=0; i<size; i++) {
    // why not use memcmp()
    // use unsigned char * to calculate the address
    if ( cmpFn ((unsigned char *)a + elem_size * i, target) == 0 ) {
    return i;
    }
    }
    return -1;
    }

第二步:数据结构(数据容器)的泛型:不区分顺序型的数据结构 、非顺序型的数据结构

  • 模板编程
  • 不同数据结构:操作符重载
  • 不同数据结构:包含迭代器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 使用typename T抽象了数据结构中存储数据的类型
    // 使用typename Iter,这是不同的数据结构需要自己实现的“迭代器”,这样也就抽象掉了不同类型的数据结构
    template<typename T, typename Iter>
    Iter search(Iter pStart, Iter pEnd, T target)
    {
    for(Iter p = pStart; p != pEnd; p++) {//通过操作符重载也就泛型掉了遍历
    if ( *p == target )
    return p;
    }
    return NULL;
    }

第三步:类型的泛型:我这里理解是SUM中的问题:那个T假设了 Iter 中出来的类型是T

  • 内部类:
    • 迭代器。begin, end返回的是迭代器,其实就是一个内部类对象
    • 迭代器内部类中实现了大量的重载方法。
      问题是:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      template<typename T, typename Iter>
      T sum(Iter pStart, Iter pEnd) {
      T result = 0;
      for(Iter p=pStart; p!=pEnd; p++) {
      result += *p;
      }
      return result;
      }
      /*
      这个代码中最大的问题就是 T result = 0
      1.那个0假设了类型是int
      2.那个T假设了 Iter 中出来的类型是T
      */
      解决方法:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      //定义result的T应该可以从Iter中来。这样就可以保证类型是一样的,而且不会有被转型的问题
      template <class T>
      class container {
      public:
      class iterator {
      public:
      typedef iterator self_type;
      typedef T value_type;
      typedef T* pointer;
      typedef T& reference;

      reference operator*();
      pointer operator->();
      bool operator==(const self_type& rhs);
      bool operator!=(const self_type& rhs);
      self_type operator++() { self_type i = *this; ptr_++; return i; }
      self_type operator++(int junk) { ptr_++; return *this; }
      ...
      ...
      private:
      pointer _ptr;
      };

      iterator begin();
      iterator end();
      ...
      ...
      };
1
2
3
4
5
6
7
8
9
10
template <class Iter>
typename Iter::value_type
sum(Iter start, Iter end, T init) {
typename Iter::value_type result = init;//这条语句是关键
while (start != end) {
result = result + *start;
start++;
}
return result;
}

调用算法:

1
2
3
container<int> c;
container<int>::iterator it = c.begin();
sum(c.begin(), c.end(), 0);

2020年3月18日 下午12:08

程序语言的类型系统主要提供如下的功能

  • 程序语言的安全性。
    • 使用类型可以让编译器侦测一些代码的错误,例如:可以识别出一个错误无效的表达式,如“Hello, World” + 3这样的不同数据类型间操作的问题。
    • 强类型语言提供更多的安全性,但是并不能保证绝对的安全。
  • 利于编译器的优化。
    • 静态类型语言的类型声明,可以让编译器明确地知道程序员的意图。因此,编译器就可以利用这一信息做很多代码优化工作。
    • 例如:如果我们指定一个类型是 int ,那么编译就知道,这个类型会以 4 个字节的倍数进行对齐,编译器就可以非常有效地利用更有效率的机器指令。
  • 代码的可读性。
    • 有类型的编程语言,可以让代码更易读和更易维护,代码的语义也更清楚,代码模块的接口(如函数)也更丰富和清楚。
  • 抽象化。
    • 类型允许程序设计者对程序以较高层次的方式思考,而不是烦人的低层次实现。例如,我们使用整型或是浮点型来取代底层的字节实现,我们可以将字符串设计成一个值,而不是底层字节的数组。
    • 从高层上来说,类型可以用来定义不同模块间的交互协议,比如函数的入参类型和返回类型,从而可以让接口更有语义,而且不同的模块数据交换更为直观和易懂。

类型带来的问题

  • 类型带来的问题就是我们作用于不同类型的代码,虽然长得非常相似,但是由于类型的问题需要根据不同版本写出不同的算法,如果要做到泛型,就需要涉及比较底层的玩法

核心:每个语言都需要一个类型检查系统

  • 静态类型检查是在编译器进行语义分析时进行的。
    • 如果一个语言强制实行类型规则(即通常只允许以不丢失信息为前提的自动类型转换),那么称此处理为强类型,反之称为弱类型。
  • 动态类型检查系统更多的是在运行时期做动态类型标记和相关检查。
    • 所以,动态类型的语言必然要给出一堆诸如:is_array(), is_int(), is_string() 或是 typeof() 这样的运行时类型检查函数。

2020年3月18日 下午8:01

具体来说,了解 Java 虚拟机有如下(但不限于)好处。

  • 首先,Java 虚拟机提供了许多配置参数,用于满足不同应用场景下,对程序性能的需求。学习 Java 虚拟机,你可以针对自己的应用,最优化匹配运行参数。(你可以用下面这个例子看一下自己虚拟机的参数列表。)
  • 其次,Java 虚拟机本身是一种工程产品,在实现过程中自然存在不少局限性。学习 Java 虚拟机,可以更好地规避它在使用中的 Bug,也可以更快地识别出 Java 虚拟机中的错误,
  • 再次,Java 虚拟机拥有当前最前沿、最成熟的垃圾回收算法实现,以及即时编译器实现。学习 Java 虚拟机,我们可以了解背后的设计决策,今后再遇到其他代码托管技术也能触类旁通。
  • 最后,Java 虚拟机发展到了今天,已经脱离 Java 语言,形成了一套相对独立的、高性能的执行方案。除了 Java 外,Scala、Clojure、Groovy,以及时下热门的 Kotlin,这些语言都可以运行在 Java 虚拟机之上。学习 Java 虚拟机,便可以了解这些语言的通用机制,甚至于让这些语言共享生态系统。

Java 代码为何在虚拟机中运行,以及如何在虚拟机中运行

  1. 之所以要在虚拟机中运行,是因为它提供了可移植性。一旦 Java 代码被编译为 Java 字节码,便可以在不同平台上的 Java 虚拟机实现上运行。此外,虚拟机还提供了一个代码托管的环境,代替我们处理部分冗长而且容易出错的事务,例如内存管理。
  2. 内存划分:
    • Java 虚拟机将运行时内存区域划分为五个部分,分别为方法区、堆、PC 寄存器、Java 方法栈和本地方法栈。Java 程序编译而成的 class 文件,需要先加载至方法区中,方能在 Java 虚拟机中运行。
  3. 不同的编译策略:
    • 为了提高运行效率,标准 JDK 中的 HotSpot 虚拟机采用的是一种混合执行的策略。
    • 它会解释执行 Java 字节码,然后会将其中反复执行的热点代码,以方法为单位进行即时编译,翻译成机器码后直接运行在底层硬件之上。
    • HotSpot 装载了多个不同的即时编译器,以便在编译时间和生成代码的执行效率之间做取舍。

Java 里的基本类型

  1. 虚拟机眼中的boolean 类型
    • boolean 类型在 Java 虚拟机中被映射为整数类型:“true”被映射为 1,而“false”被映射为 0。Java 代码中的逻辑运算以及条件跳转,都是用整数相关的字节码来实现的。
  2. Java 还有 8 个基本类型
    • 除 boolean 类型之外,Java 还有另外 7 个基本类型。它们拥有不同的值域,但默认值在内存中均为 0。这些基本类型之中,浮点类型比较特殊。基于它的运算或比较,需要考虑 +0.0F、-0.0F 以及 NaN 的情况。
  3. 占用内存大小:
    • 除 long 和 double 外,其他基本类型与引用类型在解释执行的方法栈帧中占用的大小是一致的,但它们在堆中占用的大小确不同。在将 boolean、byte、char 以及 short 的值存入字段或者数组单元时,Java 虚拟机会进行掩码操作。在读取时,Java 虚拟机则会将其扩展为 int 类型。

Java 虚拟机将字节流转化为 Java 类的过程

  1. 这个过程可分为加载、链接以及初始化三大步骤。
  2. 加载
    • 加载是指查找字节流,并且据此创建类的过程。加载需要借助类加载器,在 Java 虚拟机中,类加载器使用了双亲委派模型,即接收到加载请求时,会先将请求转发给父类加载器。
  3. 链接
    • 链接,是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。链接还分验证、准备和解析三个阶段。其中,解析阶段为非必须的。
  4. 初始化
    • 初始化,则是为标记为常量值的字段赋值,以及执行 < clinit > 方法的过程。类的初始化仅会被执行一次,这个特性被用来实现单例的延迟初始化。

Java 以及 Java 虚拟机是如何识别目标方法的

  1. 编译期和虚拟机重载、重写、识别方法的方式的不同:
    • 在 Java 中,方法存在重载以及重写的概念,重载指的是方法名相同而参数类型不相同的方法之间的关系,重写指的是方法名相同并且参数类型也相同的方法之间的关系。
    • Java 虚拟机识别方法的方式略有不同,除了方法名和参数类型之外,它还会考虑返回类型。
  2. 在 Java 虚拟机中,静态绑定与识别目标方法的的联系
    • 在 Java 虚拟机中,静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。由于 Java 编译器已经区分了重载的方法,因此可以认为 Java 虚拟机中不存在重载。
    • 在 class 文件中,Java 编译器会用符号引用指代目标方法。在执行调用指令前,它所附带的符号引用需要被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用为目标方法的指针。对于需要动态绑定的方法调用而言,实际引用为辅助动态绑定的信息。

虚方法调用在 Java 虚拟机中的实现方式

  1. 基本概念:
    • 虚方法调用包括 invokevirtual 指令和 invokeinterface 指令。如果这两种指令所声明的目标方法被标记为 final,那么 Java 虚拟机会采用静态绑定。
    • 否则,Java 虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。
  2. 基本原理:
    • Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。
    • 在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法
  3. 加速动态绑定:
    • Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。
    • 当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。
    • 否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。

Java 虚拟机的异常处理机制

  1. 异常的分类
    • Java 的异常分为 Exception 和 Error 两种,而 Exception 又分为 RuntimeException 和其他类型。RuntimeException 和 Error 属于非检查异常。其他的 Exception 皆属于检查异常,在触发时需要显式捕获,或者在方法头用 throws 关键字声明。
  2. Java 字节码中,每个方法对应一个异常表
    • Java 字节码中,每个方法对应一个异常表。当程序触发异常时,Java 虚拟机将查找异常表,并依此决定需要将控制流转移至哪个异常处理器之中。Java 代码中的 catch 代码块和 finally 代码块都会生成异常表条目。
  3. Java 7 引入了 Suppressed 异常、try-with-resources,以及多异常捕获。后两者属于语法糖,能够极大地精简我们的代码。

JVM实现反射:这里需要及时编译、方法内联、逃逸分析部分的知识

  1. 在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。
  2. 方法的反射调用会带来不少性能开销,原因主要有三个:变长参数方法导致的 Object 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。

Java 虚拟机构造对象的方式,所构造对象的大小,以及对象的内存布局

  1. 常见的 new 语句会被编译为 new 指令,以及对构造器的调用。每个类的构造器皆会直接或者间接调用父类的构造器,并且在同一个实例中初始化相应的字段。
  2. 压缩指针
    • Java 虚拟机引入了压缩指针的概念,将原本的 64 位指针压缩成 32 位。压缩指针要求 Java 虚拟机堆中对象的起始地址要对齐至 8 的倍数。
    • Java 虚拟机还会对每个类的字段进行重排列,使得字段也能够内存对齐。

垃圾回收的一些基础知识

  1. 可达性分析
    • Java 虚拟机中的垃圾回收器采用可达性分析来探索所有存活的对象。它从一系列 GC Roots 出发,边标记边探索所有被引用的对象。
  2. 安全点机制
    • 为了防止在标记过程中堆栈的状态发生改变,Java 虚拟机采取安全点机制来实现 Stop-the-world 操作,暂停其他非垃圾回收线程。
  3. 回收死亡对象的内存
    • 回收死亡对象的内存共有三种方式,分别为:会造成内存碎片的清除、性能开销较大的压缩、以及堆使用效率较低的复制。

Java 虚拟机中垃圾回收具体实现的一些通用知识

  1. 堆分为新生代和老年代,采用不同的垃圾回收算法
    • Java 虚拟机将堆分为新生代和老年代,并且对不同代采用不同的垃圾回收算法。
    • 其中,新生代分为 Eden 区和两个大小一致的 Survivor 区,并且其中一个 Survivor 区是空的。
  2. 新生代 变 老年代
    • 在只针对新生代的 Minor GC 中,Eden 区和非空 Survivor 区的存活对象会被复制到空的 Survivor 区中
    • 当 Survivor 区中的存活对象复制次数超过一定数值时,它将被晋升至老年代。
  3. 老年代 到 新生代
    • 因为 Minor GC 只针对新生代进行垃圾回收,所以在枚举 GC Roots 的时候,它需要考虑从老年代到新生代的引用。
    • 为了避免扫描整个老年代,Java 虚拟机引入了名为卡表的技术,大致地标出可能存在老年代到新生代引用的内存区域。

Java 的内存模型

  1. happens-before 机制
    1. Java 内存模型通过定义了一系列的 happens-before 操作,让应用程序开发者能够轻易地表达不同线程的操作之间的内存可见性。
  2. 在遵守 Java 内存模型的前提下,即时编译器以及底层体系架构能够调整内存访问操作,以达到性能优化的效果。如果开发者没有正确地利用 happens-before 规则,那么将可能导致数据竞争。
  3. 内存屏障对重排序优化和CPU缓存都有作用:这点和C++不同
    1. Java 内存模型是通过内存屏障来禁止重排序的。对于即时编译器来说,内存屏障将限制它所能做的重排序优化。对于处理器来说,内存屏障会导致缓存的刷新操作。

Java 虚拟机中 synchronized 关键字的实现

  1. 介绍了 Java 虚拟机中 synchronized 关键字的实现,按照代价由高至低可分为重量级锁、轻量级锁和偏向锁三种。
  2. 重量级锁
    • 重量级锁会阻塞、唤醒请求加锁的线程。它针对的是多个线程同时竞争同一把锁的情况。Java 虚拟机采取了自适应自旋,来避免线程在面对非常小的 synchronized 代码块时,仍会被阻塞、唤醒的情况。
  3. 轻量级锁
    • 轻量级锁采用 CAS 操作,将锁对象的标记字段替换为一个指针,指向当前线程栈上的一块空间,存储着锁对象原本的标记字段。它针对的是多个线程在不同时间段申请同一把锁的情况。
  4. 偏向锁
    • 偏向锁只会在第一次请求时采用 CAS 操作,在锁对象的标记字段中记录下当前线程的地址。在之后的运行过程中,持有该偏向锁的线程的加锁操作将直接返回。它针对的是锁仅会被同一线程持有的情况。

2020年3月18日 下午5:04

JVM学习方法经验:

  • 第一遍学:
    • 认为老师讲的不好,文字叙述太多,很宏观,没有落实到代码上,记不住,睡一觉就忘了
  • 第二遍学:
    • 不是老师讲的不好,而是自己没有正确的认识:学JVM,到底是学什么?,第一遍学习的时候心里没有一个正确的预期,每学一篇文章,都伴随着对自己的怀疑、对知识的怀疑、甚至对老师的怀疑,这些都是正常现象
    • JVM不像你学编程语言、框架等等知识,最后让你拿来当做工具来使用
    • JVM不是工具,JVM是运行机制!具体可以展开各个部分如何运行的。
  • 总结:对自己完全没有接触过的、没有概念的知识,第一遍总是看着想撞墙,第二遍总是觉得第一遍的自己就是一傻叉!正常、正常!
  • 一开始要规划处空白的大段时间进行攻坚,当第二遍对她有了感觉之后,剩下的就可以滚雪球般的进行每日少量的持续性学习。

目前学习的知识:

2020年3月18日 下午2:20

编译的过程:词法分析 语法分析 语义分析

  1. 词法分析:
    1. 读取的内容是字符,根据词法规则输出token。
    2. 几乎不涉及语言的语法特性,是编译器的基础。
  2. 语法分析:
    1. 读取的内容是token,输出的是语法树AST。
    2. 语言的表达式等功能由这部分中定义的上下文无关文法来实现。
  3. 语义分析:
    1. 操作的对象是AST,输出:给AST节点打上更多的属性信息
    2. 所谓语义主要完成上下文相关的推理逻辑,如类型问题,定义声明问题等

说说我对编译原理的初次见面感觉:人为定义规则的重要性

  1. 编译原理相比于其他计算机基础知识而言,他的难主要集中在需要高度的对现实生活规则的抽象能力、逻辑思维能力,否则写不出没问题的上下文无关文法规则,以及无法发现、处理其中蕴含的一些“逻辑坑”,如左递归等问题。
  2. 而一些其他的知识点,如算法部分,这些其实相比于抽象能力来说,就要简单、通用、好理解的多,更加考验你的编程基础,而不是脑子。

2020年3月17日 下午11:50

基础知识:

函数式编程:本质意义来源于数学函数f(x)

* 在实现方式上,有函数套函数、函数返回函数、函数里定义函数……把人搞得很糊涂
* C++的是通过模板来实现函数式编程,起点比较高
* Python是目前使用函数式变成最简单的方式
* eg:

语言不只是工具。语言和编程范式极大的影响了我们的思维

C++:一个人就能实现三种范式

  • 过程式、函数式、面向对象式
  • 对于C++而言,模板是泛型编程、函数式编程的实现方式
  • 函数式编程是泛型编程的进一步抽象,但他们的实现方式都是模板

面向对象 与 实例编程 区别联系

  • 面向对象就像大公司病一样,干个啥都是重量级的,不鼓励创新
  • 实例编程,理解成函数对象编程,结合了函数式的控制特点,也结合了面向对象的数据、组合关联的能力(把接口这个能力去掉了),就像是大公司下的独立子公司,鼓励创新

从设计、选型的角度:

编程范式是各种解决现实问题进行逻辑设计工具的,最重要的是学会区分、选择

我们需要分离什么:

  • 通俗:
    • 数据:面向对象
    • 控制(控制程序):函数式
    • 业务(逻辑):
    • 关联:面向对象类 函数式中的装饰器
  • 精炼:
    • 有效地分离 Logic、Control 和 Data 是写出好程序的关键所在!

如何实现分离的?

  • 泛型:是控制与数据分离的手段
  • 函数式:是控制与业务分离的手段
    Ps:先控制与数据分离,后控制与业务分离

其实唯一目标很清晰:独立出控制

  • 控制一个程序流转的方式,即程序执行的方式,并行还是串行,同步还是异步,以及调度不同执行路径或模块,数据之间的存储关系,这些和业务逻辑没有关系。
  • 控制,也许换成调度更好理解一些

如何使用独立出来的控制来完成我们需要功能:

  • 后期添加logic + data
    • Logic 部分才是真正有意义的(What),也就是你要实现的功能呢
    • Control 部分只是影响 Logic 部分的效率(How)

一共就四种范式而已: