• 设计类型(二):基元类型、引用类型和值类型


    本章要讨论的是.net的各种类型。这章开始,我想摒弃以前的抄书模式,尝试自己阅读后先行总结,然后再写博客。

    基元类型

    所谓基元类型,指的是编译器直接支持的数据类型。基元类型直接映射到Framework类库中存在的类型。下面四行代码可以生成完全相同的IL:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 
     7 namespace Program1
     8 {
     9     class Program
    10     {
    11         static void Main(string[] args)
    12         {
    13             int a = 0;
    14             Int32 b = 0;
    15             int c = new int();
    16             Int32 d = new Int32();
    17         }
    18     }
    19 }

    再看他们的IL代码:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       10 (0xa)
      .maxstack  1
      .locals init (int32 V_0,
               int32 V_1,
               int32 V_2,
               int32 V_3)
      IL_0000:  nop
      IL_0001:  ldc.i4.0
      IL_0002:  stloc.0
      IL_0003:  ldc.i4.0
      IL_0004:  stloc.1
      IL_0005:  ldc.i4.0
      IL_0006:  stloc.2
      IL_0007:  ldc.i4.0
      IL_0008:  stloc.3
      IL_0009:  ret
    } // end of method Program::Main

    由此可知,这四个写法是完全等价的。

    在本书中,坚持使用FCL名称,主要有以下原因:

    1.很多人纠结于使用string还是System.String,其实这两者没有区别。类似的,还有int和Int32:C#的int永远映射到Int32.C#的long固定映射到Int64.

    2.FLC的许多方法都将类型名作为方法名的一部分。

    3.方便些其他面向CLR的代码(代码风格一致)。

     在高精度基元类型隐式转换到低精度基元类型的时候,往往会进行截断处理(区别于向上取整)。

    C#自带checked操作符来在特定的区域控制溢出检查:

           Byte b = 100;
                b = checked((Byte)(b + 200));

    会抛出异常:

    还可以使用checked语句:

    1         static void Main(string[] args)
    2         {
    3             checked {
    4                 Byte b = 100;
    5                 b = (Byte)(b + 200);
    6             }
    7             
    8         }

    结果是一样的。如果使用了checked语句块,还可以将+=应用于Byte:

     1     class Program
     2     {
     3         static void Main(string[] args)
     4         {
     5             checked {
     6                 Byte b = 100;
     7                 b += 200;
     8             }
     9             
    10         }
    11     }

    在日常编程时,给予诸位如下建议:

    1.尽量使用有符号数值类型Int32之类而不是UInt32,这样编译器会检查更多的上溢下溢。此外,类库中的很多方法的返回值都是有符号的,这样子可以减少强制类型转换。以及,无符号数值类型不符合CLS。

    2.如果代码可能发生溢出,请放到checked语句块中。

    3.将允许溢出的代码放到unchecked中。

    4.对于没有使用checked和unchecked的代码,溢出默认会抛出异常,

    引用类型和值类型

    首先,要认清楚四个事实:

    1.内存必须从托管堆中分配;

    2.堆上的每一个对象都有额外成员,这些成员必须初始化;

    3.对象的其他字节总是为零;

    4.从托管堆分配对象时,可能强制执行一次GC。

    因此,使用引用类型而非值类型的时候,性能会下降。在设计自己的类型时,要考虑是否应该定义成值类型而不是引用类型。除非满足以下全部条件,否则不应该声明为值类型:

    1.类型具有基元类型的行为,是不可变类型(没有提供会更改其字段的成员);

    2.不需要从其他任何类型继承;

    3.没有派生类型;

    4.类型实例较小(小于等于16字节);

    5.实例类型较大,但不作为方法传递实参,也不从方法返回。

    列出值类型和引用类型的一些区别:

    1.值类型有两种形式:已装箱和未装箱。引用类型总是处于已装箱;

    2.值类型从ValueType派生

    #region 程序集 mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
    // C:Program Files (x86)Reference AssembliesMicrosoftFramework.NETFrameworkv4.6.1mscorlib.dll
    #endregion
    
    using System.Runtime.InteropServices;
    using System.Security;
    
    namespace System
    {
        //
        // 摘要:
        //     提供值类型的基类。
        [ComVisible(true)]
        public abstract class ValueType
        {
            //
            // 摘要:
            //     初始化 System.ValueType 类的新实例。
            protected ValueType();
    
            //
            // 摘要:
            //     指示此实例与指定对象是否相等。
            //
            // 参数:
            //   obj:
            //     要与当前实例进行比较的对象。
            //
            // 返回结果:
            //     如果 obj 和该实例具有相同的类型并表示相同的值,则为 true;否则为 false。
            [SecuritySafeCritical]
            public override bool Equals(object obj);
            //
            // 摘要:
            //     返回此实例的哈希代码。
            //
            // 返回结果:
            //     一个 32 位有符号整数,它是该实例的哈希代码。
            [SecuritySafeCritical]
            public override int GetHashCode();
            //
            // 摘要:
            //     返回该实例的完全限定类型名。
            //
            // 返回结果:
            //     包含完全限定类型名的 System.String。
            public override string ToString();
        }
    }

    而ValueType继承自System.Object;

    3.不能在值类型中加入虚方法,所有的方法都不能抽象,不可重写;

    4.引用类型包含了堆中对象的地址。引用类型变量在创建的时候默认初始化为NULL,而值类型总是0。null引用类型会抛出异常。值类型可以添加可空标识;

    5.值类型复制是完全拷贝,而引用类型只拷贝地址;

    6.修改引用类型,会导致其引用也受到影响;

    7.因为值类型是没有被装箱的,所以一旦一个实例不再活动,为它分配的存储就会被释放,而不是等待GC。

    拆箱和装箱

    这部分是这一章的重中之重之重中之重中重。

    很多时候,要获取值类型的实例引用。这也是“什么时候会进行装箱”的答案。

    先例举一个简单的例子:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Collections;
     7 
     8 namespace Program4
     9 {
    10     class Program
    11     {
    12         internal struct Point {
    13             private Int32 m_x, m_y;
    14             public Point(Int32 x, Int32 y) {
    15                 m_x = x;
    16                 m_y = y;
    17             }
    18             public void Change(Int32 x, Int32 y)
    19             {
    20                 m_x = x;
    21                 m_y = y;
    22             }
    23             public override String ToString()
    24             {
    25                 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString());
    26             }
    27         }
    28         static void Main(string[] args)
    29         {
    30             ArrayList a = new ArrayList();
    31             Point p = new Point(0, 0);
    32             for (Int32 i = 0; i < 5; i++) {
    33                 p.Change(i, i);
    34                 a.Add(p);
    35             }
    36         }
    37     }
    38 }

    本例中的Add方法原型如下 :

            //
            // 摘要:
            //     将对象添加到 System.Collections.ArrayList 的结尾处。
            //
            // 参数:
            //   value:
            //     要添加到 System.Collections.ArrayList 末尾的 System.Object。该值可以为 null。
            //
            // 返回结果:
            //     value 已添加的 System.Collections.ArrayList 索引。
            //
            // 异常:
            //   T:System.NotSupportedException:
            //     The System.Collections.ArrayList is read-only.-or- The System.Collections.ArrayList
            //     has a fixed size.
            public virtual int Add(object value);

    可以看出来,Add要获取的是一个Object,是一个引用类型,但是Point p是一个值类型。为了使代码正确工作,需要将p转换成在堆中托管的对象,以获取对该对象的引用。

    这时,就要使用装箱机制。对值类型装箱时发生了如下事情:

    1.在托管堆中分配内存。除了值类型各字段所需的内存量,还需要为类型对象指针和同步块索引分配内存空间;

    2.值类型的字段拷贝到新分配的内存;

    3.返回对象地址。

    可以看一下IL代码:

    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // 代码大小       59 (0x3b)
      .maxstack  3
      .locals init (class [mscorlib]System.Collections.ArrayList V_0,
               valuetype Program4.Program/Point V_1,
               int32 V_2,
               bool V_3)
      IL_0000:  nop
      IL_0001:  newobj     instance void [mscorlib]System.Collections.ArrayList::.ctor()
      IL_0006:  stloc.0
      IL_0007:  ldloca.s   V_1
      IL_0009:  ldc.i4.0
      IL_000a:  ldc.i4.0
      IL_000b:  call       instance void Program4.Program/Point::.ctor(int32,
                                                                       int32)
      IL_0010:  nop
      IL_0011:  ldc.i4.0
      IL_0012:  stloc.2
      IL_0013:  br.s       IL_0032
      IL_0015:  nop
      IL_0016:  ldloca.s   V_1
      IL_0018:  ldloc.2
      IL_0019:  ldloc.2
      IL_001a:  call       instance void Program4.Program/Point::Change(int32,
                                                                        int32)
      IL_001f:  nop
      IL_0020:  ldloc.0
      IL_0021:  ldloc.1
      IL_0022:  box        Program4.Program/Point
      IL_0027:  callvirt   instance int32 [mscorlib]System.Collections.ArrayList::Add(object)
      IL_002c:  pop
      IL_002d:  nop
      IL_002e:  ldloc.2
      IL_002f:  ldc.i4.1
      IL_0030:  add
      IL_0031:  stloc.2
      IL_0032:  ldloc.2
      IL_0033:  ldc.i4.5
      IL_0034:  clt
      IL_0036:  stloc.3
      IL_0037:  ldloc.3
      IL_0038:  brtrue.s   IL_0015
      IL_003a:  ret
    } // end of method Program::Main

    会发现其中有装箱操作。

    这里稍微扩展一个,关于for循环在IL中的知识:在IL中,for循环通过两个指令:br.s(无条件地将控制转移到目标指令)和brtrue.s(如果 value 为 true、非空或非零,则将控制转移到目标指令)两个指令实现循环,clt(比较两个值。如果第一个值小于第二个值,则将整数值 1 (int32) 推送到计算堆栈上;反之,将 0 (int32) 推送到计算堆栈上)来控制是否继续循环的那个值。

    下面来看拆箱。假定我们要获取ArrayList的第一个元素:

    Point p2 = (Point)a[0];

    它获取了ArrayList的元素0包含的引用,试图将其放到Point值类型的实例p中。为此,已装箱Point对象中的所有字段都必须复制到值类型变量p2中。为此,已装箱Point对象中的所有字段都必须复制到值类型变量p2中,后者在线程栈上。CLR分两步完成复制:第一步获取已装箱Point对象中哥哥Point字段的地址,这个过程被称为拆箱。第二步就是将字段包含的值从堆复制到基于栈的值类型实例中。

    拆箱不是将装箱的过程倒过来。拆箱只是获取指针的过程,该指针指向包含在一个对象中的原始值类型。指针指的是已装箱实例中的未装箱部分。

    已装箱值类型的实例在拆箱时,会发生下面的事情:

    1.如果包含“对已装箱值类实例的引用”的变量为null,抛出异常;

    2.如果引用的对象不是所需值类型以装箱的实例,抛出异常。

    第二条的具体情况举例:

                Int32 x = 5;
                Object o = x;
                Int16 y = (Int16)o;

    正确的写法应该是:

                Int32 x = 5;
                Object o = x;
                Int16 y = (Int16)(Int32)o;

     再来看一个例子:

                Int32 x = 5;
                Object o = x;
                x = 123;
                Console.WriteLine(x + "," + (Int32)o);

    请问在这里总共执行了多少次装箱?

    答案是3次。

    第一次装箱发生在Object o = x,第二次是WriteLine的x(在WriteLine需要一个String对象,而String是个引用类型。为了将Int32转换成String,需要进行一次装箱操作),第三次是在o进行了一次拆箱操作后,为了获取String,又进行了一次装箱。

    可以用下面的写法来避免第二次拆箱和第三次装箱:

    Console.WriteLine(x + "," + o);

    还可以避免第一次的装箱操作:

    Console.WriteLine(x.ToString + "," + o);

    虽然未装箱对象没有类型对象指针,但仍可调用由类型继承或重写的虚方法。如果值类型重写了虚方法,那么CLR可以非虚的调用该方法,因为值类型隐式密封,不会有类型派生,而且调用虚方法的值类型没有封装。然而。如果重写的虚方法要调用在基类中的实现的时候,值类型就会装箱,以便通过一个this指针将对一个堆对象的引用传给基方法。将值类型的未装箱实例转型为类型的某个接口时要对实例进行装箱,这是因为接口变量必须包含对堆对象的引用。可以看下面的代码,结合其IL:

     1 using System;
     2 using System.Collections.Generic;
     3 using System.Linq;
     4 using System.Text;
     5 using System.Threading.Tasks;
     6 using System.Collections;
     7 
     8 namespace Program4
     9 {
    10     class Program
    11     {
    12         internal struct Point
    13         {
    14             private Int32 m_x, m_y;
    15             public Point(Int32 x, Int32 y)
    16             {
    17                m_x = x;
    18                m_y = y;
    19             }
    20             public void Change(Int32 x, Int32 y)
    21             {
    22                 m_x = x;
    23                 m_y = y;
    24             }
    25             public override String ToString()
    26             {
    27                 return String.Format("{0}, {1}", m_x.ToString(), m_y.ToString());
    28             }
    29         }
    30         static void Main(string[] args)
    31         {
    32             Point p = new Point(0, 0);
    33             Console.WriteLine(p);
    34             p.Change(1, 2);
    35             Console.WriteLine(p);
    36             object o = p;
    37             Console.WriteLine(o);
    38             ((Point)o).Change(3, 3);
    39             Console.WriteLine(o);
    40         }
    41     }
    42 }
    .method private hidebysig static void  Main(string[] args) cil managed
    {
      .entrypoint
      // Code size       84 (0x54)
      .maxstack  3
      .locals init (valuetype Program4.Program/Point V_0,
               object V_1,
               valuetype Program4.Program/Point V_2)
      IL_0000:  nop
      IL_0001:  ldloca.s   V_0
      IL_0003:  ldc.i4.0
      IL_0004:  ldc.i4.0
      IL_0005:  call       instance void Program4.Program/Point::.ctor(int32,
                                                                       int32)
      IL_000a:  nop
      IL_000b:  ldloc.0
      IL_000c:  box        Program4.Program/Point
      IL_0011:  call       void [mscorlib]System.Console::WriteLine(object)
      IL_0016:  nop
      IL_0017:  ldloca.s   V_0
      IL_0019:  ldc.i4.1
      IL_001a:  ldc.i4.2
      IL_001b:  call       instance void Program4.Program/Point::Change(int32,
                                                                        int32)
      IL_0020:  nop
      IL_0021:  ldloc.0
      IL_0022:  box        Program4.Program/Point
      IL_0027:  call       void [mscorlib]System.Console::WriteLine(object)
      IL_002c:  nop
      IL_002d:  ldloc.0
      IL_002e:  box        Program4.Program/Point
      IL_0033:  stloc.1
      IL_0034:  ldloc.1
      IL_0035:  call       void [mscorlib]System.Console::WriteLine(object)
      IL_003a:  nop
      IL_003b:  ldloc.1
      IL_003c:  unbox.any  Program4.Program/Point
      IL_0041:  stloc.2
      IL_0042:  ldloca.s   V_2
      IL_0044:  ldc.i4.3
      IL_0045:  ldc.i4.3
      IL_0046:  call       instance void Program4.Program/Point::Change(int32,
                                                                        int32)
      IL_004b:  nop
      IL_004c:  ldloc.1
      IL_004d:  call       void [mscorlib]System.Console::WriteLine(object)
      IL_0052:  nop
      IL_0053:  ret
    } // end of method Program::Main

    对象哈希码

    FLC的设计者认为,如果能将对象的任何实例放到哈希表集合中,能带来很多好处。为此,System.Object提供了虚方法GetHashCode,能获取任意对象的Int32的哈希码。所以,如果重写了Equals方法,一定要重写GetHashCode方法。

  • 相关阅读:
    数据库异常处理记录
    FINEMVC重定向和显示合计
    有意思的文章的链接
    oralce 创建用户和权限
    FINEUI(MVC) grid 双击弹窗功能
    FINEUI(MVC)布局问题记录
    通过判断cookie过期方式向Memcached中添加,取出数据(Java)
    通过数组方式向Oracle大批量插入数据(10万条11秒)
    Python基础学习13--面向对象
    Python基础学习12--变量作用域
  • 原文地址:https://www.cnblogs.com/renzhoushan/p/10410658.html
Copyright © 2020-2023  润新知