ILBC 规范 2
接上篇 《ILBC 规范》 https://www.cnblogs.com/KSongKing/p/10354824.html ,
ILBC 的 目标 是 跨平台 跨设备 。
D# / ILBC 可以 编写 操作系统 内核 层 以上的 各种应用,
其实 除了 进程调度 虚拟内存 文件系统 外, 其它 的 内核 模块 可以用 D# 编写, 比如 Socket 。
D# / ILBC 的 设计目标 是 保持简单, 比如 D# 支持 Lambda 表达式, 但是 LinQ 应该 由 库 来 支持, 与 语言 无关 。
另一方面, ILBC 不打算 发展一个 庞大 的 优化体系 。 C++ , .Net / C# 的 优化体系 已经 庞大复杂 到 成为 大公司 也很难 承受 之重 了 。
我们不会这么干 。
ILBC 认为 “简单 就是 优化” 。
保持 简单设计 和 模块化, 模块化 会 带来一些 性能损耗, 这些 性能损耗 是 合理 的 。
保持 简单设计 和 模块化, 对于 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) 和 字段偏移量 。
这会不会 有点 过度设计 了 ?