Skip to content

值类型和引用类型

值类型

所有的值类型都直接或间接地继承自System.ValueType,值类型的变量包含类型的实例,在赋值、传参、作为返回值的时候,都会直接拷贝其完整的字段 —— 即赋值完整的实例,所以对副本进行的修改不会对原始数据进行改动。

基本上,值类型的内存分配以及操作是直接在栈区实现

常见的值类型数据类型:

  • 结构类型
    • 内置数据类型(也称之为简单类型):
      • 整型
      • 浮点型
      • bool
      • char
    • 值元组
    • struct
  • 枚举类型enum

引用类型

引用类型存储的是数据的地址,然而实际数据存放在堆中,所以当一个引用类型被赋值给其他变量时,会拷贝一份引用(即生成两份别名),所以对副本的修改也会使得原始数据(被引用的对象)被修改。

常见的引用类型:

  • 内置引用类型
    • dynamic
    • object
    • string
  • 声明引用类型使用的关键字
    • class
    • interface
    • delegate
    • record

一点简单例子

C#
Person Tom = new Person("Tom");
var temp = Tom;
temp.Name = "Jack";
Console.WriteLine(Tom.Name);

假设有一个Person类,其中封装了Name成员。在上述代码中 ——

  1. 实例化了一个Tom对象,并且在堆区为它进行了内存的分配
  2. 创建了一个temp引用对象,指向Tom在堆区的内存
  3. 直接修改temp,等价于直接修改Tom在堆区的数据
  4. 所以最后输出Tom.NameJack

IMPORTANT

  • 如果不是对引用对象进行内部成员的修改,是不会操作堆中数据的;
  • 直接对一个引用对象赋值,相当于指向了另一个已实例化对象在托管堆中的数据

Method Modifiers

ref, out, in

其底层定义都是T&,使用指针传递,所以对于大型的值类型(比如超大结构体)来说,使用这些来进行传参,可以提高效率(少了一次复制)

ref

ref关键字有点类似在 C 中使用取地址符使得一个函数直接对实参进行操作 —— 在C#中,即为把方法内部对参数的修改回传给调用者

C#
void Add5(ref int x) => x += 5;

int x = 0;
Add5(ref x);

Console.WriteLine(x);

使用ref修饰参数,使得最后x的值为5 ( 0 + 5 = 5 )

注意!——

  • 声明与调用都要加 ref 关键字。
  • 调用前变量必须已被初始化,否则编译报错。
  • 方法内既能读取也能写回调用者的变量。

底层原理:

  • IL 中的参数类型为 T&(ByRef)。
  • 传入的是变量的地址,JIT 在栈或寄存器上传递这个地址,方法直接操作该内存。

out

常用于Try-Prase等模式,其和ref有点类似,都是作为引用传递。但是必须在函数体内对out修饰的参数进行赋值。

例子

C#
bool TryPrase(string s, out int x)
{
    try
    {
        int.TryParse(s, out x);
        return true;
    }
    catch
    {
        x = 0;
        return false;
    }
}

string s = "31";
if (TryPrase(s, out int x))
    Console.WriteLine($"{s} convert to {x}");

in

in关键字让参数按引用传递且只读(函数体内无法修改)

Released under the MIT License.