Kotlin中ResultWrapper类泛型实现的两处疑问咨询
先确认你的初始理解:完全正确。
- T确实是成功场景下预期接收的响应模型;
- 错误场景(GenericError/NetworkError)不会返回有效成功数据,用
Nothing(所有类的子类,表示无返回值)是合理的; - 给ResultWrapper加上
out T实现协变,能让ResultWrapper<Nothing>成为任意ResultWrapper<T>的子类型,这样safeApiCall不管是成功返回Success<T>,还是出错返回NetworkError/GenericError,都能统一返回ResultWrapper<T>类型,完美适配函数的返回值要求。
疑问1:为什么Success要声明为协变子类?
先看Success的定义:
data class Success<out T>(val value: T): ResultWrapper<T>()
这是为了保持整个ResultWrapper体系的协变一致性。父类ResultWrapper已经用out T声明了协变,作为它的子类,Success的泛型参数必须也声明为协变,否则会破坏整个类型体系的协变特性。
举个例子:假设我们有Animal和它的子类Dog,如果Success没有out T,那么Success<Dog>就无法被当成ResultWrapper<Animal>使用——但父类ResultWrapper的协变设计本来就是允许这种向上转型的。只有给Success加上out T,才能让Success<Dog>成为ResultWrapper<Animal>的合法实例,保证协变特性在整个密封类的所有子类中生效。
另外,虽然Success持有T,但这里的out是为了和父类的协变规则对齐,T在Success中是只读的val,属于输出位置,完全符合协变的使用要求。
疑问2:协变的T只能用于输出位置,为什么safeApiCall能把T当参数用?
这里要区分两个T:
- 一个是
ResultWrapper<out T>中的协变泛型T,它的限制是在ResultWrapper类内部,T只能出现在输出位置(比如返回值、只读属性); - 另一个是
safeApiCall函数自己的泛型T,这是独立的泛型参数,和ResultWrapper的T只是名字相同而已。
看safeApiCall的逻辑:
suspend fun <T> safeApiCall(dispatcher: CoroutineDispatcher, apiCall: suspend () -> T): ResultWrapper<T> { return withContext(dispatcher) { try { ResultWrapper.Success(apiCall.invoke()) } catch (throwable: Throwable) { when (throwable) { is IOException -> ResultWrapper.NetworkError is HttpException -> { val code = throwable.code() val errorResponse = convertErrorBody(throwable) ResultWrapper.GenericError(code, errorResponse) } else -> { ResultWrapper.GenericError(null, null) } } } } }
这里的apiCall是suspend () -> T,也就是它的返回值是T,我们把这个返回值传入Success的构造函数——而Success的value是只读的val,属于输出位置(我们只能从Success中读取value,不能修改它),这完全符合ResultWrapper协变的规则。协变的限制从来不是“外部不能把T传入ResultWrapper”,而是“ResultWrapper内部不能把T作为输入位置(比如函数参数、可变属性)”,所以这里的用法是完全合法的。
内容的提问来源于stack exchange,提问作者Android Developer

