大家好,我是刚子。
说实话,写代码这么多年,我发现一个挺有意思的事儿:面试的时候,问"值类型和引用类型有什么区别",大家都能答上来------什么栈啊堆啊,值传递引用传递啊,背得比我都溜。
但一到真写代码,就翻车。
要么改了半天对象发现没改对,要么程序跑得卡得不行还不知道为啥。其实说白了,就是没搞明白这俩玩意儿在实际开发里到底怎么影响咱们的代码。
今天咱不背概念,就聊聊我这些年踩过的坑,给你说说这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可以是null,List可以是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一忙,用户就卡了。
最后,刚子想说
值类型和引用类型,说大不大,说小不小。面试背概念不难,难的是写代码的时候能自然而然地想到这些区别。
我刚入行那会儿也在这上面栽过跟头,改一个对象改了半天发现改的是副本,排查到半夜。后来慢慢才悟出来:概念不是用来背的,是用来救命的。
如果你觉得这篇文章帮到了你,点个赞,转给身边还在背概念、写代码还迷糊的兄弟。
我是刚子,一个还在写代码的.NET老程序员。咱们下回见!