- A+
1.值类型和引用类型
1.1 什么是值类型和引用类型
- 值类型:包括简单类型,枚举类型,结构体类型等,值类型通常被分配在线程的堆栈上,变量保存的内容就是实例数据本身
- 引用类型:引用类型实例则被分配在托管堆上,变量保存的是实例数据的内存地址,引用类型主要包括类类型、接口类型、委托类型、字符串类型等
1.2 值类型和引用类型的区别
值类型和引用类型最主要的区别是——不同的内存分布
我们之前介绍过,值类型分配在线程的堆栈上,引用类型分配在托管堆上,不同的分配位置导致了不同的管理机制,值类型由操作系统负责管理,引用类型则由垃圾回收机制(GC)负责管理
管理的主要是内存的分配与释放
class Program { static void Main(string[] args) { // valuetype 是值类型 int valuetype = 3; // reftype 是引用类型 string reftype = "abc"; } }
在程序中,每个变量都有其堆栈地址,并且不同的变量,堆栈地址不同,valuetype 和 reftype 在堆栈地址占用了不同的位置,从下图可以看出,无论是值类型还是引用类型,变量本身都是存储在堆栈中,变量只是实例数据的一个引用
值类型的变量和实际数据通常会存储在线程堆栈中,而引用类型则是,变量存储在线程堆栈中,而实际数据存储在托管堆中,此时变量存储的是实际数据的地址,这个地址就像是我们实际生活中的地址,像快递员,想要送包裹给你,也是需要你的地址
注意
:我们对值类型的说法是通常在线程堆栈中,而也有不在堆栈的情况
1.引用类型嵌套值类型
如果类的字段类型是值类型,它作为引用类型的一部分,会被分配到托管堆中,但是局部变量的值类型,则仍会被分配到线程栈中
public class NestedValueTypeInRef { // 这个和引用类型一起分配到托管堆中 private int valueType = 3; public method() { // 方法的局部变量分配到线程栈中 char c = 'c'; } } class Program { static void Main(string[] agrs) { NestedRefTypeInValue reftype = new NestedRefTypeInValue(); } }
2.值类型嵌套定义引用类型
值类型嵌套定义引用类型,堆栈上将保存该引用类型的引用,而实际的数据则将保存在托管堆上
public class TestClass { public int x; public int y; } public struct NestedRefTypeValue { // 注意结构体的字段不能初始化 private TestClass classinValueType; // 注意结构体的构造函数不能无参 public NestedRefTypeValue(TestClass t) { classinValueType.x = 3; classinValueType.y = 5; classinValueType = t; } } class Program { static void Main(string[] args) { NestedRefTypeValue valueType = new NestedRefTypeInValue(new TestClass()) } }
总结:
- 值类型继承自ValueType,ValueType又继承自System.Object;而引用类型则直接继承自System.Object
- 值类型的内存不受GC控制,作用域结束,值类型会被系统自动释放,从而减少了托管堆的压力,而引用类型受到GC控制,所以与引用类型相比,值类型性能方面更占优势
- 值类型是密封的(sealed),你不能把值类型作为其它任何类型的基类,而引用类型一般具有继承性
- 值类型不能为null值,它的默认初始值为数值0,而由于类型在默认情况下为null值
- 由于值类型的变量包含其实际数据,因此在默认情况下,值类型之间的参数传递不会影响变量本身,而引用类型保存的数据的地址,它们作为参数传递,参数会发生变化,从而影响引用类型变量的值
1.3 两大类型转换——装箱与拆箱
由于C#存在这两种类型,自然需要对它们进行转换,类型转换指的是将数据的类型转化为另外一种类型
类型转换的方式:
- 隐式类型转换:由低级类型向高级类型转换的过程。例如:派生类可以隐式的转换为它的父类,装箱的过程就属于指针隐式类型转换
- 显式类型转换:强制类型转换。这种转换可能会导致精度丢失,或出现运行异常
强制类型转换的格式
type就是你想要转换的类型
(type)(变量、或函数)
- 通过
is
和as
运算符可以进行安全的类型转换
if (myObj is MyClass) { // myObj 是 MyClass 类型的实例 } MyClass myObj = someObj as MyClass; if (myObj != null) { // someObj 成功转换为 MyClass 类型 }
- 通过 .NET 类库中的Convert类来完成类型转换
string str = "123"; int num = Convert.ToInt32(str);
string str = "2023-07-19"; DateTime date = Convert.ToDateTime(str);
string str = "Red"; Color color = (Color)Enum.Parse(typeof(Color), str);
1.3.1 装箱和拆箱的原理
class Program { static void Main(string[] args) { int i = 3; // 装箱操作 object o = i; // 拆箱操作 int y = (int) o; } }
装箱(box)可以具体为三个步骤
- 内存分配:在托管堆中分配好内存空间存放复制的实际数据
- 完成数据的复制:将值类型实例的实际数据复制到新分配的内存中
- 地址返回:将托管对象的地址返回给由于类型变量
拆箱(unbox)操作的步骤:
- 检查实例:首先检查要进行拆箱操作的引用类型变量是否为null,如果为null则抛出异常,如果不为null,则继续检查变量是否和拆箱之后的类型是否是同一个类型
- 地址返回:返回已装箱变量的实际数据部分的地址
- 数据复制:将托管堆中的实际数据复制到栈中
注意
:
- 装箱和拆箱堆性能有比较大的影响,如果代码中有大量的装箱和拆箱会消耗很多运行时间
- 装箱和拆箱必然会产生多余的对象,进一步加重了GC的压力
应该避免多次的装箱和拆箱,最好使用泛型来编程
2. 参数传递的问题
C# 中的参数传递,我们可以分为四种情况
- 值类型参数按值传递
- 引用类型的参数按值传递
- 值类型参数按引用传递
- 引用类型的参数按引用传递
2.1 值类型的参数按值传递
参数可以分为实参和形参两种,形参指的是被调用方法中的参数,而实参指的是我们传递过去的参数
class Program { static void Main(string[] args) { int addNum = 1; // addNum 就是实参 Add(addNum); } // addnum就是形参,即被调用方法中的参数 private static void Add(int addnum) { addnum += 1; Console.WriteLine(addnum); } }
值类型按值传递,其实传递的是该值类型实例的一个副本,也就是说,方法对参数的操作,并不会影响实际的参数
class Program { static void Main(string[] args) { int addNum = 1; // addNum 就是实参 Add(addNum); Console.WriteLine("调用方法之后的实际参数的值:"+addNum); Console.Read(); } // addnum就是形参,即被调用方法中的参数 private static void Add(int addnum) { addnum += 1; Console.WriteLine("方法中形参的值:"+addnum); } }
我们可以看到图中,并没有一根线将 addNum 与 addnum 进行绑定,addnum使用只是 addNum 的副本
2.2 引用类型按值传递
当传递的是引用类型的时,传递和操作的目标时指向对象的地址,而传递的实际内容对地址的复制,由于地址指向的是实际参数的值,当方法对地址进行操作的时候,实际上操作的地址所指向的实际值,当调用方法之后,实参也会被修改
public class RefClass { public int addNum; } class Program { static void Main(string[] args) { Console.WriteLine("引用类型按值传递的情况"); RefClass refClass = new RefClass(); refClass.addNum = 1; AddRef(refClass); Console.WriteLine("调用方法之后,实际参数的值:"+refClass.addNum); Console.Read(); } private static void AddRef(RefClass addnumRef) { addNumRef.addNum += 1; Console.WriteLine("方法中的addNum值:"+addNumRef.addNum); } }
2.3 string引用类型参数按值传递的特殊情况
虽然string类型也是引用类型,但是它按值传递,传递的参数斌不会因为方法中的形参改变而修改
这个特殊情况是因为string具有不变性,一旦string类型被赋值之后,则它就是不可改变的,即不能通过代码修改它
2.4 值类型和引用类型的按引用传递
不管是值类型还是引用类型,都可以使用 ref 或 out 关键字来实现参数按引用传递,并且按引用进行传递的时候,方法的定义和调用都必须是显式的使用 ref 或 out 关键字,不可省略
按引用传递时,不管是值类型,还是引用类型,本质都一样是告诉编译器,方法传递的是参数地址,而非参数本身
class Program { static void Main(string[] args) { // num 作为实际参数 int num = 1; // refStr 是引用类型实参 string refStr = "Old string"; // 值类型按引用传递 ChangeByValue(ref num); Console.WriteLine(num); // 引用类型按引用传递 ChangeByRef(ref refStr); Console.WriteLine(refStr); Console.Read(); } private static void ChangeByValue(ref int numValue) { numValue = 10; Console.WriteLine(numValue); } private static void ChangeByRef(ref string numRef) { numRef = "new string"; Console.WriteLine(numRef); } }