c#中的引用类型和值类型

一,c#中的值类型和引用类型面试

     众所周知在c#中有两种基本类型,它们分别是值类型和引用类型;而每种类型均可以细分为以下类型:编程

    

  1.  什么是值类型和引用类型
    • 什么是值类型:
      • 进一步研究文档,你会发现全部的结构都是抽象类型System.ValueType的直接派生类,而System.ValueType自己又是直接从System.Object派生的。根据定义所知,全部的值类型都必须从System.ValueType派生,全部的枚举都从System.Enum抽象类派生,然后者又从System.ValueType派生。  
      •  全部的值类型都是隐式密封的(sealed),目的是防止其余任何类型从值类型进行派生。       
    • 什么是引用类型:
      • 在c#中全部的类都是引用类型,包括接口。
  2.  区别和性能
    • 区别:
      • 值类型一般被人们称为轻量级的类型,由于在大多数状况下,值类型的的实例都分配在线程栈中,所以它不受垃圾回收的控制,缓解了托管堆中的压力,减小了应用程序的垃圾回收的次数,提升性能。
      • 全部的引用类型的实例都分配在托管堆上,c#中new操做符会返回一个内存地址指向当前的对象。因此当你在建立个一个引用类型实例的时候,你必需要考虑如下问题:
        • 内存是在托管堆上分配的
        • 在分配每个对象时都会包含一些额外的成员(类型对象指针,同步块索引),这些成员必须初始化
        • 对象中的其余字节老是设为零
        • 在分配对象时,可能会进行一次垃圾回收操做(若是托管堆上的内存不够分配一次对象时)
    • 性能:
      • 在设计一个应用程序时,若是都是应用类型,那么应用程序的性能将显著降低,由于这会加大托管堆的压力,增长垃圾回收的次数。
      • 虽然值类型是一个轻量级的类型,可是若是大量的使用值类型的话,也会有损应用程序的性能(例以下面要讲的装箱和拆箱操做,传递实例较大的值类型,或者返回较大的值类型实例)。
      • 因为值类型实例的值是本身自己,而引用类型的实例的值是一个引用,因此若是将一个值类型的变量赋值给另外一个值类型的变量,会执行一次逐字段的复制,将引用类型的变量赋值给另外一个引用类型的变量时,只须要复制内存地址,因此在对大对象进行赋值时要避免使用值类型。例以下面的代码
         1  class SomRef
         2     {
         3         public int x;
         4     }
         5     struct SomeVal {
         6         public int x;
         7     }
         8     class Program {
         9         static void ValueTypeDemo() {
        10             SomRef r1 = new SomRef();//在堆上分配
        11             SomeVal v1 = new SomeVal();//在栈上分配
        12             r1.x = 5;//提领指针
        13             v1.x = 5;//在栈上修改
        14             SomRef r2 = r1;//只复制引用(指针)
        15             SomeVal v2 = v1;//在栈上分配并复制成员
        16         }
        17     }

  3. 常见误区
    • 引用类型分配在托管堆上,值类型分配在线程栈上:其实这种说法的前半部分是对的,后半部分是错的。由于变量的值在它声明的位置存储的,因此假如某一个引用类型中有一个值类型的变量, 那么该变量的值老是和该引用类型的对象的其它数据在一块儿,也就是分配在堆上。(只有局部变量(方法内部声明的变量)和方法的参数在栈上)
    • 结构是轻量级的类:这种错误的信息主要是由于有人认为值类型不该该有方法或者其它有意义的行为-它们应该做为简单的数据转移来使用,因此不少人分不清DateTime究竟是值类型仍是引用类型。
    • 对象在c#中默认的是用过引用传递的:其实在调用方法的时候,参数值(对象的一个引用)是以传值得方式传递的,若是你想以引用方式传递的话,可使用ref或者out关键字。

二,值类型的装箱和拆箱操做c#

1 int i = 5;
2 object o = i;
3 int j = (int)o;
4 Int16 y=(Int16)o;

 

  1.  什么是装箱,什么是拆箱
    • 什么是装箱:所谓装箱就是将值类型转化为引用类型的过程(例如上面代码的第2行),在装箱时,你须要知道编译器内部都干了什么事:
      • 在托管堆中分配好内存,分配的内存量是值类型的各个字段须要的内存量加上托管堆上因此对象的两个额外成员(类型对象指针,同步块索引)须要的内存量
      • 值类型的字段复制到新分配的堆内存中
      • 返回对象的地址,这个地址就是这个对象的引用
    • 什么是装箱:将已装箱的值类型实例(此时它已是引用类型了)转化成值类型的过程(例如上面代码的第3行),注意:拆箱不是直接将装箱过程倒过来,拆箱的代价比装箱要低的多,拆箱其实就是获取一个指针的过程。一个已装箱的实例在拆箱时,编译器在内部都干了下面这些事:
      • 若是包含了“对已装箱类型的实例引用”的变量为null时,会抛出一个NullReferenceException异常。
      • 若是引用指向的对象不是所期待的值类型的一个已装箱实例,会抛出一个InvalidCastException异常(例如上面代码的第4行)。  
  1.  它们在什么状况下发生,以及如何避免
  2. 1    static void Main(string[] args)
    2         {
    3             int v = 5;
    4             object o = v;
    5             v = 123;
    6             Console.WriteLine(v+","+(int)o);
    7        }

          经过上面的分析咱们已经知道了,装箱和拆箱/复制操做会对应用程序的速度和内存消耗产生不利的影响(例如消耗内存,增长垃圾回收次数,复制操做),因此咱们应该注意编译器在何时会生成代码来自动这些操做,并尝试手写这些代码,尽可能避免自动生成代码的状况。编程语言

    • 你能一眼从上面的代码中看出进行了几回装箱操做吗?正取答案是3次。分别进行了哪三次呢,咱们来看一下:第一次object o=v;第二次在执行 Console.WriteLine(v+","+(int)o);时将v进行装箱,而后对o进行拆箱后又装箱。也就是说装箱过程老是在咱们不经意的时候进行的,因此只有咱们充分了解了装箱的内部机制,才能有效的避免装箱操做,从而提升应用程序的性能。因此对上面的代码进行以下修改能够减小装箱次数,从而提升性能:性能

      1  static void Main(string[] args)
      2         {
      3             int v = 5;
      4             object o = v;
      5             v = 123;
      6             Console.WriteLine(v.ToString() + "," + ((int)o).ToString());//((int)o).ToString()代码自己没有任何意义,只为演示装箱和拆箱操做
      7        }
    • 下面来讨论一下编译器都会在何时自动生成代码来完成这些操做
      • 使用非泛型集合时:好比ArrayList,由于这些集合须要的对象都是object,若是你将一个值类型的对象添加到集合中时会执行一次装箱操做,当你取值时会执行一次拆箱操做,因此在应用程序中应避免使用这种非泛型的集合。
      • 你们都知道System.Object是全部类型的基类,当你调用object类型的非虚方法时会进行装箱操做(例如GetType方法)。在调用object的虚方法时,若是你的值类型没有重写虚方法也要进行装箱操做,因此在定义本身的值类型时,应重写object内部的虚方法(例如ToString方式)
      • 将值类型转化为接口类型时也会进行装箱操做,这是由于接口类型必须包含对堆上的一个对象的引用。

三,泛型的出现(本节只简单介绍泛型对装箱和拆箱所起的做用,关于泛型的具体细节请参考下一篇文章)spa

    •  什么泛型
      • 泛型是CLR和编程语言提供的一种特殊机制,它在c#2中才被提供出来。
    •  它对避免装箱有什么做用?
      • 在使用泛型时须要指定要装配的类型,这样能够减小装箱操做,好比下面的代码
         1   static void Main(string[] args)
         2         {
         3             ArrayList dateList = new ArrayList { 
         4             DateTime.Now
         5             };
         6 
         7             IList<DateTime> dateT = new List<DateTime> { 
         8             DateTime.Now
         9             };
        10         }

        使用ArrayList时,每添加一个时间都会进行一次装箱操做,而使用List<DateTime>时就不会进行装箱操做,从而提升应用程序的性能。线程

    •  C#中常见的泛型集合:

      Queue<T>;设计

      Stack<T>;3d

      List<T>;指针

      Dictionary<Tkey,Tvalue>;

      HashSet<T>;

       

       在使用这些集合以前咱们必需要理解每一种集合的工做原理(没事本身能够实现一下),了解每一种集合的适合场合,这样才能写出高效的代码。

四,在设计时如何选择类和结构体

在面试的时候,咱们常常被问的一个问题(还有另一个问题,如何选择抽象类和接口,下次我会单独聊聊这个问题),下面咱们来聊聊在设计时应该如何选择结构体和类

    •  什么是结构体
      • 结构体是一种特殊的值类型,因此它拥有值类型因此的特权(实例通常分配在线程栈上)和限制(不能被派生,因此没有 abstract 和 sealed,未装箱的实例不能进行线程同步的访问)。
    •  什么状况下选择结构体,什么状况下选择类
      • 在大多数的状况下,都应该选择类,除非知足如下状况,才考虑选择结构体:
      • 类型具备基元类型的行为
      • 类型不须要从其它任何类型继承
      • 类型也不会派生出任何其它类型
      • 类型的实例较小(约为16字节或者更小)
      • 类型的实例较大,可是不做为方法的参数传递,也不做为方法的返回值。

都说程序是一门注重实践的学科,可是也只有熟悉理解了这些概论的东西,才能在实践时写出优秀的代码,有不对或者不合理的地方欢迎在下面讨论;

相关文章
相关标签/搜索