错误处理?

其实我一直不能太分清楚什么是错误什么是异常,不过我倒是觉得区分这些个东西意义不大,重要的是认清本质。一个程序在运行过程中总会碰到一些错误,有的是因为用户的不当操作,有的是因为期望的结果没有发生,当然还有直接就是无法恢复的程序bug,无论如何,这些问题,如果我们考虑到了,都是需要一套解决方案的,所以我们就来稍微谈谈这些解决方法,还有我最近学到Rust的解决方法。

从Error Code谈起

我能想到的最原始的方法,应该就是C的方法,也就是错误代码,这样的方法至今也在使用,非常有效。比如系统调用会返回错误代码,错误代码一般会有规定,C中经常会用宏当做常量,这样可以便捷的去记录Error Code的含义,比如:

#define RET_OK 0
#define ERR_NO_SUCH_FILE -1
#define ERR_INVALID_ARG -2
int some_function() {
    ...
    if (...) {
        return ERR_NO_SUCH_FILE;
    } else if (...) {
        return ERR_INVALID_ARG;
    }
    return RET_OK;
}

这样的好处当然就是非常简单,很容易辨识,坏处也很明显,一个是包含的信息不够丰富,比如知道是文件不存在造成的错误,但是不知道是由什么文件不存在,在哪儿找不到,才造成的错误,这样会导致错误信息可能不能提供足够的信息来改正错误。

另外,一个更加严重的问题其实是,由于C本身的限制,这样的方法,不能返回多个值。这就意味着不能满足返回一个值,再返回一个错误码的要求。于是C的方法,要么像libc一样,使用errno全局变量,使得错误代码不在返回值里返回,而是在得到不正确的值,比如应当得到有效指针时得到NULL时,调用者主动获取。这样的话,多线程的情况就变得麻烦了起来。要么就是使用指针传出需要的值,将指针传入,作为输出的地址,将输出值放在指针位置,这样的方法其实依然不够好,因为这违背了我们认为返回值才是我调用函数需要的结果的观念,使得接口比较混乱,而且参数也会很长。

总的来说,error code是一个足够简单的解决方法,不过还存在一些问题,go语言采用了error code作为错误解决的方法,不过做了一个改进,就是可以传出多个值,这样的方案非常符合go语言简单明了的风格,也解决了C语言采用error code的痛点。但是go这样的解决方案的问题就是,每一次调用如果存在可能的出错,都得多一个if来判断是否发生了错误,虽然说从道理上来讲是必须的,也是应该的,但是会显得比较冗长。

Try it except…

现在,来聊聊很火的PythonPython其实不是用来谈错误处理很好的一个语言,它的错误处理显得有些“权力过大”,有时候甚至直接代替了一部分不是与错误相关的逻辑处理。不过至少它采用的方案也是一种典型的方案,java也是采用的类似的方案,就是直接将异常融入进语法里,采用特殊的关键词,一般用trycatchexcept之类的关键词隔离开一个块,在这个块中的代码就是可能发生错误的代码,然后catchexcept用来指明发生错误的类型,并且做相应的处理。而实际错误发生的时候,将其包装成一个异常或错误对象,然后“抛出”,也就是中断代码块内其他的内容,直接跳到异常处理的位置进行处理。 大概的方法像这样:

try:
    some_function_called()
except SomeError as e:
    some_error_handle_code_with(e)

这样的做法其实很直观,就是发生异常了,说明我们该进行异常处理了,于是跳到异常处理去进行,不过,Python异常权力过大导致的到处都是try-except的问题,javanull pointer exception 其实都能说明这种方法存在的问题。一个是函数内发生的异常种类对于函数外来说可能并不知晓,经常会有未捕捉到的异常发生,如果直接捕捉了所有异常,又会导致范围过大,有时候有的异常应当导致程序无法进行(let it crash!),这其实都还是比较小的缺点,主要还是java语言体现的null pointer exception,那就是,当某个函数可能出现异常,出现异常之后逻辑不再连续,原本需要返回的对象构造未完成,返回了null,然后调用者很可能在调用情况复杂的时候没有去判断,于是导致出现null pointer,由于没有catch彻底,可能就丑陋的中断了程序。

这种方法总的来说,问题还是在于由于逻辑跳跃的变化,可能在写程序的时候很多地方考虑都不够细致,另外,像java那样的null pointer,不但有了try-catch,还不得不进行if判断,可谓是非常冗长了(现在java好像已经改进,不过我还没有接触过)。

谈谈chain

刚才我们说了java的错误处理,有个null pointer的问题,比如这样:

a().b().c()

如果a函数或者b函数发生错误,返回了null,就会导致null pointer,但是好的错误处理应该想要怎样呢?那就是在a或b出错时,不再进行c调用,而是直接返回错误,最好还能带上错误信息。

熟悉函数式的同学估计能够想到一个叫做Monad的东西,十分符合我们的要求。

Monad?

Monad其实概念上讲起来还是比较复杂的,不过我们可以适度简化一下。
首先是类型,我们考虑有泛型的情况,比如T表示任意类型,那么T<A>就表示任意类型T,接收一个任意类型A作为参数。额,好像比较绕了,举个栗子的话,比如某个类型,取名为Option,然后他里边的某个值需要用到T作为泛型参数,也就是任意类型,于是就是Option<T>,这样应该就比较清楚了。

于是Monad 主要可以帮我们做这样的事情:
假设我有一个Option<T>,然后我还有一个函数,需要一个T参数,得到的结果又是一个Option<T>,Monad使得我们可以把一开始有的那个Option<T>经过这个函数进行处理,然后把它的返回值作为我的返回值。

是不是很像我们要的那个chain? 也就是说,a函数返回一个Option<T>,然后b函数需要的其实是里边的T,然后返回的也是Option<T>,c函数也是一样,最后的结果,还是一个Option<T>,于是我们就做到了将每一个的结果串起来,那如果中间出错了怎么办?别忘了Option<T>并不是T,我们只需要让Option<T>能够表示错误值,比如None,用来表示错误,如果出现了None,后面的函数接受到None的值,返回也是None,一切就都串起来了。

现在,我们就能够没有Null pointer了。

Rust的方案

Rust采用的就是这样一种偏函数式的方案,除了Option,还有ResultResultOption优秀在于,他还可以放入一个错误类型,也就是说,Result大概长这样:Result<ReturnType, ErrorType>,接收两个类型参数。经过刚才monad的讨论,我们就知道,ResultOption一样,可以将函数调用串起来,还能够将错误集中在一起最后返回。具体的方法?当然是使用神奇的?了。

Rust本来应该是使用try!这个宏的,不过后来加了个糖,使用在后面加上?代替了try!这个宏。try!的作用其实是做那个串起来的操作,使得monad性质能够成立,因为Rust不仅有函数式风格,更多的还是指令式,比如调用的结果可以不生成值(生成unit值,也就是()),那么像Haskell一样直接用函数式的方案解决这个问题就变得麻烦了起来,于是他们的解决方法是用try!,这个宏的目的其实是做Haskell里差不多的事情的,不过并不像Haskell的monad实现那么通用和强大,这里只针对了Result monad,他通过match的方法,保证出现Error的时候返回,否则继续进行,非常简单,保证了没有多余的if和match,但是又保证了Error的正确返回,且错误信息能够被收集,可以说是非常好的一种解决方案了。

不过,问题没完,Error的类型该是什么?按照Rust的方法,它使用了一个trait,差不多就是接口,可是我最近在写的时候又发现一个问题:Error trait实现的具体Error struct不同?这下可就不好了,由于不同的库都有自己的Error类型,现在,直接就变得没用了,因为他返回的时候不知道该返回什么Error了。

当然,问题肯定是能够解决的,因为并不是像我说的那么显然,它除了直接返回以外,他还调用了From::fromFrom其实就是Rust用来转换类型的一个trait,由于From::from的调用,导致错误在返回之前,是要先试图转换成返回值所指定的类型的!(这里当然是要得益于Rust强大的类型推导了)

所以最终的方案:实现自己的Error 类型,一般用一个Enum,每一个元素就是一种可能出现的错误,然后实现各种可能出现的错误到我们自定义类型的转换,也就是From trait,之后,就可以用进行错误接收之后返回了,最终的错误处理,就可以在自己认为合适的位置进行,利用Result自带的方法进行啦!

总结

总的来说,Rust方法结合了函数式,是一个思考的比较完备的方案,将可能出错的Error都集中到一起,像错误代码一样,很清晰,但是又避免了错误代码的问题,既可以返回值同时返回错误代码(错误类型),同时还可以通过的语法糖来避免很多额外的代码,可以说是一个比较清晰的方案了。

当然,坏处就是,实现自定义类型的代码还是比较繁多的,甚至比使用if之类的还要多,不过,至少他们组织的比较好,而且不是多次重复,每一个都是针对不同的类型,还是比较能够接受的啦。