C# 中的类型可以分为两种——值类型和引用类型,本文详细分析两种类型,并讨论它们之间的类型转换方法
值类型主要包括简单类型、枚举类型和结构体类型等。值类型的实例通常被分配在线程的堆栈上,变量保存的内容就是实例数据本事。引用类型的实例则被分配在托管堆上,变量保存的是实例数据的内存地址。引用类型主要包括类类型、接口类型、委托类型和字符串类型等
这里很多人搞不清楚什么是堆栈和托管堆,它们和内存有什么联系
其实很简单,内存有两种存储数据的结构,一种是堆栈(Stack),另一种是(堆)。堆是先进先出,栈是先进后出。根据这两种存储不同的特性,值类型数据放在栈上,引用类型数据放在堆上。(先进后出意味着内存寻址是高位到地位,根据编译前预设字节大小去挪动)
值类型的内存不受 GC(垃圾回收器)控制,作用域结束时,值类型会被操作系统自行释放,从而减少了托管堆的压力;而引用类型的内存管理则有 GC 完成
既然 C# 中存在这两种类型,自然需要对它们进行转换。类型转换指的是将一种数据类型转换成另一种数据类型的过程。例如将 “1235” 转换成整数类型的 12345。但并不是所有类型之间都可以进行转换(例如不能把DateTime对象转换为int类型),类型之间不能完成的转换会导致编译错误火运行时错误
类型转换的方式有以下几种:
下面主要介绍值类型和引用类型之间的一种转换——装箱和拆箱。装箱指的是将值类型转换为引用类型的过程,而拆箱指的是将引用类型转换为值类型
class Program
{
static void Main(string[] args)
{
int i = 3;
// 装箱
object o = i;
// 拆箱
int y = (int)o;
}
}以上内容分别执行了一次装箱和拆箱操作
装箱步骤:
拆箱步骤:
NullReferenceException异常;如果不为null则继续检查变量是否和拆箱后的类型时同一类型,若结果为否,会导致InvalidCastException异常理解了装箱和拆箱,我们就知道转换类型实际上对系统会产生性能影响,还有可能产生异常错误,我们在辨析代码的时候,应尽量避免装箱和拆箱操作,最好用泛型来编程
在默认情况下,C# 方法中的参数传递都是按值进行的,但实际上参数传递的方式共有4种不同的情况,分别为:
参数分为形参和实参两类。形参指的是被调用方法中的参数,也就是说方法定义中的参数为形参;实参指的是调用方法时,传递给对应参数的值
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);
}
}值类型的按值传递,传递的是该值类型实例的一个副本,所以,方法是中对参数的修改是不会影响到实参的值的
当传递的参数是引用类型时,传递和操作的目标时指向对象的地址,而传递的实际内容是对地址的复制。由于地址指向的是实参的值,当方法对地址进行操作时,实际上操作了地址所指向的值,所以调用方法后原来实参的值会被修改
public class RefClass
{
public int addNum;
}
class Program
{
static void Main(stirng[] args)
{
RefClass refClass = new RefClass();
refClass.addNum = 1;
// refClasss 是实参,此时此时参数是引用类型
AddRef(refClass);
Console.WriteLine(refClass.addNum); // 2
}
private static void AddRef(RefClass addNumRef)
{
addNumRef.addNum += 1;
}
}虽然 string 类型也是引用类型,然而在按值传递是,传递的实参却不会因方法中形参的改变而被修改
class Program
{
static void Main(string[] args)
{
string str = "old string";
ChangeStr(str);
Console.WriteLine(str); // old string
}
private static void ChangeStr(string oldStr)
{
oldStr = "New string";
}
}按照前面对“引用类型参数按值传递”过程的分析,这里对字符串的修改会导致实参的值发生改变,然而实际的运行结果却并非如此。造成这个原因的是 string 具有不可变性,一个 string 类型被赋值,则它就是不可改变的,即不能通过代码去修改它的值
方法中的oldStr = "New String"代码表面上是对字符串的内容进行了修改,但由于 string 的不可变性,系统会重新分配一块内存空间存放 New String 字符串,然后把分配的内存首地址赋值给 oldStr 变量
不管是值类型还是引用类型,你都可以使用 ref 或 out 关键字来实现参数的按引用传递。并且在按引用进行传递时,方法的定义和调用都必须要显示地使用 ref 和 out 关键字,不可将他们忽略,否则会引起编译错误
在按引用传递时,不管参数时值类型还是引用类型,其本质都是一样的,都是通过 ref 或 out 关键字来告诉编译器,方法传递的是参数地址,而非参数本身
class Program
{
static void Main(string[] args)
{
int num = 1;
string refStr = "Old String";
// 值类型按引用传递
ChangeByValue(ref num);
Console.WriteLine(num); // 10
// 引用类型按引用传递
ChangeByRef(refStr);
Console.WriteLine(refStr); // New string
}
private static void ChangeByValue(ref int numValue)
{
numValue = 10;
}
private static void changeByRef(ref string numRef)
{
numRef = "New string";
}
}本文主要是围绕值类型和引用类型来展开讨论,我们重点要掌握值类型和引用类型不同的内存存储方式,理解装箱和拆箱带来的影响。对于值类型和引用类型,进行参数传递的时候,我们要头脑清晰,一不留神可能没注意值类型传递的问题,导致 Bug,即使事后发现问题,也很难注意到是值类型值传递修改的问题