zoukankan      html  css  js  c++  java
  • C#值类型与引用类型

         要了解一门编程语言,首先就要了解它的类型。我们知道,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,难度很大,有什么不对的地方还请见谅。

        

       

       

  • 相关阅读:
    ViewData,ViewBag,TempData
    http和https
    Array与ArrayList
    程序员与书和视频
    技术学习的方法研究
    文章发布声明
    面向对象JAVA多态性
    嵌入式开发总结
    CSDN博客代码显示乱码的原因
    将Windows的桌面目录设置到D盘
  • 原文地址:https://www.cnblogs.com/wenjiang/p/2958643.html
Copyright © 2011-2022 走看看