設計類型(二):基元類型、引用類型和值類型

本章要討論的是.net的各類類型。這章開始,我想摒棄之前的抄書模式,嘗試本身閱讀後先行總結,而後再寫博客。編程

基元類型ide

所謂基元類型,指的是編譯器直接支持的數據類型。基元類型直接映射到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代碼:this

.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

由此可知,這四個寫法是徹底等價的。spa

在本書中,堅持使用FCL名稱,主要有如下緣由:.net

1.不少人糾結於使用string仍是System.String,其實這二者沒有區別。相似的,還有int和Int32:C#的int永遠映射到Int32.C#的long固定映射到Int64.線程

2.FLC的許多方法都將類型名做爲方法名的一部分。設計

3.方便些其餘面向CLR的代碼(代碼風格一致)。指針

 在高精度基元類型隱式轉換到低精度基元類型的時候,每每會進行截斷處理(區別於向上取整)。code

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 Assemblies\Microsoft\Framework\.NETFramework\v4.6.1\mscorlib.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方法。

相關文章
相關標籤/搜索