那么,我们如何得到一个可能返回或者不返回给定类型的数据的函数,或是一个可能内部包含或者没有包含一个给定类型的数据的数据结构呢?也就是说,我们如何将错误状况(failure condition)封装到我们的类型系统中来呢?Rust使用Option,Haskell使用一个叫Maybe的类型来解决这个问题。
我们想象这样一个函数,它所作的事情是搜索一个非空字符串的数组,寻找一个以这‘H’开头的字符串,返回第一个找到的这样的字符串,如果没有找到,就返回某种错误状况。在Go语言中,我们可以通过返回nil来表示“没找到”这个错误。但是在Haskell和Rust中,不使用危险的指针,我们就可以安全地完成这个任务。
(Rust)
fn search<'a>(strings: &'a[String]) -> Option<&'a str>{ for string in strings.iter() { if string.as_slice()[0] == 'H' as u8 { return Some(string.as_slice()); } } None }(Haskell)
search [] = Nothing search (x:xs) = if (head x) == 'H' then Just x else search xs我们可以返回一个包含或者没有包含一个字符串的对象来代替返回一个字符串或者null指针的做法。使用search()函数的程序员也会很清楚地知道这个函数可能会失败(因为它返回的对象的类型已经这么说了),而且程序员必须处理这两种状况,否则报错。这样我们就跟null指针解引用所造成的bug说再见了。
类型推导(Type Inference) 问题给程序中的每个值都指定类型, 有时看起来点过老土。 某些场合, 值的类型显而易见,如
int x = 5 y = x*2
这里的 y 明显就是整形。更复杂点的,我们甚至可以根据函数的参数类型推断出它的返回类型(反之亦然)。
出色的解决方案: 通用类型推导(General Type Inference)Rust 和 Haskell 都基于 Hindley-Milner 类型系统, 他们都很擅长类型推导, 你可以实现像下面这样好玩的功能:
(Haskell)
map :: (a -> b) -> [a] -> [b] let doubleNums nums = map (*2) nums doubleNums :: Num t => [t] -> [t]函数 (*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) 问题