接上篇 《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) 和 字段偏移量 。
这会不会 有点 过分设计 了 ?