惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

大猫的无限游戏
大猫的无限游戏
S
SegmentFault 最新的问题
量子位
A
Arctic Wolf
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
WordPress大学
WordPress大学
V
Vulnerabilities – Threatpost
博客园 - Franky
C
Cyber Attacks, Cyber Crime and Cyber Security
The Cloudflare Blog
Last Week in AI
Last Week in AI
The Hacker News
The Hacker News
I
Intezer
J
Java Code Geeks
P
Privacy International News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
S
Secure Thoughts
Cisco Talos Blog
Cisco Talos Blog
阮一峰的网络日志
阮一峰的网络日志
S
Securelist
Security Latest
Security Latest
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
小众软件
小众软件
Jina AI
Jina AI
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
博客园_首页
酷 壳 – CoolShell
酷 壳 – CoolShell
T
The Exploit Database - CXSecurity.com
雷峰网
雷峰网
T
Tenable Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Privacy & Cybersecurity Law Blog
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 【当耐特】
T
Threat Research - Cisco Blogs
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MongoDB | Blog
MongoDB | Blog
D
DataBreaches.Net
N
News | PayPal Newsroom
Google Online Security Blog
Google Online Security Blog
K
Kaspersky official blog
H
Help Net Security
宝玉的分享
宝玉的分享
罗磊的独立博客
Webroot Blog
Webroot Blog
月光博客
月光博客
B
Blog RSS Feed
Recorded Future
Recorded Future

Yesterday17's Blog

2026 新年解密红包 / Melody Flag | Yesterday17's Blog 谈谈 Iori 的设计思路(二):如何实现一个 Showroom 录制工具? | Yesterday17's Blog 谈谈 Iori 的设计思路(一):从 Nico Timeshift 说起 | Yesterday17's Blog Iori Minyami 0.1.0 发布 | Yesterday17's Blog 2025 新年解密红包 / Melody Flag | Yesterday17's Blog 使用 Cloudflare Warp 解决罗森票务的海外登录问题 | Yesterday17's Blog How To Blog 04: The Astro v5 Era | Yesterday17's Blog 谈谈 tokio::select! 的公平性 | Yesterday17's Blog Learning Pingora 05 - Connect with TLS | Yesterday17's Blog Leaving Bytedance | Yesterday17's Blog 大橋彩香 AsiaTour「Reflection」上海公演 个人向记录 & Repo | Yesterday17's Blog Recoving from burnout - What happened? | Yesterday17 Yubikey 重建手册 | Yesterday17's Blog How To Blog 03: Heimus | Yesterday17's Blog 🪧 Blog Migration Accouncement | Yesterday17's Blog Learn Your IDE - VSCode 是如何仅重启插件的? | Yesterday17's Blog How To Blog 02: Astro❤️Password | Yesterday17's Blog How To Blog 01: Why, How, and the Future | Yesterday17's Blog Learning Pingora 04 - Establish L4 Connection | Yesterday17's Blog Learning Pingora 03 - Upstreams and Peers | Yesterday17's Blog Learning Pingora 02 - A Simple HTTP Server | Yesterday17's Blog Learning Pingora 01 - Getting Started | Yesterday17's Blog 2024 新年解密红包 / Melody Flag | Yesterday17's Blog 向新的一年飞驰——记录 2023 | Yesterday17's Blog 「サクラノ刻」对话选摘(2) | Yesterday17's Blog PGP Key Revocation 注销声明 | Yesterday17's Blog 「サクラノ刻」对话选摘(1) | Yesterday17's Blog 2023 新年解密红包 / Melody Flag | Yesterday17's Blog 『蒼の彼方のフォーリズム』通关感想 | Yesterday17's Blog 单显卡直通教程 | Yesterday17's Blog 对博客与笔记的思考 | Yesterday17's Blog Project Anni 之旅(3)自动化 Flutter 应用 CI/CD 上架流程 | Yesterday17's Blog AsobiStage 直接播放链接 | Yesterday17 如何在后分P时代进行投稿——sswa使用详解 | Yesterday17's Blog JSON RPC 与 LSP 协议基础 | Yesterday17's Blog Grajapa Shueisha / BookEnd 加密方式调查 | Yesterday17's Blog 【2022篇+WriteUp】如何再收一个新年红包? | Yesterday17's Blog 如何将良心云的良心功能清理干净 | Yesterday17's Blog 【油猴脚本】bilibili 投稿页面返回旧版+旧版页面强制允许分P上传 | Yesterday17's Blog Cloudr1v1 授权方式分析 | Yesterday17 Typora 1.0.2 逆向实录 | Yesterday17's Blog Project Anni 之旅(2)ValueAfterTable——toml-rs的实现与限制 | Yesterday17's Blog IPv4透明代理+IPv6 Passthrough——树莓派单臂软路由折腾记 | Yesterday17's Blog Chaos; Child 汉化补丁 神秘编码探索 | Yesterday17's Blog 镣铐与舞蹈——个性与共性之迷思 | Yesterday17's Blog Go 学习笔记 02 - 找准 io 之道 | Yesterday17's Blog NAT Slipstreaming v1 原理浅析 | Yesterday17's Blog 绕过「9-nine-」的 CDKEY 验证——KrkrPlugin 正(?)向实录 | Yesterday17's Blog 静流的青春纪念册——「サクラノ刻 -櫻の森の下を歩む-」体验版感言 | Yesterday17's Blog Project Anni 之旅 01 - 从 clap-builder 到 derive | Yesterday17's Blog [Google CTF 2021] CPP WriteUp | Yesterday17's Blog 获取 アソビステージ 的实际播放链接 | Yesterday17's Blog 90 行 Rust 代码实现 AsyncTeeReader | Yesterday17's Blog 或许还算有价值一读的文章列表 | Yesterday17's Blog 从零开始的 Seedbox 之旅 | Yesterday17's Blog [随笔]技术型博客行文迷思(1) | Yesterday17's Blog 浅谈 git fetch 的工作方式 | Yesterday17's Blog 『ソーサレス*アライヴ! ~the World's End Fallen Star~』通关感想" | Yesterday17's Blog Rust std::fmt 格式语法简述 | Yesterday17's Blog 日亚修改居住国的解决方案 | Yesterday17's Blog [Windows/Linux] GC553 的 Switch 完美采集之路 | Yesterday17's Blog Berd's Red Envelope 2021 WriteUp | Yesterday17's Blog 【中英对照】ALSA 音频 API 使用教程/A Tutorial on Using the ALSA Audio API | Yesterday17's Blog 从 cue_scanner.l 看 CUE Sheet 的词法单元 | Yesterday17's Blog Postman 历史记录导出的解决方案 | Yesterday17's Blog 《恋爱绮谭 不存在的夏天》通关感想 | Yesterday17's Blog [微机实验/TD-PITE] 微机接口综合实验 | Yesterday17's Blog [微机实验/TD-PITE] 键盘扫描及数码管显示实验 | Yesterday17's Blog [微机实验/TD-PITE] 数码管显示实验 | Yesterday17's Blog Airsonic Advanced+Google Drive+Caddy 部署纪实 | Yesterday17's Blog X-NUCA 2020 - hellowasm 题解 | Yesterday17's Blog [微机实验/TD-PITE] 8251 串行接口实验 | Yesterday17's Blog Node.js child_process.fork 与 env 污染 RCE | Yesterday17's Blog EP.01 「夜の向日葵」 | Yesterday17's Blog [微机实验/TD-PITE] 8254 定时/计数器实验+选做实验 | Yesterday17's Blog [JLU CTF/2020] babywasm WriteUp | Yesterday17's Blog PHP 反序列化与经典利用 | Yesterday17's Blog WebAssembly 逆向简述 | Yesterday17's Blog 『彼女、お借りします』一期完结点评 | Yesterday17's Blog [微机实验/TD-PITE] D/A 转换实验+选做实验 | Yesterday17's Blog [微机实验/TD-PITE] A/D 转换实验+选做实验 | Yesterday17's Blog 开源项目申请 JetBrains Open Source License 简单流程 | Yesterday17's Blog 微软拼音与 JetBrains 搜索快捷键冲突的解决方案 | Yesterday17's Blog [微机实验/TD-PITE] 8259 中断优先级实验+选做实验 | Yesterday17's Blog IFTTT 测试(续) | Yesterday17 IFTTT 测试 | Yesterday17's Blog [微机实验/TD-PITE] 存储器扩展实验+选做实验 | Yesterday17's Blog 新版 GCC 针对 -fdump-translation-unit 的替代方案 | Yesterday17's Blog 一次 HSTS 策略配置的排错之旅 | Yesterday17's Blog YukiNative 踩坑记——Windows 的消息队列 | Yesterday17's Blog 我是我自己——论获取 HTTPS 证书时的验证步骤 | Yesterday17's Blog 【设计文档】对 PUG 的大规模设计修订(1.1) | Yesterday17's Blog GS65 折腾记(2)加装固态,分区,Grub2 引导 Manjaro LiveCD | Yesterday17's Blog 「さくら、もゆ。」的空白字体列表——一次逆向问题定位过程实录 | Yesterday17's Blog GSuite 探索篇(1)使用 Service Account 向 Google Drive 传输文件 | Yesterday17's Blog 『サクラノ詩 -櫻の森の上を舞う-』通关感想 | Yesterday17's Blog 《ATRI -My Dear Moments-》通关感想 | Yesterday17's Blog [工具][VSCode 扩展] AegiKit——方便 Aegisub 使用的工具箱 | Yesterday17's Blog 贝塞尔曲线、字体矢量化与曲线运算 | Yesterday17's Blog NAT 类型初探 | Yesterday17's Blog
【翻译】Subtyping and Variance / 子类型与变型 | Yesterday17's Blog
Yesterday17 · 2021-01-05 · via Yesterday17's Blog

本文于 2021 年 1 月 5 日译自 《The Rustonomicon》中 Subtyping and Variance 一章,在原文基础上增加了部分目录格式以便于阅读。

ToC

  • 前言
  • 子类型
    • Objective Rust
    • 子类型解决的问题
    • 简单 find-replace 的问题
    • 生命周期
  • 变型
    • 类型构造器
    • 协变、逆变与不变
    • Rust 中的变型
    • 回顾 meowing dog 问题
    • 生命周期的协变
    • 拥有所有权时的协变
    • 函数指针
    • 自定义类型的变型

前言

子类型是类型之间的一种关系,使静态类型语言更加宽松、灵活。

Rust 中的子类型和其他语言有所不同,更难举出简单的例子——毕竟子类型,尤其是变型,其本身已经很难理解了。

为了方便理解,这章首先考虑的是 Rust 的简单扩展——其增加了一个简单的子类型关系。在建立好知识框架之后,我们再回头看实际 Rust 的子类型是如何运作的。

子类型

Objective Rust

这就是我们的扩展:Objective Rust,有三种新类型:

trait Animal {

fn snuggle(&self);

fn eat(&mut self);

}

trait Cat: Animal {

fn meow(&self);

}

trait Dog: Animal {

fn bark(&self);

}

和一般的 Trait 不同,我们可以像 struct 一样把它们当作具体的类型使用。

我们来看一个简单的例子:

fn love(pet: Animal) {

pet.snuggle();

}

默认情况下,静态类型必须和编译时类型完全一致。由此,下述的代码就无法编译了:

let mr_snuggles: Cat = ...;

love(mr_snuggles); // ERROR: expected Animal, found Cat

mr_snugglesCat,而 Cat 并不等同于 Animal,因此 love 无法执行(😿

很奇怪吧,毕竟 Cat 属于 Animal,它有着 Animal 拥有的一切特性。所以根据直觉,love 不应该在意这是 Cat 而非 Animal。它应该抛弃 Cat 的一切非 Animal 特性——毕竟这对于 love 并不重要。

子类型解决的问题

这就是子类型希望解决的问题。因为 CatAnimal+其他,所以我们描述:CatAnimal 的子类型(subtype)——因为猫是所有动物中的一种;同样地,我们描述:AnimalCat 的超类型(supertype)。

有了子类型,我没就可以给上述的严格静态类型系统增加一条简单的规则了:当某处希望接收类型 T 时,除 T 外还接收类型 T 的子类型。

或者更具体一点:希望接收 Animal 的地方也可以接收 CatDog

在这一章剩下的内容中,子类型会比这个描述复杂而微妙地多,但这条简单的规则表示了 99% 的直觉。并且,编译器会自动处理所有的 corner case——除非你要写 unsafe 代码。

但这是 Rustonomicon,我们要写的就是 unsafe 代码,所以我们需要理解它们的运作原理,以及我们在什么地方可能出问题

简单 find-replace 的问题

问题的核心就是这条规则,如果简单地应用,可能会导致 meowing dog 问题。在这种情况下,我们可以让别人相信:一条狗实际上是一只猫。这种情况完全摧毁了静态类型系统的结构(还会导致未定义行为的出现)。

来看一个例子,如果完全应用 find-replace 规则会如何:

fn evil_feeder(pet: &mut Animal) {

let spike: Dog = ...;

// `pet` 是 Animal, Dog 是 Animal 的子类型

// 所以没问题……吧?

*pet = spike;

}

fn main() {

let mut mr_snuggles: Cat = ...;

evil_feeder(&mut mr_snuggles); // 把 mr_snuggles 替换成 Dog

mr_snuggles.meow(); // OH NO, MEOWING DOG!

}

显然,我们需要一个比 find-replace 规则更可靠的系统。这个系统就是变型(variance,一套管理子类型组成方式的规则。最重要的是,变型定义了禁止套用子类型的情形。

生命周期

不过在介绍变型之前,我们来看一下子类型在 Rust 中的实际应用:生命周期(lifetimes)!

注:生命周期的类型性是一个相当随意的构造,有些人并不认同。但将生命周期统一为类型能够简化我们的分析。

生命周期是代码的区域,而区域可以通过包含(outlives)关系进行部分排序。生命周期的子类型就是以这种关系为条件的:如果 'bit: 'smallbig 大于 small,这里的大于是 containsoutlives),则 'big'small 的子类型。这看上去有点反直觉:大区域是小区域的子类型。但仔细想想 Animal 的例子就能明白了:CatAnimal+其他,而 big 则是 small+其他。

生命周期的 meowing dog 问题会导致短生命周期被应用于期望长生命周期的地方,形成悬垂引用,最终导致 UAF

需要注意的是 'static——永久生命周期,是所有生命周期的子类型,因为它比所有生命周期都要长。我们会利用这种关系简化后续的示例。

说了这么多,我们仍然不知道如何实际使用生命周期的子类型,因为没有任何存在是 'a 类型的。生命周期只会在一些更大的类型中作为其中的一部分出现,例如 &'a u32IterMut<&'a, u32>。为了应用生命周期的子类型,我们需要知道如何组成子类型。繰り返す:我们需要变型。

变型

说到变型,事情就开始变得有点复杂了。

类型构造器

变型是类型构造器对其参数(应用)的一种属性。Rust 中的类型构造器是指任何具有非绑定参数的通用类型。比如 Vec 就是一个接收类型 T 并返回 Vec<T> 的类型构造器;&&mut 则是接收两个参数:生命周期指向类型的类型构造器。

注:方便起见,下文使用 F<T> 代表类型构造器,以便于关注其中的 T

协变、逆变与不变

类型构造器 F 的变型指输入子类型如何影响输出子类型Rust 中有三种变型。给定两个类型 SubSuper,其中 SubSuper 的子类型,则有:

  • 如果 F<Sub>F<Super> 的子类型,则 F 是**协变(covariant)**的
  • 如果 F<Super>F<Sub> 的子类型,则 F 是**逆变(contravariant)**的
  • 否则,F 是**不变(invariant)**的

如果 F 有多个类型参数,我们可以讨论单个参数的变型,比如:F<T, U>T 协变而对 U 逆变。

从实用角度来看,变型基本指的就是协变(It is very useful to keep in mind that covariance is, in practical terms, “the” variance.)。几乎所有针对变型的考量都是考虑其是协变还是不变。在 Rust 很少能看到逆变的存在——虽然有还是有。

Rust 中的变型

下面的表格简单总结了重要的变型类型,下文也是围绕着它讲述的:

*类型’aTU
*&'a T协变协变
*&'a mut T协变不变
*Box<T>协变
Vec<T>协变
*UnsafeCell<T>不变
Cell<T>不变
*fn(T) -> U逆变协变
*const T协变
*mut T不变

* 的行是我们重点关注的,某种意义上的基础。其他未带 * 的可以通过类比进行理解:

  • Vec<T> 和其他拥有指针/集合类型遵循 Box<T> 的逻辑
  • Cell<T> 和其他内部可变类型遵循 UnsafeCell<T> 的逻辑
  • *const T 遵循 &T 的逻辑
  • *mut T 遵循 &mut T 或 UnsafeCell<T> 的逻辑

注:Rust 中唯一存在逆变的就是函数的参数,在实践中并不常用。逆变涉及到高阶编程,其中函数指针采用了指定生命周期的引用(区别于通常的生命周期,进入了高阶的生命周期,它的工作和子类型无关)。

回顾 meowing dog 问题

知识铺垫就到这里,接下来来看一些例子。首先我们先来回顾一下 meowing dog 问题:

fn evil_feeder(pet: &mut Animal) {

let spike: Dog = ...;

// `pet` 是 Animal, Dog 是 Animal 的子类型

// 所以没问题……吧?

*pet = spike;

}

fn main() {

let mut mr_snuggles: Cat = ...;

evil_feeder(&mut mr_snuggles); // 把 mr_snuggles 替换成 Dog

mr_snuggles.meow(); // OH NO, MEOWING DOG!

}

查阅上面的变型表,我们发现 &mut TT不变的。也就是说,问题已经解决了:尽管 CatAnimal 的子类型,但 &mut Cat 不再是 &mut Animal 的子类型了。由此,静态类型检查器就能够阻止我们将 Cat 类型传给 evil_feeder

子类型化的合理性是基于部分细节可以忽略的前提的。但对于引用而言,这样的细节并不能忽略——有一个变量存储着这种细节。当使用这个变量时,我们期望细节信息被保留;当违背期望时,则可能会产生错误的行为。

使得 &mut T 对于 T 协变的问题在于在不知道细节的前提下,我们被赋予了修改原始值的权力,由此导致了 meowing dog 问题。

我们再来看 &T&T 对于 T协变的,因为 &T 不允许修改,在不修改的前提下,任何操作都不会影响细节。同样地,UnsafeCell 和其他内部可变类型必须保持不变:它们使得 &T 的工作方式类似于 &mut T

生命周期的协变

那生命周期呢?为什么引用的生命周期是协变的呢?

首先,生命周期引用化是 Rust 类型的根本所在。拥有类型系统的根本目的在于我们能够将长生命周期的参数传递给接收短生命周期的函数。

其次,更准确地说,生命周期只是引用的一部分。引用者的类型是共享的,这就是为什么在一个地方调整这个类型会出问题的原因。但如果你在把生命周期移交给他人时削减生命周期(从 'long'short),生命周期信息就不再共享了。现在有了两个独立的、不相关的生命周期,二者也就不会互相扰乱了。

简单来说,唯一的扰乱生命周期的方式就是制造出 meowing dog。但当你试图构建一只 meowing dog 时,其生命周期已经是不变的一部分了,生命周期也就不会被削减。

为了更好地理解这一部分,我们将 meowing dog 问题迁移到实际的 Rust 中。在 meowing dog 问题中,我们取了子类型 Cat,并且将其转换为超类型 Animal,最后用一个满足超类型 Animal 约束条件但不满足子类型 Cat 约束条件的 Dog 覆盖子类型。

因此,对于生命周期,我们希望将生命周期(longest转换为短生命周期(shortest),然后以不够长的生命周期(longer覆盖。如下所示:

fn evil_feeder<T>(input: &mut T, val: T) {

*input = val;

}

fn main() {

let mut mr_snuggles: &'static str = "meow! :3"; // mr. snuggles forever!!

{

let spike = String::from("bark! >:V");

let spike_str: &str = &spike; // 只能活过这个 block

evil_feeder(&mut mr_snuggles, spike_str); // EVIL!

}

println!("{}", mr_snuggles); // UAF?

}

如果我们尝试运行,会得到什么结果呢?

error[E0597]: `spike` does not live long enough

--> src/main.rs:9:32

|

9 | let spike_str: &str = &spike;

| ^^^^^ borrowed value does not live long enough

10 | evil_feeder(&mut mr_snuggles, spike_str);

11 | }

| - borrowed value only lives until here

|

= note: borrowed value must be valid for the static lifetime...

很好,编译失败了。让我们看看这一切到底是如何发生的。首先是新的 evil_feeder 函数:

fn evil_feeder<T>(input: &mut T, val: T) {

*input = val;

}

它接收了一个可变引用和一个值,然后用值覆盖了引用。这个函数中重要一点是它创建了一个类型平等的约束。它在签名中明确指出引用和值的类型必须完全相同

同时,调用者这边,我们传入了 &mut 'static str&'spike_str str

由于 &mut T 对于 T不变的,因此编译器得出结论:第一个参数不能接收子类型。因此 T 的类型就必须是 &'static str

另一个参数的类型是 &'a str,对 'a协变的,因此编译器约束:&'spike_str str 必须是 &'static str 的子类型,也就是说 'spike_str 必须是 'static 的子类型,也就是说 'spike_str 必须包含 'static——但只有 'static 自身能包含 'static

这就是为什么当我们试图将 &spike 赋值给 spike_str 时会出错。编译器工作的结论是 spike_str 必须永远存在,而 &spike 根本不可能活那么久。

因此,尽管在引用它们的生命周期中是协变的,但只要放到一个有问题的上下文中,它们就会继承这种不变性。在上文例子中,我们就是从 &mut T 中继承了不变性。

因此 BoxVecHashMap、……)之所以是协变的原因也和生命周期协变的原因相同:一旦你试图使用诸如可变引用,它们就会继承不变性,以防止坏事发生。

拥有所有权时的协变

Box 允许我们从值的层面关注被我们忽视的部分。和那些允许任意别名的语言不同,Rust 有着非常严格的规则:如果你能够修改或移动所有权,则你是唯一能够访问该变量的存在。

考虑以下代码:

let mr_snuggles: Box<Cat> = ..;

let spike: Box<Dog> = ..;

let mut pet: Box<Animal>;

pet = mr_snuggles;

pet = spike;

这段代码没有任何问题,因为当我们移动后,我们就完全忘记了 CatDog 存在的事实——它们只剩下了 Animal我们解决了提出问题的人!(笑)

和不可变引用协变的原因相反,拥有所有权的值之所以协变是因为你能够改变一切。旧地与新地之间不存在任何关系,而进行子类型转换所破坏的内容也就没人知道,因此就不会因为这部分信息产生矛盾了。

函数指针

我们只剩下了一件需要说明的事情:函数指针。

想要理解为什么 fn(T) -> UU 协变,考虑下面的函数签名:

fn get_animal() -> Animal;

这个函数声明了返回 Animal,因此这样的函数签名是完全有效的:

毕竟 CatAnimal,因此产生 Cat 也是产生 Animal 的有效方式。或者回到实际的 Rust,如果我们想要返回一个 'short 的内容,返回 'long 自然也是可行的。我们完全可以忘记实际的长短,对其一视同仁。

然而,这种魔法对函数参数无效。考虑以下代码:

fn handle_animal(Animal);

如果用子类型替换:

前者可以接收 Dog,但后者就只能接收 Cat 了。但如果我们把它反过来,就完全没有问题了。如果我们希望一个函数能接收 Cat 类型的参数,那一个能接收 Animal 类型的函数自然也可以处理这个参数。回到 Rust,如果我们需要一个能处理至少为 'long 的函数,则一个能处理 'short 的函数也符合要求。

这也就是函数参数和语言中其他部分不同,是逆变的原因。

自定义类型的变型

到目前位置,标准库中定义的类型基本已经解释清楚了,那自定义类型又该如何确定变型呢?非正式地讲,一个 struct 从它的 field 中继承变型。如果 struct MyType 有着 a: xxxA,则 MyTypeA 的变型即为 aA 的变型。

而当 A 被用于多个 field 时:

  • 如果所有对 A 的使用都是协变的,则 MyTypeA 也是协变
  • 如果所有对 A 的使用都是逆变的,则 MyTypeA 也是逆变
  • 否则,MyTypeA不变

use std::cell::Cell;

struct MyType<'a, 'b, A: 'a, B: 'b, C, D, E, F, G, H, In, Out, Mixed> {

a: &'a A, // 对 'a 和 A 协变

b: &'b mut B, // 对 'b 协变,对 B 不变

c: *const C, // 对 C 协变

d: *mut D, // 对 D 比变

e: E, // 对 E 协变

f: Vec<F>, // 对 F 协变

g: Cell<G>, // 对 G 不变

h1: H, // 对 H 本应协变,然而……

h2: Cell<H>, // 对 H 不变

i: fn(In) -> Out, // 对 In 逆变,对 Out 协变

k1: fn(Mixed) -> usize, // 对 Mixed 本应逆变,然而……

k2: Mixed, // 对 Mixed 不变

}