异常不可用于逻辑处理
异常是错误处理,但是不可以用于逻辑处理,假设我们封装了一个MsgQueue,这个类负责收集由服务端一条接受线程收集客户端消息,另一条工作线程负责取出消息,并对消息进行处理。
class Msg {...};
class MsgQueue {
public:
void put(Msg msg) throw (std::length_error);
Msg get() throw (std::out_of_range);
.....
}
这里定义了MsgQueue的两个方式,分别是收集客户端消息,然后保存消息的put方法,还有处理客户端消息,取出客户消息的get方法。
对于put方法,由于内存有限,我们不可能让这个MsgQueue无限度的保存消息,所以当保存的消息过多,无法再保存客户端消息时,我们抛出std::length_error异常。
对于get方法,假设queue里没有任何元素了,我们要告诉调用者queue是空的,但是这里无法返回一个空元素,所以我们的做法就是抛出一个std::out_of_range(STL的queue当queue是空时,调用front()方法居然可以取出元素,不抛出任何异常)。
工作线程的部分代码如下:
Msg msg;
while (true) {
try {
msg = msgQueue.get(); //取出客户端消息
handle(msg); //处理客户端消息
} catch (std::out_of_range) {
sleep(1);
}
}
msgQueue由工作线程和工作线程共享,工作线程不断轮询,尝试msgQueue是否可以取出消息,不可以表示没有新的消息的到来,就等待一秒,可以取出消息就进行处理。
这段代码的问题在于把异常用于逻辑处理了,msgQueue为空,这个场景是正常场景,因为不可能每个时刻都有消息过来,异常是用于错误处理的,所以合适的做法我们是应该先判断msgQueue是否为空,不为空,我们才调用msgQueue.get()取出消息。
class MsgQueue {
public:
bool isEmpty();
void put(Msg msg) throw (std::length_error);
Msg get() throw (std::out_of_range);
.....
}
添加判断queue是否为空的成员函数后,工作线程的代码修改如下:
Msg msg;
while (true) {
if (msgQueue.isEmpty()) {
sleep(1);
continue;
}
msg = msgQueue.get(); //取出客户端消息
handle(msg); //处理客户端消息
}
这样代码就简洁明了很多,没有像try {} catch{}导致成块代码的出现,尤其是层次一多,代码很难一目了然。
作为MsgQueue不仅要考虑调用者各个调用场景,应该提供判断isEmpty()之类的方法,避免调用者在做逻辑处理时,不得去用异常捕获,写出一些难以理解的代码。
当局部的控制可以满足我们的业务时,就不要使用异常处理了,请用异常做错误处理。
何时该使用try和catch
异常适用于哪些错误处理呢,哪里我需要try {} catch{}呢,例如网络异常,文件IO异常这些典型的错误,从一个服务器中,读取服务器的响应,可能会出现网络中断,这样的处理代码是合适使用try {} catch{}处理的。
try {
writeRequestToServer();
readResponseFromServer();
} catch (NetworkError) {
//对网络错误进行处理,例如像浏览器说服务器无法连接
}
例如像文件IO,我们读写的过程中,如果真的那么倒霉,遇到硬盘坏道,也必须做处理,一个健壮的程序是要考虑任何能想到的错误的
try {
writeFileToDisk();
} catch (IO_Error) {
//假设是IO坏道,一般能做的处理就是提示用户说磁盘或者分区损坏了
}
这些都是错误处理,是使用异常try {} catch{}的场景,看代码人的一看就知道try{}里的代码可能会发生错误,出现错误是非法场景,但是又不能完全避免。
总结
try{} catch{}这些错误处理语句,能少用就少用,代码成块出现,很容易让代码变得难以阅读,如果你用异常来做逻辑处理,代码中出现try{} catch{}的概率会大大提高,导致代码可阅读性很差。
例如当if else的代码块有三层以上
if (...) {
if (...) {
if (....) {
if (...) {
....
}
}
}
}
这样的代码阅读性很差(还没有加else,否则更晕),if的层次超过了3层,我就觉得代码该重构了,因为我觉得就算是这段代码的开发者,过了一个星期后,要看看这段代码,重新梳理逻辑,至少要一个多小时,如果你重构了,你只需要十分钟就可以看懂代码逻辑了(题外话了)
使用try {} catch {}会导致代码块的层次增加,代码可阅读性变差,公认的做法是用来做错误处理,如果用来做逻辑处理,会带来没有必要的麻烦,请尽量避免使用。