控件中国网现已改版,您看到的是老版本网站的镜像,系统正在为您跳转到新网站首页,请稍候.......
中国最专业的商业控件资讯网产品咨询电话:023-67870900 023-67871946
产品咨询EMAIL:SALES@COMPONENTCN.COM

C#----值类型与引用类型

作者:未知 出处:cnblog 2013年03月14日 阅读:

要了解一门编程语言,首先就要了解它的类型。我们知道,C#一共分为两大类型:值类型和引用类型,但值类型并不单纯是我们java中的基本数据类型那么简单,有关于是否使用值类型还是个值得讨论的问题:因为装箱机制。C#的值类型还能够自定义方法,甚至能够实现引用类型的接口类型!这已经超出了我的想象范围了!
 
 先来点基础的东西:
 
一.基本内容
 
     文档是我们学习的好帮手,在C#的文档中,我们必须注意,凡是引用类型的,名字都是"xx类",凡是值类型的,就叫"xx结构"或"xx枚举"。
 
     很多时候,我们的初始化操作的右值是表达式。如果左值是值类型,那么它的值就是表达式的值,但如果是引用类型,则是一个引用,并不是该引用指向的对象(学过java,所以对这现象非常熟悉),所以,在C#中,String.Empty的值不是一个空字符串,而是对空字符串的引用。
 
     声明一个变量,除了它的类型信息外,最重要的就是值的存储地点。变量的值一般是在它声明时的位置存储的,而实例变量的值存储在实例本身存储的地方,引用类型和静态变量则存储在堆中。
 
     也许我们会以为,值类型一定在栈(stack,有些地方叫线程栈)上,引用类型一定在堆(heap,有些地方叫托管堆)上,其实这是错误的,值类型也可以在堆上,像是前面讲过的,变量的值存储在它声明的地方,如果我的值类型是在一个引用类型中声明的,那么该值类型就是在堆上。方法参数一定是在栈(方法是用栈帧存储它的信息)上,但局部变量不一定,它也可以是在堆上(让我们想想匿名方法,如果它捕获了一个外部变量,该变量就会作为隐藏委托类型而保存在堆上)。好了,一个潜在的想法出现了:如果一个引用类型保存在值类型中,像是结构中,会怎么样呢?引用类型的数据依然保存在堆中,但它的引用保存在栈上,因为值类型也只拥有它的引用而已。
 
     值类型不能派生出其他类型,因为它是隐式密封(sealed)的,所以它并没有引用类型实例对象开头的额外信息(用于标识对象的实际类型和其他信息),即类型对象指针和同步块索引。这些额外信息并不能修改,所以我们永远也不能修改对象的类型,但我们可以转换为其他类型。执行强制类型转换时,我们会获取一个引用,该引用会检查它引用的对象本身是否是该目标类型的有效对象,若是,返回该引用并赋给目标类型,否则抛出异常。引用并不清楚它实际引用的对象的类型,所以我们的引用可以指向其他类型。
 
    基础的东西讲完后,就应该讲讲一些不一样的东西(虽然大部分很多人都已经非常熟悉了)。细节不讨论,只是单单从几个话题出发并做一下延伸。
 
话题1:结构是轻量级的类
 
      结构虽然是值类型,但它可以定义属性和方法,类的行为它都具备,加上它是值类型,比起引用类型来说,对内存更加友好。所以,就有一种说法:结构是轻量级的类。这种说法见仁见智,值类型的确是"轻":不需要垃圾回收(不在堆上分配),不会因类型标识而产生开销(没有类型对象指针和同步块索引),而且不需要取值这一步操作(引用类型的字段一般都是私有的,我们只能通过访问器getter取得其值)。但引用类型也有它的好处:在执行传参,赋值,返回值等类似操作时,只需要赋值4或8字节(具体得看CLR),因为我们传递的只是一个引用(我想说指针,但又觉得不合适,从来就没有任何一种说法认为引用就等同于指针,虽然CLR的引用的确是一个地址,但CLR强调,它们是引用),而不是复制所有数据。这点在容器那里非常好用,像是List,如果容量很大,那这个传参动作就太可怕了。
 
    值类型也并不总是对内存友好,因为隐式的装箱机制,会使某些情况像是循环等,使用值类型会造成可怕的负担。C#中的值类型使用起来并不像java的基本数据类型那么简单(java并没有隐式的装箱机制,基本数据类型不可能直接转换为类),除非是以下几种情况,我们才能放心使用值类型:
 
    1.值类型是不可变(immutable)的,即该类型没有提供任何方法来修改它的字段。要做到这点,我们必须将值类型的所有字段都设置为readonly(只读)。这主要针对结构这种封装其他值类型的值类型。
 
   2.类型的实例较小(小于16字节),因为按值传递需要复制字段,但类型实例较大以致不可能作为实参传递,也不可能作为返回值的话,也可以考虑使用值类型。
 
   就算满足上面的条件,我们也必须考虑到值类型的缺点(装箱机制不在列表中,因为这个话题前面已多次提醒了):
 
   1.值类型继承自System.ValueType,该类除了提供与System.Object一样的方法外,还做了一个动作:覆写了Equals()方法和GetHashCode()方法(值类型的比较需要考虑到它的字段,但默认的比较是引用),而默认的实现存在性能问题。我们不能苛求设计者能够考虑到所有情况,所以,大部分情况都要我们自己覆写这两个方法(这个问题不知道是不是从java而来,java也存在这样的问题)。
 
   2.值类型可以有自己的方法,但它不能派生也不能继承(虽然能实现接口),因此它不能含有虚方法,所有方法也是不可覆写的。
 
   3.引用类型的默认值是null,但我们引用一个null的引用类型时会抛出异常:NullReferenceException,但值类型默认值是0,并不会抛出异常。CLR为了弥补这点,提供了可空性(nullability)标识---可空类型(nullable)。
 
   4.值类型之间的相互赋值,会导致字段的复制,但引用类型只是复制引用。
 
  5.值类型并不在堆上分配,所以当它被销毁时,不会通过Finalize方法接到一个通知(这点在有些地方很重要,这时就需要装箱)。
 
  看了以上的讨论,相信对使用值类型是有点怕怕的:自己是否用错了呢?程序员是不需要顾虑那么多的,写代码最主要是能够表达清楚自己的意图,至于性能这方面,是可以在后期进行重构和优化的。
 
话题2:对象在C#中默认是通过引用传递
 
     引用传递(pass by reference)的定义非常复杂,百度百科的解释是这样的:可以将一个变量通过引用传递给函数,这样该函数就可以修改其参数的值,而引用的解释就是某一变量的一个别名,对引用的操作与对变量直接操作完全一样(很多人都说java是按引用传递,其实这种说法是不严谨的,严格意义上是传递引用对象地址值的按值传递)。如果我们以按引用传递的方式传递变量,那么调用的方法可以通过更改其参数值,来改变调用者的变量值。但C#中引用类型变量的值虽然叫引用,但不是对象本身,它更接近于指针。
 
 就算是传参,有些情况也不能修改它的值:
 
class Program
    {
        public static void Main(String[] args)
        {
            String builder = "hello";
            Show(builder);
            Console.WriteLine(builder);
        }
 
        static void Show(String str)
        {
            str = "word";
            Console.WriteLine(str);
        }
    }      
 在Show()方法里,修改的只是builder的一个副本。当然,String虽然是引用类型,但它是不可变的。我们来传一个真正的引用类型:
 
 class Program
    {
        public static void Main(String[] args)
        {
            People people = new People();
            Show(people);
            Console.WriteLine(people.name);
        }
 
        static void Show(People people)
        {
            people.name = "男人";
        }
    }
 
    public class People
    {
        public String name = "人";
    }        
 
这里我们看到,字段name改变了。
 
       很困惑吧?为什么还说C#是按值传递呢?C#中,引用类型作为方法参数确实是按"值"传递,因为引用类型的值是引用,而该引用是一个地址值,相当于指针(只是相当于,并不等于)。真正的引用传递的就是对象本身,因为引用本身就是对象的别名,但C#是不会传递对象本身的。
 
      这个问题非常让人纠结,尤其是CLR采取了引用这个说法,使得我们更加困扰了。
 
       C#的值类型非常奇怪,我们甚至可以用new来声明:
 
int number = new int();
number = 5;      
 
这并没有错,但刚从java中跳出来的我非常惊讶!
 
C#编译器是很聪明的,它知道number是一个值类型,因为它并没有类型对象指针,于是在栈上为它分配内存,然后确保所有字段都初始化为0。这样的动作就算不用new也行:
 
int number;    
 
但是用new,编译器就认为该实例已经初始化了,而上面的情况如果我们为它赋值就会发生错误。所以,声明一个值类型最好就是为它进行初始化,哪怕只是默认值。
 
 关于这方面的讨论,很多时候我都有心无力,毕竟自己这个初学者要想啃下CLR,难度很大,有什么不对的地方还请见谅。
 

热推产品

  • ActiveReport... 强大的.NET报表设计、浏览、打印、转换控件,可以同时用于WindowsForms谀坔攀戀Forms平台下......
  • AnyChart AnyChart使你可以创建出绚丽的交互式的Flash和HTML5的图表和仪表控件。可以用于仪表盘的创......
首页 | 新闻中心 | 产品中心 | 技术文档 | 友情连接 | 关于磐岩 | 技术支持中心 | 联系我们 | 帮助中心 Copyright-2006 ComponentCN.com all rights reserved.重庆磐岩科技有限公司(控件中国网) 版权所有 电话:023 - 67870900 传真:023 - 67870270 产品咨询:sales@componentcn.com 渝ICP备12000264号 法律顾问:元炳律师事务所 重庆市江北区塔坪36号维丰创意绿苑A座28-5 邮编:400020
在线客服
在线客服系统
在线客服
在线客服系统