Arrow库ensure非空校验后变量仍为可空类型问题排查
Arrow either块内ensure非空校验不触发智能转换问题
问题表现
在Arrow库的either作用域中使用ensure完成非空判断后,变量仍被Kotlin编译器识别为可空类型,无法实现空安全,问题复现代码:
suspend fun checkCanConnectDirectChat( senderId: Int?, receiverId: Int?, chatRoomId: Int? ) = either { ensure(chatRoomId != null && receiverId != null) { BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } val isSenderInChat = isUserInChat(chatRoomId, senderId).bind() val isReceiverInChat = isUserInChat(chatRoomId, receiverId).bind() ensure(isSenderInChat && isReceiverInChat){ BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } }
校验完成后IDE仍提示chatRoomId、receiverId为可空类型,提示截图:
原因
这不是使用错误。Kotlin的智能类型转换只能识别内置控制流、以及标准库中声明了编译器契约的函数(比如require/check),无法自动识别第三方高阶函数的谓词约束。普通的ensure方法没有声明对应编译器契约,编译器无法推断出「执行到ensure后续代码时条件必然为真」,因此不会自动做非空转换。
解决方法
不建议使用!!非空断言,以下两种是类型安全、符合Arrow设计规范的写法:
使用Arrow内置的
ensureNotNull方法(推荐)
Arrow专门为非空校验场景提供了带编译器契约的ensureNotNullAPI,校验通过后会自动将值转换为非空类型,校验失败则返回指定的左值错误:suspend fun checkCanConnectDirectChat( senderId: Int?, receiverId: Int?, chatRoomId: Int? ) = either { val validChatRoomId = ensureNotNull(chatRoomId) { BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } val validReceiverId = ensureNotNull(receiverId) { BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } // 如果isUserInChat要求senderId非空,也可以对senderId加一层ensureNotNull校验 val isSenderInChat = isUserInChat(validChatRoomId, senderId).bind() val isReceiverInChat = isUserInChat(validChatRoomId, validReceiverId).bind() ensure(isSenderInChat && isReceiverInChat){ BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } }这是Arrow官方推荐的非空校验范式,没有空指针风险,逻辑也更清晰。
配合
rightIfNotNull绑定非空值
如果习惯用bind的写法,可以用rightIfNotNull扩展函数将可空值直接转换为Either类型,空值时自动返回指定错误,绑定后得到非空值:suspend fun checkCanConnectDirectChat( senderId: Int?, receiverId: Int?, chatRoomId: Int? ) = either { val validChatRoomId = chatRoomId.rightIfNotNull { BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) }.bind() val validReceiverId = receiverId.rightIfNotNull { BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) }.bind() val isSenderInChat = isUserInChat(validChatRoomId, senderId).bind() val isReceiverInChat = isUserInChat(validChatRoomId, validReceiverId).bind() ensure(isSenderInChat && isReceiverInChat){ BadRequestExceptionResponse(message = ErrorConstants.INVALID_PAYLOAD) } }
注意:不要尝试把多个变量的非空判断合并写在同一个普通
ensure里,Kotlin编译器对第三方函数的契约支持无法覆盖多变量同时智能转换的场景,拆分单个变量校验是最稳定的写法。
内容的提问来源于stack exchange,提问作者Victor Orlyk
相关产品推荐
相关产品推荐

