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

    1引言

            对值类型和引用类型理解,是理解C#语言基础的重要主题之一。这里会介绍一下内容:

      • 1)值类型和引用类型的内存分配情况
      • 2)什么时候用值类型,什么时候用引用类型。
      • 3)装箱与拆箱

     1.1值类型与引用类型内存分配

          值类型:值类型实例要么分配在栈上,要么分配在堆上(此时以类型的字段存在托管堆上)。值类型包括简单类型、结构等。定义值类型用关键字Struct。(由于值类型均是密封的,意味着所有定义的值类型均继承字ValueType,且不能派生,但是可以实现接口),如果值类型实例分配在栈上,那么方法调用退出后则销毁;若位于GC堆上,则随其所在对象消亡而消亡,主要由GC控制。由于值类型本身包含值,通常分配在栈上,访问无需地址转换,分配空间自动释放,所以值类型的效率更高。

         引用类型:引用类型实例分配在托管堆上,而引用类型变量仅仅是持有对象在托管堆上的引用,等于是建立了实例和变量之间的关系。定义引用类型通常用关键字calss,interface,delegate.引用类型实例由GC控制回收。

         如下:在Main方法中定义了值类型变量a,引用类型变量stu   

        class Program
        {
            static void Main(string[] args)
            {
                Pointer a;
                a.X = 100;
                a.Y = 200;
                Console.WriteLine("鼠标当前位置为{0},{1}",a.X,a.Y);
    
                Student stu=new Student("Josh","Smith",100,Sex.Shemale);
                stu.Learn();
            }
        }
    
        /// <summary>
        /// 定义一个代表鼠标位置的pointer类
        /// </summary>
        public struct Pointer
        {
            /// <summary>
            /// 水平位置
            /// </summary>
            public  int X ;
    
            /// <summary>
            /// 垂直位置
            /// </summary>
            public int Y ;
        }
    
        /// <summary>
        /// 学生
        /// </summary>
        public class Student
        {
            //名称
            private string _lastName;
    
            //
            private string _firtName;
    
            //名子
            public string Name
            {
                get { return _firtName+ _lastName; }
            }
    
            private Sex _sex=Sex.UnKnow;
    
            /// <summary>
            /// 性别
            /// </summary>
            public Sex Sex
            {
                get { return _sex; }
            }
    
    
            /// <summary>
            /// 年龄
            /// </summary>
            public int Age { get; set; }
    
    
            /// <summary>
            /// 学习
            /// </summary>
            public void Learn()
            {
                Console.WriteLine("{0}学生学习",Name);
            }
    
            /// <summary>
            /// 构造函数
            /// </summary>
            /// <param name="argFirstName"></param>
            /// <param name="argLastName">名称</param>
            /// <param name="argAge">年龄</param>
            /// <param name="argSex">性别</param>
            public Student(string argFirstName, string argLastName, int argAge, Sex argSex)
            {
                _firtName= argFirstName;
                _lastName= argLastName;
                _sex = argSex;
                Age = argAge;
            }
        }
    
        /// <summary>
        /// 人的性别
        /// </summary>
        public enum Sex
        {
            /// <summary>
            /// 男性
            /// </summary>
            Man,
            
            /// <summary>
            /// 女性
            /// </summary>
            Women,
            
            /// <summary>
            /// 人妖
            /// </summary>
            Shemale,
       
            /// <summary>
            /// 未知
            /// </summary>
            UnKnow,
        }

    值类型变量a,和引用类型变量stu所引用的实例内存概况如下图

     

      1.2什么时候用值类型,什么时候用引用类型?

               引用类型有行为、有多态、有继承。可以发现只要用到struct的场合,基本都可以用class替代。那么为什么还要有值类型呢?答案显而易见是效率问题。我们知道引用类型分配在堆上,而堆上的每个对象都有额外的一些成员(同步索引块,类型对象指针(指向类型对象的方法表)),这些都要初始化;另外从托管堆上分配一个对象,可能强制执行一次垃圾回收操作。由上可见软件系统的所有类型全是引用类型,那么引用程序的性能会显著下降。而值类型通常分配在栈上,通常为较小的带有数值含义的数据结构,访问的时候不需要像访问对象那样通过引用类型变量查找,使用完也是自动释放,这样效率就会很高。

               通常定义类型的时候应该优先考虑值类型,因为它的效率更高。值类型一般在以下场合使用:

                    1类型本身数据量较小,且主要用于存储数据,表现出明显的数值含义。

                    2类型不需要从其他任何类型继承(接口除外),也不派生出任何其他类型

     相反,选用引用类型的时候会考虑其 1本身数据结构较大;2可能会扩展(即会派生)。目前个人知识有限,貌似也只能想到这两点。

    3装箱与拆箱

                  1概念:装箱与拆箱,就是值类型与引用类型的转换。装箱就是值类型数据转换为引用对象,可以将值类型视为对象来处理,通常转换为Object类型的或者该值类型实现的任何接口引用类型;拆箱就是引用类型转换为值类型,通常伴随着从堆中复制对象的数据字段的操作。注:只有被装过箱的对象才能被拆箱。

                 2先看一段代码

      static void Main(string[] args)
            {
                int x = 100;
                object o = x;//装箱
                int y = (int) o;//拆箱
            }

                装箱过程三步曲:

      • 内存分配:  在托管堆中分配内存空间,内存大小为待装箱值类型的大小加上其他额外的内存空间(主要为类型对象指针和同步索引块)
      • 实例拷贝:将值类型的字段值拷贝到新分配的内存中
      • 引用返回:将托管堆中的对象地址返回给新的引用类型变量 (即例子中的对象o)

               拆箱过程两部曲

      •  实例检查:检查对象是否为null,检查对象是否为给定值类型的装箱值,不满足检查条件抛异常。
      •  取数据字段地址:从实例中获取数据字段的地址(本例中为o中存储100至的地址)
      •  值拷贝:根据获取的数据字段地址将值拷贝到基于栈的值类型的实例中(本例中的y)

       总结:装箱拆箱会带来大量的性能损失,我们平时写程序的时候应尽可能用FCL提供的重载方法或者泛型。如大家经常用的StringBuilder 用来拼接大量的字符串 比用"+"性能要高的多。

     

  • 相关阅读:
    零拷贝
    RxJava2源码解析
    一次博客崩溃日志分析
    Spring循环依赖的解决
    解决网络卡顿问题
    软工第一次作业
    3月26-27号训练笔记
    Codeforces Round #708 (Div. 2)题解A,B,C1,C2,E1,E2
    求出所有LIS的可行起点
    2020小米邀请赛决赛补题G,I,J(三DP)
  • 原文地址:https://www.cnblogs.com/gzrnet/p/3204321.html
Copyright © 2011-2022 走看看