类型推导(Type Inference)问题给程序中的每个值都指定类型, 有时看起来点过老土。 某些场合, 值的类型显而易见,如 int x = 5 这里的 y 明显就是整形。更复杂点的,我们甚至可以根据函数的参数类型推断出它的返回类型(反之亦然)。 出色的解决方案: 通用类型推导(General Type Inference)Rust 和 Haskell 都基于 Hindley-Milner 类型系统, 他们都很擅长类型推导, 你可以实现像下面这样好玩的功能: (Haskell)
函数 (*2) 有一个 Num 类型参数, 返回也是一个Num 类型, Haskell 由此推断 a 和 b 也是 Num 类型. 最后推断出, 该函数有若干个 Num 类型参数, 返回若个 Num 类型的值. 这种方式比 Go 和 C++ 的简单类型推导强大多了. 有了它, 哪怕是结构复杂的程序, 就算我们不声明这么多显性类型, 编译器也能正确处理. Go 的解决方案 : :=Go 支持 := 赋值操作符, 用法如下: (Go) foo := bar() 它的原理是: 查找 bar() 的返回类型, 然后赋给 foo. 下列代码的道理也一样: (C++) auto foo = bar(); 没什么稀奇的, 无非省去了人工查找函数 bar() 的返回类型, 在键盘上多敲几个字声明 foo 的类型那点时间而已. 不变性(Immutability)问题不变性是指,在程序生成的时候,设好的值,以后不会再变。 它的优势很明显, 能减少因程序某个地方的数据结构改变,导致另一个地方出现问题的概率。 此外对程序优化也有利。 出色的解决方案: 默认使用不变性程序员应当尽可能使用不可变数据结构。 不变性使得判断负面影响和安全性变得更简单。同时也能减少各种 Bug 。 Haskell 默认情况下, 所有的值都是不可变的。改变数据结构就意味着, 在保证正确性的前提下, 重新创建一个新的数据结构。由于 Haskell 采用的是惰性求值(lazy evaluation)和永久性数据结构(persistent data structures), 所以运行的速度还是粉快的。Rust 属于系统级编程语言。不可能使用惰性求值,也就不能像 Haskell 那样始终使用不变性。 因此,虽然 Rust 默认情况下,变量的值是不可变的。 但是,在需要的时候, 还是可以将变量设置成可变的。这样挺好,因为它迫使程序员问自己, 底需不需要将这个变量设成可变的。 这是很好的变成习惯, 对编译器优化代码也有好处。 Go 的方案: 无Go 不支持这项功能。 控制流结构(Control Flow Structures)问题控制流结构是高级编程语言有别于汇编的原因之一. 它允许我们在抽象层面, 有条理地控制程序流程. 毫无疑问, 所有高级语言都支持控制流结构, 否则, 我还说个毛啊. 可惜, 有那么几种相当不错的控制流结构 Go 不支持. 出色的解决方案:模式匹配和复合表达式模式匹配配合数据结构或值使用的时候, 效果相当好. 简直就是 case/switch 的加强版. 我们可以像这样对值进行匹配: (Rust)
或者像这样解构数据结构(deconstruct data structures): (Rust)
上面的例子, 有时也称作复合表达式. C 和 Go 中的 if 和 case/switch 语句只用来控制程序流程, 不会返回值; 而 Rust 和 Haskell 的 if 和 模式匹配语句则可以. 既然有值返回, 当然也能用来赋给其他东东. 这里给出一个 if 语句的例子: (Haskell)
Go 的方案: C语言风格的无值语句( Valueless Statements)不是我故意找 Go 的茬; 它确实有几个不错的的控制流元素, 如, 用于并行计算的 select. 可惜没有我钟爱的复合表达式和模式匹配. Go 唯一支持赋值的语句, 是像这样的原子表达式 x := 5 或 x := foo(). 嵌入式编程给嵌入式系统编写程序与在一个有完整操作系统的计算机上编写程序有很大不同。某些语言相比而言更适合嵌入式编程的需要。 对于不少人赞成Go语言可以给机器人编程这件事我很疑惑。基于一些原因,Go语言并不适合用来为嵌入式系统编写程序。这一节并不是对Go语言的指责,Go语言并不是被设计用来编写嵌入式程序的语言。这一章节针对那些吹捧Go语言可以胜任嵌入式编程的人。 子问题 #1:堆和动态内存分配 堆是一块在运行期创建的可以存储任意数量对象的内存区域。我们将对堆的使用称作”动态内存分配“。 通常,在嵌入式系统中使用堆存储空间是不明智的。较大的内存开销和需要管理复杂的数据结构是主要的原因,尤其是当你在一块主频只有8MHz,RAM只有2KB的MCU上写程序的时候。 在实时系统(因为某一操作耗时过长就可能会跪的系统)中使用堆也是不明智的,因为对堆上空间的申请和释放所消耗的时间有很大的不确定性。举个例子,如果你的MCU正在控制一个火箭的引擎,就在这时,如果一个对栈空间的申请比平常多消耗了几百毫秒,导致对阀门的错误计时,就会发生大爆炸。 还有一些原因致使动态内存分配对嵌入式编程没有多大用。例如,许多使用堆的语言同时也拥有垃圾收集机制。垃圾收集机制经常会暂停整个程序一会儿,在堆上寻找垃圾(不再被程序使用的内存)并清除它们。这比单纯的堆空间申请更加具有不确定性。 好的解决方案:让动态内存分配成为可选项 Rust语言的标准库中有很多特性依赖于堆。然而,Rust语言的编译器支持完全关闭这些有关堆的语言特性,并且能够静态地确保这些特性在程序中不被使用。写出完全不使用堆的Rust程序是完全可行的。 Go语言的解决方案:没有 Go语言严重依赖于对堆的运用。没有可行的方式让Go程序完全不使用堆。这不是Go语言的问题。这在Go语言的目的应用领域完全没有问题。 Go并不是一门实时的语言,通常我们不能担保合理复杂的Go程序的执行时间。这可能有点费解,我来解释一下:Go相对而言很快,但不是实时的,这两个概念非常不同。执行速度快对嵌入式程序来说很重要,但是真正重要的是能否担保某些操作的最大执行时间,而这恰恰是Go不能预测的。这个问题有很大一部分是Go语言对于堆空间和垃圾收集机制的使用造成的。 Haskell也有相似的问题。Haskell同样由于对堆的大量使用而不能胜任嵌入式或者实时编程。然而,我没有看见任何人推荐使用Haskell对机器人编程,所以我不用指出这点。 子问题#2:不安全的底层代码 当我们写嵌入式程序的时候,写一些不安全的代码(不安全的的类型转换,或者指针运算)是不可避免的。在C或C++中,做这样的事情是很简单的。如果我需要向0x1234这个内存地址写入0xff这个值来点亮一个LED,我可以这样写: (C/C++) *(uint8_t*)0x1234 = 0xFF; 这样做很危险,只有当我们写非常底层的系统代码的时候才有意义。这就是Go和Haskell没有简单的方式来做这样的事的原因:它们不是系统编程语言。 好的解决方案:将不安全的代码孤立开来 注重安全和系统编程的Rust语言有一个非常好的解决方案:unsafe代码块。unsafe代码块是一种显示地将不安全的代码分离出来的方式。我们通过如下的方式在Rust语言中向0x1234地址写入0xff: (Rust) unsafe{ 如果我们在unsafe代码块外面做这样的事情,Rust的编译器会警告我们。这样允许我们在满足嵌入式编程需要的同时,保持了程序的安全和稳定。 Go的解决方案:没有 Go语言本来就不是为了做这样的事而出现的,所以没有任何内建的支持。 总结现在你可能会说,“那么为什么你说Go语言不好?这只是一大堆你的抱怨而已。你可以针对任何语言发牢骚。“没有语言是完美的,这很正确。然而,我希望我的抱怨能在某种程度上说明:
|