QLExpression学习使用教程

QLExpress

简介

由阿里的电商业务规则、表达式(布尔组合)、特殊数学公式计算(高精度)、语法分析、脚本二次定制等强需求而设计的一门动态脚本引擎解析工具。

为了解决当时电商规则动态编译、表达式高精度计算、复杂布尔运算、自定义函数和操作符号、语法树生成等需求而设计的。

支持特性

  • 1、线程安全,引擎运算过程中的产生的临时变量都是threadlocal类型。
  • 2、高效执行,比较耗时的脚本编译过程可以缓存在本地机器,运行时的临时变量创建采用了缓冲池的技术,和groovy性能相当。
  • 3、弱类型脚本语言,和groovy,javascript语法类似,虽然比强类型脚本语言要慢一些,但是使业务的灵活度大大增强。
  • 4、安全控制,可以通过设置相关运行参数,预防死循环、高危系统api调用等情况。
  • 5、代码精简,依赖最小,250k的jar包适合所有java的运行环境,在android系统的低端pos机也得到广泛运用。

Maven引入

<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>QLExpress</artifactId>
  <version>3.2.2</version>
</dependency>

例子github源码

Github地址:QLExpressionStudy

图分解

在这里插入图片描述

从语法树分析、上下文、执行过程三个方面提供二次定制的功能扩展

提示

若要展示其中的编译过程,将log4j.properties中的log4j.logger.com.imooc.mapper改成DEBUG。

例子

初次使用

参见 TestQLExpress 类的 testQLExpression函数:

   		ExpressRunner runner = new ExpressRunner();
        /**
         * 表达式计算的数据注入接口
         */
        DefaultContext<String, Object> context = new DefaultContext<String, Object>();
        context.put("a",1);
        context.put("b",2);
        context.put("c",3);
        String express = "a+b*c";//===> 定义规则,计算表达式等等
        Object r = runner.execute(express, context, null, true, true);// 解析规则+执行规则
        System.out.println(r);

Runner执行器设置

创建ExpressRunner的执行器示例时,可以设置下面两个参数用以达到精度计算。

  • aIsPrecise 是否需要高精度计算支持
  • aIstrace 是否跟踪执行指令的过程

执行器执行命令的设置

针对执行器的execute方法,可以设置如下参数。

  • expressString 程序文本,即要执行的表达式或规则
  • context 执行上下文。【IExpressContext对象(如果是Spring的Bean,则创建SpringBeanContext对象) 表示执行上下文】
  • errorList 输出的错误信息List
  • isCache 是否使用Cache中的指令集,多次执行同一语句的情况下用以提高执行效率
  • isTrace 是否输出详细的执行指令信息
  • log 输出的log

从简单的例子来看,QLExpress的运行过程为:

单词分解–>单词分析–>构建语法树进行语法分析–>生成运行期指令集合–>执行生成的指令集合。

其中前4个过程涉及语法的匹配运算等非常耗时,可以设置execute方法的 isCache 是否使用Cache中的指令集参数,它可以缓存前四个过程。
即把 express语句在本地缓存中换成一段指令,第二次重复执行的时候直接执行指令,极大的提高了性能。

或者ExpressRunner设置成singleton(结合spring是非常容易做到的)。

支持普通的Java语法执行

运算符支持

运算符 备注 示例
+,-,*,/,<,>,<=,>=,==,!=,%,++,– mod等同于% a * b
in,like,&&,! in,like类似于sql语法 a in [1,2,3]
for,break、continue,if then else 不支持try{}catch{}
不支持java8的lambda表达式
不支持for循环集合操作
弱类型语言,请不要定义类型声明,更不要用Templete
array的声明不一样
min,max,round,print,println,like,in 都是系统默认函数的关键字,请不要作为变量名
int n=10;
int sum=0;
for(int i=0;i<n;i++){   sum=sum+i;}
return sum;

运算符分类

分类 运算符 示例
位运算 ^~&|<<>> 1<<2
四则运算 +-*/,%,++,-- 3%2
Boolean运算 !,<,>,<=,>=,==,!=,&&,|| 2==3
其他运算 =,?: 2>3?1:0
示例
n=10;
sum=0;
for(i=0;i<n;i++){   
	sum=sum+i;
}
return sum;

参见 TestQLExpress类的 testExpressForOperater函数。

a=1;
b=2;
maxnum = a>b?a:b;

参见 TestQLExpress类的 testTernaryOperator函数。

部分运算符列举

运算符 描述 运算符 描述
mod %等同 for 循环语句控制符
return 进行值返回 if 条件语句控制符
in 类似sql语句的in then 与if同用
exportAlias 创建别名,并转换为全局别名 else 条件语句控制符
alias 创建别名 break 退出循环操作符
macro 定义宏 continue 继续循环操作符
exportDef 将局部变量转换为全局变量 function 进行函数定义
like 类似sql语句的like new 创建一个对象
import 引入包或类,需在脚本最前头 class 定义类
NewMap 创建Map NewList 创建集合

部分说明:

include:在一个表达式中引入其它表达式。例如: include com.taobao.upp.b; 资源的转载可以自定义接口IExpressResourceLoader来实现,缺省是从文件中装载
[]:匿名创建数组.int[][] abc = [[11,12,13],[21,22,23]];
NewMap:创建HashMap. Map abc = NewMap(1:1,2:2);Map abc = NewMap("a":1,"b":2)
NewList:串接ArrayList.List abc = NewList(1,2,3);
exportDef: 将局部变量转换为全局变量,例如:exportDef long userId
alias:创建别名,例如: alias 用户ID user.userId
macro: 定义宏,例如: macro 降级  {level = level - 1}
in: 操作符号,例如: 3 in (3,4,5)
like:操作符号,例如: "abc" like ‘ab%‘

自定义系统函数

max:取最大值max(3,4,5)
min:最最小值min(2,9,1)
round:四舍五入round(19.08,1)
print:输出信息不换行print("abc")
println:输出信息并换行 println("abc")

样例使用

表达式样例
n=10;
for(sum=0,i=0;i<n;i++){
sum=sum+i;
}
return sum;

参见 TestQLExpress类的 testExpressForOperater 函数。

三目运算符样例
a=1;
b=2;
maxnum = a>b?a:b;

参见 TestQLExpress类的 testTernaryOperator 函数。

数组定义
keys = new ArrayList();
deviceName2Value = new HashMap();
deviceNames = ["ng","si","umid","ut","mac","imsi","imei"];
mins = [5,30];

参见 GrammarTest 类的 TestArrayCreate 函数。

Java对象使用
object.amount*2+object.volume

参见 GrammarTest 类的 testJavaObjectOperate 函数。

import org.envision.tqw.study.BeanDefine.ObjectBean;
aObject = new ObjectBean();
aObject.setAmount(100);
aObject.volume = 20;
System.out.println("Volume:\n"+aObject.getVolume());

参见 GrammarTest 类的 testJavaObject2Operate 函数。

使用注意

  • 对象调用属性赋值实际上是调用相应的Set,故而Set和Get方法不能为私有。其次属性私有并不影响调用。例如 aObject.volume = 20;相当于调用 aObject.setVolume(20);
  • 调用对象,确保能访问到对象,即对象的访问权限。此外,该对象所属类需要先import,而且是在脚本定义之前部分【参考java类的使用,import语句一般位于定义类的文件前面部分】。
  • import 自定义类,默认会导入 import java.lang.*,import java.util.*;
数组遍历
list = new ArrayList();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
for(i=0;i<list.size();i++){
    item = list.get(i);
    System.out.println(item);
}

参见 GrammarTest 类的 testArrayErgodicTest 函数。

map遍历
map = new HashMap();
map.put("T","tan");
map.put("Q","qi");
map.put("W","wei");
keySet = map.keySet();
objArr = keySet.toArray();
for (i=0;i<objArr.length;i++) {
  key = objArr[i];
  System.out.println(map.get(key));
}

参见 GrammarTest 类的 testMapErgodicTest 函数。

定义函数

示例

function add(int a,int b){
  return a+b;
};

function sub(int a,int b){
  return a - b;
};

a=10;
return add(a,4) + sub(a,9);

参见 FunctionTest 类的 testFunction1 函数。

预定义装载函数定义

装载表达式,但不执行,例如一些宏定义,或者自定义函数。

runner.loadMutilExpress("",funExp);

操作符

中文操作符替换

如果  (语文+数学+英语>270) 则 {return 1;} 否则 {return 0;}

参见 OperatorTest 类的 testOperateReplace 函数。

二元操作符自定义

1 addT 22 addT 2

参见 OperatorTest 类的 testAddBinaryOperate 函数。

二元操作符定义示例

/**
 * @ClassName: BinaryOperator
 * @Description:
 *      定义二元操作符,该操作符实现
 *      a + ( b + b)
 * @Author: qiwei.tan
 * @Date: 2019/8/26 16:05
 * @Version: 1.0
 */
public class BinaryOperator extends Operator {


    public Object executeInner(Object[] list) throws Exception {
        int a = (Integer)list[0];
        int b = (Integer)list[1];
        return a + b + b;
    }
}
自定义操作符需注意
  • 自定义操作符类需继承com.ql.util.express.Operator
  • 二元操作符和多元操作符考虑实现方式的不同

多元操作符

4 addN (1,2,3)

参见 OperatorTest 类的 testNElementOperate 函数。

多元操作符定义示例

/**
 * @ClassName: AddNOperator
 * @Description:
 *      多元操作符
 *      4 addN (1,2,3)
 *      4+1+2+3
 * @Author: qiwei.tan
 * @Date: 2019/8/29 15:35
 * @Version: 1.0
 */
public class AddNOperator extends Operator {

    public Object executeInner(Object[] list) throws Exception {
        int r = 0;
        for(int i=0;i<list.length;i++){
            r = r + (Integer)list[i];
        }
        return r;
    }
}

操作符的使用

1 join 2 join 3
1 + 2 + 3
join(1,2,3)

操作符定义

/**
 * @ClassName: JoinOperator
 * @Description:
 *      自定义连接操作符(二元或多元运算符【支持成函数列表调用】)
 *      1 join 2 join 3 ===> [1,2,3]
 *      返回ArrayList数组
 * @Author: qiwei.tan
 * @Date: 2019/8/29 11:39
 * @Version: 1.0
 */
public class JoinOperator extends Operator {
    public Object executeInner(Object[] list) throws Exception {
        Object opdata1 = list[0];
        Object opdata2 = list[1];
        if(opdata1 instanceof java.util.List){
            ((java.util.List)opdata1).add(opdata2);
            return opdata1;
        }else{
            java.util.List result = new java.util.ArrayList();
            result.add(opdata1);
            result.add(opdata2);
            if(list.length > 2){
                int index = -1;
                for(Object obj:list)
                    if(++index < 2)
                        continue;
                    else
                        result.add(obj);
            }
            return result;
        }
    }
}

官方Readme文件的定义有误,无法支持当变成function时的多元操作符,故而更改。

addOperator

runner.addOperator("join",new JoinOperator());

实现表达式中的词“name”与自定义操作符的绑定

表达式:

1 join 2 join 3

参见 OperatorTest 类的 testOperateUserDefine 函数。

replaceOperator

runner.replaceOperator("+",new JoinOperator());

实现将内置的操作符定义变换为自定义操作符定义

表达式:

1 + 2 + 3

参见 OperatorTest 类的 testOperateUserDefine2 函数。

addFunction

runner.addOperator("join",new JoinOperator());

实现表达式转换为函数的使用,实际上是多元运算符的一种操作

表达式:

join(1,2,3)

参见 OperatorTest 类的 testOperateUserDefine3 函数。

小技巧

本部分不提供相应案例代码

1、提供表达式上下文,属性的值不需要在初始的时候全部加入,而是在表达式计算的时候,需要什么信息才通过上下文接口获取。

避免因为不知道计算的需求,而在上下文中把可能需要的数据都加入。
runner.execute(“三星卖家 and 消保用户”,errorList,true,expressContext) "三星卖家"和"消保用户"的属性是在需要的时候通过接口去获取。

2、可以将计算结果直接存储到上下文中供后续业务使用。例如:

  runner.execute("c = 1000 + 2000",errorList,true,expressContext); 
  //则在expressContext中会增加一个属性c=3000,也可以在expressContext实现直接的数据库操作等。

3、支持高精度浮点运算,只需要在创建执行引擎的时候指定参数即可:new ExpressRunner(true,false);
4、可以将Class和Spring对象的方法映射为表达式计算中的别名,方便其他业务人员的立即和配置。例如

     //将 Math.abs() 映射为 "取绝对值"。
      runner.addFunctionOfClassMethod("取绝对值", Math.class.getName(), "abs",new String[] { "double" }, null); 
      runner.execute("取绝对值(-5.0)",null,true,null); 

5、可以为已经存在的boolean运算操作符号设置别名,增加错误信息同步输出,在计算结果为false的时候,同时返回错误信息,减少业务系统相关的处理代码

   runner.addOperatorWithAlias("属于", "in", "用户$1不在允许的范围")//用户自定义的函数同样也可以设置错误信息:例如: 
   runner.addFunctionOfClassMethod("isOk", BeanExample?.class.getName(),"isOk", new String[] { "String" }, "$1 不是VIP用户"); 
  则在调用:
     List errorList = new ArrayList?(); 
     Object result =runner.execute("( (1+1) 属于 (4,3,5)) and isOk("玄难")",errorList,true,null); 
    // 执行结果 result = false.同时在errorList中还会返回2个错误原因: 
    //     1、"用户 2 不在允许的范围"
    //     2、玄难 不是VIP用户 

6、可以自定义函数,自定一个操作函数 group

class GroupOperator extends Operator {
	public GroupOperator(String aName) {
		this.name= aName;
	}
	public Object executeInner(Object[] list)throws Exception {
		Object result = new Integer(0);
		for (int i = 0; i < list.length; i++) {
			result = OperatorOfNumber.Add.execute(result, list[i]);
		}
		return result;
	}
}

则执行:

     runner.addFunction("累加", new GroupOperator("累加"));
     runner.addFunction("group", new GroupOperator("group"));
    //则执行:group(2,3,4)  = 9 ,累加(1,2,3)+累加(4,5,6)=21

7、可以自定操作符号。自定义的操作符号优先级设置为最高。例如自定一个操作函数 love:

class LoveOperator extends Operator {	
	public LoveOperator(String aName) {
		this.name= aName;
	}
	public Object executeInner(Object[] list)
			throws Exception {
		String op1 = list[0].toString();
		String op2 = list[1].toString();
		String result = op2 +"{" + op1 + "}" + op2;		
		return result;
	}
}

然后增加到运算引擎:

 runner.addOperator("love", new LoveOperator("love"));
    //则执行:'a' love 'b' love 'c' love 'd' = "d{c{b{a}b}c}d"

8、可以重载已有的操作符号。例如替换“+”的执行逻辑。
9、可以延迟运算需要的数据
10、一个脚本可以调用其它脚本定义的宏和函数
11、可以类似VB的语法来使用操作符号和函数。print abc; 等价于 print(abc).
12、支持类定义
13、对 in 操作支持后面的是一个数组或者List变量

绑定java类或者对象的method

  • addFunctionOfClassMethod

        /**
         * 添加一个类的函数定义,例如:Math.abs(double) 映射为表达式中的 "取绝对值(-5.0)"
         * @param name 函数名称
         * @param aClassName 类名称
         * @param aFunctionName 类中的方法名称
         * @param aParameterTypes 方法的参数类型名称
         * @param errorInfo 如果函数执行的结果是false,需要输出的错误信息
         * @throws Exception
         */
         public void addFunctionOfClassMethod(String name, String aClassName,
    			String aFunctionName, String[] aParameterTypes, String errorInfo)
    			throws Exception{...}
    
  • addFunctionOfServiceMethod

    /**
     * 用于将一个用户自己定义的对象(例如Spring对象)方法转换为一个表达式计算的函数
     * @param name
     * @param aServiceObject
     * @param aFunctionName
     * @param aParameterTypes
     * @param errorInfo
     * @throws Exception
     */
	public void addFunctionOfServiceMethod(String name, Object aServiceObject,
			String aFunctionName, String[] aParameterTypes, String errorInfo)
			throws Exception

两种方法,无论是何种形式,启用errorInfo一般都应该是boolean类型函数合适。

参见 GrammarTest 类的 testBindingClassOrObjectMethod 函数。

macro 宏定义

测试脚本

(语文+数学+英语)/3.0
计算平均成绩>90
是否优秀

参见 GrammarTest 类的 testMacro 函数。

编译脚本,查询外部需要定义的变量和函数

脚本:

int 平均分 = (语文+数学+英语+综合考试.科目2)/4.0;
return 平均分;

参见 GrammarTest 类的 testVarNeedDef 函数。

关于不定参数的使用

脚本:

getTemplate([11,'22',33L,true])
getTemplate(11,'22',33L,true)

参见 GrammarTest 类的 testMethodReplace 函数。

集合的快捷写法

脚本

abc = NewMap(1:1,2:2); return abc.get(1) + abc.get(2);
abc = NewList(1,2,3); return abc.get(1)+abc.get(2)
abc = [1,2,3]; return abc[1]+abc[2];

参见 GrammarTest 类的 testSetCreateFast 函数。

其他相关

缓存性能比较

参见 PerfomanceTest 类的 testLocalCacheMutualImpact 函数。

检查自定义的java类

running.checkSyntax(String text, boolean mockRemoteJavaClass, List<String> remoteJavaClassNames) 

参见 GrammarTest 类的 testCheckySyntax 函数。

是否将char类型识别为String

runner的setIgnoreConstChar方法。

  • 是否忽略charset类型的数据,而识别为string,比如 'a' -> "a"
  • 默认为忽略,正常识别为String【官方注释此处有误或实现有误】

参见 GrammarTest 类的 testIgnoreConstChar 函数。

类定义

参见 GrammarTest 类的 TestClassDefine 函数和TestClassDefine2 函数。

位相关的定义

参见 GrammarTest 类的 testBitTest 函数。

类需导入才能使用

参见 GrammarTest 类的 testImportClassPath 函数。

关于栈的深度优化

参见 GrammarTest 类的 testStack 函数。

短路逻辑

针对runner的 isShortCircuit 属性,来判断,是否使用逻辑短路特性增强质量的效率

runner.setShortCircuit(false);

参见 GrammarTest 类的 testShortCircuit 函数和 testNoShortCircuit 函数。

场景

场景一

参见 SceneOne 类

在业务系统中存在一些逻辑判断,例如 "商品A"只能是三星卖家,而且已经开通淘宝店铺的用户才能订购
那么我们期望业务人员看到的配置信息就是:“三星卖家 而且 已经开店”
则通过下列步骤可能实现这个功能:

定义一个用户信息对象

class UserInfo {
    long id;
    long tag;
    String name;

    public UserInfo(long aId,String aName, long aUserTag) {
        this.id = aId;
        this.tag = aUserTag;
        this.name = aName;
    }
    public String getName(){
        return name;
    }
    public long getUserId() {
        return id;
    }

    public long getUserTag() {
        return tag;
    }
}

定义两个基础的功能函数

 /**
     * 判断一个用户TAG的第X位是否为1。这个的demo,其实现合理性不考虑
     * @param user
     * @param tagBitIndex
     * @return
     */
    public boolean userTagJudge(UserInfo user,int tagBitIndex){
        boolean r =  (user.getUserTag() & ((long)Math.pow(2, tagBitIndex))) > 0;
        return r;
    }

    /**
     * 判断一个用户是否订购过某个商品
     * @param user
     * @param goodsId
     * @return
     */
    public boolean hasOrderGoods(UserInfo user,long goodsId){
        //随机模拟一个
        if(user.getUserId() % 2 == 1){
            return true;
        }else{
            return false;
        }
    }

初始化语句执行器

	public void initial() throws Exception{
		runner.addOperatorWithAlias("而且","and",null);
		runner.addFunctionOfClassMethod("userTagJudge", SceneOne .class.getName(), "userTagJudge",new String[] {UserInfo.class.getName(),"int"}, "你不是三星卖家");
		runner.addFunctionOfClassMethod("hasOrderGoods", SceneOne .class.getName(), "hasOrderGoods",new String[] {UserInfo.class.getName(),"long"}, "你没有开通淘宝店铺");
		runner.addMacro("三星卖家", "userTagJudge(userInfo,3)");//3表示三星卖家的标志位
		runner.addMacro("已经开店", "hasOrderGoods(userInfo,100)");//100表示旺铺商品的ID
	}

定义一个逻辑判断函数

	/**
	 * 判断逻辑执行函数
	 * @param userInfo
	 * @param expression
	 * @return
	 * @throws Exception
	 */
	public String hasPermission(UserInfo userInfo,String expression) throws Exception {			
        	IExpressContext<String,Object> expressContext = new DefaultContext<String,Object>();
    		expressContext.put("userInfo",userInfo);
    		List<String> errorInfo = new ArrayList<String>();
            Boolean result = (Boolean)runner.execute(expression, expressContext, errorInfo, true, false);
            String resultStr ="";
            if(result.booleanValue() == true){
            	resultStr = "可以订购此商品";
            }else{
              for(int i=0;i<errorInfo.size();i++){
            	  if(i > 0){
            		  resultStr  = resultStr + "," ;
            	  }
            	  resultStr  = resultStr + errorInfo.get(i);
              }
              resultStr = resultStr  + ",所以不能订购此商品";
            }
            return "亲爱的" + userInfo.getName() + " : " + resultStr;
    }	

调用执行器执行判断逻辑

	public static void main(String args[]) throws Exception{
		SceneOne demo = new SceneOne();
		demo.initial();
		System.out.println(demo.hasPermission(new UserInfo(100,"xuannan",7),  "三星卖家   而且   已经开店"));
		System.out.println(demo.hasPermission(new UserInfo(101,"qianghui",8), "三星卖家   而且   已经开店"));
		System.out.println(demo.hasPermission(new UserInfo(100,"张三",8), "三星卖家 and 已经开店"));
		System.out.println(demo.hasPermission(new UserInfo(100,"李四",7), "三星卖家 and 已经开店"));

场景二-审核流程

简单的流程管理,本场景涉及用于展示如何定义表达式,方法,并使用上下文变量。

脚本如下:

如果 (审批通过(经理,金额)){
   如果  (金额  大于 5000){ 
     如果  (审批通过(总监,金额)){
        如果  (审批通过(财务,金额)){
           报销入账(金额)
        }否则  {
            打回修改(申请人)
        }
     }否则 {
        打回修改(申请人)
     }
   }否则  {
      如果  (审批通过(财务,金额)){
        报销入账(金额)
      }否则 {
         打回修改(申请人)
      }
   }
}否则 {
   打回修改(申请人)
}
打印("完成");

查看SceneTwo

场景三-物流分配

非加权:

费用科目(物流订单.仓储TP,"仓储费")= 物流订单.重量 * 0.5 ;

if(物流订单.重量  > 5) then{ 
       费用科目(物流订单.物流TP,"运输费")= 3.0 + (物流订单.重量 - 5 ) * 1.5 ; 
} else { 
      费用科目(物流订单.物流TP,"运输费")= 3.0; 
};
费用科目(物流订单.包装TP,"包装费")= 物流订单.重量 * 2.5 ; 

加权:

费用科目(物流订单.仓储TP,"仓储费")= 物流订单.重量 * 0.5 ;

if(物流订单.重量  > 5) then{
       费用科目(物流订单.物流TP,"运输费")= 3.0 + (物流订单.重量 - 5 ) * 1.5 ;
} else {
      费用科目(物流订单.物流TP,"运输费")= 3.0;
};
费用科目(物流订单.包装TP,"包装费")= 物流订单.重量 * 2.5 ;

查看SceneThree

示例来源于官方文案

总结

  • QLExpress属于弱类型脚本语言,一般不推荐声明局部变量的类型。语法编译阶段不会做类型校验,也不会做方法的参数校验,所以很灵活。
  • QLExpress的这套自定义的指令集属于解析执行,RunEnvironment中通过programPoint函数指针的跳转来实现每条指令的逐个计算,通过dataContainer作为栈来存储中间计算结果。
  • QLExpress定义的指令类型比较少,粒度比较粗,但是足够完成所有的语法功能。
  • QLExpress整个运算过程是通过threadLocal来保证线程安全的。
  • QLExpress的脚本编译过程比较耗时,但是是上下文无关的,所以同一个脚本运行缓存之后性能提升非常明显。
  • QLExpress指令计算运算过程中,基本不会new新对象,是通过缓存池技术来尽量减少资源的消耗。
  • QLExpress的宏,function,Operator,变量是非常开放的,名字可以为中文字符,也可以随意扩展,还可以通过脚本静态分析出包含哪些变量、函数,很方便的进行二次业务开发。
  • 脚本调用classLoader资源的时候可以import,也可以使用类的全路径,构造函数、静态方法、对象方法、类的字段、函数的不定参数调用统统搞定。

参考文献

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章