Skip to content

泛型的 in 与 out:哪些类型可以互换 ​

本节接在泛型数据模板之后,讨论一种进阶需求:函数需要 Source<Reward> 时,能否传入 Source<CoinReward>?

先约定 CoinReward 继承 Reward。每个金币奖励也是奖励。

out:通过接口取出数据 ​

mcfpp
data Reward {
    var points: int = 10
}

data CoinReward: Reward {}

abstract data Source<out T> {
    abstract func read(): T
}

data CoinSource: Source<CoinReward> {
    override func read(): CoinReward {
        return CoinReward()
    }
}

func main() {
    var coins = CoinSource()
    print(coins.read().points)
}

示例通过具体实现 CoinSource 读取金币奖励,显示 10。从类型关系看,Source<CoinReward> 提供的每个值也都是 Reward,所以它满足 Source<Reward> 的输出要求。

out T 表示接口向外提供 T。它适合用于返回值,以及同样向外提供数据的嵌套类型。

in:通过接口交入数据 ​

mcfpp
data Reward {}
data CoinReward: Reward {}

abstract data Sink<in T> {
    abstract func accept(value: T)
}

data RewardLog: Sink<Reward> {
    override func accept(value: Reward) {
        print("Reward accepted")
    }
}

func main() {
    var log = RewardLog()
    log.accept(CoinReward())
}

示例把金币奖励交给具体实现 RewardLog,显示 Reward accepted。从类型关系看,需要 Sink<CoinReward> 的代码只会交入金币奖励。能够处理所有 Reward 的 Sink<Reward> 也满足这个输入要求。

in T 表示接口接收 T。它适合用于方法的输入参数。

同时读写时保持类型一致 ​

假设 Box<T> 同时提供 read(): T 和 write(value: T)。一个 Box<CoinReward> 要持续保存金币奖励。若把它当作可写的 Box<Reward>,调用者就可能写入另一种奖励。

因此,这种读写容器使用普通 <T>,保留具体类型的一致性。选择规则如下:

接口用途参数写法示例
主要提供数据<out T>Source<T>.read(): T
主要接收数据<in T>Sink<T>.accept(value: T)
同时读取和写入数据<T>Box<T>

编译器会检查 in、out 在字段和方法中的使用方向。给可写字段保留普通类型参数,通常更容易表达它的读写约定。

本页示例通过具体实现类型调用方法。当前编译器对普通参数中的抽象模板方法调用仍有运行时生成限制,泛型替换关系首先用于检查类型是否匹配。