C++和C#的值类型与引用类型
最近在看 《CLR Via C#》,看到这本书讨论值类型和引用类型时,我觉得很有必要做点笔记。
我常常在 C++ 项目和 C# 项目之间切换,这两个语言虽然名字很像,但有很大的不同。我曾用着 C++ 的习惯来写 C# 代码,现在发现我当时对 C# 产生了很多误会。
比如两个语言常见的顺序容器。C++ 中的 vector<T> v;
和 C# 中的 List<T> l;
。往容器里加入一个对象,C++ 会把这个对象完整地复制一份到 vector 里。我原以为 C# 也是这样,但其实并不是。
所以下面的代码在当时让我很困惑。
1 | var m = new MyClass(); |
我当时会有这样的困惑,是因为我完全不了解 C# 的类型系统。
C# 的引用类型和值类型
C# 支持两种类型:引用类型和值类型。引用类型的变量存储对其数据(对象)的引用,而值类型的变量直接包含其数据。 对于引用类型,两种变量可引用同一对象;因此,对一个变量执行的操作会影响另一个变量所引用的对象。 对于值类型,每个变量都具有其自己的数据副本,对一个变量执行的操作不会影响另一个变量。[1]
微软文档给出的定义解释了我开头的困惑。MyClass
是引用类型,它的对象 m
只是储存了对数据的引用。而 List<MyClass>.Add
方法创造了另一个引用,所引用的数据与 m
相同。
简单来说,可以用 C++ 中指针的概念来理解引用。
C++ 应该是没有值类型和引用类型的说法的(或者说不存在与 C# 的引用类型和值类型相对应的概念)。但是 C++ 类型的行为默认是 C# 中值类型的行为。
比如函数传递参数时,C++ 和 C# 的值类型都会把参数完整复制一份。C++ 往往用传递 const 引用的方式来省去复制的开销。而 C# 可以用 ref 关键词来传递值类型的引用。
1 | // C++ 每次调用 Print 都会将 string 复制一份 |
1 | // C# struct MyStringValue 是值类型,每次调用 print 都会将 string 复制一份 |
传递引用来节省复制是 C++ 引用的常见用法。
写到这里我不知道怎么写下去了,所以就不写了。
// 未完待续。。。
C#的可空值类型
本质是 Nullable 类 + 一些语法糖。c++ 也有类似的东西,也就是 std::optional。
C#的装箱与拆箱
这是 C++ 没有的概念。这概念我也刚认识,下面是笔记,可能有错,建议不看。
C# 里所有类型都基于 object 类型。因此将某个值类型转换为 object 这个基类型是行得通的。
但这其中都发生了什么。要知道 object 可是引用类型,object 储存的是什么呢,是值类型变量的引用吗?
1 | var p = new Point(3,4); // 假设 Point 是值类型 |
实际上, object 储存的是值类型变量副本的引用。将值类型转化成引用类型的机制称为装箱。
装箱做了以下的工作,来实现将值类型转化为引用类型:
- 在托管堆中分配足够容纳值类型的内存;
- 将值类型各个字段复制到托管堆申请的内存中;
- 返回对象的地址,也就是引用。
装箱的逆操作是拆箱。比如下面的代码,将引用类型 object 转化为值类型 int:
1 | int v = (int)object; |
拆箱做了下面的工作[2]:
- 检查类型,确保可以进行转换
- 将值从引用的内存中复制到值类型变量
值得一提的是,在《CLR via C#》这本书里对拆箱的定义是获取值类型各个字段的地址,不包括复制的过程。
CLR分两步完成复制。第一步获取已装箱Point对象中的各个Point字段的地址。这个过程称为拆箱(unboxing)。第二步将字段包含的值从堆复制到基于栈的值类型实例中。
两个定义对我来说都是权威的,我不知道该采用哪个。
不过不管是哪个定义,应该要知道将值类型与引用类型的转换是需要复制的,是有较大成本的,应该在编写代码时避免不必要的装箱和拆箱。
参考文献
[1] https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/reference-types
[2]https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/types/boxing-and-unboxing
C++和C#的值类型与引用类型