基于 C# 反射特性的脚本语言
快速创建一个 RikaScript 运行环境:
var rs = new Engine(new ConsoleLogger());
rs.Execute("log('Hello, World')");
运行即可在控制台看到 Hello, World。
快速构建一个 RikaScript 尝鲜环境:
var engine = new Engine(new ConsoleLogger());
while (true){
Console.Write("RS > ");
engine.Execute(Console.ReadLine());
}
通过 RikaScript ,可以快速调用预先准备好的 C# 方法并进行简单的编程计算:
log("Hello, World")
// [INFO] Hello, World
var num = 2.33
log(num)
// [INFO] 2.33
这个语言一个特点就是“函数=运算符”,看似是运算符的操作其实都是语法糖:
log(1 + 1)
// 等同于
log(+(1,1))
只不过对函数进行了额外的封装,使其可以正确计算优先级关系:
log(1 + 2 * 3)
// 7
这个语法产生的缺点,导致运算符两侧必须加上空格,1+1
是不正确的写法。
另外注意只有两个参数的函数可以这样写:
make 1 // 错误
1 make // 错误
1 + 1 // 正确
1 log 2 //正确,因为log有一个两个参数的重载
RikaScript 是弱类型的脚本语言,数据在 C# 运行环境中都保存为object类型,在执行函数时会转换成相应的数据类型。
RikaScript 中默认数据类型是 double
和string
,不要对用 double 产生多余的性能担忧,因为解释这个语言本身就已经浪费了很多性能,可以通过 f\i\l
或 F\I\L
作为数字结束符号来指定数字的确切类型。
log(type(1L))
// [INFO] System.Int64
log(type(1.0))
// [INFO] System.Double
类型转换用int
float
str
三个函数,虽然它们叫做 int、float,但结果还是转换成了对应的 long
double
string
类型。
使用 var 声明变量,使用 set 给变量赋值:
var a = 12
set a = 10
// += 会被翻译成 a = a + 100
// 所以这个等号前面可以写任何支持的中缀运算符
set a += 100
var 的变量如果已经存在,那么就会更新变量的值,如果不存在就创建
set 的变量如果已经存在就更新值,否则报错错
set 变量的时候,如果当前作用域没有,则会向上级作用域寻找变量
我特地省略了 if 或 while 语句条件表达式的括号:
var num = 0
while num < 10 {
if num % 2 == 0{
log(num)
}
set num += 1
}
// 输出 0 2 4 6 8
注意这个语言的代码块开头括号{
不能写在语句的下一行,结束代码块的}
要求单独写一行,前后不能有其他东西,除了空格。
if 语句同理
// 可以有中文变量的!
if 上班时间 >= 12{
log('996 ICU!')
}
使用 if xxx return
的方式可以退出当前代码体。
这种操作在 while 中使用就类似 continue:
set n = 0
while n < 10{
set n += 1
log(n)
if n > 4 return
log('----------')
}
// 前几行带有横线分隔,后面几行没有。
在 if 中使用就忽然结束这段代码了:
if true{
log(1)
if true return
log(2)
}
// 只有 1 ,没有 2
在 func 中使用就类似在 if 中使用的效果,或类似无返回值的 return:
func test(){
log(1)
if true return
log(2)
}
test()
// 只有 1 ,没有 2
不在任何代码段里写 if xxx return,就可以结束当前文件的执行,其实还是类似 return。
使用 func 关键字定义函数,剩下的闭眼睛写就行:
// 返回两个数里最大的
func max(a, b){
var res = a
if b > a{
set res = b
}
res
}
log(max(1,2))
// [INFO] 2
RikaScript 没有单独的 return 语句,函数的返回值就是函数内执行的最后一行代码,这行代码不能被 if 或 while 等包裹,所以建议函数最后一行当做返回值就行。
自己定义的函数目前不支持中缀表达式调用!
除了直接调用函数,还可以使用内置的 call 方法调用:
import("RikaScript.Libs.DataStructLib")
// 第一个参数是函数名,第二个是表示参数列表的list
log(call('Max',ds.create('list') push 10 push 20))
ds是导入的数据结构库的默认别名,相信大家看得懂那个 ds.create('list') push 10 push 20
通过 import("类库全名","别名")
的方式导入类库,通过别名点取类库下的方法。
别名可以不写,每个类库有自己的默认别名,可以导入后通过 show_libs() 查看导入的全部类库别名。
目前支持的类库有:
RikaScript.Libs.DataStructLib
RikaScript.Libs.FileLib
RikaScript.Libs.RandomLib
RikaScript.Libs.StringLib
RikaScript.Libs.TimeLib
RikaScript.Libs.StandardLib (标准库已经导入)
可以一个个导入一下,看看 help()
每个 RikaScript 运行时都会自带上 std 类库,这个库在 C# 中对应的 class 是 RikaScript.Libs.StandardLib ,其中包含了 RikaScript 几乎必备的函数,且这些函数都不能被覆盖。
std.log("Woooooooo~")
// [INFO] Woooooooo~
如果要使用其他类库,可以在 C# 代码中对 Runtime 运行环境调用 AddLib 方法添加一个类库:
// C# 代码,实例化异步 RikaScript 执行引擎
var run = new AsyncEngine(new ConsoleLogger());
// 添加文件操作类库
run.Runtime.AddLib(new FileLib()); // 这里可以给类库一个别名,这个类库别名默认是 file
可惜的是 v0.6 版本中删除了 FileLib 类库,计划 v0.7 中重做
然后在 RikaScript 中通过别名调用(当然我没有改过别名):
file.write("D:\test.txt", 'Wooooooo~')
// 然后你的磁盘可用空间就少了12字节
每个类库都会尽量提供一个help()方法用来显示帮助,例如 file 类库:
file.help()
FileLib:file - RikaScript 基本文件交互库 read(path) - 读取文件内容并返回 write(path, text) - 覆盖写入文件 append(path, text) - 追加写入文件 set_encoding(encoding) - 设定类库采用的文件编码,例如utf-8、gbk set_new_line(str) - 指定追加文件时的前置符号,默认是换行符
如果不想在 C# 中导入类库,还可以在代码中使用 std 类库的 import 方法导入,这要求类库必须有一个可用的空参构造方法:
// 随机数类库作为例子,这个库的默认别名是random,嫌长就可以改成r来用
std.import("RikaScript.Libs.RandomLib", "r") // 可惜的是这个随机数类库也在v0.6中删除了
std.log(r.range(0, 100))
// [INFO] 12.5500561262248
当然你可能会纳闷,为啥之前写代码都没加std
前缀,这是因为导入每一个类库函数的时候,都会存两份,一份有类库前缀,一份没有类库前缀,所以两种方式都可以调用类库。
当然如果函数名重名了,后导入的无前缀函数名会覆盖之前那个,这时就只能用带前缀的方式调用函数了。
写C#类库时,可以通过给 Method 特征加上 Keep = true 来让这个函数不会被覆盖,例如 std 类库中的全部方法都是这样做的。
运行环境主要指 Runtime
、Engine
这两个类
整个系统最核心的就是 Runtime 类,这个类实现了代码的逐行执行、变量报错、参数传递、注释忽略等工作,但它不能支持结构性的语句,比如func、if、call等,只能调用函数和保存变量,这个类的 Execute 方法可以一次遍历解决一条字符串代码。
Engine 对 Runtime 进行一些包装,实现了复杂的语法,可以完整的执行上文介绍过的那些代码,例如 if、while、func等。这里还存储了过程、提供了set、if xxx {}这样的语法糖支持
还有个 AsyncEngine 继承自 Engine ,实现了异步执行,让 RikaScript 不干扰主线程任务。但没怎么测试过稳定性,请谨慎使用。
RikaScript 的类库都要继承自 ScriptLibBase
类。这个父类中实现了一些每个类库的基本功能,比如类库信息和获取帮助相关代码。
给 class 增加 Library 特征标签来设定一些额外信息,这几乎是必须加上的
RikaScript 导入类库时,不论是通过在C#中AddLib还是 RikaScript 中 import ,实质上都是实例化了一个类库对象,因为可以通过 imprt 时指定别名来同时引入多个相同的类库。
基于 ScriptLibBase 扩展自己的函数时,需要给方法增加 Method 特征标签来暴露函数,这种函数要求返回值和参数必须都是object类型。需要注意 RikaScript 可直接调用的函数的返回值和参数类型必须都是 object,因此这些函数重载只能通过不同参数数量来实现,并且参数数量最大不能超过4个。
Method 中包含别名设置、优先级设置、持久设置和帮助设置,例如 std 库的方法:
[Method(Name = "+", Priority = 10, Keep = true)] // 别名、优先级、持久
public object add(object a, object b){...}
[Method(Name = "*", Priority = 100, Keep = true)]
public object mul(object a, object b){...}
[Method(Keep = true, Help = "显示一下全部变量")] // 加了帮助
public void show_vars(){...}
在代码中使用时:
log(1 + 2 * 3) // 结果肯定是7了
log(+(1,2) * 3) // 结果是9
Runtime 类会用到 LoggerBase
类输出信息,继承 LoggerBase 来实现自己的输出,例如为 Unity 实现一个输出类:
using UnityEngine;
using RikaScript.Logger;
public class UnityLogger : LoggerBase
{
public override void Print(object message) {
Debug.Log(message);
}
public override void Info(object message) {
Debug.Log(message);
}
public override void Warning(object message) {
Debug.LogWarning(message);
}
public override void Error(object message) {
Debug.LogError(message);
}
}
然后在创建 RikaScript 时指定上这个类即可:
var rs = new Engine(new UnityLogger());
工具类是 ScriptTools
,可以查看源码获得帮助
在扩展类库开发中,把 object 类型数据转换成基本数据类型的方法就写在了这个工具里
语法 | 意义 |
---|---|
var var[ = value] | 声明变量 |
set var = value | 变量赋值 |
func name[(参数们)] { | 开始定义名为 name 的方法 |
if value { | 开始定义一个if代码段 |
while value { | 开始定义一个条件循环代码段 |
} | 结束代码段方法 |
call func | 执行一个方法 |
if value return | 根据一个变量,判断是否停止当前方法或文件 |
if value func | 根据一个变量,判断是否执行一个方法 |
while value func | 根据一个变量,判断是否反复执行一个方法 |
exec path | 执行一个 RikaScript 文件 |
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。
1. 开源生态
2. 协作、人、软件
3. 评估模型