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    是 不需要 的   。

 

 

posted on 2019-04-14 11:46  凯特琳  阅读(2467)  评论(0编辑  收藏  举报

导航