C# 学习笔记(8)—— 深入理解类型

2023-10-20 18:49:27 浏览数 (1)

C# 中的类型——值类型和引用类型

C# 中的类型可以分为两种——值类型和引用类型,本文详细分析两种类型,并讨论它们之间的类型转换方法

什么是值类型和引用类型

值类型主要包括简单类型、枚举类型和结构体类型等。值类型的实例通常被分配在线程的堆栈上,变量保存的内容就是实例数据本事。引用类型的实例则被分配在托管堆上,变量保存的是实例数据的内存地址。引用类型主要包括类类型、接口类型、委托类型和字符串类型等

这里很多人搞不清楚什么是堆栈和托管堆,它们和内存有什么联系

其实很简单,内存有两种存储数据的结构,一种是堆栈(Stack),另一种是(堆)。堆是先进先出,栈是先进后出。根据这两种存储不同的特性,值类型数据放在栈上,引用类型数据放在堆上。(先进后出意味着内存寻址是高位到地位,根据编译前预设字节大小去挪动)

  • 值类型
    • 简单类型
      • 有符号整型:int、long、short 和 sbyte
      • 无符号整型:unit、ulong、ushort 和 byte
      • 字符类型:char
      • 浮点型:float、double 和高精度小数类型 decimal
      • 布尔类型:bool
    • 枚举类型:enum
    • 结构体类型:struct
  • 引用类型
    • 类类型
      • 字符串类型:string
      • 类类型:Console 类和自己自定义类类型
    • 数组类型:int[] 与 int[,]
    • 接口类型:interface
    • 委托类型:delegate

回收

值类型的内存不受 GC(垃圾回收器)控制,作用域结束时,值类型会被操作系统自行释放,从而减少了托管堆的压力;而引用类型的内存管理则有 GC 完成

装箱和拆箱

既然 C# 中存在这两种类型,自然需要对它们进行转换。类型转换指的是将一种数据类型转换成另一种数据类型的过程。例如将 “1235” 转换成整数类型的 12345。但并不是所有类型之间都可以进行转换(例如不能把DateTime对象转换为int类型),类型之间不能完成的转换会导致编译错误火运行时错误

类型转换的方式有以下几种:

  • 隐式类型转换。由低级别类型向高级别类型的转换过程。例如,派生类可以隐式地转换为它的父类,装箱过程就属于这种隐式类型转换
  • 显示类型转换。也叫强制类型转换。但是这种转换可能导致精度丢失或者出现运行时异常
  • 通过 is 和 as 运算符进行安全类型转换
  • 通过 .Net 类库中的 Convert 类完成类型转换

下面主要介绍值类型和引用类型之间的一种转换——装箱和拆箱。装箱指的是将值类型转换为引用类型的过程,而拆箱指的是将引用类型转换为值类型

代码语言:javascript复制
class Program
{
    static void Main(string[] args)
    {
        int i = 3;
        // 装箱
        object o = i;
        // 拆箱
        int y = (int)o;
    }
}

以上内容分别执行了一次装箱和拆箱操作

装箱步骤:

  1. 内存分配:在托管堆中分配好内存空间以及存放赋值的实际数据
  2. 完成实际数据的复制:将值类型实例的实际数据复制到新分配的内存中
  3. 地址返回:将托管堆中的对象地址返回给引用类型变量

拆箱步骤:

  1. 检查实例:首先检查要进行拆箱操作的引用类型变量是否为 null,如果为 null 则抛出NullReferenceException异常;如果不为null则继续检查变量是否和拆箱后的类型时同一类型,若结果为否,会导致InvalidCastException异常
  2. 地址返回:返回已装箱变量的实际数据部分的地址
  3. 数据复制:将托管堆中的实际数据复制到栈中

理解了装箱和拆箱,我们就知道转换类型实际上对系统会产生性能影响,还有可能产生异常错误,我们在辨析代码的时候,应尽量避免装箱和拆箱操作,最好用泛型来编程

参数传递问题剖析

在默认情况下,C# 方法中的参数传递都是按值进行的,但实际上参数传递的方式共有4种不同的情况,分别为:

  • 值类型参数的按值传递
  • 引用类型参数的按值传递
  • 值类型参数的按引用传递
  • 引用类型参数的按引用传递

值类型参数的按值传递

参数分为形参和实参两类。形参指的是被调用方法中的参数,也就是说方法定义中的参数为形参;实参指的是调用方法时,传递给对应参数的值

代码语言:javascript复制
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);
    }
}

值类型的按值传递,传递的是该值类型实例的一个副本,所以,方法是中对参数的修改是不会影响到实参的值的

引用类型参数的按值传递

当传递的参数是引用类型时,传递和操作的目标时指向对象的地址,而传递的实际内容是对地址的复制。由于地址指向的是实参的值,当方法对地址进行操作时,实际上操作了地址所指向的值,所以调用方法后原来实参的值会被修改

代码语言:javascript复制
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 引用类型参数按值传递的特殊情况

虽然 string 类型也是引用类型,然而在按值传递是,传递的实参却不会因方法中形参的改变而被修改

代码语言:javascript复制
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 关键字来告诉编译器,方法传递的是参数地址,而非参数本身

代码语言:javascript复制
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,即使事后发现问题,也很难注意到是值类型值传递修改的问题

0 人点赞