最接近这个可以使用迭代器的关键字的方式是写一个包装函数,这个函数以目标数据结构为参数并返回一个可迭代的对象,我们通过使用这个对象在目标数据结构上迭代(译者注:参见设计模式中的迭代器模式或C++中的迭代器实现)。但是这样做可能会很慢并且复杂,而且无法保证不引入其他的bug。
对于这样一个问题,有人辩解道,“这样更容易让人理解代码,并且我看到的代码就是真正被执行的代码。”也就是说,如果Go语言允许我们扩展像range这样的东西,那么range本身的机制和实现就会变得复杂难以理解。我认为这样的说法没有什么营养,因为不管Go是否通过这种方式让其变得更简单,更易懂,人们总要进行这种在某些数据结构上进行迭代操作。如果我们不想把实现细节隐藏在range()函数里,我们就要把它隐藏在其他的工具函数里,没什么改进。所有的好代码都是易读的,大多数糟糕代码让人很难懂,很显然Go不能改变这个事实。
基础案例与失败条件那么问题来了
当遇到递归的数据结构(如链表和树)时,我们希望找到一个途径来指出我们到达数据结构的末端。
当遇到可能会执行失败的函数或包含缺失数据片的数据结构时,我们希望找到一个途径明示我们遇到的几种失败情况。
Go 的方解决案: Nil (和多个返回值)这回我先说 Go 的, 才好引出其他更好解决方案的讨论.
Go 支持 null 指针(nil). 每次看到新的编程语言(如:tabula rasa), 实现这个导致 bug 满天飞的功能, 我替他们可惜.
null 指针的历史, 满满的都是 bug. 无论是历史, 还是现实, 我都看不出来, 数据存在内存地址为 0x0 的地方有什么意义. 指向 0x0 的指针通常都有特定的含义. 比如, 返回类型是指针的函数出错, 会返回 0x0 . 递归数据结构把 0x0 当作基底(base case), 如: 树结构的页节点, 或链表的结尾. 这也是 null 指针在 Go 中的用法.
然而,这样使用null指针也是不安全的。事实上,null指针是类型系统的后门,它让你能够创造某个根本不是所属类型的实例。程序员有时候会忘记某个指针的值可能是null这个事实,这是一个很常见的情况。在最好的情况下,你的程序会挂掉,而在最坏的情况下,这会产生一个可以被人利用的漏洞。编译器无法轻易地阻止这种情况的发生,因为null指针破坏了语言的类型系统。
对于Go来说,使用多重返回值这个机制,利用它第二个返回值来返回一个代表“失败”的值是一个正确也被鼓励的做法。然而,这种机制很容易被忽略或者误用,并且在表示递归数据结构的时候没有什么用用处。
好的解决方案:代数数据类型和类型安全的错误模式
我们可以使用类型系统来包装错误状况,基底,而不是试图打破类型系统。
现在我们想要构建一个表示链表的类型。我们想表示两种情况:我们是否已经到达了链表的末尾,某个链表的节点上到底有没有被存放在那里的数据。一种类型安全的方式是分别使用不同的类型来表示这些情况,最后将它们组合成一个单独的类型(使用代数数据类型)。现在我们有一个叫做Cons的类型来表示一个存放有某些数据的链表,一个叫做End的类型来表示链表的末尾。我们可以这样写:
(Rust)
enum List<T> { Cons(T, Box<List<T>>), End } let my_list = Cons(1, box Cons(2, box Cons(3, box End)));(Haskell)
data List t = End | Cons t (List t) let my_list = Cons 1 (Cons 2 (Cons 3 End))每个类型都为递归操作这个数据结构的算法声明了一个基底(End)。。Rust和Haskell都不允许null指针的出现,所以我们永远都不会碰到null指针解引用所造成的bug(除非我们做一些很大胆的底层操作)。
这些代数数据结构通过像模式匹配(后面讲它)这样的技术,允许我们写出非常明了的代码。