02|串讲:编程开发中,那些你需要掌握的基本概念

193 阅读7分钟

正式开始

上一节总结

栈上存放的数据是静态的,固定大小,固定生命周期;堆上存放的数据是动态的,不固定大小,不固定生命周期

基本概念分类

image.png

数据

数据是程序操作的对象

值和类型

类型是对值的区分,它包含了值在内存中的长度、对齐以及值可以进行的操作等信息。

一个值是符合一个特定类型的数据的某个实体

值以类型规定的表达方式(representation)被存储成一组字节流进行访问

值是无法脱离具体的类型讨论的

原生类型(primitive type)

编程语言提供的最基础的数据类型。比如字符、整数、浮点数、布尔值、数组(array)、元组(tuple)、指针、引用、函数、闭包等。

所有原生类型的大小都是固定的,因此它们可以被分配到栈上

组合类型(composite type)/ 复合类型

由一组原生类型和其它类型组合而成的类型

结构体(structure type)

多个类型组合在一起共同表达一个值的复杂数据结构。结构体是 product type

标签联合(tagged union)

存储 一组不同但固定的类型中的 某个类型的对象,具体是哪个类型由其标签决定。 标签联合是 sum type image.png

指针和引用

在内存中,一个值被存储到内存中的某个位置,这个位置对应一个内存地址

指针和引用是原生类型,它们可以分配在栈上

指针

指针是一个持有内存地址的值,可以通过解引用(dereference)来访问它指向的内存地址,理论上可以解引用到任意数据类型

引用

引用的解引用访问是受限的,它只能解引用到它引用数据的类型,不能用作它用

代码

数据是程序操作的对象,而代码是程序运行的主体

函数、方法和闭包

函数

函数是对完成某个功能的一组相关语句和表达式的封装,也是对代码中重复行为的抽象。

在现代编程语言中,函数往往是一等公民,可以作为参数传递,或者作为返回值返回

方法

在面向对象的编程语言中,在类或者对象中定义的函数,被称为方法(method)。方法往往和对象的指针发生关系

闭包

闭包是将函数或者说代码和其环境一起存储的一种数据结构。

闭包引用的上下文中的自由变量,会被捕获到闭包的结构中,成为闭包类型的一部分

image.png

接口和虚表

作为一个抽象层,接口将使用方和实现方隔离开来,使两者不直接有依赖关系,大大提高了复用性和扩展性

在生成这个引用的时候,我们需要构建胖指针,除了指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表

image.png

运行方式

并发(concurrency)与并行(parallel)

并发是同时与多件事情打交道的能力

并行是同时处理多件事情的手段

并发是一种能力,而并行是一种手段 image.png

同步异步

同步是指一个任务开始执行后,后续的操作会阻塞,直到这个任务结束, 同步执行保证了代码的因果关系(causality),是程序正确性的保证

异步是指一个任务开始执行后,与它没有因果关系的其它任务可以正常执行,不必等待前一个任务结束

在异步操作里,异步处理完成后的结果,一般用 Promise 来保存,它是一个对象,用来描述在未来的某个时刻才能获得的结果的值,

一般存在三个状态;

  1. 初始状态,Promise 还未运行;
  2. 等待(pending)状态,Promise 已运行,但还未结束;
  3. 结束状态, Promise 成功解析出一个值,或者执行失败。

async定义一个可以并发执行的任务, await触发这个任务并发执行

编程范式

主要讲解泛型编程

数据结构的泛型

数据结构的泛型,它也往往被称为参数化类型或者参数多态

把参数化数据结构理解成一个产生类型的函数,在“调用”时,它接受若干个使用了具体类型的参数,返回携带这些类型的类型

代码的泛型化

通俗来讲就是编写的函数的泛型化

总结

image.png

值无法离开类型单独讨论

类型一般分为原生类型和组合类型。

指针和引用都指向值的内存地址,只不过二者在解引用时的行为不一样。引用只能解引用到原来的数据类型,而指针没有这个限制,然而,不受约束的指针解引用,会带来内存安全方面的问题。

函数是代码中重复行为的抽象

方法是对象内部定义的函数

闭包是一种特殊的函数,它会捕获函数体内使用到的上下文中的自由变量,作为闭包成员的一部分

接口将调用者和实现者隔离开,大大促进了代码的复用和扩展。

在代码的运行方式中,并发是并行的基础,是同时与多个任务打交道的能力;

并行是并发的体现,是同时处理多个任务的手段。

同步阻塞后续操作,异步允许后续操作。

被广泛用于异步操作的 Promise 代表未来某个时刻会得到的结果,async/await 是 Promise 的封装,一般用状态机来实现。

泛型编程通过参数化让数据结构像函数一样延迟绑定,提升其通用性,类型的参数可以用接口约束,使类型满足一定的行为,同时,在使用泛型结构时,我们的代码也需要更高的抽象度。

好用链接

virtual table check 神器 godbolt.org/

精选问答

  1. 虚表会存储在哪里

    a. 虚表相当于在运行时生成的一个涵盖了一系列函数指针的数据结构

    b. 虚表一般存储在堆上。Rust 下也有虚表的栈实现:github.com/archshift/d…

  2. 虚表是每个类有一份,还是每个对象有一份,还是每个胖指针有一份?

    a. 虚表是每个 impl TraitA for TypeB {} 时就会编译出一份。比如 String 的 Debug 实现, String 的 Display 实现各有一份虚表,它们在编译时就生成并放在了二进制文件中(大多是 RODATA 段中) b. 虚表是每个 (Trait, Type) 一份。并且在编译时就生成好了

    use std::fmt::{Debug, Display};
    use std::mem::transmute;
    
    fn main() {
        let s = String::from("hello world!");
        let s1 = String::from("goodbye world!");
        // Display / Debug trait object for s
        let w1: &dyn Display = &s;
        let w2: &dyn Debug = &s;
    
        // Display / Debug trait object for s1
        let w3: &dyn Display = &s1;
        let w4: &dyn Debug = &s1;
    
        // 强行把 triat object 转换成两个地址 (usize, usize)
        // 这是不安全的,所以是 unsafe
        let (addr1, vtable1) = unsafe { transmute::<_, (usize, usize)>(w1 as *const dyn Display) };
        let (addr2, vtable2) = unsafe { transmute::<_, (usize, usize)>(w2 as *const dyn Debug) };
        let (addr3, vtable3) = unsafe { transmute::<_, (usize, usize)>(w3 as *const dyn Display) };
        let (addr4, vtable4) = unsafe { transmute::<_, (usize, usize)>(w4 as *const dyn Debug) };
    
        // s 和 s1 在栈上的地址,以及 main 在 TEXT 段的地址
        println!(
            "s: {:p}, s1: {:p}, main(): {:p}",
            &s, &s1, main as *const ()
        );
        // trait object(s / Display) 的 ptr 地址和 vtable 地址
        println!("addr1: 0x{:x}, vtable1: 0x{:x}", addr1, vtable1);
        // trait object(s / Debug) 的 ptr 地址和 vtable 地址
        println!("addr2: 0x{:x}, vtable2: 0x{:x}", addr2, vtable2);
    
        // trait object(s1 / Display) 的 ptr 地址和 vtable 地址
        println!("addr3: 0x{:x}, vtable3: 0x{:x}", addr3, vtable3);
    
        // trait object(s1 / Display) 的 ptr 地址和 vtable 地址
        println!("addr4: 0x{:x}, vtable4: 0x{:x}", addr4, vtable4);
    
        // 指向同一个数据的 trait object 其 ptr 地址相同
        assert_eq!(addr1, addr2);
        assert_eq!(addr3, addr4);
    
        // 指向同一种类型的同一个 trait 的 vtable 地址相同
        // 这里都是 String + Display
        assert_eq!(vtable1, vtable3);
        // 这里都是 String + Debug
        assert_eq!(vtable2, vtable4);
    }
    /*
    s: 0x7ffefae35178, s1: 0x7ffefae35190, main(): 0x559564708ab0
    addr1: 0x7ffefae35178, vtable1: 0x55956474e060
    addr2: 0x7ffefae35178, vtable2: 0x55956474e080
    addr3: 0x7ffefae35190, vtable3: 0x55956474e060
    addr4: 0x7ffefae35190, vtable4: 0x55956474e080
    */