论接口/特征方法不设计错误返回带来的问题,以及为什么很多标准接口/特征方法不设计错误返回

126 阅读4分钟

在编程中常有重写方法、为方法特供自己特有的实现的情况,例如子类重写父类非抽象方法、实现类实现接口(java的interface)、实现特征(rust的trait)等。

而一个子模块的方法中如果遇到一些可预料的错误:如用户输入不合法、文件不存在等错误,一般是需要将这个错误反馈给上层处理。而不应该是自己擅自主张直接进入panic恐慌模式来终止程序或者线程

!!!【本问题不讨论编程错误引起的错误情况:如数组越界访问,使用悬垂引用、NPE等,在java的语境中,”错误“一词没有特殊说明,不表示java中的Error类】

不同语言特供了自己的方式来反馈错误。在java中常用受检异常来抛出这种异常又或者用是Optional,而C和rust都偏向与使用返回值来反馈这种异常(rust中,返回Result<T, E>或Option)。这些反馈错误的方式往往就表示在方法的签名中。

一种重现父类方法的方法它往往是根据自己的业务有自己的实现方式,而这是实现方式决定了这个方法是否会抛出异常和会抛出哪些异常。作为一个父类方法、接口方法或者trait方法它的函数签名通常写好就固定了,也就是说它是否会反馈错误,怎么反馈错误都是写死固定的。同时这些父类方法、接口方法或者trait方法,也不能确定使它们被重写的方法是否会抛出错误和会抛出哪些错误,那如果说这些父类方法、接口方法或者trait方法如果要能支持重写方法能抛出自己的与业务相干的错误。那所有的父类方法、接口方法或者trait方法难道不应该都要在签名中设计错误的反馈方式吗?但是很显然,许多标准库/包中的接口和trait并没有这么做,很多方法签名上根本没有涉及任何的错误返回方式,这是为什么呢?这样做不就是在限制具体实现的功能吗?因为不能反馈错误,所以所有可能出错的实现都不能重写那些父类方法、接口方法或者trait方法,这个限制实在令人太失望了。

在java中有些人会建议使用RuntimeException去包装这些错误然后再抛出,这样方法签名就可以不用写明这个错误,就和接口方法(父类方法)的签名达成一致。但是我认为这个方法真是太糟糕了,令人头皮发麻。因为接口方法(父类方法)的文档是不会说明这些实现方法(重写方法)可能犯下的具体错误的,其他方法在使用这个接口方法(父类方法)是无法知道会出现什么错误的,因此使用这个接口方法(父类方法)的方法会出现什么样错误也是未知,这给人感觉就像程序的任何一个函数都会出现错误,并且还不知道它的具体错误类型,正如开头所说的那样,他是程序运行中会出现的正常情况,因此我们应该要求程序去处理这种错误,但是上层的代码要处理这些错误要先知道哪会发生错误和有哪些错误。但是使用RuntimeException去包装这些错误后,这个问题就难以回答了。如果有人尝试经过认真的分析程序中所有被RuntimeException包装的错误的源头和传播路径(这必然要深入了解方法的具体实现),在上层的代码中找到了所有可能会出现这些错误的范围,那真是一件辛苦的事,但是更可怕的是,如果在将来又有一些新的RuntimeException包装的错误的源头出现或者消失,那么上层的代码中可能会出现这些错误的范围又将会改变,你又要重新小心翼翼地去分析一遍这些错误的传播路径,这真是一件非常不愉快的事情。为了避免这种耦合,有的人可能就直接将上层代码整个都泡在try-catch块中,防止有漏网之鱼,想想就太疯狂了,这让我不禁怀疑自己是不是不应该这样写代码。

换做其他语言,可能就不能像java这样做到用RuntimeException包装错误然后丢到上层处理了,所以这不是一个广泛适用的办法,更何况这个办法本来就很鸡肋,RuntimeException一般也不是用于这类错误的,它本应该对应的是造成以恐慌模式结束程序或者线程的错误。