这天小明问我说
我这个API 方法会回传错误代码, 呼叫端要处理这些各种不同的错误代码, 可以帮忙看看这些程式码有没有没考虑到的地方?
许多人没有使用异常处理的藉口有很多, 但是大多数归结为两种看法:
异常处理语法是不可取的, 因此以某种方式返回错误代码是比较好的做法."抛出异常"方式 的性能不如 "传回错误代码"方式最好用 "例外" 取代 "回传错误代码"
我们看看下面的示範程式码:
switch( checkLogin() ){ case -1: //Invalid credentials ... break; case -2: //Too many login attempts ... break; default: // Successful break;}
以上程式码有两个问题
为了知道执行 checkLogin 方法的回传值 "-1" 是什么意思, 我必须看一下 checkLogin 方法的实作内容.更改错误代码值怎么办? 我将必须检查 checkLogin 方法的所有用法, 才能更改接收到的回传值!您可以採用另一种方法来解决前面所述的2个主要问题
switch( checkLogin() ){ case ErrorCode.INVALID_LOGIN_CREDENTIALS: ... break; case ErrorCode.TOO_MANY_LOGIN_ATTEMPTS: ... break; default: // Successful scenario, log in the user break;}
但是如果我们想在 checkLogin 方法中重构一个 Extract 方法, 那将是很困难的工作: 我们必须从 checkLogin 方法中携带错误代码, 在 Extract 方法中有用到的错误代码必须将其传回去. 为了在我们的应用程序的外层中委派处理这种特殊情况的逻辑, 我们可能需要更大的灵活性.
例如以下 checkLogin 程式码, 虽然用了 enum 方式取代了错误代码的魔法数字
private ErrorCode checkLogin() { ... if ( hasNotValidCredentials ) { return ErrorCode.INVALID_LOGIN_CREDENTIALS; } ... if ( hasTooManyLoginAttempts ) { return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS; } return ErrorCode.LOGIN_SUCCESSFUL;}
当我们想要尝试 Extract 这一段程式码
if ( hasTooManyLoginAttempts ) { return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS;}
重构变成下面程式码...
private ErrorCode Extract() { if ( hasTooManyLoginAttempts ) { return ErrorCode.TOO_MANY_LOGIN_ATTEMPTS; } ???}
进行到这里, 你就会发现这还得花更大的力气才能往下做...
考虑到前面描述的问题, 在这种情况下唯一可以帮助的是用 "丢例外错误" 替换 "错误代码"
private void checkLogin() { ... if ( hasNotValidCredentials ) { throw new InvalidLoginCreadentialsException(); } ... if ( hasTooManyLoginAttempts ) { return new TooManyLoginAtteptsException(); }}
然后你就可以轻鬆的进行重构(Refactoring), 变成下面程式码
private void checkLogin(){ checkLoginCredentials(); checkLoginAttempts(); checkBannedUser();}
在重构过程中(Refactoring), 完全根本不需要考虑回传值的问题
如果将异常用于经常失败的代码, 则程式码执行的性能将是不可接受的. 这是一个的确令人担忧的问题. 当程式码抛出异常时, 其性能可能会降低几个数量级. 但是在严格遵守不允许使用错误代码的例外处理準则的同时, 我们也可以获得良好的性能. 有两种建议可以解决这个问题.
测试者-执行者模式(Tester-Doer Pattern)
有时候将发生例外的方法内容可以拆成两部分, 这可以提高其性能. 例如下面程式码示範:
让我们看一下Dictionary 类的indexed 属性.
var table = new Dictionary<string,int>();...int value = table["key"];
如果table 字典中不存在该键值(Key), 则索引器将引发例外错误. 在这段程式码经常执行失败的情况下, 这会导致引起执行性能问题(Performance Problem). 缓解问题的方法之一是在访问键值之前测试键是否在字典中.
var table = new Dictionary<string,int>();...if( table.Contains("key") ){ int value = table["key"];}
在上面的示例中, 包含条件的用于测试条件的成员称为"测试者".
if( table.Contains("key") )
用于执行潜在发生例外的成员(索引器) 称为"执行者".
int value = table["key"];
TryParse 模式(TryParse Pattern)
对于性能要求极高的API , 应使用Tester-Doer 更快的模式.
例如DateTime 定义了一个Parse 方法, 该方法在字符串解析失败时抛出例外. 但它还定义了一个相应的TryParse 方法, 该方法尝试进行解析, 但是如果解析失败则返回false, 并使用out 参数返回成功解析的结果.
使用此模式时, 在"try" 的功能中, 如果尝试了所有方法无效, 最后仍然失败时, 则该方法仍必须抛出例外.