优化你的代码返回值:不要再返回 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<SomeObject> func() {
	...
	if (发生异常) {
		log.error(...);
		return Optional.empty();
	}
	...
	return Optional.of(正产值);
}

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

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

2. 返回包装类

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

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

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

// 包装类
public class FunctionResult<T> {
	private boolean success;
	private String msg;
	private ErrorCodeEnum code;
	private T data;

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

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

// 调用方代码
FunctionResult<SomeObject> 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<SomeObject> 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 的一种设计
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值