C# 值类型和引用类型再探讨

129 阅读3分钟

1. 大体介绍

微软自从转型.NET Core、.NET统一版后,技术日新月异,现在已经去到C# 11.NET 7了。所谓温故知新,做了个控制台探讨一下值类型和引用类型的区别,还有string字符串为何是特殊的引用类型。

先放微软官方对于C#类型系统的描述链接

下图展示了值类型和引用类型分别包括的数据类型:image.png


2. 先基于NET 7做个小控制台,测试赋值问题

Console.WriteLine("值类型:int");
int i1 = 1111;
int i2 = i1;
Console.WriteLine($"i1:{i1},i2:{i2}");
i1 = 3333;
Console.WriteLine($"i1:{i1},i2:{i2}");
Console.WriteLine();

Console.WriteLine("引用类型:string");
string s1 = "aaaa";
string s2 = s1;
Console.WriteLine($"s1:{s1},s2:{s2}");
s1 = "bbbb";
Console.WriteLine($"s1:{s1},s2:{s2}");
Console.WriteLine();


Console.WriteLine("自定义类:class");
CustomClass c1 = new CustomClass(1111);
CustomClass c2 = c1;
Console.WriteLine($"c1:{c1.Value},c2:{c2.Value}");
c1.Value = 3333;
Console.WriteLine($"c1:{c1.Value},c2:{c2.Value}");
Console.WriteLine();


public class CustomClass
{
    public int Value { get; set; }

    public CustomClass(int value)
    {
        Value = value;
    }
}



输出结果如图:不出所料,i2值没变,c2值变了,问题就出现在s2,说好的string是引用类型,为什么s1变了,s2缺没变?这就涉及到string是特殊的引用类型问题了

image.png

可以先看官方对于字符串类型的解释

引用其中一段:字符串是不可变的,即:字符串对象在创建后,其内容不可更改。 例如,编写此代码时,编译器实际上会创建一个新的字符串对象来保存新的字符序列,且该新对象将赋给 b。 已为 b 分配的内存(当它包含字符串“h”时)可用于垃圾回收。

结论:字符串是不可变的,即属于只读实例,因此当字符串改变后,相当于在内存中分配一个新的对象保存数据,即s1="bbbb"相当于s1=new string("bbbb")。这里就能解析,当进行大量的字符串拼接时,为什么要使用StringBuilder进行拼接了。

3. 再测试集合下,数据类型处理的问题

Console.WriteLine("List<int>:");
var iList = new List<int> { 1 };
Console.WriteLine(iList[0]);
iList.ForEach(i =>
{
    i++;
});
iList[0]++;
Console.WriteLine(iList[0]);
Console.WriteLine();

Console.WriteLine("List<string>:");
var sList = new List<string> { "a" };
Console.WriteLine(sList[0]);
sList.ForEach(i =>
{
    i = i + "b";
});
for (int i = 0; i < sList.Count; i++)
{
    sList[i] = sList[i] + "c";
}
Console.WriteLine(sList[0]);
Console.WriteLine();

Console.WriteLine("List<CustomClass>:");
var cList = new List<CustomClass> { new CustomClass(1) };
Console.WriteLine(cList[0].Value);
cList.ForEach(i =>
{
    i.Value = i.Value + 1;
});
for (int i = 0; i < cList.Count; i++)
{
    cList[i].Value = cList[i].Value + 1;
}
Console.WriteLine(cList[0].Value);
Console.WriteLine();


public class CustomClass
{
    public int Value { get; set; }

    public CustomClass(int value)
    {
        Value = value;
    }
}

输出结果如下图所示,List<int>List<CustomClass>的结果是显然的,值类型的集合在ForEach里面的操作不影响原本的集合,而引用类型则会影响,但string是属于特殊的引用类型,是不受影响的。

image.png

为什么不受影响?那就要看这里关键步骤IList<T>.ForEach()方法是如何处理,先查看微软官方的list.cs的源代码。从源代码可以看到,这里ForEach()操作的是_items[i]变量 image.png

该变量是一个新创建的私有变量,并且在构造函数时,使用CopyTo()完成了创建 image.png

这里又涉及一个问题,CopyTo是一个浅层次拷贝,与第一个例子实现的原理是相同的:
类型栈中新增变量托管堆内存分配新对象
值类型值类型没有分配
引用类型
string
因此sList[0]的结果是"ac"而不是"abc"

image.png