接上篇 《ILBC 規範》 http://www.javashuo.com/article/p-hsmtjoox-s.html ,html
ILBC 的 目標 是 跨平臺 跨設備 。java
D# / ILBC 能夠 編寫 操做系統 內核 層 以上的 各類應用, 程序員
其實 除了 進程調度 虛擬內存 文件系統 外, 其它 的 內核 模塊 能夠用 D# 編寫, 好比 Socket 。docker
D# / ILBC 的 設計目標 是 保持簡單, 好比 D# 支持 Lambda 表達式, 可是 LinQ 應該 由 庫 來 支持, 與 語言 無關 。數組
另外一方面, ILBC 不打算 發展一個 龐大 的 優化體系 。 C++ , .Net / C# 的 優化體系 已經 龐大複雜 到 成爲 大公司 也很難 承受 之重 了 。安全
咱們不會這麼幹 。數據結構
ILBC 認爲 「簡單 就是 優化」 。架構
保持 簡單設計 和 模塊化, 模塊化 會 帶來一些 性能損耗, 這些 性能損耗 是 合理 的 。ide
保持 簡單設計 和 模塊化, 對於 ILBC / D# / c3 / …… 以及 應用程序 都是 有益的 。模塊化
ILBC 的 目標 是 創建一個 基礎設施 平臺 。
就像 容器(好比 docker, kubernetes), 容器 打算 在 操做系統 之上 創建一個 基礎設施 平臺,
咱們的 作法 不一樣,
ILBC 是 用 語言 創建一個 基礎設施 平臺 。
爲了 避開 「優化陷阱」, 我決定仍是 啓用 以前的 「ValueBox」 的 想法 。 ValueBox 的 想法 以前 想過, 但後來又放棄了 。
ValueBox 相似 java C# 裏的 「裝箱」 、 「拆箱」 。
ValueBox 就是 對於 int long float double char 等 值類型 (或者說 簡單類型) , 用一個 對象(ValueBox) 裝起來, 用於 須要 按照 對象 的 方式 處理 的 場合 。
原本我以前是放棄了 這個 想法, 以爲 仍是 按照 C# 的 「一切都是對象」 的 作法, 讓 值類型 也 做爲 對象, 繼承 Object 類, 而後 讓 編譯器 在 不須要 做爲對象, 只是 對 值 計算 的 場合 把 值類型對象 優化回 值類型 (C 語言 裏的 int long float double char 等) 。
但 如今 既然談到 優化陷阱, 上面說的 「一切都是對象」 的 架構 就 有點 呵呵 了 。
這有一個問題, 把 值對象 優化回 值類型, 這個 優化 是 放在 C 中間代碼 裏 仍是 InnerC 編譯器 裏,
放在 C 中間代碼 是指 由 高級語言(D# c3 等) 編譯器 來 優化, 這樣 高級語言 編譯 生成 的 C 中間代碼 裏面 就已是 優化過的 代碼, 好比 在 值 計算的 地方 就是 C 語言 的 int long float double char 等, 而不是 值對象 。
但 這樣 要求 高級語言 的 編譯器 都 按照 這個 標準 進行優化, 否則 在 各 高級語言 寫的 庫 之間 動態連接 時 會 發生問題 。
好比 D# 調用 c3 寫的 庫 的 Foo(int a) 方法, c3 作過優化, 因此 須要的 a 參數是 一個 C 語言 裏的 int 類型, 而 D# 未做優化, 傳給 Foo(int a) 的 a 參數 是 一個 int 對象, 這就 出錯了, 這是 不安全的 。
但 要求 高級語言 的 編譯器 都 按照 標準 優化, 這是一個比較 糟糕 的 事情 。
這會 讓 高級語言 編譯器 變得 麻煩 和 作 重複工做, 且 ILBC 會因 規則 累贅 而 缺少活力 。
若是 把 優化 放在 InnerC 編譯器 裏 優化 , 那 會 和 咱們的一些想法 不符 。 咱們但願 InnerC 是一個 單純的 C 編譯器, 不要把 IL 層 的 東西 摻雜 到 裏面 。
InnerC 是 一個 單純的 C 編譯器, 這也是 ILBC 的 初衷 和 本意 。
因此, 咱們採用這樣的設計, 值類型 就是 值類型, 對應到 C 語言 裏的 基礎類型(int long float double char 等), 值類型 不是 對象, 也不 繼承 Object 類, 對象 是 引用類型, 繼承 Object 類 。
當 須要 以 對象 的 方式來 處理 時, 把 值類型 包到 ValueBox 裏 。
每一個 值類型 會 對應一個 ValueBox, 好比 int 對應 IntBox, long 對應 LongBox, float 對應 FloatBox, double 對應 DoubleBox, char 對應 CharBox, bool 對應 BoolBox 等等 。
ValueBox 的 使用 代碼 好比:
IntBox i = new IntBox( 10 ); // 10 就是 IntBox 包裝的 Value
或者,
int i = 10;
IntBox iBox = new IntBox( i ); // 把 int 類型的 變量 i 的 值 包裝到 IntBox
何時須要 把 值類型 包到 ValueBox 裏 ? 或者說, 何時須要 以 對象 的 方式 來 處理 值類型 ?
通常是在 須要 動態傳遞參數 的 時候,
好比, Foo ( object o ) 方法 的 o 參數 可能 傳入 各類類型, 那麼能夠把 o 參數 聲明爲 object 類型, 這樣在 Foo() 方法內部 判斷 o 參數 的 類型, 根據類型執行相關操做 。
又好比, 反射, 經過 反射 調用 方法, 參數 是 經過 object [ ] 數組 傳入,
這 2 種 狀況 對於 參數 都是 以 對象 的 方式 處理, 若是 參數 是 值類型 的話, 就須要 包裝 成 ValueBox 再傳入 。
D# / ILBC 支持 值類型 數組 、 值類型 泛型 容器 。
值類型 數組 就是 數組元素 就是 值類型, 假設 int 類型 佔 4 個 字節, 那麼 int [ ] 數組 的 每一個元素 佔用空間 也是 4 個 字節, 這和 C 語言 是同樣的 。
值類型 泛型 容器 好比 List<int> , List<int> 的 內部數組 就是 int [ ] 。
值類型 數組, 值類型 泛型 容器 直接存取 值類型, 不須要 對 值類型 裝箱 。
可是要注意, 好比 Dictionary<TKey, TValue> , value 能夠是 值類型, 但 key 須要是 對象類型, 由於會 調用 key.GetHashCode() 方法 。
因此, 若是 key 是 值類型, 須要 裝箱 成 ValueBox 。
好比
Dictionary < string , int > , value 能夠是 值類型 ,
Dictionary < IntBox , object > , key 須要是 對象類型, 若是是 int , 須要 裝箱 成 IntBox
若是聲明 Dictionary < int , object > , 則 編譯器 會對 key 的 類型 報錯, 提示 應 聲明 爲 引用類型(對象類型) 。
值類型 又稱 簡單類型 ,
引用類型 又稱 對象類型 ,
(這有點 呵呵)
編譯器 是 依據 什麼 檢查 key 類型 應爲 引用類型 呢 ?
咱們能夠在 D# 裏 加入一個 語法, 好比, Dictionary 的 定義 是這樣:
public class Dictionary < object TKey , TValue >
{
……
public void Add ( TKey key , TValue value )
{
int hash = key.GetHashCode() ;
……
}
}
能夠看到, TKey 的前面 加了一個 object , 這表示 TKey 的 類型 應該是 object 類型 或者 object 的 子類,
這個 object 能夠 換成 其它 的 類型, 好比 其它 的 類 或者 接口 。
這樣的話, 若是 TKey 被 聲明 爲 值類型, 好比 Dictionary < int , object > , 因爲 int 不是 引用類型, 固然 也就不是 object 或者 object 的 子類, 因而 不知足 TKey 的 類型約束, 因而 編譯器 就 報錯了 。
若是 TKey 的 前面 不聲明 object , 會怎麼樣 ? 仍是會報錯 。
由於在 Add ( TKey key , TValue value ) 方法 裏 調用了 key.GetHashCode() 方法, 調用方法 意味着 必須是 引用類型(對象類型), 因此 編譯器 會要求 Dictionary 的 定義 裏 要 聲明 TKey 的 類型 , 且 TKey 的 類型 必須是 引用類型(對象類型) 。
這 也有點 呵呵 。
IntBox override(重寫) 了 Object 類的 GetHashCode() 方法, 用於 返回 IntBox 包裝的 int 值 的 HashCode, 不過 int 類型 的 GetHashCode() 方法 多是 最簡單的了, 直接返回 int 值 就能夠 。 ^^
String 類 會 override(重寫) Object 類 的 Equals(object o) 方法, 而且會 增長 一個 Equals(string s) 方法, Equals( object o ) 方法內部會調用 Equals( string s ) 方法 。 Equals ( object o ) 方法 先 判斷 o 是否是 String 類型, 若是不是, 則 返回 false, 若是是, 則 調用 Equals( string s ) 判斷 是否相等 。
D# 裏 用 「 == 」 號 比較 2 個 String 的 代碼 會被 編譯器 處理成 調用 Equals( string s ) 方法 。
除了 最底層 的 模塊 用 C 編寫, D# / ILBC 能夠編寫 各個層次 各個種類 的 軟件 ,
用 C 寫 能夠用 InnerC 寫, 只要 符合 ILBC 規範, InnerC 寫的 代碼 就能夠 和 ILBC 程序集 同質連接 。
從這個 意義 來看, ILBC / InnerC 能夠 編寫 包括 操做系統 在內 的 各個層次 各個種類 的 軟件 ,
從這個 意義 來看, ILBC 是 一個 軟件 基礎設施 平臺 。
能夠看出, C# 8.0 標誌着 C# 開始成爲 「保姆型」 語言 , 而不是 程序員 的 語言 。
D# 將 一直 會是 程序員 的 語言 , 這是 D# 的 設計目標 和 使命 。
補充一點, ValueBox 的 使用 小技巧 ,
在一段代碼中, ValueBox 能夠只 new 一個, 而後 重複使用 。
ValueBox 有一個 public value 字段, 就是 ValueBox 包裝的 值, 對 value 字段 賦上新值 就能夠 從新使用 了 。
好比, IntBox ,有 public int value 字段,
IntBox i = new IntBox( 1 );
i.value = 2;
i.value = 3;
i.value = 4;
重複使用 ValueBox 能夠 減小 new ValueBox 和 GC 回收 的 開銷 。
有 網友 提議 D# 的 名字 能夠叫 Dava , 這名字 挺好聽, 挺美麗的, 和 女神(Diva) 相近, 好吧, 就叫 Dava 吧, D# 又名 Dava 。
接下來 咱們 討論 泛型 原理 / 規範 ,
泛型 在 ILBC 裏 和 C++ 相似 , 由 高級語言 編譯器 生成 具體類型,
假設 有 一個 List<T> 類, 這個類 的 C 中間代碼 以下:
struct List<T>
{
T arr [ 20 ] ; // 20 是 內部數組 的 初始化 長度
int length = 0 ;
}
void List<T><>Add<>T ( List<T> * this , T element )
{
this -> arr [ this -> length ] = element ;
this -> length ++ ;
}
T List<T><>Get<>T ( List<T> * this , int index )
{
return this -> arr [ index ] ;
}
若是在 代碼 中 使用 了
List<int> list1 = new List<int>();
List<string> list2 = new List<string>();
那麼 編譯器 會 爲 List<int> 生成一個 具體類型 List~int 類, 也會爲 List<string> 生成一個 List~string 類 , 代碼以下:
struct List~int
{
int arr [ 20 ] ; // 20 是 內部數組 的 初始化 長度
int length = 0 ;
}
void List~int<>Add<>int ( List~int * this , int element )
{
this -> arr [ this -> length ] = element ;
this -> length ++ ;
}
int List~int<>Get<>int ( List~int * this , int index )
{
return this -> arr [ index ] ;
}
struct List~string
{
string * arr [ 20 ] ; // 20 是 內部數組 的 初始化 長度
int length = 0 ;
}
void List~string<>Add<>string ( List~int * this , string * element )
{
this -> arr [ this -> length ] = element ;
this -> length ++ ;
}
int List~string<>Get<>int ( List~int * this , int index )
{
return this -> arr [ index ] ;
}
能夠看出來, 把 泛型類型 裏的 List<T> 替換成 具體類型(List<int>, List<string>), 把 T 替換成 泛型參數類型 (int , string *) 就是 具體類型 。
注意 , 值類型 把 T 替換爲 值類型 就能夠, 好比 int, 引用類型 要把 T 替換成 引用(指針), 好比 string * 。
這部分 由 高級語言 編譯器 完成 。
複雜一點的狀況是, 跨 程序集 的 狀況, 假設 有 程序集 A , B , A 引用了 B 裏的 List<T> , 那 …… ?
這個須要 把 List<T> 的 C 中間代碼 放在 B 的 元數據 文件 (B.ild) 裏, A 引用 B.ild , 編譯器 會 從 B.ild 中 獲取到 List<T> 的 C 中間代碼, 根據 List<T> 的 C 中間代碼 生成 具體類型 的 C 中間代碼 。
這好像 又 有點 呵呵 了 。
不過 這樣看來的話, 上文 關於 泛型 對 值類型 和 引用類型 的 不一樣處理 好像 不必了 。
上文 舉例 的 Dictionary<object TKey , TValue> 要把 TKey 聲明爲 object ,
這其實已經不必了 。
public class Dictionary < TKey , TValue >
{
……
public void Add ( TKey key , TValue value )
{
int hash = key.GetHashCode() ;
……
}
}
若是在 代碼 中 寫了
Dictionary< int , object > dic ;
則 編譯器 會 報錯 「TKey 的 具體類型 int 不包含 GetHashCode() 方法, int 是 值類型, 值類型 不支持 方法, 建議改成 引用類型 。」
假設 有 class Foo<T> , 代碼以下:
class Foo<T>
{
void M1 ( T t )
{
t.Add();
}
}
Foo<A> foo = new Foo<A>();
A a = new A();
foo.M1 ( a ) ;
A 是 引用類型(對象類型), 若是 A 沒有 Add() 方法, 編譯器 會 報錯 「泛型參數類型 A 不包含 Add() 方法 。」
咱們還能夠把 代碼 改爲:
class Foo<T>
{
T M1 ( T t )
{
return t ++ ;
}
}
Foo<int> foo = new Foo<int>();
int i = 0 ;
int p = foo.M1 ( i ) ;
這 能夠 編譯 經過, 由於 int 支持 ++ 運算符, 實際上, 只要 支持 ++ 運算符 的 類型 均可以 使用 Foo<T> , 或者說, 只要 支持 ++ 運算符 的 類型 都 能夠做爲 Foo<T> 的 泛型參數類型 T 。
其實 說白了, 你 按照 C++ 模板 來 理解 ILBC 泛型 就能夠了 。 哈哈哈哈
接下來 討論 繼承 , 繼承 就是 繼承 基類 的 字段 和 方法, 進一步 是 重寫 虛方法 。
咱們先來看 繼承 基類 的 字段 和 方法 ,
假設
class A1
{
int f1;
}
class A2 : A1
{
int f2;
}
那麼, A2 佔用的 內存空間 就是 A1 的 空間 加上 A2 的 空間, 就是 f1 和 f2 的 空間,
由於 f1, f2 都是 int , 假設 int 是 4 個字節, 那麼 f1 , f2 共 佔用 8 個字節 的空間, 這就是 A2 佔用 的 空間 。
因此 new A2() 的 時候, 就是 先 從 堆 裏 申請 8 個 字節 的 空間, 而後 再 調用 A2 的 構造函數 初始化, A2 的 構造函數 會 先調用 A1 的 構造函數 初始化 。
假設 A3 繼承 A2, A2 繼承 A1 , 那麼 new A3() 時 會 先 申請 A3 的 空間, 而後 調用 A3 的 構造函數, A3 的 構造函數 是這樣:
A3( A3 * this)
{
A2( this );
A3 的 初始化 工做
}
A2( A2 * this)
{
A1( this );
A2 的 初始化 工做
}
A1( A1 * this)
{
A1 的 初始化 工做
}
能夠看出, 會 沿 繼承鏈 依次 調用 基類 的 構造函數 。
若是 基類 在 另外一個 程序集 裏, 那麼 對 基類 構造函數 的 調用 會 編譯成 動態連接 的 方式, 和 普通方法 的 動態連接 同樣 。
對於 方法 的 繼承, 編譯器 會 把 調用 基類 方法 的 地方 直接 編譯成 調用 基類方法, 傳入 子類對象 的 this 指針, 這個跟 基類對象 調用 自己的 方法 同樣 。
若是 是 基類 在 另外一個 程序集 裏, 就會 編譯成 動態連接 的 方式, 跟 基類對象 調用 自己的 方法 仍然同樣 。
對於 虛方法, 假設 有 程序集 A , B, B 裏有 A1 , A2 類, A2 是 A1 的 子類 , 並 override(重寫) 了 M1() , M2() 方法 。
虛方法 經過 引用 實現, 引用 裏 有一個字段 是 虛函數表 。
因此, 咱們要對 引用 作一點 改進,
以前 咱們 在 C 中間代碼 裏 寫的 引用 都是 指針, 但爲了實現 虛方法 , 須要 把 引用 改進成一個 結構體 :
struct ILBC<>Reference
{
void * objPtr ; // 對象指針
void * virtualMethods ; // 虛函數表 指針
}
A 裏 的 代碼:
A1 a = new A2();
a.M1();
這段 代碼 會編譯成:
ILBC<>Reference a ; // 建立 引用 a
a.objPtr = ILBC_gcNew( sizeof(ILBC<>Class<>A2 ) ) ; // 給 A2 對象 分配空間
(* ILBC<>Class<>A2<>Constructor) ( a.objPtr ) ; // 調用 A2 構造函數 初始化 a
a.virtualMethods = ILBC_GetVirtualMethods( "B.A2", "B.A1" ); // 寫入 A2 對於 A1 虛函數表 指針
( * ( a.virtualMethods [ ILBC<>Class<>A1<>VirtualMethodNo<>M1 ] ) ) ( ) ; // 調用 a.M1() ;
// ILBC<>Class<>A1<>VirtualMethodNo<>M1 是一個 全局變量, 保存 A1.M1() 方法 的 虛方法號, 虛方法號 由 ILBC 在 加載 A1 類 時產生 並 寫入 這個 全局變量
以上就是 編譯器 產生 的 代碼 。
ILBC_GetVirtualMethods( "B.A2", "B.A1" ) 方法 返回 A2 對於 A1 的 虛函數表 指針,
參數 "B.A2" 表示 A2 的 全名, "B.A1" 表示 A1 的 全名, 全名 包含了 名字空間 。
ILBC_GetVirtualMethods( subClassFullName, baseClassFullName ) 方法 是 ILBC 調度程序 提供的 ILBC 系統方法,
這個方法 會 先根據 subClassFullName, baseClassFullName 查找 子類 對於 父類 的 虛函數表 是否存在, 若是 不存在 , 則 生成一份, 下次直接返回 。
虛函數表 是一個 數組, 數組元素 是 子類 對於 父類 虛函數 重寫 的 函數 的 地址, ILBC 在 加載類 時 會對 類 的 虛函數 排一個序, 而後 對於 該類的 每一個 子類 的 虛函數表, 都 按照 這個 順序 把 相應 的 虛函數 重寫 的 函數 的 地址 放到 數組(虛函數表) 裏 。
若是 子類 沒有 重寫函數, 則 存放 基類 的 函數地址 。
虛函數 排序 的 序號(從 0 開始) 就是 虛方法號(VirtualMethodNo),
以 虛方法號 做爲 下標(index) 從 虛函數表 裏 取出 的 就是 這個 虛方法 的 函數地址 。
加載類 是 在 ILBC_GetType( assemblyName, className ) 方法 裏 進行的, 實際上 應該改爲 ILBC_GetType( classFullName ) , 由於 classFullName 已經包含了 名字空間, 不須要 assemblyName 了 , 事實上 在 ILBC 運行時 對於 類(Class) 的 識別 就是 用 Full Name, 不須要涉及 assemblyName , 也能夠說, 在 一個 運行時 內, 不能 有 相同 Full Name 的 2 個 類 , 無論 這 2 個 類 是否是 在 一個 程序集 裏 。
ILBC_Type( classFullName ) 方法 會 檢查 類 是否 已加載, 若是 已加載 就 直接返回 ILBC_Type * , 若是 沒有 則 加載 並 返回 ILBC_Type * 。
ILBC_GetVirtualMethods( 「B.A2」, "B.A1" ) 方法 會 查找 A1 中 全部的 虛方法, 排一個序, 並 建立一個 長度 等於 虛方法個數 的 數組(虛方法表), 而後 從 A2 中 按名稱 逐個 查找 A2 對 虛方法 的 重寫實現 的 函數地址, 按 順序 填入 虛方法表 中, 若是 未重寫, 則 直接使用 基類 的 實現, 即 填入 基類 的 函數地址 。
好比 A2 繼承 A1, A1 繼承 Object , A2 重寫了 Object.GetHashCode() 方法, 那麼 A2 對於 A1 的 虛函數表 中 GetHashCode() 方法 對應的 位置 就會 寫入 A2.GetHashCode() 的 函數地址,
若是 A1 重寫了 Object.GetHashCode() 而 A2 未重寫, 則 會 填入 A1.GetHashCode() 的 函數地址,
若是 A1 A2 都沒有 重寫 Object.GetHashCode() , 則 會 填入 Object.GetHashCode() 的 函數地址 。
也就是說, ILBC 會 沿着 繼承鏈 向上 查找 虛函數 的 重寫實現 。
好比 有 如下 繼承關係 :
A3 -> A2 -> A1 -> Object
又有 這樣的 代碼:
A1 a1 = new A3();
A2 a2 = new A3();
A3 a3 = new A3();
對於 引用 a1 , a1.virtualMethods 應該是 「A3 對於 A1 的 虛函數表」,
什麼是 「A3 對於 A1 的 虛函數表」, 就是 「A3 對象 以 A1 的 身份 運行」 的 虛函數表 。
因此 a1.virtualMethods 指向 的 虛函數表 應 包含 A1 的 所有 虛方法 ,
a2.virtualMethods 指向 的 虛函數表 應 包含 A2 的 所有 虛方法 ,
a3.virtualMethods 指向 的 虛函數表 應 包含 A2 的 所有 虛方法 ,
A1 的 所有 虛方法 包括 A1 本身 聲明 的 虛方法 和 Object 的 虛方法 ,
A2 的 所有 虛方法 包括 A2 本身 聲明 的 虛方法 和 A1 的 虛方法 和 Object 的 虛方法 。
A3 的 所有 虛方法 包括 A3 本身 聲明 的 虛方法 和 A2 的 虛方法 和 A1 的 虛方法 和 Object 的 虛方法 。
因此, 虛函數表 裏的 方法 也是 沿着 繼承鏈 向上 查找 的 。
接口 也是 同樣的 處理方式 。
好比
IFoo foo = new A();
表示 A 對象 foo 以 IFoo 的 身份 運行 。
接口 能夠 區分 顯示實現 和 隱式實現 , 這在 元數據 中能夠 區分, 在 建立 虛函數表 查找 元數據 的 時候 能夠 判斷 出來 。
能夠看出, 查找 和 建立 虛函數表 用到 較多 根據 名字 查找 成員 的 操做, 因此 前文 在 動態連接 的 篇幅 也 提到 能夠用 HashTable 來實現 快速 查找, 提高 反射 和 動態連接 的 效率 。
查找 和 建立 虛函數表 也是 反射 和 動態連接 。
咱們還能夠 順便 看一下 Object 類 的 結構 :
struct Object
{
ILBC_Type * type ; // 類型信息
char lock ; // 用於 IL Lock , 當 鎖定 該對象時, lock 字段 寫入 1, 未鎖定時 lock 字段 是 0
}
昨天 一羣 網友 嚷嚷着 「沒有 結構體(Struct) 是 如何如何 的 糟糕,,」 ,
ILBC 能夠支持 結構體, 這很容易, 結構體 有方法, 能夠繼承, 但不能多態 。
不能 多態 是指 結構體 不能聲明 虛方法, 子類結構體 也不能 重寫 基類結構體 的 方法 。
加入 結構體 能夠 讓 程序員 本身 選擇 棧 存儲數據 仍是 堆 存儲數據 , 能夠 由 程序員 本身 決定 這個 設計策略 或者說 架構 。
這很清晰 。
目前 不打算 讓 Struct 支持 可爲空(Nullable)類型, 即 Struct ? 類型 , 能夠用 一個字段 來 表示 初始 等狀態,
若是實在想要 null , 那就用 Class 吧 , Oh ……
Struct 經過 關鍵字 struct 聲明, 不繼承 ValueType, 也不繼承 Struct, 實際上也沒有 ValueType , Struct 這樣的 基類 。
在 ILBC 裏, 「一切都是對象是不成立的」 , 對象(Class) 只是 數據類型 的 一種 。
DateTime 能夠用 Struct 來實現, 由於 DateTime 可能就是一個 64 位 整數, 表示 公元元年 到 某時 的 Ticks 數,
若是是這樣的話, 如 網友 所說 「引用 都 比 Struct(DateTime) 大」 。
討論到這裏, 能夠看出來, C# 爲了實現 「一切都是對象」 付出了多大的代價 ,
並且 C# 還支持 Struct 能夠是 可爲空(Nullable) 類型, 這讓人無語, 只想 呵呵 。 ^^ ^^ ^^
到 目前爲止, ILBC 裏的 數據類型 有 3 種 :
1 簡單類型 (值類型) , int long float double char 等等
2 結構體 Struct (值類型)
3 對象 Class (引用類型)
值類型 的 優勢 是:
1 一次尋址, 不須要 經過 引用 二次尋址
2 只包含 值, 不包含 類型信息 等 數據, 不冗餘
3 存儲 在 棧空間, 分配快 不須要回收, 事實上 對於 靜態分配 的 棧 變量, 函數 入棧 的 時候 修改了 棧頂, 則 該 函數 中 全部的 棧 變量 都被 分配 了 。
如今有個 問題 是, 一個 參數 是 值類型 的 方法, 若是要經過 反射 調用, 怎麼調用?
反射 須要 把 參數 放到 object[ ] 數組, object[ ] 數組 的 元素 是 引用 。
我懷疑 C# 中 把 Struct 放到 object[ ] 裏時, 會對 Struct 裝箱 。
因此 咱們 也能夠 對 Struct 進行 裝箱, 能夠用 ValueBox 對 Struct 裝箱, 好比:
[ ValueBox( typeof ( ABox ) ) ] // 告訴 ILBC 運行時 A Struct 對應的 ValueBox 是 ABox
struct A
{
}
class ABox : ValueBox<A>
{
}
ValueBox 是一個 泛型類, 由 ILBC 基礎庫 提供, 代碼以下:
class ValueBox<T>
{
T value ;
}
那麼, 在 動態傳遞參數 的 場合, 好比:
void Foo( object o )
{
……
}
能夠這樣寫:
void Foo ( object o )
{
Type type = o.GetType();
if ( type.IsValueBox ) // IsValueBox 是 Type 的 屬性, 若是 Type 表示的類型 是 ValueBox 或者 ValueBox 的 子類, 則 IsValueBox 返回 true
{
Type valueType = type.GetValueType() ; // GetValueType() 方法 是 Type 的 方法, 若是 Type 表示的類型 是 ValueBox 或者 ValueBox 的 子類, 則 返回 ValueBox 包裝的 值 的 類型, 即 value 字段 的 類型
if ( valueType == typeof(int) ) // typeof(int) 返回的 Type 對象 由 編譯器 生成
// do something for int
else if ( valueType == typeof(A) ) // typeof(A) 返回的 Type 對象 由 編譯器 生成
// do something for A Struct
else if ( …… )
……
return ;
}
// do something for Object (引用類型)
}
咱們能夠這樣調用 Foo() 方法:
Foo ( 1 );
A a = new A() ; // A 是 Struct
Foo ( a );
Foo ( "a string" ) ;
Person person = new Person() ; // Person 是 Class
Foo ( person ) ;
對於 反射 的 狀況, 能夠這樣寫:
class Class1
{
void Foo ( Struct1 s1 )
{
……
}
}
MethodInfo mi = typeof ( Class1 ).GetMethod( "Foo" ) ;
Struct1 s1 = new Struct1() ;
Struct1Box s1Box = new Struct1Box( s1 ) ;
mi.Invoke ( new object [ ] { s1Box } ) ;
把 s1 裝箱 到 s1Box 裏, 再把 s1Box 放到 object [ ] 裏, 這樣 MethodInfo 內部會 「拆箱」 把 s1 傳給 Foo() 方法 。
若是 直接 把 s1 放到 object [ ] 裏, 好比 new object [] { s1 } 會怎麼樣? 會 編譯 報錯 「s1 不是 對象, 不能轉換爲 object 類型, 請考慮用 ValueBox 裝箱 。」 。
把 反射 調用 方法 的 參數 放到 object [ ] 數組 裏傳入, 這一方面是爲了 統一處理, 另外一方面 也是 爲了 安全, 引用 是 一個 固定格式 的 Struct, 因此 ILBC 能夠 安全 規範 的 從 object [ ] 中 訪問 每一個 引用 。 若是能夠直接傳遞 值 的話, object [ ] 就會變成 C 的 void * 的 狀況 , void * 容易致使 訪問內存錯誤, 好比 方法 訪問 的 地址 已經 超過了 對象 的 地址範圍, 或者 訪問了 錯誤的 地址(好比 訪問 A 字段 可能變成了 訪問 B 字段, 或者是 把 B 字段 中的 某個字節 的 地址 做爲 A 字段 的 首地址) 。 這會形成 意想不到 的 錯誤 或者 程序 崩潰 。 也可能 被 用於 攻擊 。
而在 上面 Foo( object o ) 方法 裏, 若是 o 參數 實際傳入的是 IntBox 的話,
那麼, 會 這樣 取出 裏面 的 int 值:
Type type = o.GetType () ;
if ( type.IsValueBox )
{
Type valueType = type.GetValueType() ;
if ( valueType == typeof ( int ) )
{
IntBox iBox = ( IntBox ) o ;
int i = iBox.value ; // 取出 int 值
}
}
值類型(int long float double char 結構體 ) 在 內存空間 裏 是 不包括 類型信息 的, 只 單純 的 存儲 值, 這是爲了 執行效率 。
可是, 沒有 類型信息 的 運行期 類型轉換 是 不安全 的, 由於 不能 檢查類型, 跟 上面 假設 的 反射 參數 經過 void * 傳入 的 情形 同樣, 會形成 內存 的 錯誤訪問,
可是, ILBC 巧妙 的 避開 了 這一點 。
首先, 編譯期 類型轉換, 這個 能夠 由 編譯器 檢查, 這沒有問題 。
運行期 類型轉換, 就像 上面的代碼 ,
IntBox iBox = ( IntBox ) o ;
int i = iBox.value ; // 取出 int 值
是把 object o 轉換成 IntBox , IntBox 是 對象 , 有 類型信息, 能夠 類型檢查, 因此 IntBox iBox = ( IntBox ) o ; 是 安全 的 。
這其實就是一個 正常 的 引用類型 的 類型轉換 。
轉換爲 IntBox iBox 後, iBox.value 是 明確的 int 型, 這就能夠安全的使用了 。
那若是 把 o 轉換成 ValueBox 會 怎樣 ?
ValueBox vBox = ( ValueBox ) o ;
int i = vBox.value ; // 取出 int 值
這樣 編譯時 會 報錯 「不能把 泛型參數 T 類型 的 vBox.value 字段 賦值 給 int 類型 的 i 變量 。」 ,
若是 對 vBox.value 轉型, 轉型成 int :
ValueBox vBox = ( ValueBox ) o ;
int i = ( int ) vBox.value ; // 取出 int 值
這樣 編譯時 會 報錯 「不能把 泛型參數 T 類型 的 vBox.value 字段 轉型爲 int 類型 。」 。
我忽然以爲 D# Dava 還能夠叫 D++ 。 哈哈哈哈
上面提到 用 ValueBoxAttribute [ ValueBox ( typeof ( ABox ) ) ] 來 聲明 ABox 做爲 A Struct 的 ValueBox,
實際上這不必, ILBC 能夠 提供一個 ValueBox 基類, ValueBox<T> 繼承 ValueBox 類, 那麼 ValueType<T> 的 具體類型 也繼承於 ValueBox,
因此, ILBC 只要 判斷 ABox 是不是 ValueBox 的 子類, 就能夠知道 ABox 是否是 ValueBox,
同時, 經過 ValueBox<T> 的 泛型參數 T 能夠知道 value 的 類型 。
在 反射調用 方法 的 時候, 若是 傳給 MethodInfo 的 Invoke( object [ ] args ) 的 args 數組 裏 包含了 ValueBox 類型 的 參數,
ILBC 會 取出 ValueBox<T> 的 T value 字段 的 值 傳給 MethodInfo 包含的 方法,
那麼, 怎麼從 不一樣的 ValueBox 裏 來 取出 value 字段 的 值 呢?
好比 IntBox, ABox, DateTimeBox ,
這須要在 元數據 ILBC_Type 增長 2 個 字段 :
struct ILBC_Type
{
……
int valueOffset ; // value 字段 的 偏移量
int valueSize ; // value 字段 的 大小
}
對應的 ValueType 的 classLoader 裏 要 增長一段 代碼, 取得 當前類型 的 value 字段 的 偏移量 和 大小, 寫入 當前類型 的 ILBC_Type 結構體 的 valueOffset , valueSize 字段 。
好比, 以 IntBox 爲例, IntBox 的 classLoader 裏會增長這樣一段代碼:
ILBC_Type * type = ILBC_gcNew( sizeof ( ILBC_Type ) ) ;
……
type -> valueOffset = offsetOf ( IntBox, value ) ; // offsetOf 是 InnerC 提供的 關鍵字, 用於 取得 結構體 字段 的 偏移量
type -> valueSize = sizeOf ( IntBox ) ;
當 加載 IntBox 類 時, 會 調用 classLoader, 這段代碼 也會執行, 這樣就把 IntBox 的 value 字段 的 偏移量 和 大小 都 記錄到 IntBox 的 元數據 ILBC_Type 中了 。
ILBC 的 MethodInfo.Invoke( object [ ] args ) 方法 裏的 代碼 是 這樣:
ILBC_Reference o = object [ 0 ] ;
……
int offset = o.type -> valueOffset ; // value 字段 在 ValueBox 裏的 偏移量
int size = o.type -> valueSize ; // value 字段 在 ValueBox 裏的 大小
// 根據 offset 和 size 取出 value 字段 的 值
以上是 代碼 。
能夠看出, 以上過程 比 在 代碼中
IntBox iBox = new IntBox( 1 );
int i = iBox.value;
強類型 直接 取得 value 要 多 2 次 尋址, 會增長一些 性能損耗 。
經過上述設計, 程序員 能夠 自由的 定義 ValueBox, 一個 Value 類型 能夠 有 任意多個 ValueType ,
好比 ILBC 基礎庫 提供了 IntBox, DateTimeBox, 開發者還能夠 本身定義 任意個 int , DateTiime 的 ValueBox 。
這樣一來, ILBC 的 數據類型 數據結構 的 架構 就 打通了 。
還有一個問題, ILBC_Type 是 元數據 , 因此 每一個程序集 編譯 的 時候 都要 include struct ILBC_Type 所在的 頭文件 (.h 文件),
爲何每一個 程序集 都要 引用 ILBC_Type 的 頭文件 ?
由於 ILBC 調度程序 在 加載 Class 時 是 調用 classLoader 返回 ILBC_Type * , 就是說, ILBC_Type 結構體 是在 classLoader 裏 建立 和 構造 的 。
而 classLoader 是 屬於 程序集 的, 是 高級語言 編譯器 編譯 產生的,
若是 程序集 和 調度程序 之間 , 或者 程序集 之間 的 ILBC_Type 的 定義 不同, 就會發生錯誤 。
什麼是 定義 不同, 好比 ILBC 2.0 的 ILBC_Type 比 ILBC 1.0 增長了一些 字段, 或者 改變 了 字段 的 順序 。
這樣, 若是 把 1.0 的 程序集 放到 2.0 的 調度程序(運行時)裏 運行 就會有問題, 或者 2.0 和 1.0 的 程序集 放在一塊兒使用, 也會有問題 。
一般, 若是 2.0 增長了 ILBC_Type 的 字段, 那 1.0 的 程序集 放到 2.0 的 調度程序(運行時) 會有問題, 由於 2.0 的 調度程序 可能 越界訪問內存, 由於 1.0 的 ILBC_Type 沒有 2.0 新增 的 字段, 2.0 調度程序 對 1.0 的 ILBC_Type Struct 方法 訪問 新增的 字段 就會 越界 。
若是 2.0 沒有 新增 字段, 可是改變了 C 源代碼 裏 ILBC_Type 字段 的 順序, 那 會 形成 1.0 中 ILBC_Type 的 字段 偏移量 和 2.0 的 字段 偏移量 不一致, 一樣會形成 字段數據 的 錯誤訪問 。
因此, 爲了解決這個問題, 須要對 ILBC_Type 也進行 動態連接, 就是 把 當前 調度程序(運行時) 的 各字段 的 偏移量 告訴 各程序集 。
可是 ILBC 不會使用 加載 程序集 和 類 時候 的 動態連接, 而是會用 一段 專門 的 代碼 進行 元數據對象 好比 ILBC_Type 的 動態連接 。
ILBC 調度程序 會 提供 2 個 方法:
iint ILBC_GetTypeSize() // 返回 ILBC_Type 的 大小(Size)
ILBC_Type * ILBC_GetTypeFieldOffset ( fieldName ) // 返回 ILBC_Type 的 名爲 fieldName 的 字段 的 偏移量
程序集 能夠 調用 這 2 個 方法 來 得到 當前 ILBC 調度程序(運行時) 的 ILBC_Type 的 大小(Size) 和 字段偏移量 。
這會不會 有點 過分設計 了 ?