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关键字。

二,值类型的装箱和拆箱操作

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方式)
      • 将值类型转化为接口类型时也会进行装箱操作,这是因为接口类型必须包含对堆上的一个对象的引用。

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

    • 什么泛型

      • 泛型是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>;

      List<T>;

      Dictionary<Tkey,Tvalue>;

      HashSet<T>;

      在使用这些集合之前我们必须要理解每一种集合的工作原理(没事自己可以实现一下),了解每一种集合的适合场合,这样才能写出高效的代码。

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

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

    • 什么是结构体

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

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

时间: 2024-10-05 23:48:07

c#中的引用类型和值类型的相关文章

java中的引用类型和值类型

值类型和引用类型的不同 [定义] 引用类型 表示你操作的数据是同一个,也就是说当你传一个参数给另一个方法时,你在另一个方法中改变这个变量的值,那么调用这个方法时,传入的变量的值也将改变. 值类型 表示复制一个当前变量传给方法,当你在这个方法中改变这个变量的值时,最初声明的变量的值不会变. [值类型] 值类型就是基本数据类型 基本数据类型常被称为四类 八种 整型:byte.short.int.long 浮点型:float.double 字符型:char 逻辑型:boolean [引用类型] 除了四

php中的引用类型和值类型

PHP中的四种简单类型和复杂类型array都是值类型.同类型间赋值传递的是值,即创建一个副本给新变量. 例如: $int1 = 123; $int2 = $int1;//直接传递的是值,只是做了一个叫int1的副本叫int2 $int2 = 456; echo $int1;//输出 123echo $int1 === $int2;//为假 $int1 = 123; $int2 = &$int1;//取地址符,传递的是引用 $int2 = 456; echo $int1;//输出 456 echo

.net中String是引用类型还是值类型 以及 C#深层拷贝浅层拷贝

http://www.cnblogs.com/yank/archive/2011/10/24/2204145.html http://www.cnblogs.com/zwq194/archive/2012/08/06/2625403.html 关于String为值类型还是引用类型的讨论一直没有平息,最近一直在研究性能方面的问题,今天再次将此问题进行一次明确.希望能给大家带来点帮助. 如果有错误请指出. 来看下面例子: //值类型 int a = 1; int b = a; a = 2; Cons

C#中引用类型和值类型的区别,分别有哪些

C#的值类型包括:结构体(数值类型,bool型,用户定义的结构体),枚举,可空类型. C#的引用类型包括:数组,用户定义的类.接口.委托,object,字符串. 数组的元素,不管是引用类型还是值类型,都存储在托管堆上. 引用类型在栈中存储一个引用,其实际的存储位置位于托管堆.为了方便,本文简称引用类型部署在托管推上. 值类型总是分配在它声明的地方:作为字段时,跟随其所属的变量(实例)存储:作为局部变量时,存储在栈上. 值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体:引

【转】c#引用类型与值类型的区别大盘点

解析:CLR支持两种类型:值类型和引用类型.用Jeffrey Richter(<CLR via C#>作者)的话来说,“不理解引用类型和值类型区别的程序员将会把代码引入诡异的陷阱和诸多性能问题”.这就要求我们正确理解和使用值类型和引用类型. 值类型包括C#的基本类型(用关键字int.char.float等来声明),结构(用struct关键字声明的类型),枚举(用enum关键字声明的类型):而引用类型包括类(用class关键字声明的类型)和委托(用delegate关键字声明的特殊类).C#中的每

C#引用类型和值类型的区别

1***.C#中有两种类型: 值类型和引用类型: 值类型的变量直接包含他们的数据,而引用类型的变量存储 引用类型存储对他们的数据的引用,后者称为对象: 简单说:值类型直接存储其值,引用类型存储对值得引用.引用类型分为引用和引用的对象. 2***. 值类型:简单类型.枚举.结构 引用类型: 类类型.接口类型.数组类型和委托类型 3***.值类型与引用类型的内存存储 单纯的说值类型存储在栈上,引用类型存储在托管堆上是不对的. 4***.区别: 1. 托管堆: 同步块和方法表, x,y,托管堆上的需要

12-12面向对象--引用类型与值类型、命名空间与类库

五.引用类型与值类型 .NET将变量的类型分为值类型与引用类型.例如int和float之类的变量属于值类型,而“类“类型的变量属于引用类型. 值类型与引用类型的变量在使用上是有区别的. 值类型的变量在一定义之后就可以马上使用. 引用类型的变量定义之后,还必须用new关键字创建对象后才可以使用. 如上图,创建两个新的对象时,直接比较两个对象是否相等,返回的bool值为false.因为两个对象都是新开辟的一个空间变量,彼此没有任何关系,所有是不相等的. 如图2:1)创建的第一个新的对象ss1,给ss

引用类型与值类型在编码上的区别

一.引入类型与值类型简介 值类型:直接存放于栈中,取的时候是直接取得值.值类型继承自System.ValueType.(自定义对象) 引用类型:存在于托管堆中,取的时候取的是该对象的地址,然后用这个地址去托管堆中取值.引用类型继承自System.Object.(int,bool) 二.在代码编写上的区别 在赋值的时候,值类型是复制一份,新的和旧的在以后的操作中互不影响,而引用类型复制的只是地址,在以后的修改中,修改新的旧的也会受到影响. 代码示例: static void Main(string

.NET 基础 一步步 一幕幕[面向对象之堆、栈、引用类型、值类型]

堆.栈.引用类型.值类型 内存分为堆和栈(PS:还有一种是静态存储区域 [内存分为这三种]),值类型的数据存储在栈中,引用类型的数据存储在堆中. 堆.栈: 堆和栈的区别: 栈是编译期间就分配好的内存空间,因此你的代码中必须就栈的大小有明确的定义:局部值类型变量.值类型参数等都在栈内存中. 堆是程序运行期间动态分配的内存空间,你可以根据程序的运行情况确定要分配的堆内存的大小. 引用类型.值类型 1.将一个值类型变量赋给另一个值类型变量时,将复制包含的值.引用类型变量的赋值只复制对对象的引用,而不复