• C# 类型基础(中)


    前一篇文章中我们讲到了值类型和引用类型的一些区别,那这篇我们将深入的分析一下到底有什么不一样

    先总结一下两者的差别:

    黄金法则:

    1.引用类型总是被分配到托管堆上。

    2.值类型总是分配到它声明的地方:

      a.作为引用类型的成员变量分配到托管堆上

      b.作为方法的局部变量时分配到栈上

     

    这就是栈和托管堆的区别,栈是线程级别的存储,很规整的存储结构,有先进后出的规则。在每一个线程开启的时候系统默认会给线程分配大约1M左右的栈内存。而托管堆的存储是很随意的,我们对引用类型的操作都是在托管堆上的。

    那么线程栈和托管堆是如何工作的呢?我们上代码,先看栈

     public int AddFive(int pValue)
    {
      int result; 
      result = pValue + 5; 
      return result;
    }

     执行示意图如下:

    1.方法AddFive()被压入“栈”

    2.紧接着方法参数pValue被压入“栈”

    3.然后是需要为result变量分配空间,这时被分配到“栈”上。

    4.最后返回结果

    通过将栈指针指向 AddFive()方法曾使用的可用的内存地址,所有在“栈”上的该方法所使用内存都被清空,且程序将自动回到“栈“上最初的方法调用的位置。

     再看托管堆

     
    public class MyInt
    {
        public int MyValue;
    }
    
    public MyInt AddFive(int pValue)
    {
        var result = new MyInt();
        result.MyValue = pValue + 5;
        return result;
    }
    

    MyInt 为引用类型,它被分配在“堆”上,并且由一个位于“栈”上的指针引用,执行完之后只剩下

    一个 MyInt 类被留在“堆”上(“栈”上再也没有指向这个 MyInt 类的指针),这个时候GC就回收处理

    一定要记住我们上边将的黄金法则,以后千万不要一拍脑袋告诉别人值类型是在栈上,引用类型在堆上。作为一名资深的.net开发,这样的话有点幼稚了。 

    前面我们一直说托管堆,什么是托管呢? 我们先看一下.net的CLR执行模型:

    托管代码(Managed Code): 由公共语言运行库时(CLR)执行的代码,而不是由操作系统直接执行。托管代码也可以调用CLR的运行时服务和功能,比如GC、类型检查、安全支持等等。代码编写完毕后进行编译,此时编译器把代码编译成中间语言(IL),而不是能直接在你的电脑上运行的机器码。程序集(Assembly)的文件负责封装中间语言,程序集中包含了描述所创建的方法、类以及属性的所有元数据。

     

    非托管代码(Unmanaged Code):直接编译成目标计算机的机器码,这些代码只能运行在编译出这些代码的计算机上,或者是其他相同处理器或者几乎一样处理器的计算机上。非托管代码不能享受公共语言运行库所提供的一些服务,例如内存管理、安全管理等。 如果非托管代码需要进行内存管理等服务,就必须显式地调用操作系统的接口,通常非托管代码调用Windows SDK所提供的API来实现内存管理。 非托管程序也可以通过调用COM接口来获取操作系统服务。

    各个值类型及其基类:

    结构体:struct(直接派生于System.ValueType)

    数值类型:

    整型:

    short(System.Int16),ushort(System.UInt16)

    int(System.Int32),uint(System.UInt32)

    long(System.Int64),ulong(System.UInt64)

    sbyte(System.SByte的别名),byte(System.Byte)

    字符型:char(System.Char);

    浮点型:float(System.Single),double(System.Double)

    用于财务计算的高精度decimal型:decimal(System.Decimal)

    bool型:bool(System.Boolean的别名)

    枚举:enum(派生于System.Enum)

    可空类型(派生于System.Nullable泛型结构体,语法 T? 是 System.Nullable<T> 的简

    写,此处的 T 为值类型。)

    值类型不能为赋值为null,而可空类型是值类型为什么可以给null值?

    按照惯例,这个时候应该说一句:小二,上代码!

    System.Nullable<int> number1 = null;
     
    System.Nullable<int> number2 = new System.Nullable<int>();
     
    System.Nullable<int> number3 = 23;
     
    System.Nullable<int> number4 = new System.Nullable<int>(88);  

    这是我们对Nullable类型的赋值操作,我们来看一下生成的IL代码是什么样的:

    这下子就清楚了,实质上是编译器提供了这样的支持,从生成的中间代码可以看出:可空类型的赋值直接等效于构造实例。赋null时其实就是调用空构造函数,有值时就就把值传入带参数的构造函数。所以可空类型的null值和引用类型的null是不一样的。(可空类型的并不是引用类型的null,而是用结构的另一种表示方式来表示null)

    各个引用类型及其基类:

    数组:(派生于System.Array)数组的元素,不管是引用类型还是值类型,都存储在托管堆上

    类:class(派生于System.Object)

    接口:interface(接口不是一个“实际的类型”,所以不存在派生于何处的问题。)

    委托:delegate(派生于System.Delegate)

    object:(System.Object的别名)

    字符串:string(System.String的别名)

     问题1:Int[]是引用类型还是值类型 ?  (根据前面讲的内容,你觉得呢?)

     问题2:所有的值类型都存储在栈上吗?根据前面讲的内容,你觉得呢?)

    总结一下:

    特殊类型 dynamic

    dynamic是C#4.0 引入的新类型,声明为dynamic的类型与”静态类型”(编译时确定的类型,例如int,double类型)相比最大的特定它是”动态类型”,它会在运行时尝试调用方法,这些方法的存在与否不是编译时期检查的,而是在运行时查找,如果方法存在并且参数正确,会正确调用,否则会抛出异常.

    dynamic类型只在编译时存在,运行时不存在,会转换为object

    dynamic和var的区别

    用var声明的局部变量只是一种简化语法,它要求编译器根据表达式推断数据的实际类型

    var只能用于方法内部的局部变量,而dynamic可以用于方法内部的局部变量、字段和参数

    表达式不能转型为var,但是可以转型为dynamic

    必须显示初始化var声明的变量,但dynamic声明的变量无需初始化

     

    关于dynamic还可以讲很多东西的,后面我们会专门讲解这个特殊类型

    问题来了:只要继承自System.ValueType的都是值类型吗?

    值类型都是System.ValueType的后代,但System.ValueType的后代不全是值类型,System.Enum就是唯一的特例,在System.ValueType的所有后代中,除了System.Enum之外其它都是值类型。

    Enum的源码定义如下:

    public abstract class Enum : ValueType, IComparable, IFormattable, IConvertible

     我们可以看到它是抽象类,根据前面讲的class属于引用类型,所以他是引用类型。所以枚举值到底是值类型还是引用类型呢,各位看官请看:

    枚举的特殊性:

    1. 所有枚举类型(enum type)都是值类型。

    2. System.Enum和System.ValueType本身是引用类型。

    3. 枚举类型(enum type)都是隐式的直接继承自System.Enum,并且这种继承关系只能由   编译器自动展开。但System.Enum本身不是枚举类型(enum type)。

    4. System.Enum是一个特例,它直接继承自System.ValueType(参见Code #03),但本身却是一个引用类型。

    下面两位仁兄讲的很清楚,各位可以参考一下:

    http://www.cnblogs.com/cdts_change/archive/2009/09/20/1570414.html

    http://www.cnblogs.com/yank/archive/2009/02/27/1399423.html

    讲了这么多引用类型和值类型的东西,我们来系统的总结一下:

    值类型和引用类型的区别

    a.所有继承System.Value的类型都是值类型(Enum特殊),其他类型都是引用类型

    b.引用类型可以派生出新的类型,而值类型不能

    c.引用类型存储在堆中,而值类型既可以存储在堆中也可以存储在栈中

    d.引用类型可以包含null值,值类型不能(可空类型功能允许将 null 赋给值类型)

    e.引用类型变量的赋值只复制对对象的引用,而不复制对象本身。而将一个值类型变量赋给另一个值类型变量时,将复制包含的值

    f.当比较两个值类型时,进行的是内容比较;而比较两个引用类型时,进行的是引用比较

    g.值类型在内存管理方面具有更好的效率,并且不支持多态,适合用作存储数据的载体;引用类型支持多态,适合用于定义应用程序的行为

    最后再来一句:小二,上代码!

     public void ValueTypeDemo()
            {
                RefObj ref1 = new RefObj(); // 在堆上分配
                ValObj val1 = new ValObj(); //在栈上分配
    
                ref1.Count = 5;  //赋值并返回指针
                val1.Count = 5;  //在栈上直接修改值
    
                Console.WriteLine(ref1.Count); //都显示5
                Console.WriteLine(val1.Count);
    
                RefObj ref2 = ref1; // 只复制引用
                ValObj val2 = val1; //在栈上分配并复制成员
                ref1.Count = 8; // ref1 和 ref2 都会改
                val1.Count = 9; // ref1.Count 会改,但是ref2.Count 不会
    
                Console.WriteLine(ref1.Count); // 显示 8 
                Console.WriteLine(ref2.Count); // 显示 8
                Console.WriteLine(val1.Count); // 显示 9
                Console.WriteLine(val2.Count); // 显示 5
            }
    
     public class RefObj
        {
            public int Count { get; set; }
        }
    
        public struct ValObj
        {
            public int Count { get; set; }
        }
    

    转载的童鞋请说明出处,谢谢。

  • 相关阅读:
    php安全模式笔记
    ./configure,make,make install的作用(转)
    composer自动载入类库的方式
    Specified key was too long; max key length is 1000 bytes
    海量数据中找出前k大数(topk问题)
    斐波那契数列n项的值。(递归和非递归算法Golang实现)
    基于Docker和Golang搭建Web服务器
    Nginx简单介绍以及linux下使用Nginx进行负载均衡的搭建
    php实现商城秒杀
    一致性hash (PHP)
  • 原文地址:https://www.cnblogs.com/Wolfmanlq/p/6937169.html
Copyright © 2020-2023  润新知