写点什么

每日一 R「23」回顾基本概念

作者:Samson
  • 2022 年 9 月 08 日
    上海
  • 本文字数:2548 字

    阅读完需:约 8 分钟

每日一R「23」回顾基本概念


01-数据

01.1-值和类型

回忆一下在学习类型系统时的知识。类型系统是对值的区分,包含了值在内存中的长度、对齐方式以及值允许的操作方式等。它可以看作是一种工具,在编译期的静态检查和运行期的动态检查中,来保证处理的数据是开发者期望的类型。


一个值是符合一个特定类型的数据实体,可以按照类型规定的表示方式从二进制流中解析出来。因此,值是无法脱离具体类型讨论的。


原生(primitive)类型,或者称基本类型,是编程语言提供的最基础的数据类型。Rust 中的基础类型包括:字符、整数、浮点数、布尔值、数组、元组、指针、引用、函数、闭包等;所有的原生类型的大小都是固定的,因此它们可以存储在栈上。


组合(composite)类型,或者称复合类型,是由一组原生类型和其他类型组合而成的类型。Rust 中组合类型包括:结构体、标签联合。结构体指多个类型组合在一起来共同表达一个值的复杂数据结构;标签联合也叫做不相交并集,可以存储一组不同但固定的类型中的某个类型的对象,具体是哪个类型由其标签决定。枚举(enumerate)类型是标签联合的子类型,功能较弱。

01.2-指针和引用

指针是一个持有内存地址的值,可以通过解引用(dereference)来访问它指向内存地址,理论上可以解引用到任意数据类型。根据指向数据的不同,除了内存地址外,可能还需要其他信息(例如已分配容量和已占用长度)。类似这种携带了其他信息的指针,也被称为是胖指针。Rust 中的 String 类型变量就是一个胖指针,包含有三个内存字,分别用来存储内存地址、已分配内存大小、已占用内存大小。


引用与指针非常类似,只不过对引用的解引用只能得到它引用数据的类型。


指针和引用都是原生类型,因此可以分配在栈上。

02-代码

02.1-函数、方法和闭包

函数是对某个功能的封装,也是代码复用的基本单元。在支持函数式编程的语言中,函数与变量一样是一等公民,可以作为参数传递,可以作为返回值,也可以作为数据结构的一部分。


在支持面向对象编程的语言中,封装在类中的函数,也被称为方法(method)。它们往往与对象指针(例如 Java 中的 this,Python 中的 self)相关联。


闭包是将函数及其上下文一起存储的一种数据结构,它会捕捉上下文中的自由变量,称为闭包的一部分。支持函数编程的语言,往往支持闭包。

02.2-接口和虚表

接口是一个抽象层,将接口的使用方和实现方隔离开来,提高了复用性和扩展性。许多编程语言中都支持接口,例如 Java 中的 interface,Rust 中的 trait。


在编程中使用接口而非具体类型,程序变具备了运行时多态的能力。但这种运行时的灵活性带来了其他的问题,即无法单纯地从一个接口的引用去分析其具备什么样的能力。因此,在生成接口引用时,需要构建一个胖指针,除指向数据本身外,还需要指向一张涵盖了这个接口所支持方法的列表,也称为虚表(virtual table)。下图为课件中用来解释 trait object 虚表的示意图:



虚表记录了指针能够执行的所有接口,而不关系数据的真实类型。因此,对接口的不同实现,可以根据上下文动态分派。

03-运行方式

03.1-并发和并行

并行 vs. 并发


  • 并行(parallel),指有多个任务执行单元,从物理上多个任务可以同时执行;与并行相对的概念是串行,指只有一个执行单元(或者任务之间有前后以来关系),每次只能执行一个任务,然后是下一个任务;

  • 并发(concurrent),指无论上一个开始执行的任务是否完成,当前任务都可以执行。与并发相对的概念是顺序,即上一个任务未结束,则当前任务不能执行。


多线程:在单核 CPU 上,并发、串行;在多核 CPU 上,并发、并行;



Erlang 之父有一个非常形象的图解释了并发与并行的区别。


并发是一种能力,而并行是一种手段。

03.2-同步和异步

同步 vs. 异步


  • 同步是指一个任务开始执行后,其他任务必须在这个任务结束后才能运行。同步保证了代码的因果关系(或者换句话说,任务间的前后依赖关系),是程序正确性的保证。

  • 异步是指一个任务开始执行后,与它没有因果关系任务可以正常执行,不必阻塞。

04-编程范式

编程范式是在编程技术发展过程中逐渐总结出来的提高编程质量的种种规范或模式。

04.1-数据结构的泛型

数据结构的泛型是一种高级别的抽象技术。它带来的好处是延迟绑定,或者说将数据结构的具体类型延迟到使用时才决定。这使得数据结构的通用性更高,使用得场合也更广阔;同时也大大降低了重复代码,提高了可维护性。


数据结构的泛型可以理解为内部数据可能不一样,但从外面看起来却基本一样的数据结构。典型的泛型数据结构就是泛型容器,例如 Java 中的 ArrayList<T> / HashMap<K, V>,在定义时不关心具体的容器内部的数据类型,延迟到使用时才决定具体类型。


Rust 中的泛型数据结构与 Java 基本类似。

04.2-代码的泛型

除了数据结构的泛型,代码也支持泛型,典型的就是泛型函数和泛型接口,例如:


泛型方法:


fn max<T>(array: &[T]) -> T { ... }// 调用时max(&[1, 2, 3]);max(&['a', 'b', 'c', 'd']);
复制代码


泛型接口:


trait Area<T> {  fn get_area(&self) -> T;}
复制代码


使用泛型的代码,应用范围更广,也更简洁、更易维护。

05-思考题

1.有一个指向某个函数的指针,如果将其解引用成一个列表,然后往列表中插入一个元素,请问会发生什么?(对比不同语言,看看这种操作是否允许,如果允许会发生什么)


函数放在代码段,往往是只读的,写入时会出发段保护机制。


  1. 要构造一个数据结构 Shape,可以是 Rectangle、 Circle 或是 Triangle,这三种结构见如下代码。请问 Shape 类型该用什么数据结构实现?怎么实现?


struct Rectangle {   a: f64,   b: f64,}
struct Circle { r: f64,}
struct Triangle { a: f64, b: f64, c: f64,}
复制代码


可以用 enum


enum Shape {    Rectangle(Rectangle),    Circle(Circle),    Triangle(Triangle),}
复制代码


  1. 对于上面的三种结构,如果我们要定义一个接口,可以计算周长和面积,怎么计算?


trait SomeTrait {    type Output;    fn perimeter(&self) -> Self::Output;    fn area(&self) -> Self::Output;}
impl SomeTrait for Rectangle { type Output = f64 ; fn area(&self) -> Self::Output{ self.a * self.b } fn perimeter(&self) -> Self::Output{ 2 as f64 * (self.a + self.b) }}// 其他同理
复制代码


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

发布于: 2022 年 09 月 08 日阅读数: 37
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

InfoQ签约作者 | 阿里云社区签约作者

评论

发布
暂无评论
每日一R「23」回顾基本概念_学习笔记_Samson_InfoQ写作社区