值类型与引用类型:别再只背“栈和堆”了,看这 4 个实际影响

5 阅读5分钟

实话,写代码这么多年,我发现一个挺有意思的事儿:面试的时候,问“值类型和引用类型有什么区别”,大家都能答上来——什么栈啊堆啊,值传递引用传递啊,背得比我都溜。

但一到真写代码,就翻车。

要么改了半天对象发现没改对,要么程序跑得卡得不行还不知道为啥。其实说白了,就是没搞明白这俩玩意儿在实际开发里到底怎么影响咱们的代码。

今天咱不背概念,就聊聊我这些年踩过的坑,给你说说这4个你肯定遇到过(或者迟早会遇到)的实际影响。


1. 赋值:一个是给钱,一个是给钥匙

这个是最基础的,但也是最容易犯迷糊的。

值类型赋值,就是给钱。

你想啊,我给你100块钱,你拿去买东西了,我兜里那100块钱还在不?肯定在啊。咱俩的钱各是各的。

代码里也是这样:

int a = 10;
int b = a;
b = 20;
Console.WriteLine(a); // 还是10

a是10,b是另一个10,你改b,a纹丝不动。各自安好,互不打扰。

引用类型赋值,是给钥匙。

我给你配了把我家门的钥匙,你用钥匙开门进去把电视搬走了,等我一回家——哎我电视呢?

代码里就是这样:

var list1 = new List<int> { 1, 2, 3 };
var list2 = list1;
list2.Add(4);
Console.WriteLine(list1.Count); // 变成4了

你本来以为我只是给list2加了个数,结果list1也被改了。

刚子划重点:碰到类、数组、List这种引用类型,赋值的时候心里得有根弦——这给出去的是钥匙,不是钱。不想被改?要么用new重新造一个,要么用ToList()拷一份出来。


2. 传参:我为啥改不了外面的变量?

这个更坑人。很多人以为“引用类型传进去就能改”,结果一写就懵。

我直接给你看个例子:

void ChangeValue(int x) { x = 100; }
void ChangeList(List<int> list) { list = new List<int>(); }

int num = 10;
ChangeValue(num);
Console.WriteLine(num); // 还是10,没变

var myList = new List<int> { 1, 2, 3 };
ChangeList(myList);
Console.WriteLine(myList.Count); // 还是3,咋也没变??

这时候你就纳闷了:不是说引用类型能改吗?为啥我这list也没变?

其实道理特简单:引用类型传进去的,是钥匙的复印件。

你用复印件去开门,当然能改房子里的东西(比如list.Add没问题)。但你想换一把新钥匙(list = new List<int>()),你换的是复印件,原件还在我手里呢,当然改不了。

刚子划重点:

  • 想改引用类型里面的内容,随便改,没问题。
  • 想改变量本身(比如重新new一个),必须加ref
void ChangeListReal(ref List<int> list) { list = new List<int>(); }
ChangeListReal(ref myList);
// 这下myList真的变了

3. null:谁可以为空,谁不行

你肯定见过这个报错:Nullable object must have a value。这啥意思?

值类型天生不能是null。

你想啊,int就是整数,它怎么可能“没有数”呢?要么是0,要么是1,不存在“没值”这种状态。你想让它能空,得用int?,这叫“可空值类型”。

引用类型天生就能是null。

string可以是nullList可以是null,这也是为啥老报NullReferenceException的原因——你忘了判断它是不是空就直接用了。

不过这里有个小细节:

string? name = null; // 这个没问题,就是提醒你别忘了判空
int? age = null; // 这个也没问题,表示“年龄未知”
int age2 = null; // 这个编译不过,直接报错

划重点:写代码的时候,值类型想表达“没有值”,用int?DateTime?。引用类型别动不动就null,该初始化就初始化,不然线上崩了你都不知道咋回事。


4. 性能:一个能把程序拖垮的细节

这个新手基本不会注意,但老鸟都知道。

简单说:值类型大多在栈上,引用类型在堆上。堆上的东西需要垃圾回收(GC)。

GC一干活,你的代码就得暂停。一次两次没事,但如果你在循环里new一堆引用类型对象,GC频繁触发,程序就一卡一卡的。

我给你举个极端例子:

// 这样写,每次循环都产生垃圾
for (int i = 0; i < 1000000; i++)
{
    object obj = new object(); // 引用类型,堆上分配
}

// 换成值类型,压力小很多
struct MyPoint { public int X; public int Y; }
for (int i = 0; i < 1000000; i++)
{
    MyPoint p = new MyPoint(); // 值类型,栈上分配,不触发GC
}

划重点:

  • 高频创建的小对象,能用struct就用struct
  • 但也别滥用,struct太大(超过16字节)反而不好,而且它是值传递,复制也有成本。
  • 追求性能的时候,心里要有根弦:引用类型多了,GC就忙了,GC一忙,用户就卡了。

最后想说

值类型和引用类型,说大不大,说小不小。面试背概念不难,难的是写代码的时候能自然而然地想到这些区别。

我刚入行那会儿也在这上面栽过跟头,改一个对象改了半天发现改的是副本,排查到半夜。后来慢慢才悟出来:概念不是用来背的,是用来救命的。

如果你觉得这篇文章帮到了你,点个赞,转给身边还在背概念、写代码还迷糊的兄弟。

文章转载自: 码农刚子

原文链接: www.cnblogs.com/shenchuanch…

体验地址: www.jnpfsoft.com/?from=330