playbook/docs/tsl/syntax/09_units_and_scope.md

13 KiB
Raw Blame History

TSL unit 与作用域

文档类型:语法主线 是否可直接用于生成代码:仅部分 是否含可直接照写示例:是 是否含不可照写反例:是 遇到不确定时:先按本页候选页继续判断;02_core_model.md(优先)、19_namespace_libpath_and_unit_runtime.md11_pitfalls.md;仍不命中时回到语法路由中心 index.md;如果问题已经超出语法层,回到 TSL 总入口 ../index.md

这一篇只讲“怎么写和怎么调用 unit”。namespace、查找路径、unit 生命周期、tsl.conf 与运行时入口统一放到 19_namespace_libpath_and_unit_runtime.md

本篇职责

回答下面几类最常见的问题:

  • 什么时候该把一组函数写成 unit
  • uses 到底能放在哪里
  • interfaceusesimplementationuses 的可见性差别是什么
  • 多个 unit 里有同名接口时,未限定调用到底命中谁
  • 怎样显式指定要调用哪个 unit 的接口

智能体 unit/作用域判断流程

  1. 先判断当前交付是 .tsl 可执行脚本、.tsf 扩展模块,还是 unit 文件。
  2. unit 文件只描述可复用单元;脚本入口仍放在 .tsl
  3. uses 必须放在普通语句之前;普通语句后不要再追加顶层 uses
  4. 默认参数、接口段、实现段和作用域边界只照本页文档明确形态写。
  5. 命中 unit 生命周期、命名空间、查找路径、tsl.conf 或命令行查找路径参数时,跳到 19_namespace_libpath_and_unit_runtime.md
  6. 没有对应代码块时不要发明 unit/作用域写法。

核心规则

  • unit 是完整的顶层主体;常见完整形态是 unit Name; interface ... implementation ... end.
  • unit 也可以省略 interface / implementation 写成简写形态;这种简写里定义的函数对外可调用。
  • 如果没有特殊需求,默认优先用完整形态;简写形态只在不需要显式区分 interface / implementation 时再用。
  • 完整 unit 示例中,implementation 前保留空行,避免接口段和实现段挤在一起。
  • unit 允许接口声明与实现段分离:interface 段可以只声明函数签名或类方法签名,函数体和类方法体放到 implementation 段。
  • 类方法放在 implementation 段实现时,使用 function ClassName.Method(...) 形态。
  • 顶层 uses 可以放在文件最前面,让后续整个文件直接看到被引入 unit 的接口。
  • 顶层导入多个 unit 时,只生成单条逗号合并写法:uses UnitA, UnitB;;不要生成连续多条顶层 uses。真正会失败的是前面已经出现普通语句后,再写顶层 uses
  • 函数体里的 uses 必须是函数体第一条语句,而且一个函数体里只能写一次。
  • 类定义体里的 uses 必须是类定义体第一条语句,而且一个类定义体里只能写一次。
  • 如果函数体或类定义体需要多个 unit,默认把这些 uses 提升到顶层并合并成单条 uses UnitA, UnitB;;不要在函数体或类定义体里连写两条 uses
  • 写在 interface 段的 uses 对整个 unit 可见;写在 implementation 段的 uses 只对实现段和在实现段里定义的方法体可见。
  • 只写在 implementation 里的函数,对 unit 外部不可见。
  • 出现重名接口时,未限定调用按 uses 从后往前解析;最后一个 uses 的同名接口优先。
  • 要显式指定某个 unit 的接口,可以用 UnitA.Ping()unit(UnitA).Ping()call("UnitA.Ping")
  • unit 生命周期、接口状态、命名空间和查找路径细节,转去看 19_namespace_libpath_and_unit_runtime.md

可直接照写示例

最小 unit

代码块身份:可直接照写示例

unit DemoUnit;

interface

function Ping();

implementation

function Ping();
begin
    return 1;
end;

end.

结果说明:

  • 上面这就是最小完整 unit 骨架。
  • unit 文件结尾要用 end.,不是普通函数或类的 end;

如果该 unit 被脚本 uses DemoUnit; 后调用 writeLn(Ping());,可观察输出为:

代码块身份:输出片段

1

简写 unit

代码块身份:可直接照写示例

unit SpecialUnit;

function Abcd();
begin
    return 10;
end;
function Abcd2();
begin
    return 20;
end;

initialization
    writeLn("INIT");

finalization
    writeLn("FINAL");

end.

结果说明:

  • unit SpecialUnit; 后可以省略 interface / implementation
  • 这种简写形态里定义的 Abcd()Abcd2() 外部可以直接调用。
  • 简写 unit 里同样可以写 initializationfinalization

顶层、函数体、类体里的 uses

沿用同一个 DemoUnit.tsf,分别看顶层、函数体、类体三种 uses 位置:

代码块身份:配置片段 / 概念骨架 代码块说明:多文件结构骨架;依赖 unit 查找路径,不是可直接复制的单文件最小示例。

// DemoUnit.tsf
unit DemoUnit;

interface

function Ping();

implementation

function Ping();
begin
    return 11;
end;

end.

// main.tsl
uses DemoUnit;
obj := new Worker();
writeLn(Ping());
writeLn(RunInFunction());
writeLn(obj.Run());

function RunInFunction();
begin
    uses DemoUnit;
    return Ping();
end;
type Worker = class
    uses DemoUnit;
public
    function Run();
    begin
        return Ping();
    end;
end;

结果说明:

  • 顶层 uses DemoUnit; 后,脚本主体可以直接调用 Ping()
  • 函数体第一行写 uses DemoUnit; 后,RunInFunction() 输出 11
  • 类定义体第一行写 uses DemoUnit; 后,obj.Run() 也输出 11
  • 这一类多文件场景依赖真实查找路径;如果 DemoUnit.tsf 所在目录不在查找路径里,脚本仍会找不到对应接口。

uses 的位置限制

代码块身份:反例 / 不可照写

// 顶层前面已经有普通语句
a := 1;
uses DemoUnit;

// 函数里不是第一行
function Run();
begin
    a := 1;
    uses DemoUnit;
    return Ping();
end;

// 函数里写两次
function Run();
begin
    uses UnitA;
    uses UnitB;
    return Ping();
end;

// 类里不是第一行
type Worker = class
public
    value;
    uses DemoUnit;
end;

// 类里写两次
type Worker = class
    uses UnitA;
    uses UnitB;
end;

结果说明:

  • 顶层前面如果已经出现 a := 1; 这类普通语句,再写顶层 uses 会报 invalid statement
  • 函数体里,uses 不是第一行或者重复出现,都会失败。
  • 类定义体里,uses 不是第一行或者重复出现,都会失败。
  • 函数体或类定义体里重复 uses 的失败点是“第二条 uses”,不是顶层导入多个 unit
  • 顶层导入多个 unit 时只生成 uses UnitA, UnitB; 这种逗号合并写法;连续多条顶层 uses 不再作为文档明确生成形态。

interfaceusesimplementationuses

代码块身份:配置片段 / 概念骨架 代码块说明:多文件结构骨架;依赖 unit 查找路径,不是可直接复制的单文件最小示例。

// UnitB.tsf
unit UnitB;

interface

function FB();

implementation

function FB();
begin
    return 100;
end;

end.

// UnitC.tsf
// 与 UnitB.tsf 同形,只把 FB / 100 改成 FC / 200

// UnitA.tsf
unit UnitA;

interface

uses UnitB;

type TBox = class
public
    function FromInterface();
    begin
        return FB();
    end;
    function FromImplementation();
end;
function CallB();
function CallC();

implementation

uses UnitC;

function TBox.FromImplementation();
begin
    return FC();
end;
function CallB();
begin
    return FB();
end;
function CallC();
begin
    return FC();
end;

end.

// main.tsl

uses UnitA;
obj := new TBox();
writeLn(CallB());
writeLn(CallC());
writeLn(obj.FromInterface());
writeLn(obj.FromImplementation());

结果说明:

  • interface 段写 uses UnitB; 后,FB() 对整个 unit 都可见,CallB() 输出 100
  • implementation 段写 uses UnitC; 后,FC() 对实现段函数和在实现段定义的方法体可见,CallC()obj.FromImplementation() 都输出 200
  • 接口段里的内联方法 FromInterface() 能访问 interfaceuses 引入的接口,因此输出 100
  • 接口段里的内联方法不能看到只在 implementationuses 引入的接口。

接口声明与实现段分离

代码块身份:可直接照写示例

unit SplitDemo;

interface

function MakeValue();
type UnitBox = class
public
    value;
    function create(_value);
    function ReadValue();
end;

implementation

function MakeValue();
begin
    return 10;
end;
function UnitBox.create(_value);
begin
    value := _value;
end;
function UnitBox.ReadValue();
begin
    return value;
end;

end.

结果说明:

  • 普通函数可以在 interface 段只声明 function MakeValue();,再在 implementation 段写函数体。
  • 类方法可以在类体里只声明签名,再在 implementation 段写 function UnitBox.Method(...) 实现。
  • 类方法实现放在 implementation 段时,仍属于该 unit 的实现段。

实现段私有函数

代码块身份:配置片段 / 概念骨架 代码块说明:多文件结构骨架;依赖 unit 查找路径,不是可直接复制的单文件最小示例。

// PrivateDemo.tsf
unit PrivateDemo;

interface

function PublicFunc();

implementation

function PublicFunc();
begin
    return PrivateFunc();
end;
function PrivateFunc();
begin
    return 77;
end;

end.

// main.tsl

uses PrivateDemo;
writeLn(PublicFunc());

结果说明:

  • PublicFunc() 输出 77,说明接口函数可以调用实现段私有函数。
  • 外部直接调用 PrivateFunc() 时,会报 function:PrivateFunc compile error or not found

代码块身份:反例 / 不可照写

uses PrivateDemo;
writeLn(PrivateFunc());

上面这种写法不可照写;PrivateFunc() 只写在 implementation 段,对 unit 外部不可见。

重名解析与限定调用

重名函数最小对照:

代码块身份:配置片段 / 概念骨架 代码块说明:多文件结构骨架;依赖 unit 查找路径,不是可直接复制的单文件最小示例。

// UnitA.tsf
unit UnitA;

interface

function Ping();
function Hello();

implementation

function Ping();
begin
    return 101;
end;
function Hello();
begin
    return 111;
end;

end.

// UnitB.tsf
// 与 UnitA.tsf 同形,只把 101 / 111 改成 202 / 222

// main.tsl
uses UnitA, UnitB;
writeLn(Ping());
writeLn(UnitA.Hello());
writeLn(UnitB.Hello());
writeLn(unit(UnitA).Ping());
writeLn(call("UnitB.Ping"));

结果说明:

  • 未限定的 Ping() 输出 202,说明解析顺序是 uses 从后往前。
  • UnitA.Hello() 输出 111UnitB.Hello() 输出 222
  • unit(UnitA).Ping() 输出 101
  • call("UnitB.Ping") 输出 202

重名类最小对照:

代码块身份:配置片段 / 概念骨架 代码块说明:多文件结构骨架;依赖 unit 查找路径,不是可直接复制的单文件最小示例。

// UnitA.tsf
unit UnitA;

interface

type Box = class
public
    value;
    function create();
end;

implementation

function Box.create();
begin
    value := 101;
end;

end.

// UnitB.tsf
// 与 UnitA.tsf 同形,只把 Box.create() 里写入的 101 改成 202

// main.tsl
uses UnitA, UnitB;
obj := new Box();
writeLn(obj.value);
objA := createObject("UnitA.Box");
writeLn(objA.value);
objB := createObject("UnitB.Box");
writeLn(objB.value);

结果说明:

  • 未限定的 new Box() 输出 202
  • createObject("UnitA.Box") 输出 101
  • createObject("UnitB.Box") 输出 202
  • 重名类的未限定解析顺序和重名函数一致,也是按 uses 从后往前。

默认生成模板

如果你只是要先写出一个最小、可被其他文件引用的 unit,从下面这个骨架起步:

代码块身份:可直接照写示例

unit DemoUnit;

interface

function Ping();

implementation

function Ping();
begin
    return 1;
end;

end.

后面的多文件片段只在需要 uses、可见性或重名解析时再回来看。

禁止项

  • 把依赖外部 unituses 例子误当成单文件最小示例。
  • 在还没把目标 unit 放进查找路径前,就把“找不到接口”误判成 uses 语法错。
  • 把函数体或类定义体里的 uses 写在第一条语句之后。
  • 在同一个函数体或类定义体里重复写第二个 uses
  • 生成连续多条顶层 uses;多个 unit 默认写成 uses UnitA, UnitB;
  • 把重名接口的未限定解析顺序理解成“从前到后”。
  • 忘了显式指定 UnitA.Ping()unit(UnitA).Ping()call("UnitA.Ping"),结果误调用到最后一个 uses 的同名接口。