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,难度很大,有什么不对的地方还请见谅。

        

       

       

  • 相关阅读:
    跃迁方法论 Continuous practice
    EPI online zoom session 面试算法基础知识直播分享
    台州 OJ 2648 小希的迷宫
    洛谷 P1074 靶形数独
    洛谷 P1433 DP 状态压缩
    台州 OJ FatMouse and Cheese 深搜 记忆化搜索
    台州 OJ 2676 Tree of Tree 树状 DP
    台州 OJ 2537 Charlie's Change 多重背包 二进制优化 路径记录
    台州 OJ 2378 Tug of War
    台州 OJ 2850 Key Task BFS
  • 原文地址:https://www.cnblogs.com/wenjiang/p/2958643.html
Copyright © 2011-2022 走看看