playbook/docs/tsl/syntax_book/04_modules_and_namespace.md

23 KiB
Raw Blame History

04 单元/命名空间/函数文件

本章整理 UNIT 单元、TSF 函数文件与 NAMESPACE 命名空间等组织机制。

目录

UNIT 单元管理

UNIT 单元的支持

UNIT 单元的支持简介

什么是单元?

当公共的函数库越来越多的时候函数很容易同名虽然可以通过更名的方式解决函数同名问题但是当函数越来越多的时候用户需要把一组处理一类问题的函数放在一起这样便于使用的查找也便于源代码的管理这个时候TSL 引入了 UNIT 关键字,提供对单元的支持。

在许多其他的语言中,提供了类似于 TSL 语言的单元的功能,例如有的称之为 Package 即包,有的称之为 Library 即库,在这里我们姑且均称之为单元。大体的使用模式无外乎在某个单元内封装很多的内容,这些内容在没有引用的时候用户是无法调用的,调用之时需要利用特殊的引用关键字去使用。在 TSL 语言中,引用单元的关键字是 uses这点与 Object Pascal 语言相同,在其他的语言中,有用 import、using 等做为关键字,事实上,从语义上看起来也非常类似。

事实上,单元不仅仅提供函数的整合管理,其中还可以包括有类等内容,对于支持全局常量和变量的语言而言,其中还可以定义变量和常量。单元中的内容可以是可视的,亦可以是私有非可视的。

单元还提供初始化和结束的方法,当有一组函数都需要有初始化代码的时候,这样可以使得在单元的函数可以共用初始化代码,而不需要在每一个函数中去初始化并且检查初始化的状态。

UNIT 单元的定义规范

我们不妨来看一段 UNIT 的范例代码:

unit
SampleUnit; // 单元名称
interface
// 单元属性及单元方法的声明,在此处声明的属性与方法可被外部访问
uses Unit1, Unit2; // uses statement 引用其它单元
function InterfaceFunction(); // place publish function header here
implementation
// 可定义私有属性及单元方法的实现,在此处新增的属性及方法仅能当前单元内部引用
uses Unit3, Unit4.; // uses statement
function InterfaceFunction(); // place function body here
begin
    PrivateFunction();
end;
function PrivateFunction(); // this is a private function can be called inside only
begin
end;
procedure InitCode();
begin
    echo "Initialization\r\n";
end;
procedure FinalCode();
begin
    echo "Finalization\r\n";
end;
Initialization
// 单元初始化
// Unit Initial statement here
InitCode();
Finalization
// 单元释放前执行的过程
// Unit Final statement here
FinalCode();
end.

我们可以看到,单元的定义是以关键字 unit 开始的,其后紧跟单元的名字,在其后有一个 interface 关键字,表示接口段。

Interface

声明外部访问的属性及方法等,在 interface 之内声明的属性与方法可被外部访问。

可以用 uses 来引用其他的单元(在接口段内 uses 的单元将在本单元内全局可视可直接使用),之后就是接口段内的内容,放在接口段内的内容(例如属性、函数头定义、类)将变得可视可被引用。

紧跟着 Interface 段的是由关键字 Implementation 开始的实现段。

Implementation

定义私有属性及单元方法的实现,在此处新增的属性及方法仅能在当前单元内部引用

在实现段里也可以引用其他单元,如果需要引用,可以用 uses 紧跟在 Implementation 后来引用(这里的引用的单元对 interface 段不可视),实现段内的内容对引用者不可视,除非该内容已经在 Interface 中将接口引出。绝大多数的语言的单元引用不支持出现在函数或者类中,而 TSL 语言的 uses 不仅仅支持在单元中引用,还支持在函数和类中引用,其后我们会具体讲述。

在 Implementation 后可以跟 Initialization 和 Finalization 段,从事单元的初始化和清理工作。

Initialization

Initialization 段内的代码将在单元被第一次引用使用的时候调用,主要从事单元的初始化工作。

Finalization

Finalization 段内的代码则在运行的任务结束的时候调用,主要从事单元的清理工作。

End.单元结束符

单元是以 End.作为结束的。

Uses 引用单元

在单元的 Interface 段Implementation 段的开始,在函数的 Begin 后的第一条代码,在类定义的第一条代码可以用 Uses 引用单元,多个单元以,分隔。

单元方法参数缺省值支持单元常量

单元中的函数支持参数缺省值定义中使用可以访问的 const 定义的常量。

例如,有一个单元 Unit_DfParamsDemo分别定义了两个方法 funcA 与 funcB这两个方法分别使用声明中定义缺省参数为外部可访问到的单元常量及在实现中定义缺省参数为单元内部才能访问到的单元常量

unit Unit_DfParamsDemo;
interface//可在单元外引用访问
const CS = 888; // 定义一个常量,定义之后不能被修改
// 声明中定义--支持指定interface中定义的常量
function funcA(a, b = 100, c = CS); // 缺省参数支单元常量
// 不在声明中定义而在implementation实现中定义缺省参数
function funcB(a, b, c);
implementation//只能被当前Unit单元内的方法访问
const CN = 999; // 定义一个私有常量
function funcA(a, b, c); // 已在声明中定义缺省参数,此处不能重复再定义
begin
    echo "funcA-a:", a, " b:", b, " c:", c;
    return a + b + c;
end;
function funcB(a, b, c = CN); // 此处定义可指定interface及implementation中已定义到的常量
begin
    echo "funcB-a:", a, " b:", b, " c:", c;
    return a + b + c;
end;
initialization//初始化
Finalization//析构前
echo "Unit_DfParamsDemo End.";
end.

调用如:

uses Unit_DfParamsDemo;
echo funcA(1);
echo funcA(1, 2);
echo funcB(1, 2);

打印结果:

funcA-a:1 b:100 c:888

989

funcA-a:1 b:2 c:888

891

funcB-a:1 b:2 c:999

1002

Unit_DfParamsDemo End.

单元的意义

封装

单元可以将同类的功能模块放置在一起,不用使用这些功能模块的用户完全不需要关心,而需要使用的用户则可以在一个单元中集中看到所有的相关内容而不需要在一个个的分散的函数中去寻找。

单元中的函数往往有一些内部共用的中间函数,既无需将这些中间函数写成全局的函数,亦不需要在每一个需要的函数中独立定义私有函数,亦起到封装的效果。

隐藏
unit 的使用具有代码重用,信息隐藏的优势。一个单元的 interface 中的所有标识符(函数,类等)对于使用该 unit 的任何程序都是可用的,而这些标识符的实现部分都隐藏在相应的 unit 中(implementation 部分)。调用者只需要知道接口部分的语法,利用 unit 中的公有方法unit 中的内部运行机制并不是调用者需要关心的,只在 implementation 中定义而不在 interface 定义的内容是不能被 unit 的引用者访问的。

一个例子:

unit
UnitB;
interface
function
PublicFunc;
implementation
function
PublicFunc();
begin
    echo 'Public Function is called!';
    PrivateFunc();
end;
;
function
PrivateFunc();
begin
    Echo
    'Private Function is called!';
end;
;
end.

function
FunctionUsingUnit();
begin
    uses
    UnitB
;
    PublicFunc(); // "Public Function is called!Private Function is called!"
    PrivateFunc(); // 函数privatefunc编译错误或者找不到该函数
end;
;
初始化和清理

用户的多个功能模块可能均需要初始化某个全局的数据,没有单元的话,要么在应用的启动代码中,要么在每个模块中均检查初始化状态并调用初始化的代码,然后在应用中执行清理工作,如果采用单元的初始化和清理的便利,就会让开发变得简便很多。

单元的特殊定义

单元可以省略 Interface 和 Implementation 关键字,一旦省略关键字,所有单元内的内容均对引用者可视。

例:

unit SpecialUnit;

function Abcd();
begin
end;

function Abcd2();
begin
end;

initialization
    ...

finalization
    ...

end.

UNIT 单元中常量与变量

TSL 语言 UNIT 支持在 interface 或 implementation 中定义变量或者常量,支持 findfunction 获得一个 unit 对象unit 对象支持用.来调用方法或者 interface 里定义的变量或常量。

用来支持在单元中对全局变量的需求。

版本说明:新一代 TSL 语言中支持该功能。

unit 中变量的作用范围:全局变量

interface 中声明的变量或常量,可被对象访问。

implementation 中声明的变量或常量,只能被当前 unit 单元内的方法访问。

变量的定义:由关键字 var 开头,后面接变量名,多个变量名之间用逗号分隔。

// 如定义变量 UA,UCvar UA,UC;

常量的定义:由关键字 Const 开头,后面接常量,具体用法可参考:常量及常量成员的定义与初始化

// 如定义一个常量const UcA=100;

外部对单元变量或常量的引用findfunction(UnitName).UnitA

示例:

定义一个单元 DeMo_Unit_Test01 如下:

unit DeMo_Unit_Test01;
interface
var Ua, Ub;
const CS = 888;
function addv();
function NumbJO(v); // 判断奇偶
function print();
implementation
const CN = array("奇数", "偶数");
var Uc;
function print(); // 打印变量与常量
begin
    echo "当前值-Ua:", Ua, " Ub:", Ub, " Uc:", Uc, " CS:", CS;
end;
function addv();
begin
    Ua += 10;
    Uc += 1;
end;
function NumbJO(v);
begin
    if (v mod 2) = 0 then echo v, "是一个", CN[1];
    else echo v, "是一个", CN[0];
end;
initialization//初始化
Ua := 100;
Ub := "Tinysoft Unit";
Uc := 1;
Finalization//析构前
echo "DeMo_Unit_Test01 End.";
end.

应用展示:

uses DeMo_Unit_Test01;
obj := findfunction("DeMo_Unit_Test01"); // 获取单元的对象
// --获取Unit中变量Ua的值
echo obj.Ua;
obj.print();
// --给变量重新赋值
obj.Ua := 500;
print();
// --调用Unit中的方法
{对方法的调用,当只存在一个单元时,可省略,若存在多个单元且有同名方法时,建议通过对象方式进行指定}
obj.NumbJO(58);
NumbJO(33);
addv();
print();
return obj.Ub;

打印结果:

100

当前值-Ua:100 Ub:Tinysoft Unit Uc:1 CS:888

当前值-Ua:500 Ub:Tinysoft Unit Uc:1 CS:888

58 是一个偶数

33 是一个奇数

当前值-Ua:510 Ub:Tinysoft Unit Uc:2 CS:888

DeMo_Unit_Test01 End.

程序返回结果:"Tinysoft Unit"

Unit 的使用

Uses 子句

unit 可以在 functionprocedure,unit,class,.tsf 和.tsl 中使用,我们用 uses 子句声明需要用到的 unit,uses 子句后可以跟多个 unit用逗号分隔。

在 function 中使用 uses
function
FunctionUsingUnit();
begin
    uses
    UnitA, UnitB, UnitC;
    FuncInUnitA();
end;
;

在 function 中 uses 子句必须写在函数体的第一行,且一个函数体中只能使用一次 uses 语句,否则无法通过语法检查。引用后,函数可以直接调用在各个被引用单元中的接口函数和类。

在 Class 中使用 uses
type
ClassUsingUnit
uses
UnitA, UnitB, UnitC;
FuncInClassA();
end;
;

在 class 中的 uses 字句必须写在类定义体内的第一行,且一个类定义体中只能使用一次 uses引用的单元的接口函数和类对类函数均有效。

在 unit 中使用 uses

在 unit 中有两个地方可以使用 uses 子句:interfaceimplementation但 uses 字句必须紧跟在 interface 或 implementation 之后。

使用 uses 子句后,被引用单元的接口函数可以在单元内使用,但在 interface 中定义的类的内联函数不能访问 Implementation 中 uses 子句引用的单元接口函数和类。

unit
Unit1;
interface
// Uses Unit1,Unit2....; // Uses statement
// Function InterfaceFunction(); // place publish function header here
function
func1();
begin
    // 调用在unitB中定义的函数;
    FuncInUnitB(); // Error:找不到该函数
end;
;
implementation
uses
UnitB
;
end.
在.tsf 或者.tsl 中使用 uses

对于.tsf 和.tsl 文件,除了可以在文件中的 function 和 class 中使用 uses 子句,还可以在文件的第一行使用 uses 子句,被 uses 声明的 unit 中的接口函数和类对于整个文件都是可以访问的,如在一个 tsf 文件中定义的多个函数都需要用到某个 unit只需要声明一次

uses
UnitA
;
function
Func1();
begin
    // 调用UnitA中的一个函数
    FuncInUnitA();
end;
;
function
Func2();
begin
    // 调用UnitA中的另一个函数
    AnotherFuncInUnitA();
end;
;

注意,一个文件在其顶层的代码只能使用一次 uses 子句。

调用 Unit 中的接口函数

一般来说,我们使用 uses 声明要引用的 unit在之后的代码调用被声明后的 unit 中的接口函数。比如:

function
FunctionUsingUnit();
begin
    uses
    UnitA;
    // 调用UnitA中的一个函数
    FuncInUnitA();
end;
;

某些时候,我们可以通过 Unit(UnitName). InterfaceName 的方式调用 unit 中的接口函数,而不用使用 uses 子句,如:

function
FunctionUsingUnit();
begin
    // 使用Unit(UnitName).InterfaceName调用UnitA中的一个函数
    unit
    (
    UnitA
    ).FuncInUnitA();
end;
;

uses 多个 unit 时,偶尔我们需要调用制定 unit 中的接口,可以使用 UnitName.InterfaceName 的方式,如:

function
FunctionUsingUnit();
begin
    uses
    UnitA, UnitB;
    // 调用UnitA中的一个函数
    UnitA.Hello(); // A said:"hello!"
    UnitB.Hello(); // B said:"hello!"
end;
;

Unit 的作用域

在一个函数中使用的 unit只在当前函数体内有效。

对于 unit在 interface 后 uses 的 unit 可以在被当前 unit 任何位置访问,在 implementation 后 uses 的 unit在 interface 段不能访问其接口。

在.tsf和.tsl 中使用的 unit其接口函数和类可以在当前文件的任何位置访问。

重名函数的引用优先级以及处理方法

当一个文件中使用了多个 unit而在这些 unit 定义的接口存在重名的时候,编译器并不会提示警告或者错误,而是按照以下顺序优先级递减的规则调用相应的函数:

1 当前应用的私有函数

2 全局函数

3 Uses 最后的 unit 依次往前

如果要指定使用某个 unit 中的接口函数是,可以使用 UnitName.InterfaceName。

重名类的引用优先级以及处理方法

当出现名字相重的类,其优先级遵循重名函数的引用优先级,假定需要在创建对象的时候指定某个单元的类,使用 CreateObject("UnitName.ClassName")的模式访问。

单元对函数调用影响

引用函数的时候,会一次从被引用单元的先后次序对函数进行查找,如果用户需要调用指定单元的函数,则可以采用如下方式:

直接调用模式:unit(UnitA).InterfaceFunction() 调用单元 UnitAInterfaceFunction

CALL 调用模式:CALL("UnitA.InterfaceFunction")

函数文件以及命名空间

在客户端 TSL 解释器以及独立的 TSL 脚本解释器和 WEB 的 TSL 语言插件中,缺乏平台的函数存贮统一管理的功能,这个时候函数是以 TSF 文件的方式进行存贮的。在 WEB 等项目的开发的过程中,由于缺乏统一的函数管理功能,避免不了会产生函数等重名现象,当产生了函数重名又需要部署在一个解释器来运行的时候,就需要命名空间来解决这些问题。

TSF 文件函数的存贮

TSL 语言允许将函数、类、单元以同名文件存贮成一个以.tsf 为扩展名的文件,这个文件可以放置在解释器的 funcext 子目录下,亦可以存贮在 funcext 子目录下的下级子目录中,这些内容将是全局可视的,这样 TSL 语言可以直接调用访问到这些内容。

WEB 端、客户端、命令行解释器端funcext 目录存在于 tslinterp.dll 所在目录下在服务端funcext 目录存在于执行服务器的路径下。

原则上 funcext 目录下的文件名不允许同名,也就是说,一般来说,全局函数不允许同名,同名则以查找到的第一个为准。

NAMESPACE 命名空间

如果多个项目工程需要存在多个同名的全局函数,可以采用 NAMESPACE 来解决函数重名问题。(这种情况往往是经常发生的,例如,某两个项目共享一套框架,但是框架内的某几个函数内容不同,则不同的项目会有几个同名全局函数)

可以将 TSF 文件的文件名以 FunctionName@NameSpaceName.TSF 形式存贮。在调用之前使用 NameSpace "NameSpaceName" 的模式来使用。

例如:

NameSpace "NameSpaceName";
FunctionName();

当在使用 WEB 开发或者命令行解释器开发的时候,可以在 .tsl 文件的当前路径下创建 tsl.conf 文件,并在其中:

[system]
Namespace=NameSpaceName
InheritParent=1

这样,所有该目录下的 TSL 执行都将使用默认名为 NameSpaceName 命名空间InteritParent=1 则如果当前未设置 NameSpace将继承上级路径的 NameSpace 配置。

使用命令空间主要目的是解决在开发多个项目的时候函数同名问题。

TslFileName

范例

封装 tsl 文件TestEnv.tsl

echo tslfilename(), '\r\n';
return 1;

执行该文件结果:

sysgettsllibpath

使用范例可参考 FAQsyssettsllibpath

syssettsllibpath

范例

在 tsl 脚本中,需要调用非当前环境下的函数 TestTsfsum若不临时设置该函数的所在目录则调用会报找不到该函数的错误。

通过 syssettsllibpath 设置好查找路径后,就可调用到该函数了。

A := 198.86;
B := -22.34;
echo "abs(A):", abs(A), " abs(B):", abs(B), '\r\n';
echo "当前查找路径:", sysgettsllibpath(), '\r\n';
s0 := syssettsllibpath("C:\\DoTSL\\otherTest\\;");
echo "当前查找路径:", sysgettsllibpath(), '\r\n';
try
    v1 := TestTsfsum(A, B);
    echo "TestTsfsum执行成功", v1, '\r\n';
except
    echo ExceptObject.ErrInfo, '\r\n';
end;
return "执行完毕";

扩展函数查找路径

天软 TSL 程序运行过程中,若程序中有调用其它本地函数,默认从解析器所在目录的 funcext 文件夹中进行查找对应函数,若查找失败,则会报错。

为了增加函数存放位置,可以通过两种方式进行扩展函数查找范围。

第一种:在执行当前程序时,临时指定查找路径

第二种:在执行文件的同目录下,新增 tsl.conf 配置文件,指定查找路径,

需要注意,扩展函数查找路径优先级高于解析器所在目录的 funcext 文件夹。

1、临时性扩展函数查找路径TSL.EXE -libpath 参数

执行天软 TSL 程序(.tsl/.tsf 文件)时,可在执行命令末尾添加 -libpath 参数临时扩展函数查找路径。

支持指定多个路径(以分号分隔),路径必须以 / 或 \ 结尾。

例如:

tsl test.tsl -libpath "D:/TinySoft/Test/func/;D:\test\funcext\"

具体示例:

现有 test.tsl 文件,内容如下:

foo(2, 3);
function foo(a, b)
begin
    t1 := a * b;
    t2 := ts_test01(a, b);
    t := t1 + t2;
    echo "t:", t, "\n";
    return t;
end;

其中,子函数 ts_test01.tsf 内容如下:

function ts_test01(n, m)
begin
    return n + 2 * m;
end;

当未扩展函数查找路径时,执行结果如下:

临时扩展函数查找路径时,执行结果如下:

2、配置文件扩展函数查找路径tsl.conf 配置文件 system 中 libpath 参数

天软 TSL 程序(.tsl/.tsf 文件)同目录新增 tsl.conf 配置文件,并配置 system 中的 libpath 参数,可为当前目录所有天软 TSL 程序扩展函数查找路径。

1、支持指定多个路径以分号分隔路径必须以 / 或 \ 结尾。

2、支持{$P}来替换为当前主程序文件所在路径。

例如可以在 tsl.conf 加入

[system]

libpath={$P}funcext\

这样可以使得.tsl 所在的路径下的 funcext 会成为优先的 funcext 路径。

这种使用主要解决 TSL.EXE 调用所开发多个.tsl 编写的应用的函数重名问题

例如:

[system]
libpath=D:/TinySoft/Test/func/;D:\test\funcext\

具体示例:

现有 test.tsl 文件,内容如下:

foo(2, 3);
function foo(a, b)
begin
    t1 := a * b;
    t2 := ts_test01(a, b);
    t := t1 + t2;
    echo "t:", t, "\n";
    return t;
end;

其中,子函数 ts_test01.tsf 内容如下:

function ts_test01(n, m)
begin
    return n + 2 * m;
end;

未新增 tsl.conf 配置文件时,执行结果如下:

新增 tsl.conf 配置文件后,执行结果如下: