程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> CLR筆記:5.基元,引用和值類型

CLR筆記:5.基元,引用和值類型

編輯:關於.NET

5.1基元類型

編譯器(C#)直接支持的任何數據類型都稱為基元類型(primitive type),基元類型直接映射到FCL中存 在的類型。可以認為 using string = System.String;自動產生。

FCL中的類型在C#中都有相應的基元類型,但是在CLS中不一定有,如Sbyte,UInt16等等。

C#允許在“安全”的時候隱式轉型——不會發生數據丟失,Int32可以轉為Int64,但是反過來要顯示 轉換,顯示轉換時C#對結果進行截斷處理。

unchecked和check控制基元類型操作

C#每個運算符都有2套IL指令,如+對應Add和Add.ovf,前者不執行溢出檢查,後者要檢查並拋出 System.OverflowException異常。

溢出檢查默認是關閉的,即自動對應Add這樣的指令而不是Add.ovf。

控制C#溢出的方法:

1.使用 /check+編譯器開關

2.使用操作符checked和unchecked:

int b = 32767;      // Max short value

            //b = checked((short)(b + 32767));      throw 

System.OverflowException

            b = (short)checked(b + 32767);          //return -2

這裡,被注釋掉的語句肯定會檢查到溢出,運行期抱錯;而第二句是在Int32中檢查,所以不會溢出。 注意這兩條語句只是為了說明check什麼時候發揮作用,是兩條不同語義的語句,而不是一條語句的正誤 兩種寫法。

3.使用 checked和unchecked語句,達到與check操作符相同的效果:

int b = 32767;      // Max short value

            checked
            {
                b = b + 32767;

            }
            return (short)b;

System.Decimal類型在C#中是基元,但在CLR中不是,所以check對其無效。

5.2 引用類型和值類型

引用類型從托管堆上分配內存,值類型從一個線程堆棧分配。

值類型不需要指針,值類型實例不受垃圾收集器的制約

struct和enum是值類型,其余都是引用類型。這裡,Int32,Boolean,Decimal,TimeSpan都是結構。

struct都派生自System.ValueType,後者是從System.Object派生的。enum都派生自System.Enum,後 者是從System.ValueType派生的。

值類型都是sealed的,可以實現接口。

new操作符對值類型的影響:C#確保值類型的所有字段都被初始化為0,如果使用new,則C#會認為實例 已經被初始化;反之也成立。

SomeVal v1 = new SomeVal();

            Int32 a1 = v1.x;            //已經初始化為0


            SomeVal v2;

            Int32 a2 = v2.x;            //編譯器報錯,未初始化

使用值類型而不是引用類型的情況:

1.類型具有一個基元類型的行為:不可變類型,其成員字段不會改變

2.類型不需要從任何類型繼承

3.類型是sealed的

4.類型大小:或者類型實例較小(<16k);或者類型實例較大,但不作為參數和返回值使用

值類型有已裝箱和未裝箱兩種形式;引用類型總是已裝箱形式。

System.ValueType重寫了Equals()方法和GetHashCode()方法;自定義值類型也要重寫這兩個方法。

引用類型可以為null;值類型總是包含其基礎類型的一個值(起碼初始化為0),CLR為值類型提供相應 的nullable。

copy值類型變量會逐字段復制,從而損害性能,copy引用類型只復制內存地址。

值類型的Finalize()方法是無效的,不會在垃圾自動回收後執行——就是說不會被垃圾收集。

CLR控制類型字段的布局:System.Runtime.InteropServices.StructLayoutAttribute屬性, LayoutKind.Auto為自動排列(默認),CLR會選擇它認為最好的排列方式;LayoutKind.Sequential會按照 我們定義的字段順序排列;LayoutKind.Explicit按照偏移量在內存中顯示排列字段。

[System.Runtime.InteropServices.StructLayout(LayoutKind.Auto)]

    struct SomeVal 

    {

        public Int32 x;

        public Byte b;

    }

Explicit排列,一般用於COM互操作

[StructLayout(LayoutKind.Explicit)]

    struct SomeVal 

    {

        [FieldOffset(0)]

        public Int32 x;


        [FieldOffset(0)]

        public Byte b;

    }

5.3 值類型的裝箱和拆箱

boxing機制:

1.從托管堆分配內存,包括值類型各個字段的內存,以及兩個額外成員的內存:類型對象指針和同步 塊索引。

2.將值類型的字段復制到新分配的堆內存。

3.返回對象的地址。

——這樣一來,已裝箱對象的生存期 超過了 未裝箱的值類型生存期。後者可以重用,而前者一直到 垃圾收集才回收。

unboxing機制:

1.獲取已裝箱對象的各個字段的地址。

2.將這些字段包含的值從堆中復制到基於堆棧的值類型實例中。

——這裡,引用變量如果為null,對其拆箱時拋出NullRefernceException異常;拆箱時如果不能正確 轉型,則拋出InvalidCastException異常。

裝箱之前是什麼類型,拆箱時也要轉成該類型,轉成其基類或子類都不行,所以以下語句要這麼寫:

Int32 x = 5;

                Object o = x;

                Int16 y = (Int16)(Int32)o;

拆箱操作返回的是一個已裝箱對象的未裝箱部分的地址。

大多數方法進行重載是為了減少值類型的裝箱次數,例如Console.WriteLine提供多種類型參數的重載 ,從而即使是Console.WriteLine(3);也不會裝箱。注意,也許WriteLine會在內部對3進行裝箱,但無法 加以控制,也就默認為不裝箱了。我們所要做的,就是盡可能的手動消除裝箱操作。

可以為自己的類定義泛型方法,這樣類型參數就可以為值類型,從而不用裝箱。

最差情況下,也要手動控制裝箱,減少裝箱次數,如下:

Int32 v = 5;

                Console.WriteLine("{0}, {1}, {2}", v, v, v);    //要裝箱3

次


                Object o = v;   //手動裝箱

                Console.WriteLine("{0}, {1}, {2}", o, o, o);    //僅裝箱1

次

由於未裝箱的值類型沒有同步塊索引,所以不能使用System.Threading.Monitor的各種方法,也不能 使用lock語句。

值類型可以使用System.Object的虛方法Equals,GetHashCode,和ToString,由於System.ValueType 重寫了這些虛方法,而且希望參數使用未裝箱類型。即使是我們自己重寫了這些虛方法,也是不需要裝箱 的——CLR以非虛的方式直接調用這些虛方法,因為值類型不可能被派生。

值類型可以使用System.Object的非虛方法GetType和MemberwiseClone,要求對值類型進行裝箱

值類型可以繼承接口,並且該值類型實例可以轉型為這個接口,這時候要求對該實例進行裝箱

5.4使用接口改變已裝箱值類型

interface IChangeBoxedPoint

    {

        void Change(int x);    

    }


    struct Point : IChangeBoxedPoint

    {

        int x;


        public Point(int x)

        {

            this.x = x;

        }


        public void Change(int x)

        {

            this.x = x;

        }


        public override string ToString()

        {

            return x.ToString();

        }


        class Program

        {

            static void Main(string[] args)

            {

                Point p = new Point(1);


                Object obj = p;


                ((Point)obj).Change(3);

                Console.WriteLine(obj);       //輸出1,因為change(3)的對

象是一個臨時對象,並不是obj


                ((IChangeBoxedPoint)p).Change(4);

                Console.WriteLine(p);         //輸出1,因為change(4)的

對象是一個臨時的裝箱對象,並不是對p操作


                ((IChangeBoxedPoint)obj).Change(5);

                Console.WriteLine(obj);         //輸出5,因為change(5)

的對象是(IChangeBoxedPoint)obj裝箱對象,於是使用接口方法,修改引用對象obj

            }
        }

    }

5.5 對象相等性和身份標識

相等性:equality

同一性:identity

System.Object的Equal方法實現的是同一性,這是目前Equal的實現方式,也就是說,這兩個指向同一 個對象的引用是同一個對象:

public class Object

    {
        public virtual Boolean Equals(Object obj)

        {
            if (this == obj) return true;   //兩個引用,指向同一個對象
            return false;
        }
    }

但現實中我們需要判斷相等性,也就是說,可能是具有相同類型與成員的兩個對象,所以我們要重寫 Equal方法:

public class Object

    {
        public virtual Boolean Equals(Object obj)
        {
            if (obj == null) return false;   //先判斷對象不為null
            if (this.GetType() != obj.GetType()) return false;  //再比較對象類型
            //接下來比較所有字段,因為System.Object下沒有字段,所以不用比較,值類

型則比較引用的值
            return true;

        }

    }

如果重寫了Equal方法,就又不能測試同一性了,於是Object提供了靜態方法ReferenceEquals()來檢 測同一性,實現代碼同重寫前的Equal()。

檢測同一性不應使用C#運算符==,因為==可能是重載的,除非將兩個對象都轉型為Object。

System.ValueType重寫了Equals方法,檢測相等性,使用反射技術——所以自定義值類型時,還是要 重寫這個Equal方法來提高性能,不能調用base.Equals()。

重寫Equals方法的同時,還需要:

讓類型實現System.IEquatable<T>接口的Equals方法。

運算符重載==和!=

如果還需要排序功能,那額外做的事情就多了:要實現System.IComparable的CompareTo方法和 System.IComparable<T>的CompareTo方法,以及重載所有比較運算符<,>,<=, >=

5.6 對象哈希碼

重寫Equals方法的同時,要重寫GetHashCode方法,否則編譯器會有警告。

——因為System.Collection.HashTable和Generic.Directory的實現中,要求Equal的兩個對象要具有 相同的哈希碼。

HashTable/Directory原理:添加一個key/value時,先獲取該鍵值對的HashCode;查找時,也是查找 這個HashCode然後定位。於是一旦修改key/value,就再也找不到這個鍵值對,所以修改的做法是,先移 除原鍵值對,在添加新的鍵值對。

不要使用Object.GetHashCode方法來獲取某個對象的唯一性。FCL提供了特殊的方法來做這件事:

using System.Runtime.CompilerServices;
RuntimeHelpers.GetHashCode(Object o)

這個GetHashCode方法是靜態的,並不是對System.Object的GetHashCode方法重寫。

System.ValueType實現的GetHashCode方法使用的是反射技術。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved