1 Star 0 Fork 11

coder_lw / wiki

forked from deepinwiki / wiki 
加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
克隆/下载
开发流程.md 14.13 KB
一键复制 编辑 原始数据 按行查看 历史
htqx 提交于 2023-06-02 11:38 . 纯函数缓存

开发流程

前言

本文是记录一些我认为比较有价值的,关于开发流程,套路这类的东西。语言示例 rust 和 zig。

测试和调试

设计一个程序,除了聪明才智,也需要一些工程思想。

开发实质是测试调试花的时间更多,因此怎么提高我们测试,调试能力甚至比花精力在开发(目标代码)本身要更能提高我们的总体的开发效率。

防御性编程

  1. 前置条件:保证数据正确
  2. 后置条件:保证结果正确
  3. 不变量:各种必然条件,保证系统的稳定性
  4. 黑箱测试:在外部使用测试用例对其测试,从而将测试和运行代码分离(有助提高性能),也可以构造很棒的难以复现的边界用例,但不足是只能保证特定用例。
    1. std.testing.expect: 用于测试单元的断言
    2. 调试版本:很多断言只保留在调试版本。总之就是性能、正确性和稳定性(至少程序不会被 ub 关闭)之间的取舍。
      1. std.debug.assert:用于调试单元的断言
  5. 注释说明:得让客户学习怎么使用代码,否则就会有 ub。好的类型参数也是帮助,但无法提供全部信息。ub 是客户应该主动避免引发的错误。
  6. 越界:锟斤拷、烫烫烫。越界行为是经常出现,虽然现代编译器可以自动判断大部分越界问题,但是很多逻辑上的越界就无法依赖编译器。要为此类问题建立金丝雀(canary)防御。比如上限条件,顺序检查等。
    1. 未初始化内存: 0xccccccccc
    2. 未初始化堆:0xcdcdcdcdcdc
    3. 对象头尾:0xfdfdfdfdfdfdf
    4. 已回收内存:0xddddddddddd

错误的种类:

  1. 未定义行为(undefined behavior):程序自身无法处理的错误
    1. 断言(assert):调试代码人为制造的 ub,程序应该满足却没有满足的条件。它的出现说明程序有问题,因此属于不可处理错误(不应存在处理逻辑,而应该修正代码)。
    2. 异常(panic):异常在层层嵌套的函数内部,直接到达错误处理函数中。它往往是通用的错误处理,即所谓的善后工作。
  2. 错误(error):可处理的错误,该错误可以交由客户代码处理

函数应该在注释中给出前置条件(断言)。非前置条件的断言,它是当前函数作者应该解决的开发阶段错误。

当善后工作是有意义的,那么应该给出异常处理函数(panic hander),但是任何函数都可能产生异常,没必要说明,因为它不是一个可以处理的错误 ,你针对的是当前系统层面的善后。

能处理且强制客户去处理的是错误。但是因此它也变得有点唠唠叨叨,让人厌烦。因为客户通常并不希望存在那么多需要处理的错误,而只需要理想状态下的一般情况。ub 或者 error,由你选择。

闭着眼睛编程

很多时候我们是闭着眼睛编程的。在一个复杂的系统中,视角其实很有限,你就看得到那么一点点,如何能够信赖系统的其余部分能够正常运作?这就是闭着眼睛编程的要点。

抽象!构建一个封闭的系统,作为用户使用系统,从而将复杂的系统和当前要构造的代码区分开来。编程就是建立各种隔离,各种子系统,总是以客户的视角去使用系统,而不是成为系统细节的耦合。

越是复杂的系统(复杂的流程,复杂的算法),这种隔离越是有效,真正做到磨刀不误砍柴工。

有效的原因除了视角有限,也因为这种内聚系统相对容易测试。前面已经强调测试的重要性,那么即得出它有更高的编程效率。如果一个系统编写测试用例很困难,那么它就很有问题了。

套路

  1. 渐进式测试,当完成一个小功能的时候,应该尽快测试它的可用性,然后再开发新的功能。
    1. 通过提供虚假的数据桩完成测试
    2. 可以完成上层逻辑结构,具体的子函数细节可以暂时不处理。这样也便于我们写出清晰的上层代码,减少无关的细节。
  2. 通过 Result<T, Error> 来返回错误。
    1. ? 如果 Error 就立即返回
    2. unwarp() 如果是 None/Error 就 panic!
    3. Option 自己处理, Result 给调用者处理
    4. 面向调试(绝不允许):assert!
    5. 面向业务(健壮):Result<T,Error>
      1. 懒得处理的问题(异常):panic!
    6. 第三方库:thiserror / anyhow
  3. 先写同步版本,然后改成异步版本。这样好处的逻辑比较清晰,测试比较容易。
    1. 同步数据结构改异步数据结构
    2. 有序结构改无序结构
    3. 继发任务改并发任务
    4. 测试运行时间
  4. 数据库 PostgreSQL 取代 MySQL(MariaDB), sqlite 做小型应用。
    1. mock 假数据
  5. 不要使用 &str 的下标切片,因为中文在 UTF-8 编码中不只占一个字符,而范围不在边界上,会报异常。
  6. 比较操作有两种:Ord(全序)、PartialOrd(偏序),偏序的意思是存在无序的元素,如浮点的 NaN < 0 和 NaN >=0 都是 false。

设计模式

设计模式,也叫设计套路。有很多模式,会导致程序架构变得非常复杂,难以理解(因为你不熟悉它的套路),所以也不是什么时候都用设计模式来写代码。但是对于一些已经流行的套路,我们还是很有必要学习的,这样会让你的代码更有条理性。

单例模式

希望拥有全局唯一的对象。

最容易想到的方案是:static mut T ,但静态可写对象是不安全的(要使用unsafe 上下文)。

另一个方案就是只能创建一次,第二次报异常。

延迟计算(lazy evaluation)/记忆化(memoization)

这在很多算法中,是可以大幅度提高性能的模式。同时,也可以配合单例模式使用,在需要单例对象的时候,在创建单例对象,并缓存起来以后使用(不再创建)。

迭代器(iterator)和函数式编程(functional programming)

迭代器负责对集合的枚举,形成序列。函数式编程在这个过程中可以实现针对整体(或每个元素)进行运算。

在现代语言中,迭代器很多已经成为语言(或标准库)的一部分。同时也有简易的创建任意迭代器的技术:生成器。

为什么函数式编程会在迭代器编程中流行?主要是函数式编程适合处理集合(相对标量)。对集合的处理,使用函数式,会更加易于阅读和理解,用传统的流程控制,细节太多不便概括。

api 种类:

  1. 消耗型(迭代并耗尽迭代器,转化为值):sum()
    1. all(谓词):全部满足
    2. any(谓词):任一满足
    3. cmp(): 按字典比较
      1. cmp_by(): 使用比较函数
      2. eq(): 比较相等 ==
      3. eq_by(): 使用比较函数
      4. ge(): >=
      5. gt(): >
      6. le(): <=
      7. lt(): <
      8. ne(): !=
      9. partial_cmp(): 偏序
      10. parial_cmp_by()
    4. collect(): 转换成集合
    5. count(): 元素个数
      1. size_hint(): 返回上下界 (lower,upper)
    6. anvance_by(): 推进 n 个元素
      1. last(): 最后一个元素
      2. nth(): 返回指定下标的元素
    7. find(谓词): 查找
      1. find_map(f->Option): 根据转换函数查找
      2. try_find(): 包含错误返回
      3. position(谓词): 查找但返回下标
      4. rposition(谓词):从右向左搜索(逆序)
    8. fold(): 累加器(给出初始值,和累计函数,依次传递元素到累计函数,获取累计值)
      1. try_fold(): 包含错误返回
      2. reduce(): 归约,类似没有初始值的 fold()
      3. scan(): 浏览,类似累计函数参数是引用的 fold().即内部保存累计值
    9. for_each(): 依次调用函数
      1. try_for_each(): 包含错误返回
    10. is_partitioned(谓词): 是否已分区(true 元素在 false 元素前)
      1. is_sorted(): 是否从小到大排序
      2. is_sorted_by(): 是否按排序函数排序
      3. is_sorted_by_key(): 是否按指定 key 排序
    11. max(): 最大的元素
      1. max_by(): 使用比较函数
      2. max_by_key(): 使用 key 比较
      3. min(): 最小
      4. min_by(): 使用比较函数
      5. min_by_key(): 使用 key 比较
      6. sum(): 求和
      7. product(): 阶乘
    12. partition(谓词): 根据谓词建立两个集合
      1. partition-in_place(): 就地排序为两个区域
      2. unzip(): zip() 逆过程
  2. 适配器(转换为另一个迭代器,链式操作):map()
    1. filter(谓词):选择 true 的元素
      1. filter_map(f->Option): 根据转换函数,过滤掉空值,保留转换后的 Some(T)
    2. chain():串联
    3. cycle(): 循环(头尾相连)
    4. enumerate(): 枚举(index, value)
    5. flatten(): 展平(如果元素是迭代器,将其串联起来)
      1. flat_map(f->IntoIterator): 转换到迭代器,然后展平
    6. fuse(): 短路(遇到第一个 None 后都返回 None)
    7. inspect(): 每次迭代插入检查函数
    8. intersperse(): 插入分隔符(即每两元素间插入一个元素)
      1. intersperse_with(): 插入函数的返回值
    9. map(): 映射(T-->S)
      1. map(f->Option): 根据转换结果映射
    10. peekable(): 支持预览下一个元素的序列(不迭代)
    11. rev(): 逆序
    12. skip(): 跳过 n 个元素后
      1. skip_while(谓词): 根据谓词跳过元素
      2. step_by(): 调整步进,即每次相隔多个个元素
      3. take(): 前 n 个元素的序列
      4. take()_while(谓词) : 根据谓词选择
    13. zip(): 将两个序列的元素组成元素为 (a,b) 的新序列
  3. 谓词(返回 bool 的函数):一般使用闭包(closures)实现
  4. 元素所有权
    1. by_ref(): 返回迭代器的可写引用
    2. cloned(): 克隆(T:Clone)
    3. copied(): 拷贝(T:Copy)
  5. 辅助函数
    1. empty():空序列
    2. from_fn(f->Option): 创建一个序列,每次迭代调用闭包
    3. once(): 只有一个元素的序列
      1. once_with(): 根据生成函数创建
    4. repeat(): 无限重复序列
      1. repeat_with(): 根据生成函数创建
    5. successors(): 生成序列,不断根据上一个元素创建下一个元素
    6. zip(): 打包两个序列
  6. 特殊迭代器和工具
    1. DoubleEndedIterator:双端迭代器
    2. ExactSizeIterator: 精确大小迭代器
    3. Extend:可扩展(集合)特征,迭代器使用这个接口填充集合

函数式编程主要体现在使用闭包作参数,还有可以使用链式操作的适配器上。严格来说, rust 并不是为函数式编程而设计的,它只是从实用性的角度出发,好用的地方就使用,而不是为了函数式而函数式,更多时候是非函数式的。

作为补充,函数式编程的特点:

  1. 纯函数:没有副作用,就是根据参数返回结果(类似数学上的函数),而不会有隐藏的状态。好处是可以轻松缓存结果和对其进行测试。
  2. 函数可作为参数(高阶函数):闭包
  3. 函数合成 compose
    1. f(g(1)) 转换成 compose(f,g)(1)
    2. 超过 2 个函数的合成
  4. 函数柯里化 currying: 转为单参数函数(一元函数),方便后续合成
    1. f(1,2) 转换成 curry(2)(1)
  5. 函子 functor: f(1).map(g).map(h) 映射关系
    1. of : 构造
    2. Maybe : 短路
    3. Either(left, right) : 优先选择 right (if 分支)
    4. Ap : 应用 Ap.of(f).ap(g),从函子中取值,并应用
    5. Monad: Monad.of(f).flatMap(g), 展平嵌套(类似 chain 操作)

观察一下,函数式主要的特点是:

  1. 函数做参数,这个 rust 和一般的现代语言提供良好的支持,如 闭包
  2. 合成和柯里化,这个有点繁琐,大多数语言都习惯多个参数为主的普通形式
  3. 函子,也就是映射和链式调用等模式
  4. 纯函数

信道

错误处理

关于错误处理的补充,错误处理分四个层次:

  1. Option 存在可有可无的状态,这应该由当前上下文处理
  2. Result 交给调用者处理
  3. painc! 算了,直接报异常吧(rust 有非常有限的手段来处理异常)
  4. abort 干脆关了

Option.ok_or(err) 可以将错误提升到 Result 层次。而 unwarp() 可以提升到 painc! 层次,如果没有异常处理,那么 painc! 提升到 abort 层次。所以,应该在低层次上处理问题,而不是将提升它的层次视为解决问题的方法。当然如果你懒得处理,那谁也阻止不了你。

异步编程

死锁(deadlock)必要条件:

  1. 有锁(或存在对资源的独占逻辑)
  2. ABBA 顺序的锁,即出现循环获取锁
  3. 无法剥夺
  4. 不主动释放

对应的解决方法:

  1. 复制资源
  2. 保证 A 永远在 B 前
  3. 可剥夺
  4. 主动释放(如使用条件变量)

更有效的方法是封装起来,提供一个系统,如生产者-消费者模型,管道等。

数据竞争(data race):

  1. 两次读之间不一致(这里的读是概念性的,比如第二次读是根据瞬时状态来执行)
  2. 读写之间变量本身发生变化

总之就是持有状态和瞬时状态不一致,而逻辑依赖瞬时状态的情况。

解决方法:加锁(最好是条件变量),但又容易:

  1. 忘了加锁
  2. 加错锁

解决方法:包装。(ps.内存分配器的模式?)

动态分析工具:

  1. lint
  2. sanitizers
    1. AddressSanitizer : 非法内存访问
    2. TreadSanitizer: 数据竞争
    3. MemorySanitizer: 未初始化内存访问
    4. UBSanitizer: 未定义行为(整数溢出等)

性能优化

准备两套方案:

  1. fast path(快通道):大概率,性能高,并行度高
  2. slow path(慢通道):小概率,可靠性

工具

  1. lldb / gdb:调试器
  2. rr:非确定指令记录器
  3. strace:系统调用跟踪分析
  4. perf:性能分析
    1. perf record ./main
    2. perf report
  5. model-checker:状态机分析
# 跟踪程序的系统调用
strace -f -o /tmp/sh ./sh

# 另一个 shell 监视
tail -f /tmp/sh

参考

  1. 细说Rust错误处理: https://rustcc.cn/article?id=75dbd87c-df1c-4000-a243-46afc8513074
  2. 函数式编程入门教程: https://ruanyifeng.com/blog/2017/02/fp-tutorial.html
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/coder_lw/wiki.git
git@gitee.com:coder_lw/wiki.git
coder_lw
wiki
wiki
master

搜索帮助