D# 语法
这篇文章 随意 的 记录 关于 D# 语法的想法 。
有关 ILBC / D# , 见 《ILBC 白皮书》 https://www.cnblogs.com/KSongKing/p/11070978.html 。
D# 又名 Data , D++ 。
D# 支持 结构体 方法 和 继承 , D# 的 结构体 的 静态方法 和 类 一样, 实例方法 全部采用 扩展方法 的 方式 定义 。
扩展方法 就是 C# 里的 扩展方法 的 那种 扩展方法 。
在一个 静态方法 的 第一个参数 传入 结构体 自身, 这就是 实例方法 。
D# 结构体 实例方法 的 定义 比 C# 扩展方法 简单, 不需要在 第一个 自身参数 前 加 “this” 关键字 。
比如, A 结构体,
struct A
{
public static void Foo( A a )
{
// do something
}
}
可以这样调用 :
A a;
……
a.Foo();
还可以 通过 指针 的 方式 传递 自身参数, 比如 :
struct A
{
int i;
public static void Foo( A * a )
{
int j = a -> i ;
// do something
}
}
可以这样调用 :
A a;
( & a ) -> Foo() ;
或者 :
A * a;
a -> Foo() ;
嘿嘿嘿, 指针 的方式可以用于 体积 比较大 的 结构体 。 总之 D# 提供了 灵活性 。
更灵活的是, D# 允许 结构体 同时定义 按值传递 和 按指针传递 2 个 重载 的 实例方法,就是 :
struct A
{
int i;
public static void Foo( A a )
{
// do something
}
public static void Foo( A * a )
{
int j = a -> i ;
// do something
}
}
可以看到, Foo( A a ) 和 Foo( A * a ) 可以共存, 是 重载 。
当然, 指针操作 是 unsafe 操作, 所有的 指针 操作 都应该 用 unsafe 关键字 修饰, 所以 上述代码 应该 加上 unsafe 关键字 ,如下:
struct A
{
int i;
public static void Foo( A a )
{
// do something
}
unsafe public static void Foo( A * a )
{
int j = a -> i ;
// do something
}
}
看 , 在 Foo( A * a ) 前面 加上了 unsafe 关键字, 对于 C# , unsafe 代码 应包括在 unsafe 块 中,
D# 简化了 这个 做法, 不使用 unsafe 块, 而是 用 unsafe 关键字 修饰 方法,
也可以 修饰 类, 修饰 类 表示 整个类 都是 unsafe, 就不需要 每个 方法 都用 unsafe 修饰 。
unsafe 没有 实际意义, 只有 标记意义 。
unsafe 的 作用 就是 可以通过 编译器 查找 所有的 unsafe 方法 类, 让 程序员 可以容易的关注 unsafe 代码 。
为什么 要 支持 结构体 和 unsafe ? 因为 我之前在 《ILBC 规范》 中 说过, D# 的 设计目标 之一 是 执行效率 。
具体的说, D# 的 结构体 和 unsafe 主要会 用来 支持 底层应用, 基础应用, 高性能中间件 。
除了 进程(线程)调度 、 IO 内核模块 以外 , 包括 套接字(Socket) 在内 的 底层模块 都可以用 D# 编写 。
事实上 只要 不使用 堆(new) 的话, 进程(线程)调度 、 IO 内核模块 也可以用 D# 编写,
这种 情况 基本上 全部是 unsafe 代码 和 结构体, 这种情况 差不多 和 C 等价 。
但 这样的话,好像 还要 加一个 “开关”, 比如 编译选项 什么的, 在 “不使用 堆 的 模式 下” 编程(编译) 。
这会让 一个 语言 变得 复杂, 不太 符合 D# 的 初衷 。
实际上 进程(线程)调度 、 IO 内核模块 这就是 C 的 事,没必要 去和 C 抢 。 呵呵呵呵 。
C++ 就是 那种 “只要 你 选择, 什么都可以做” 的 语言, 于是 呵呵 了 。
这就好像 有的 业务系统 写的 很灵活, 什么都可以做, 只要 你 去 配置,,,,,,
然后,灵活过头了, 一堆配置, 等你学会 这堆 配置 , 也 差不多 成 这个 系统 的 专家 了 。 问题是 这个 系统 的 专家 只是在 你的 公司 里 有用, 外面的 业界 对 此 没什么 认可 , 然后 就 呵呵 了 。
那 什么 是 基础应用 呢 ? 比如 图形图像,2D 3D,游戏, 视频音频, 视频采样,音频采样,人工智能,大数据,数据库,文档数据库,大规模并行计算文档数据库,搜索引擎,爬虫,编译器 , 文本编辑器, 大型文本编辑器 ,,,
什么事 高性能中间件 ? 比如 Nginx , 上面 基础应用 中 提到的 文档数据库 之类 好像也可以算 中间件 。 还有 搜索引擎 也算 中间件 ? 不知道 ……
结构体 因为 没有 GC , 所以 难以 应用于 对象图(结构体 引用 图), 但是 单纯 在 栈 中 的 结构体 的 应用范围 也很有限 。
但是, 有一种 场合, 结构体 可以 得到 充分 的 使用, 就是 结构体 数组,
或者说, 一个 很大 的 数组 中 包含了 很多 的 结构体 。
这在 基础应用 中 很广泛,
比如 上面说的 图形图像,2D 3D,游戏, 视频音频, 视频采样,音频采样 ,,,,,,,
乃至于 人工智能,大数据,数据库,文档数据库,大规模并行计算文档数据库,搜索引擎,爬虫,编译器 , 文本编辑器, 大型文本编辑器 都 与此有关 。
D# 对 结构体 和 指针 的 支持 非常 适合于 这些 场景 。
数组 本身 是 属于 堆 的, 但是 数组 里 包含了 大量 的 结构体 。
基础应用 需要 对 这种 场景 非常良好 的 支持 。
所以 从 这里 可以看到, D# 的 意义 之一, 开发 基础应用 不一定 要用 C++ , 现在 的 状况 就是 基本都用 C++ 。
包括 图形图像 视频音频 人工智能 。
所以 D# 需要 对 数组 进行 比较 大力 的 支持, 包括 排序 等 , 其它 的 还有什么? 想不出来, 大家想到可以告诉我 。
C# 对 集合类 比如 List<T> 的 支持很多, 我也不太清楚,反正有很多 方法 和 扩展方法 。
不过 D# 首要支持 的是 数组, 别跟我说 什么 List<T> , C# 都 在 推 Span, ArraySegment 了,
D# 没有那么多 麻烦, 直接 数组 。
从 C# 的 Span, ArraySegment, ArrayPool, BufferManager 这一堆东西 可以 看出来, C# 目前 的 尴尬 。
这一堆东西 每一个 都是 一套复杂的班子, 我又想起 前几天 我查了一下 C# 的元组, 其实我需要的 只是 ( int i , string s ) 这样一个 小括号 而已,
结果 查出了 一大堆 名字空间 很长的东西, 里面好像有 “Tuple” 字样 , 又是 泛型, 又是 继承, 好像 Visual Studio 还要装什么 安装包,
然后 。
呀, 我怎么 不知不觉 的 又 批评 C# 一下 了?
D# 对于 数组 的 操作 比如 排序 什么的, 可以在 指定 的 范围(Range) 内 进行, 比如 数组 有 1000 个元素, 我只对 第 200 ~ 400 个 元素 排序, 这只需要 在 调用 Sort() 方法 时 指定 开始 index 和 结束 index 就可以了,比如:
int [] iArr ;
……
iAr.Sort( 200 , 400 ) ;
如果 实在 要 封装,自己 弄个 结构体 或者 对象 把 开始 index 和 结束 index 包装起来就行,比如:
struct Span
{
int beginIndex,
int endIndex
}
void Foo( Span span )
{
int [] iArr ;
……
iAr.Sort( span.beginIndex , span.endIndex ) ;
}
面向对象 爱好者 们 可能 喜欢 把 Sort() 也 封装到 Span 里,
class ArraySpan<T>
{
private T [] arr ;
private int beginIndex ;
private int endIndex ;
public ArraySpan(T [] arr, int beginIndex, int endIndex)
{
this.arr = arr;
this.beginIndex = beginIndex ;
this.endIndex = endIndex ;
}
public Sort()
{
this.arr.Sort( this.beginIndex, this.endIndex ) ;
}
}
这也可以, 不过 这是 程序员 的 事,不是 语言 的 事, 或者说 这是 库 的 事, 程序员 可以把 自己的 封装 贡献出来, 作为 库 。
另一方面 , List<T> 当然也要, 不仅 List, 还有 Dictionary, Queue, LiinkedList, Stack 这些都要, D# 当然也要有 一个 System.Collections 。
不过 大量 数据 的 存储操作 仍然是 数组 。
可以用 List 来存放 多个 数组 。
经过一番思索(过程 略), 我决定这样来设计 D# 的 数组 :
D# 有 2 种 数组, unsafe 数组 和 托管数组, unsafe 数组 也叫 原生数组, 就是一个 C 语言 数组, 也可以说是 数组首地址,或者 指向 某个元素 的 指针,
托管数组 是一个 对象, 内部 包含了一个 unsafe 数组, 注意, 重点来了,
托管数组 是 用 泛型 实现的, 托管数组 是一个 泛型类,
这样, 对于 多维数组, 我们 需要像 C# 的 Action 一样, 预先定义好 多个 数组类型,
就像 C# 的 Action 有 这些 定义 :
Action, Action<T>, Action<T1, T2>, Action<T1, T2, T3>, Action<T1, T2, T3, T4> , ……
D# 的 会 定义这些 数组类型 :
Array1<T>, Array2<T>, Array3<T>, Array4<T>, Array5<T>, ……
Array1 是 一维数组, Array2 是 二维数组, Array3 是 三维数组, ……
D# 允许 给 索引器(中括号 [ ] 通过 逗号 隔开 传入 多个 参数),
比如
Array2<int> iArr = new Array2<int>() ;
iArr [ 2, 3 ] = 10 ;
这样的话, 实际上, 程序员 完全 可以 自己 来 实现 托管数组, 可以自由的 实现 n 维 的 托管数组,
也就是说, 托管数组 不是 语言 的 事, 是 库 的 事, 程序员 可以 自由 的 实现 托管数组, 可以把自己 实现 贡献 出来, 作为 库 。
社区 里 可以 有 多种 托管数组 的 实现(库), 程序员 可以 任意选择 使用, 更可以 自己实现 贡献 。
这就是 “库” 的 理念 。
这是一种 简洁 开放 的 语言 理念 。
当然, unsafe 数组 由 D# 提供 。
这样的做法 有点好笑, 但是 很爽 。
我们 再 回过头来 看看 C# 的 数组, C# 有一个 Array 类, 内部 做了 很多工作, 但是这些工作 程序员 很难了解, 也不能参与, 这就是 C# 经常挥舞 的 ”CLR 技术“ 。
我们再来看看 数组 的 方法, 比如 Sort() 方法 , 我们以 一维数组 为例, 为什么要以 一维数组 为例, 因为 多维 数组 的 排序 说不清楚,,当然,你可以去 实现 你自己的 多维数组,包括排序 。
排序 中 会 对 元素 进行 比较 运算, 对于 基础类型, 比如 int float double, 比较运算 就是 > < == != >= <= 运算符 的 运算,
但是,为了 复用代码, Array1<T> 希望 对于 所有 的 T 类型 都 能够 用 同一个 Sort() 方法, 这就需要 所有的 T 类型 都支持 > < == != >= <= 运算符 的 运算,
这就涉及到 运算符 重载 。
也可以不用 运算符 重载, 而用 C# 里的 Comparer 的 方式, 给 Sort( IComparer comparer ) 方法 传一个 Comparer 对象,
但是, 对于 基础类型(int float double char 等) 来说, 调用 Comparer 的 方法 比较大小 比起 用 > < == != >= <= 运算符 会带来一些 性能损耗 。
为了 保持 基础类型 的 原生效率, 所以 需要 运算符 重载 。
可以声明 这样 一个 Template :
template TCompareOperator <T>
{
bool operator = ( T val1, T val2 ) ;
bool operator > ( T val1, T val2 ) ;
bool operator < ( T val1, T val2 ) ;
bool operator >= ( T val1, T val2 ) ;
bool operator <= ( T val1, T val2 ) ;
bool operator != ( T val1, T val2 ) ;
}
然后, 把 TCompareOperator 作为 数组 的 泛型约束,
比如,
class Array1<TCompareOperator T>
{
……
public void Sort()
{
……
}
}
但是这样的话, Array1 的 元素 类型 只能是 实现了 TCompareOperator 的 类型 。这就有了局限,
可以把 Array1 的 名字 改成 SortableArray1 , 哈哈哈哈 。
如果要让 元素 仍然 支持 所有 类型 的话, 可以 把 Sort() 方法 变成一个 静态方法,
public static void Sort< TCompareOperator T >( Array1<T> arr )
Template 是 仅用于 值类型 的, 值类型 包括 基础类型(int float double char 等) 和 结构体,
Interface 则 用于 引用类型(对象),
为了 让 Sort() 方法 支持 值类型 和 引用类型 的 元素, 还需要 给 泛型约束 加上 引用类型 的 泛型约束 :
public static void Sort< TCompareOperator ICompareOperator T >( Array1<T> arr )
ICompareOperator 是一个 实现了 > < == != >= <= 运算符 重载 的 interface ,
除了 interface, 基类 也可以作为 引用类型 的 泛型约束, 就是说, 基类 和 Interface 都可以 作为 引用类型 的 泛型约束 。
结构体 本身 应该也能 作为 结构体 的 泛型约束, 这 表示 这种 结构体 和 它 的 子类 结构体 可以 作为 泛型参数 。
问题是 结构体 可以继承,但是 不能 多态, 所以 要 注意 子类 结构体 实际上 被 调用 的 方法 是 哪个 。
结构体 的 继承 纯粹 是 为了 复用代码, 子类结构体 不能 override 基类结构体 的 方法, 但是可以用 new 覆盖,
子类 结构体 的 值 和 指针 不能 赋值 给 基类 结构体 的 变量 和 指针变量 。
注意, interface 这里 只作为 约束,不会 使用 多态, 因为 具体类型 会通过 泛型参数 T 指定, 所以, Sort() 方法 中 对于 T 对象 的 方法调用 仍然 是 静态编译 时 决定的 , 和 普通 的 调用 对象方法 是 一样 的 。
除非是 A 类 实现了 I1 和 I2 两个接口, 这两个接口 有 同签名方法 Foo(), 而 Sort() 方法中 需要 调用 Foo(),
这种情况 就要看 C# 中 对于 这样的情况 会不会 报错 :
A a = new A() ;
a.Foo() ;
A 类 对 I1 I2 两个接口 的 同签名方法 Foo() 分别进行了 显式实现,那么 在 C# 中上述 a.Foo() 代码 会不会 报错?
如果不报错,那调用的 Foo() 方法 是 哪个 接口 的 实现 ?
等, 我们在说 D# ,怎么说到 C# 了? 没错,就是说 C#, 看看 C# 怎么处理 这种情况 ,
如果 会报错, 那么 对于 D# , 可以 先把 a 对象 转型为 要调用 Foo() 方法 的 那个 接口类型, 比如 I1 a1 = (I1)a ; , 再传给 泛型方法,
比如, 有个 方法 void Hello< I1 T >() , 那么 ,
A a = new A() ;
I1 a1 = (I1)a ;
Hello<I1>( a1 ) ; // 这里将 T 声明为 I1, 如果不是这种 显式实现 多接口 同签名方法情况的话
看 Hello<I1>( a1 ) ; , 这里将 T 声明为 I1, 如果不是这种 显式实现 多接口 同签名方法情况的话,应该是
Hello<A>(a) ; 将 T 声明为 A,
而 将 T 声明为 I1, 意味着 泛型方法 中 对 a.Foo() 方法 的 调用 就是 使用了 多态 。
多态的话, 会在 向上转型 (I1)a 时 会 查找 虚函数表, 这是 查找一个 Hash 表 , 所以 多态 比 普通方法 调用 会多这么一个 性能损耗, 当然找到 虚函数表 后, 还要在 虚函数表 中 查找 函数指针, 这里会比 普通方法 的 动态链接 多作一个 加法, 当然这个 加法 的 性能开销 基本可以忽略 。
所以, 能够不使用 多态 的话 , 就 不要 使用 多态, 当然 这是说 这里 指定 泛型参数 T 的 场合,不是说 其它 场合, 不然 的话 面向对象 爱好者 可能会 生气 的 。^^
当然, 从上面举例可以看到, 指定 泛型参数 一般情况 都可以 直接 指定 具体类型 A , 只有 A 实现了 2 个 或 多个 interface, 这些 interface 又有 同签名 的 方法,A 对 这些 同签名 方法 显式实现 的 情况 才会需要将 A 向上转型 为 interface, 然后指定 泛型参数 T 为 interface 。
好吧, 其实不用这样, 就算 显式实现 多接口 同签名方法, 其实也不用 向上转型, 还是
Hello<A>(a) ;
就可以 , 我们看 方法 定义:
void Hello< I1 T >()
这里的 T 的 约束 是 I1 , 编译器 可以 据此 推断出 应该调用 A 实现 I1 的 Foo() 方法 , 所以仍然在 静态编译 时 就可以决定 调用 A 显式实现 I1 的 Foo() 方法 。
啊哈哈, 其实上面主要是 把 问题分析一下, 并且 把 D# 实现多态 的 原理 再 梳理一下 。
另外,还引出一个问题, Array.Sort() 方法
public static void Sort< TCompareOperator ICompareOperator T >( Array1<T> arr )
如果要对 参数 arr 像上面说的那样 向上转型 的话, 就 不是 I1 a1 = (I1) a ;
而是
Array1<A> arr = new Array1<A>( 10 ) ; // 参数 10 是 数组长度
Array1<I1> arr1 = ( Array1<I1> ) arr ;
这就是 Array1<A> 向上转型为 Array1<I1> 的 问题 了, 相当于 C# 里的 A [ ] 转 I1 [ ], 或者 List<A> 转 List<I1> ,
C# 里 好像 不允许 这样转, 但我想 D# 应该允许 。 ~~~
为了让 Sort() 方法 支持 基础类型 和 自定义 结构体, 基础类型 当然 会 内置 标记 为 实现了 TCompareOperator 。
虽然 标记 了 实现 TCompareOperator , 但是 基础类型 和 自定义 结构体 运算符 重载 的 实现方式 是不一样的,
基础类型 是 原生 的 C 的 运算符 运算, 而 结构体 的 运算符 重载 是一个 函数调用 。
说到 函数调用,会想起 内联, 别跟我说 内联, 我不太打算在 D# 里 支持 内联,
内联 是个 复杂的 东东, 是个 麻烦事,
另外, D# 的 第一目标 是 保持 基础类型 的 原生效率, 至于 自定义 类型(包括 结构体 和 对象), 调用 函数 的 性能损耗 不算什么 。
而 现代 CPU 架构 的 寄存器 越来越大, 所以 函数调用 入栈出栈 传参 返回值 的 开销 应该 越来越 稀释 。
这是 运算符 重载 的 方式, 上面说了, 还可以有 IComparer 的 方式,
比如 , 声明一个 IComparer< T> 接口 :
interface IComparer<T>
{
CompareResult Compare( T val1 , T val2 ) ;
}
enum CompareResult
{
Greater,
Less,
Equal
}
Sort() 方法 声明为
class Array1<T>
{
public void Sort( IComparer<T> comparer )
{
……
}
}
可以看到, IComparer<T> 的 T 是由 Array1<T> 的 T 决定的, 比如 :
Array1<int> iArr = new Array1<int>() ;
那么, 传给 Sort() 方法 的 IComparer 对象 应该是 IComparer<int> 类型 。
基础类型 也可以用 IComparer 的方式, 用 IComparer 方式的话, 比较 就是一次 函数调用, 对 基础类型 来说, 效率 比 直接 用 运算符 的 方式 低一点 。
进一步, 还要解决一个问题, 就是 按 值 传递 和 按 指针 传递 的 结构体 在 泛型 时 调用方法 的 语法 统一 问题 。
比如, 我们写一个 Hash 表,
class HashTable<THash Object TKey, TValue>
{
public void Add( TKey key, TValue value )
{
int hash = key.GetHashCode() ;
……
}
}
可以看到, 在 Add() 方法 中, 会调用 key.GetHashCode() 方法 来 计算 key 的 hash 值,
TKey 的 泛型约束 是 THash Object, Object 是有 GetHashCode() 方法 的, THash 也有 GetHashCode() 方法, 我们来看看 THash 的 定义:
template THash
{
int GetHashCode() ;
}
我们定义 2 个 结构体, A 、B :
struct A : THash
{
public int GetHashCode( A a )
{
……
}
}
struct B : THash
{
public int GetHashCode( B * b )
{
……
}
}
A B 都 实现了 THash 的 GetHashCode() 方法, A 实现的是 按值传递 的 方式, B 实现的是 按指针传递 的 方式,
按值传递 和 按指针传递 可任选一种,只要 实现了一种 就算 实现了 template 的 方法 , 当然, 2 种 都实现 也可以 。
那么, 问题来了, 对于 按指针传递 的 方式, 比如
HashTable < B *, string > ht = new HashTable < B *, string >() ;
此时, 泛型生成 的 具体类型 是 :
class HashTable~1
{
public void Add( B * key, string value )
{
int hash = key.GetHashCode() ;
……
}
}
可以看到, Add() 方法 中 计算 hash 值 的 代码 是
int hash = key.GetHashCode() ;
key 是 B * 类型, 这里应该是 key -> GetHashCode() ; 而不是 key.GetHashCode() ; , 这样就出问题了 。
那怎么解决这个问题? 我提出了 .-> 操作符, 就是 把 . 和 -> 两个 操作符 结合到一起 。
.-> 操作符 只能用于 泛型类 和 泛型方法 内的 代码 以及 只能用于 结构体 和 结构体指针 类型 。 当使用 .-> 操作符 时, 编译器 会 判断 变量 是 结构体 还是 结构体指针, 如果是 结构体, 则 按 . 操作符 处理, 如果是 结构体指针, 则 按 -> 操作符 处理 。
结构体指针 是指 结构体 的 一阶指针, 即 B * , 只有一个 星号 的 指针 , 如果是 二阶 或 多阶 指针,比如 B * * , B * * * , 这种 “指针的指针”, 则 编译器 会 报错 。
这样就很好的实现了 对 结构体 的支持, 这很重要, 这意味着 D# 可以 支持 高性能 的 底层 代码, 这可以有效的 支持 D# 应用于 底层模块 、 基础应用 、 高性能中间件 、 游戏 。 C++ 的 很多 应用场合 D# 都能 胜任 。 D# 又叫 D++ 。
D# 判断 是否是 unsafe 代码 的 标准 是 是否使用了 指针, 使用了 指针 就是 unsafe, 不使用指针 就 不是 unsafe 。
D# 鼓励 使用 unsafe 代码 。
在 底层模块 、 基础应用 、 高性能中间件 、 游戏 里 都 需要 大量使用 unsafe 代码 。 不需要 也 不提倡 使用 unsafe 代码 的 是 业务系统, 但是 业务系统 使用的 各种 基础库 内部 应该是 unsafe 代码 实现的, 比如 集合(List, Dictionary, Queue, Stack, LinkedList 等) 、 Socket 、 IO(File 等) 等等 。
还有 第三方库, 事实上 基础库 和 第三方库 没有 明显 的 界限, 上面说了, D# 开放的 “库” 的 理念, 任何人 都可以 为 D# / ILBC 写 库,
我们可以在 一定共识 的 基础上, 把 某些库 称为 “标准库”, 但 这也可以不是一定的, 你搞一个 “A 标准库”, 别人还可以搞一个 “B 标准库”, 还可以有 “C 标准库” 、 “D 标准库” ……
总之 想怎么玩 都可以 。
我们可以再来归纳一下 结构体 的 几种 数据模型 :
1 栈 内 结构体,
2 数组 内 的 结构体, 数组 属于 堆, 数组 可以 包含 大量 海量 的 结构体,
3 数组堆
为了 对 第 1 种 模型 , 栈 内 的 结构体 更好的 支持, 还可以提供 动态分配 栈 空间 的 特性 。
那 第 3 种 模型 数组堆 又是什么? “数组堆” 这个 名字 我 是 想出来的 , 也不一定 很贴切 。 就是 一次 申请一个 很大 的 byte 数组, 然后在 这个 数组 里 来 分配空间, 这样就不用 每次 需要 分配内存 都要 通过 堆 。
这种做法 在 C++ 里 也许叫做 “内存池” 。
想到 数组堆 是 因为 上面 说 基础应用 的 时候 提到 编译器, 我想 早期 的 编译器 可能 会 申请一个 比较大 的 byte 数组, 然后 把 生成的 语法成员树 保存 在 这个 byte 数组 里, 每个 语法成员 是一个 结构体, 每次可以为 这个 结构体 在 数组 里 分配一段空间,把 结构体 保存在 这个 数组 的 一段空间 里 。
只需要有一个 变量 p 保存 剩余空间 的 首地址 就可以了, 每次分配后, p 的 值 加上 分配的空间 大小 , 即 p += 分配的 Size ; ,
这有点类似 栈, p 就相当于 栈顶 , 等 ,,,, 好像就是 栈 。
这和 堆 的 区别 和 优点 是, 数组堆 的 分配 非常快, 就是 p += 分配的 Size ; 这样一个 加法 和 赋值 运算 。 而且 没有 堆 的 碎片问题, 包括 从 碎片(空闲空间) 中 寻找 最接近 分配大小 的 碎片 来 分配 的 问题, 以及 从 碎片 中 分配 后 产生 新 的 碎片 等等 复杂 的 堆 管理 问题 。
当然,相对的, 数组堆 就 没有 堆 那么 灵活 。 因为 数组堆 回收 空间 通常 时 整个 数组 全部回收, 也就是 p = 数组首地址 ; ,
可以 在 一个 任务 或者 请求 内 使用 数组堆, 比如 每次编译 使用一个 数组堆, 编译的结果(语法成员树) 就 存放在 这个 数组堆 里, 编译过程 中 产生的 表示 字符数组段 的 Span 结构体 也可以 放在 数组堆 里, 当然 可以 放在 另一个 数组堆, 语法成员 和 Span 各自 使用 一个 数组堆 。 这样 每次 编译 2 个 数组堆 搞定 。
如果 涉及 到 并发, 即 多个 任务 / 请求 / 线程 之间 需要 共享数据, 那 共享数据 不适合用 数组堆, 应该 直接 使用 堆 。
大概 是 这样 。
不考虑 并发 的话, 就 不需要有 lock, 这样, 从 数组堆 里 分配 就是一个 加法赋值 p += 分配的 Size ; , 清空 数组堆 就是 一个 赋值 p = 数组首地址 ; , 这个速度 比起 堆 的 分配 和 回收 , 简直 快到飞起 。
接下来该说什么了? 说说 Attribute, 说实在的, 真不想在 D# 里 支持 Attribute, 这会让 编译器 变得 复杂, 另外, 我是个没有耐心的人, 宏伟计划 要 做 的 事 越来越多 难免让人 心烦 。
Attribute 在 开发实践 中 最大的 用处 大概 是 ORM, 用于 描述 数据库 表 - 类 列 - 属性 的 对应关系 。 对于 D# 而言, Attribute 最大的用处 是 [ DllImport( ) ] Attribute , 这个 用于 描述 对 外部库 函数 的 调用, 这很重要 。
调用 外部库 是 ILBC / D# 的一个 核心特性 模块 基因 。
用 Attribute 描述 外部库 函数 确实 很方便, 比 关键字 的 描述能力 强很多, 而且 不需要 语言 增加 关键字 来支持 。
.Net / C# 内部 用 Attribute 倒用的很 嗨, 很多 与 开发者 无关 的 JIT / CLR 特性 都是用 Attribute 描述的 。
但是我们再看看 Ruby 和 Javascript , Ruby 有 Attribute 吗? Ruby on Rail 好像玩的很溜, Javascript 有 Attribute 吗? Javascript 的 动态特性 是 最吸引人的一个特性 。
不过总的来说, Attribute 好像还是得支持一下, 毕竟 一个 完备的 元数据 体系 还是需要的 。
支持 Attribute 也不难, 编译器 加入 对 Attribute 的 支持 也可以保持 架构 的 简洁, 这问题不大, ILBC Runtime 对 Attribute 的 支持 也 不难, 就是个 工作量 的 问题, 因为 ILBC 本来就有 元数据系统, 这是 动态链接 和 反射 的 基础, ILBC 动态链接 本身 就是 基于 反射 的, 反射 基于 Hash 表 查找, 当然 Hash 表 和 链表 可以共存, 就是说 成员(类 字段 方法) 同时保存在 Hash 表 和 链表 里, Hash 表 用于 按 名字 查找, 链表 用于 遍历 。
Attribute 只不过是 原本 类 字段 方法 的 元数据 上 增加一些 属性, 或者说 增加一个 属性集合 。
再来看看 StackTrace, 说实在的, StackTrace 我也不想支持, 支持 StackTrace 是个 麻烦 事, 估计要做比较多的工作,虽然 说起来 两句话 就能说清楚, 比如 “在 编译 的 时候 在 每个 方法调用 的 时候 生成一段 代码 用于 记录 StackTrace” , 说起来简单, 但 估计 也很烦, 主要 是 我们 没有那么多时间和精力, 这很容易分散我们的时间和精力 。
而且, 可以看到, StackTrace 会 在 函数调用 时 做一些 额外的工作,所以是会产生 性能损耗 的。
但是, 如果不支持 StackTrace, 会不会像 C / C++ 那样 太原始 太裸 ?
不过我们 看看 Chrome 里的 javascript 报错时 有没有 StackTrace ? 如果没有,那 D# 也可以没有 。 ^^
ILBC / D# 应该是一个 可以像 脚本 一样 容易的使用 的 AOT / Native 语言平台 。 就像 Python , Javascript 。 当然 ILBC 是 AOT / Native 的 。
但是 从 使用上来说, ILBC / D# 应该可以像 脚本 一样 容易的 使用, 就像 Python , Javascript 。
当然,你可以用 ILBC / D# 开发出各种类型和 风格 的 语言,比如 AOT, JIT 脚本, 面向对象, 函数式 等等 。
D# 应该用 动态编译 D# 代码 取代 C# 的 Emit 和 表达式树 。
C# 的 Emit 和 Expression 是 笨重 的 堆砌 。
协程 是 ILBC / D# 的 一个 核心特性 模块 基因 。
有关 协程, 见《协程 的 实现方法》 https://www.cnblogs.com/KSongKing/p/11127010.html 。
再谈谈 调试 的 问题, 调试, 是 IDE 的 部分, 作为一个 开放 自由 有生命力 的 语言平台, 是不应该 依赖于 IDE 的,
我们 欢迎 IDE 提供好的支持, 但是 语言平台 不应该 依赖于 IDE 。
看看 宇宙第一 IDE 和 C# 的 关系 就知道了, 离开 Visual Studio , 怎么开发 .Net 程序? 这不可想象 。
这不仅仅 是 对 C# 语法 的 语法错误 和 智能提示 的 支持, 还包括 对 具体 的 程序项目 的 支持,
比如, WinForm 程序, 没有 Visual Studio ,你怎么写?
Asp.net 程序, 没有 Visual Studio , 你怎么写?
而且 Visual Studio 的 WinForm , Asp.net 项目 拿给 VsCode 是不能直接编译的, 这是我猜的, 因为我没怎么用过 VsCode 。
这些现象, 表示 这不是 程序员 要的 “理想国” 。
ILBC 要实现的,是一个 用 记事本 也能写 程序 的 语言平台, 这是 程序员 的 理想国 。
这其实 很简单, 我们只需要一些 简单 的 规则 就可以实现, 比如, 最简单的, 编译器 是一个 命令,我们可以告诉 编译器 一个 路径, 这个 路径 就是 项目的根目录, 编译器 会 寻找 这个 目录 和 所有 的 子目录 里的 源代码 文件 进行编译, 那么 对于 Bin 目录, 或者 资源目录 等等一些 我们 需要 编译器 忽略 的 目录 怎么办?
可以类似 Git, 在 项目目录 下 放一个 ilbc_src.ignore 的 文件, 里面声明 需要 忽略 的 目录, 就可以了 。
甚至, 可以比 Git 还简单, ilbc_src.ignore 只允许 声明 项目目录 下 的 第一级 子目录, 这样 就 太简单了 。
实际上, 这也够用了 。
编译器 对 项目目录 下的 源文件 编译, 会把 所有的错误 都在 控制台 里 列出来, 哪个位置是什么错, 这和 Visual Studio 或者 其它 IDE 是一样的 。
对于 像 WPF, Asp.net 这种类型 的 项目, 有 Xml 格式 的 前端代码(文件), 这也没问题, 你可以用 Xml 编辑器 来写 前端代码(文件), 当然, 用 记事本 也可以 。 ^^
然后, 编译器 同样 对 项目目录 下 所有的 源代码文件, 包括 前端文件 和 后端文件 进行编译 , 并显示 所有错误 。
所以, 不管 后端代码 还是 前端代码 , 你可以选择 任意的 文本编辑器 来 编写, 然后 交给 编译器 来编译 。
你也可以 根据上述 规则 开发一个 IDE , 这都可以 。
你的项目 拿给 别人, 别人 可以用 自己的 文本编辑器 和 编译器 来 继续 开发 和 编译,也可以用 IDE 。
在这方面, Chrome 似乎 干的不错, 你可以用 任意 的 文本编辑器 写 Javascript, 然后 Chrome 的 DevTools 可以让你很方便 的 调试程序 。
相比之下, Visual Studio 在 不同 版本间 都 可能 不支持 互相 打开项目 。
等, 我们是说 调试 的, 怎么说着说着 就 跑题了 ? 调试 是一个 麻烦事, 原理 大概 是这样:
首先,要 调用一个 操作系统 提供的 API “附加进程”, 调用了这个 API 就可以 把 系统的 调试器 附加 到 要调试的 进程, 之后 进程 每执行一条指令 大概都会发起一个中断, 系统会在这个中断里 调用 我们的 处理程序, 我们的 处理程序 可以知道 当前是 在 执行 第几行 的 反汇编代码, 根据 汇编代码 的 行数 和 指令 去 “符号表” 中 寻找 这一行 对应的 源代码 是 哪一行, 并且 指令 也要 和 符号表 中 对应的 源代码行 的 指令 符合, 这样就可以 单步调试 了 。
符号表 就是 比如 .Net 里的 pdb 文件, 在 编译时 会 生成 汇编代码 和 源代码 的 对照表, 这个 对照表 就是 符号表 。
当然,以上 这些 是 我 猜 的 。
这些 实际上 做起来 很麻烦, 而且 这些是 最基本, 一般 调试 除了 单步调试, 还有 Step Over Function, Step Out Function, 直接运行到下一个断点, 还有 查看 局部变量 全局变量 。
所以, 我建议, 不必将 精力 放在 实现 调试 上, 但是, 应该提供 控制台 输出 的 支持 。
控制台输出 是 最古老 的 调试方式, 也是 永不过时 的 调试方法 。
数组堆
template interface
值类型 结构体 结构体 方法, 传值, 传指针, 泛型, 数组 ,集合类泛型, .-> 操作符, > < >= <= == 运算符重载, Comparer,
栈 结构体, 对象图,
底层应用,基础应用, 图形图像,2D 3D,游戏, 视频音频, 视频采样,音频采样,人工智能,大数据,数据库,文档数据库,大规模并行计算文档数据库,搜索引擎,爬虫,编译器 , 文本编辑器, 大型文本编辑器 。
Attribute,
调用 外部库 是 ILBC / D# 的一个 核心特性 模块 基因 。
StackTrace,
ILBC / D# 应该是一个 可以像 脚本 一样 容易的使用 的 AOT / Native 语言平台 。
当然,你可以用 ILBC / D# 开发出各种类型和 风格 的 语言,比如 AOT JIT 脚本,面向对象,函数式 等等 。
D# 用 动态编译 D# 代码 取代 C# 的 Emit 和 表达式树 。
调试
ILBC 源代码 项目 规范
协程 是 ILBC / D# 的 一个 核心特性 模块 基因 。
template 和 interface 同时作为一等公民
D# 是程序员的语言,不需要太多包装和修饰,
D# 是 简单的, 编译器 和 IDE 都是, 程序员 的 语言 是 简单的
let , 类似 js 的 let, 用以 支持 完备 的 闭包 特性,一般 的 变量 相当于 js 的 var , 加上 let 相当于 js 的 let,比如 let string s ;
可以起到 在 循环 中 给 闭包 传递 变量 的 作用,
Task 库
返回多个 返回值,比如 D# 中
public (int i, string s) Foo()
{
int i;
string s;
…… // 对 i, s 操作
return (i, s);
}
var r = Foo();
r.i …… // r.i 表示返回的 i
r.s …… // r.s 表示返回的 s
编译为 InnerC 是这样:
struct R<>1
{
int i
string s,
}
Foo()
{
R<>1 r ;
r.i …… // 对 i 操作
r.s …… // 对 s 操作
……
return r ;
}
R<>1 r = Foo() ;
r.i …… // r.i 表示返回的 i
r.s …… // r.s 表示返回的 s
D# 是否 要 支持 async await 语法?
我曾经想过用 闭包 + 观察者 来 取代 async + 职责链, 但 后来 发现, 闭包 和 职责链 好像 更配,这 有点讽刺 ……
但 其实 闭包 + 观察者 也是 可以的, 比如 Cef 中 ResourceHandler 的 方法里 可以 return true / false ; 和 callback.Continue() ;
如果 当前 就处理完了 任务, 则 可以直接 return true / false, 告知 Cef 可以进行下一个任务,
如果 当前 任务 异步 执行, 则 可以 等 异步任务 完成时 调用 callback.Continue() 告知 Cef 可以进行下一个任务 。
今天看到 群 里 有 网友介绍 kooboo json 使用 表达式树 提高效率 , https://github.com/Kooboo/Json/blob/master/Kooboo.Json/Formatter/Deserializer/Expression/CollectionBuild.cs
这样当然很好, 但是我还是觉得 表达式树 太复杂 了,我觉得应该写 C# 代码 来 动态编译 就可以 。
比如
“ obj.name = dataRead [ \“ name \” ] ; ”
这是 ORM 里 给 Entity 属性 赋值 的 代码, 这可以算是 C# 脚本 。
这种做法 只是 第一次 编译时 可能 比 表达式树 多一点时间 , 编译完成后 运行效率 是一样的 。
其实 第一次 编译时 也多不了 多少时间, 只是 多了 文本语法分析 的 时间 。
以后 D# 也要这么干, 提供 动态编译,支持 D# 脚本 。
D# 是 不会 提供 C# 那样的 表达式树 的 。
C# 的 表达式树 就是 面向对象 泛滥 的 结果, 认为什么都可以用 对象 来表达,
其实 字符串 表达的会更好 。
在 D# 看来, C# 的 Emit 和 Expression 是没有意义的堆砌 。
在 D# 脚本 看来, C# 的 Emit 和 Expression 是 不需要 的 。