• 优化你的代码返回值:不要再返回 null 了


    今天 review 同事的代码,发现了如下代码片段

    // 这是一段伪代码
    public SomeObject func() {
    	...
    	if (发生异常) {
    		log.error(...);
    		return null;
    	}
    	...
    	return 正常值;
    }
    

    类似的代码我见过很多,自己曾经也写过这样的代码。
    但是如此的写法实际并不安全,因为调用方很容易取到一个 null 值,进而诱发空指针问题。

    为了重构这个代码,有如下几种处理策略

    ☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️
    ☀️ 1. 返回 Optional 对象                        ☀️
    ☀️ 2. 返回包装类                                          ☀️
    ☀️ 3. 异常时返回空对象或默认值                 ☀️
    ☀️ 4. 上抛异常                                              ☀️
    ☀️ 5. 必须要返回 null 时的小技巧              ☀️
    ☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️☀️ ☀️ ☀️

    在文章的最后,我会给出另外一种 常见但是不推荐 的解决方案:通过异常的返回值标记内部的状态

    几种方案的比较

    方案额外代码量灵活度调用者额外判定错误细节感知沉默的错误
    返回 Optional 对象一般不可以可以避免
    返回包装类有,但全局可用灵活可以可以避免
    异常时返回空对象或默认值一般不可以容易发生
    上抛异常有,但不多灵活有,但解耦在 catch 中处理可以可以避免
    必须要返回 null 时的小技巧不可以可以避免

    表格中维度的解释:

    • 额外代码量:使用这种模式额外的代码量
    • 灵活度:调用方是否可以灵活使用
    • 调用者额外判定:调用者是否需要判空代码来避免异常
    • 错误细节感知:调用者是否能感知到方法内部异常的原因,并进行针对的处理
    • 是否容易出现 “沉默的错误”:是否容易造成无法感知的错误,进而产生难以排查的 bug

    1. 返回 Optional 对象

    Optional 对象是 java 特有的,但其他语言也有类似的实现。这种方法的伪代码如下

    // 函数改造
    public Optional func() {
    	...
    	if (发生异常) {
    		log.error(...);
    		return Optional.empty();
    	}
    	...
    	return Optional.of(正产值);
    }
    

    这样调用方就知道,函数返回值是可能为空的,可以通过函数式编程的方式进行接下来的处理。

    // 调用方代码
    Optional result = func();
    result.ifPresent(obj -> {...});
    

    2. 返回包装类

    这种方式和 Optional 类似,只不过包装类是自己写的。优点是大大增加了灵活性

    • 在实际使用 Optional 对象的时候发现提供的 api 不一定能满足需求
    • 对于需要感知内部错误原因的场合,Optional 就显得无能为力了(虽然这违反了范式,但有时候真的很便捷)

    例如可以使用一个全局的函数返回包装类

    // 包装类
    public class FunctionResult {
    	private boolean success;
    	private String msg;
    	private ErrorCodeEnum code;
    	private T data;
    
    	// 这里可以提供丰富灵活的 api
    	// 例如 void ifSuccess(Consumer consumer);
    	// 还可以 FunctionResult ifSuccess(Consumer consumer) 用来提供链式调用
    	// 其他的也可以大胆地进行设计
    	// 当然最简单的,就是让调用方判定 success 的值
    }
    
    // 函数改造
    public FunctionResult func() {
    	...
    	if (发生异常) {
    		log.error(...);
    		return FunctionResult.failure("错误信息");  // 可以提供丰富的构造函数
    	}
    	...
    	return FunctionResult.success(正产值);
    }
    

    这样调用方就可以知道返回值有可能是失败的,及时做出检测。

    // 调用方代码
    FunctionResult result = func();
    result.ifSuccess(obj -> {...});
    

    3. 异常时返回空对象或默认值

    这也是一种常见的设计模式,所谓 “空对象” 也被称作 “null 对象”,例如

    // 空对象示例
    public class Circle {
    	
    	/**
     	 * 半径
     	 */
    	private int r;  
    
    	public Circle(int r) {
    		this.r = r;
    	}
    
    	public int area() {
    		return pi * r * r;
    	}
    }
    
    // 一个 null 圆,半径为 0
    public class NullCircle extends Circle {
    	public NullCircle() {
    		super(0);
    	}
    }
    

    这样当发生异常时返回 “空对象” 调用方完全不需要担心空指针的问题。

    另外一种常见的思路,是返回一个默认值,很多的包也会采用类似的设计,例如返回字符串的函数,当发生异常时返回空字符串("")作为默认值而不是返回 null;或者在上面圆形的例子中,如果是特定的业务领域,有可能可以返回一个 “标准圆” 作为默认的返回值。

    4. 上抛异常

    可以在发生异常时抛出合理的异常或者是自定义的异常,伪代码如下

    // 函数改造
    public Optional func() throws 自定义的异常 {
    	...
    	if (发生异常) {
    		throw 自定义的异常;
    	}
    	...
    	return Optional.of(正产值);
    }
    
    // 调用方代码
    try {
    	SomeObject result = func();
    } catch(自定义的异常 e) {
    	// 按需进行异常处理
    }
    

    警告:不要一味的抛出 Exception 类而应该自己去继承 Exception 类实现自定义的异常。否则会造成异常处理混乱,代码难以维护

    5. 必须要返回 null 时的小技巧

    如果觉得上述方案都太过麻烦了,另一种可能是工期过紧,非得想要返回 null 值,那么通过以下两个小技巧,也能在一定程度上缓解这一问题

    函数命名

    比如原来你的函数名字是 myFunc,简单的改成 myFuncNullable,调用方就会意识到返回值可能为 null,进而避免空指针。

    注解

    在你的函数上使用 @Nullable 注解,大多数的现代 IDE 都能检测到这个注解,在编写代码的时候,如果调用方使用其返回值不合理,就会给出 “标黄” 的提示,一眼识别问题。

    在这里插入图片描述

    😖 不推荐的解决方案:通过异常的返回值标记内部的状态

    对于返回数字类型的函数,一种常见的写法如下

    // 函数改造
    pubic int func() {
    	...
    	if (发生异常) {
    		log.error(...);
    		return -1;
    	}
    	...
    	return 正产值;
    }
    

    这种“错误码”的写法可以追溯到 C 语言时代,C 语言比较偏底层,而且早期内存很小,这种写法很正常。但是时至今日,使用高级语言的我们应该用表现力更加丰富的手段,原因如下:

    • -1 这种错误码可读性很差,有时候需要深入实现才能理解 -1 是什么含义,-2 又是什么
    • 如果未来 func 返回值含义发生扩展,从正数扩展到负数,改造起来会很麻烦
    • 从设计原则上来说,一个变量承担返回值和是否成功两个含义,也是更容易造成 bug 的一种设计
  • 相关阅读:
    harbor portal密码错误
    超图iServer rest服务之最佳路径分析
    从零开始学习 Java:简单易懂的入门指南之反射(三十八)
    [动态规划] (十三) 简单多状态 LeetCode 740.删除并获得点数
    软件测试入门到高级,5k-50k,你属于哪个阶段?
    【数据结构】串的基础知识及代码实现
    7-136 温度转换
    相似度系列8:unify-BARTSCORE: Evaluating Generated Text as Text Generation
    图论第9天
    【Java进阶篇】第三章 常用类
  • 原文地址:https://blog.csdn.net/JackLang/article/details/141052225