playbook/docs/tsl/syntax/06_functions_and_calls.md

973 lines
21 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Functions And Calls
文档类型:语法主线
是否可直接用于生成代码:是
是否含已验证可执行示例:是
是否含已验证反例:是
遇到不确定时跳转到:[03_core_model.md](03_core_model.md)(优先)、[10_units_and_scope.md](10_units_and_scope.md)、[21_external_calls_and_threads.md](21_external_calls_and_threads.md)
手册位置:第 6 篇,共 32 篇。上一篇:[05_variables_and_constants.md](05_variables_and_constants.md)。下一篇:[07_expressions_and_operators.md](07_expressions_and_operators.md)。
这一篇只负责 `function` / `procedure` 的定义、调用、参数传递和值返回,不延伸到业务函数库。
## 这一篇解决什么问题
回答“如何正确声明 `function``procedure`、如何组织主函数和子函数、怎样使用参数修饰、默认参数与可变参数,以及哪些混写方式会直接编译失败”。
## 必须记住的规则
- 最稳妥的函数骨架仍然是 `function Name(...); begin ... end;`
- 不需要返回值时,可以改用 `procedure Name(...); begin ... end;`
- 在文件模型层,`function` 和 `procedure` 归同一类顶层外形;见 [03_core_model.md](03_core_model.md)。
- 当前解释器接受省略函数头后的分号,但文档默认仍保留这个分号。
- 一个函数定义体里可以同时出现主函数和子函数。
- 函数支持参数类型注解和返回值类型注解。
- 不带类型注解时,多个参数用逗号分隔。
- 带类型注解时,多个参数用分号分隔。
- 当前解释器接受 `const``var` 形参修饰。
- 当前解释器的运行时默认行为是:未修饰参数可以写回调用方;这是运行时默认,不是语言规范保证。
- `{$VarByRef-}` 会把未修饰参数切换成按值传递;`var` 形参仍保持引用语义。
-`{$VarByRef-}` 下,调用时可以用 `in` / `out` 前缀逐个参数覆盖默认传递方式。
- 如果不确定当前任务是否需要写回语义,优先显式用 `const` 形参,或先切到 `{$VarByRef-}`;不要依赖未修饰参数的运行时默认行为。
- `return expr;` 会直接返回当前函数结果。
- `exit;` 会立即结束当前函数;在当前最小样例里,如果此前没有写入返回结果,调用方观察到的是默认值 `0`
- 调用时支持命名参数,写法是 `name: value`
- 命名参数当前也支持 `call(...)` 这类按函数名或函数指针转调的模型。
- 一旦某次调用里开始使用命名参数,后面的参数就不能再退回位置参数。
- 对二进制函数 / 系统函数直接使用命名参数,当前会报 `named parameter mode can't support here`;这类函数要先用 TSL 再封一层。
- 函数参数支持默认值。
- 普通函数的默认值规则不要直接等同到 `unit interface` 声明;跨 `unit` 的默认参数边界见这一篇后面的对照例子,以及 [10_units_and_scope.md](10_units_and_scope.md)。
- 当前解释器支持尾部 `...` 形式的可变参数。
- 在可变参数函数体里,`Params`、`ParamCount`、`RealParamCount` 都已验证可用。
- 可变参数组可以通过 `...` 转发给另一个函数调用。
- 可变参数组也可以通过 `call(fc, ...)`、`##fc(...)`、`invoke(obj, name, 0, ...)` 转发。
- 当前解释器接受 `a := function(...) begin ... end;` 这种匿名函数写法。
- 匿名函数可以直接作为参数传入另一个函数。
- 当前解释器接受 `ThisFunction(FuncName)` 把已知函数绑定成函数值。
- 当前解释器里,匿名函数和函数指针的稳定调用方式仍是 `call(f, ...)``##f(...)`
- 当前解释器没有通过 `f(...)` 这种“函数变量直接调用”写法;无论 `f` 是匿名函数、`FindFunction(...)` 还是 `ThisFunction(...)` 返回的函数指针,都不要默认写成直调。
- 当前解释器接受 `::FuncName(...)` 指向全局/系统函数,用来绕过当前作用域里的同名局部函数。
- `external`、`MakeInstance` 和线程调用统一移到 [21_external_calls_and_threads.md](21_external_calls_and_threads.md)。
- 不要把顶层函数定义和松散语句混在同一个文件模型里。
## 已验证语法
### 基础函数 / 过程骨架
最短函数骨架:
代码块身份:已验证可执行示例
```tsl
function Add(a, b);
begin
return a + b;
end;
```
主函数加子函数:
代码块身份:已验证可执行示例
```tsl
function MultiFunc();
begin
return Twice(3);
end;
function Twice(x);
begin
return x * 2;
end;
```
函数头省略分号在当前解释器里也可编译:
代码块身份:已验证可执行示例
```tsl
function MissingSemi()
begin
return 1;
end;
```
最短 `procedure` 骨架:
代码块身份:已验证可执行示例
```tsl
procedure LogDone();
begin
end;
```
`procedure` 写回参数的最小运行样例:
这是验证样例外壳,不作为正式顶层模型归类依据。
代码块身份:已验证可执行示例
```tsl
program test;
procedure Bump(var x);
begin
x := x + 1;
end;
begin
a := 1;
Bump(a);
WriteLn(a);
end.
```
已验证运行结果:
- `Bump(a)` 后输出 `2`
### 签名增强
带参数类型和返回值类型:
代码块身份:已验证可执行示例
```tsl
function Demo(a: integer): integer;
begin
return a;
end;
```
带类型时的多参数分隔:
代码块身份:已验证可执行示例
```tsl
function Demo(a: integer; b: integer);
begin
return a + b;
end;
```
类型名可以写成更偏说明性的名字:
代码块身份:已验证可执行示例
```tsl
function Demo(a: input_value; b: handler): result_type;
begin
return a;
end;
```
参数修饰:
代码块身份:已验证可执行示例
```tsl
program test;
function ReadConst(const x);
begin
return x + 1;
end;
procedure SetVar(var x);
begin
x := x + 5;
end;
begin
a := 10;
WriteLn(ReadConst(a));
SetVar(a);
WriteLn(a);
end.
```
已验证运行结果:
- `ReadConst(a)` 输出 `11`
- `SetVar(a)` 之后,`a` 输出 `15`
- 直接给 `const` 形参赋值会编译失败
### 参数传递方式
默认参数写回调用方:
代码块身份:已验证可执行示例
```tsl
program test;
function TouchDefault(a);
begin
a := 9;
end;
begin
x := 1;
TouchDefault(x);
WriteLn(x);
end.
```
已验证运行结果:
- `TouchDefault(x)` 之后,`x` 输出 `9`
`{$VarByRef-}``var` 形参:
代码块身份:已验证可执行示例
```tsl
program test;
function TouchDefault(a);
begin
a := 9;
end;
{$VarByRef-}
function TouchValue(a);
begin
a := 8;
end;
function TouchForcedVar(var a);
begin
a := 7;
end;
{$VarByRef+}
begin
x := 1;
TouchDefault(x);
WriteLn(x);
y := 1;
TouchValue(y);
WriteLn(y);
z := 1;
TouchForcedVar(z);
WriteLn(z);
end.
```
已验证运行结果:
- 默认模式下,`TouchDefault(x)` 后 `x` 输出 `9`
- `{$VarByRef-}` 下,未修饰参数版本 `TouchValue(y)` 之后,`y` 仍输出 `1`
- `{$VarByRef-}` 下,`var` 形参版本 `TouchForcedVar(z)` 之后,`z` 输出 `7`
`in` / `out` 调用前缀:
代码块身份:已验证可执行示例
```tsl
program test;
{$VarByRef-}
procedure Touch3(a, b, c);
begin
a := 1;
b := 2;
c := 3;
end;
begin
a := 0;
b := 0;
c := 0;
Touch3(a, b, c);
WriteLn(a);
WriteLn(b);
WriteLn(c);
Touch3(in a, out b, c);
WriteLn(a);
WriteLn(b);
WriteLn(c);
end.
```
已验证运行结果:
-`{$VarByRef-}` 下,直接调用 `Touch3(a, b, c)` 后依次输出 `0`、`0`、`0`
- 同样在 `{$VarByRef-}` 下,`Touch3(in a, out b, c)` 后依次输出 `0`、`2`、`0`
- 说明 `in` / `out` 可以在调用点逐个参数覆盖当前默认传递方式
### `return` 与 `exit`
代码块身份:已验证可执行示例
```tsl
program test;
function Demo(x);
begin
if x > 0 then
exit;
return 99;
end;
begin
WriteLn(Demo(1));
WriteLn(Demo(0));
end.
```
已验证运行结果:
- `Demo(1)` 输出 `0`
- `Demo(0)` 输出 `99`
- 说明 `exit;` 会立即结束当前函数体
- 在这个最小例子里,因为 `exit;` 之前没有写入返回结果,调用方观察到的是默认值 `0`
### 调用增强
命名参数调用:
代码块身份:已验证可执行示例
```tsl
function NamedArgsDemo();
begin
return Pack(a: 1, b: 2);
end;
function Pack(a, b);
begin
return a * 10 + b;
end;
```
已验证运行结果:
- `Pack(a: 1, b: 2)` 返回 `12`
- `Pack(b: 2, a: 1)` 返回 `12`
- `Pack(1, b: 2)` 返回 `12`
跳过中间参数时,未命中的参数当前保持 `nil`
代码块身份:已验证可执行示例
```tsl
program test;
function TestFunc(a, b, c);
begin
WriteLn(IfNil(b));
return 0;
end;
begin
TestFunc(1, c: 3);
end.
```
已验证运行结果:
- 输出 `1`
- 说明 `TestFunc(1, c: 3)` 这种调用里,中间参数 `b` 当前会保持 `nil`
通过 `call(...)` 也支持命名参数:
代码块身份:已验证可执行示例
```tsl
program test;
function TestFunc(a, b, c);
begin
return a * 100 + b * 10 + c;
end;
begin
WriteLn(call("TestFunc", a: 1, c: 2, b: 3));
end.
```
已验证运行结果:
- 输出 `132`
- 说明 `call(...)` 当前也支持按参数名传值
- 说明命名参数传入后不再按位置解释,而是按名字绑定到形参
命名参数可以调换顺序:
代码块身份:已验证可执行示例
```tsl
function NamedArgsDemo();
begin
return Pack(b: 2, a: 1);
end;
function Pack(a, b);
begin
return a * 10 + b;
end;
```
命名参数也可以和位置参数混用:
代码块身份:已验证可执行示例
```tsl
function NamedArgsDemo();
begin
return Pack(1, b: 2);
end;
function Pack(a, b);
begin
return a * 10 + b;
end;
```
### 默认参数
默认值参数:
代码块身份:已验证可执行示例
```tsl
function AddOne(a = 1);
begin
return a + 1;
end;
```
带类型时也支持默认值:
代码块身份:已验证可执行示例
```tsl
function TypedAdd(a: integer = 1): integer;
begin
return a + 1;
end;
```
多个参数时,后面的参数可以带默认值:
代码块身份:已验证可执行示例
```tsl
function Pack(a, b = 2);
begin
return a * 10 + b;
end;
```
代码块身份:已验证可执行示例
```tsl
function Pack(a: integer; b: integer = 2): integer;
begin
return a * 10 + b;
end;
```
默认值也可以写成表达式:
代码块身份:已验证可执行示例
```tsl
function ExprDefault(a = 1 + 2);
begin
return a;
end;
```
已验证运行结果:
- `AddOne()` 返回 `2`
- `AddOne(5)` 返回 `6`
- `TypedAdd()` 返回 `2`
- `TypedAdd(5)` 返回 `6`
- `Pack(1)` 返回 `12`
- `Pack(a: 1)` 返回 `12`
- `ExprDefault()` 返回 `3`
`unit interface` 声明下的默认参数要单独看:
以下是不可照写的对比:普通函数默认参数能跑通,不等于跨 `unit` 声明边界也同样可靠。
代码块身份:反例 / 不可照写
```text
// P.tsf
function P(a = 5, b = 6);
begin
return a + b;
end;
// UnitLiteral.tsf
unit UnitLiteral;
interface
function H(a = 5);
implementation
function H(a);
begin
return a;
end;
end.
// UnitConst.tsf
unit UnitConst;
interface
const CS = 888;
function F(a, b = 100, c = CS);
implementation
function F(a, b, c);
begin
return a + b + c;
end;
end.
// main.tsl
program test;
uses UnitLiteral, UnitConst;
begin
WriteLn(P());
WriteLn(H());
WriteLn(F(1));
WriteLn(F(1, 2));
end.
// command
tsl .\main.tsl -LIBPATH "D:\path\to\dir\"
```
已验证运行结果:
- 普通函数 `P()` 输出 `11`
- `unit interface` 里声明的字面量默认值 `H()` 输出 `5`
- `F(1)` 输出 `101``F(1, 2)` 输出 `3`
- 同一组文件下,`UnitConst.CS` 当前仍可读到 `888`,而 `F(1, 2, 3)` 输出 `6`
- 因此当前解释器下,不要把“普通函数默认参数可用”直接泛化成“`unit interface` 里引用 `unit const` 的默认参数也同样可靠”
- 这组多文件结果依赖真实查找路径;当前验证命令是把对应目录放进 `-LIBPATH`
- 这类跨 `unit` 的声明边界,统一回看 [10_units_and_scope.md](10_units_and_scope.md)
### 可变参数 `...`
尾部可变参数:
代码块身份:已验证可执行示例
```tsl
function SumAll(...);
begin
s := 0;
for i, v in Params do
s := s + v;
return s;
end;
```
可变参数转发:
代码块身份:已验证可执行示例
```tsl
function Forward(...);
begin
return SumAll(...);
end;
function SumAll(...);
begin
s := 0;
for i, v in Params do
s := s + v;
return s;
end;
```
`ParamCount``RealParamCount`
代码块身份:已验证可执行示例
```tsl
function CountArgs(a, b, ...);
begin
return ParamCount * 10 + RealParamCount;
end;
```
已验证运行结果:
- `SumAll(1, 2, 3, 4)` 返回 `10`
- `Forward(1, 2, 3, 4)` 返回 `10`
- `CountArgs(1, 2, 3, 4)` 返回 `44`
- `CountArgs(1, 2)` 返回 `22`
通过 `call``##` 转发可变参数:
代码块身份:已验证可执行示例
```tsl
program test;
function Sum3(a, b, c);
begin
return a + b + c;
end;
function DoFunc(fc, ...);
begin
return call(fc, ...);
end;
function DoFunc2(fc, ...);
begin
return ##fc(...);
end;
begin
WriteLn(DoFunc("Sum3", 1, 2, 3));
WriteLn(DoFunc2(thisfunction(Sum3), 1, 2, 3));
end.
```
已验证运行结果:
- `DoFunc("Sum3", 1, 2, 3)` 输出 `6`
- `DoFunc2(thisfunction(Sum3), 1, 2, 3)` 输出 `6`
通过 `invoke` 转发可变参数:
代码块身份:已验证可执行示例
```tsl
program test;
type TestC = class
public
function FuncA(a, b, c);
begin
return a + b + c;
end;
function FuncB(a, b, c);
begin
return a * 100 + b * 10 + c;
end;
function Dispatch(kind, ...);
begin
if kind = "Int" then
return FuncA(...)
else
return FuncB(...);
end;
end;
function DoInvoke(fc_name, ...);
begin
obj := new TestC();
return invoke(obj, fc_name, 0, ...);
end;
begin
WriteLn(DoInvoke("Dispatch", "Int", 2, 20, 200));
WriteLn(DoInvoke("Dispatch", "Other", 1, 2, 3));
end.
```
已验证运行结果:
- `DoInvoke("Dispatch", "Int", 2, 20, 200)` 输出 `222`
- `DoInvoke("Dispatch", "Other", 1, 2, 3)` 输出 `123`
### 匿名函数与函数指针
匿名函数变量:
代码块身份:已验证可执行示例
```tsl
program test;
begin
a := function(x, y)
begin
return x + y;
end;
WriteLn(call(a, 1, 2));
WriteLn(##a(5, 6));
end.
```
已验证运行结果:
- `call(a, 1, 2)` 输出 `3`
- `##a(5, 6)` 输出 `11`
匿名函数也可以直接作为参数传入:
代码块身份:已验证可执行示例
```tsl
program test;
function Apply(fun);
begin
return call(fun, 2, 3);
end;
begin
WriteLn(Apply(function(x, y)
begin
return x * y;
end));
end.
```
已验证运行结果:
- 输出 `6`
函数指针变量也可以通过 `FindFunction(...)` 拿到:
代码块身份:已验证可执行示例
```tsl
program test;
function Add(a, b);
begin
return a + b;
end;
begin
f := FindFunction("Add");
WriteLn(##f(1, 2));
end.
```
已验证运行结果:
- 输出 `3`
也可以通过 `ThisFunction(...)` 从已知函数名直接拿到:
代码块身份:已验证可执行示例
```tsl
program test;
function Add(a, b);
begin
return a + b;
end;
begin
f := ThisFunction(Add);
WriteLn(Call(f, 3, 4));
end.
```
已验证运行结果:
- 输出 `7`
- 说明 `ThisFunction(Add)` 当前可以得到稳定可调用的函数值
直接 `f(...)` 在当前解释器里不要当成可靠写法:
代码块身份:反例 / 不可照写
```text
// 匿名函数变量直接调用
program test;
begin
a := function(x, y)
begin
return x + y;
end;
WriteLn(a(7, 8));
end.
// FindFunction 返回值直接调用
program test;
function Add(a, b);
begin
return a + b;
end;
begin
f := FindFunction("Add");
WriteLn(f(3, 4));
end.
// ThisFunction 返回值直接调用
program test;
function Add(a, b);
begin
return a + b;
end;
begin
f := ThisFunction(Add);
WriteLn(f(3, 4));
end.
```
已验证结果:
- 上面三种 `f(...)` 直调写法都会报 `function:<name> compile error or not found`
- 因此当前手册只把 `call(f, ...)``##f(...)` 写成稳定规则
### `::` 指向全局函数
当当前作用域里有同名局部函数时,可以用 `::FuncName(...)` 指定去调全局/系统函数:
代码块身份:已验证可执行示例
```tsl
program test;
function Demo();
begin
return array(StrToInt("123"), ::StrToInt("123"));
end;
function StrToInt(s);
begin
return 888;
end;
begin
r := Demo();
WriteLn(r[0]);
WriteLn(r[1]);
end.
```
已验证运行结果:
- 第一项 `StrToInt("123")` 命中局部函数,输出 `888`
- 第二项 `::StrToInt("123")` 命中全局系统函数,输出 `123`
- 说明 `::` 可以绕过当前作用域的同名局部函数,直接指向全局/系统函数
如果 `::FuncName(...)` 指向的全局函数本身不存在,当前解释器会在运行时报 `function:<name> compile error or not found`
### 系统交互专题
`external`、动态库函数指针、`MakeInstance` 和线程调用,统一见 [21_external_calls_and_threads.md](21_external_calls_and_threads.md)。这一篇只保留“普通函数怎样定义和调用”的主线。
## 最小可编译示例
如果你只是要写一个能被 session 稳定续写的函数 / 过程,从下面任一骨架起步:
代码块身份:已验证可执行示例
```tsl
function Hello();
begin
return 1;
end;
```
代码块身份:已验证可执行示例
```tsl
procedure HelloProc();
begin
end;
```
## 常见误写
- 把顶层函数定义和松散语句混写。
- 以为函数头后的分号是当前解释器的硬性要求。
- 以为 `procedure` 只是 `function` 的别名,不涉及参数传递语义。
- 带类型注解时仍然用逗号分隔参数。
- 以为默认未修饰参数天然就是按值传递。
-`a = 1` 这种比较表达式误当成命名参数调用。
- 以为默认值只能用于无类型参数。
-`const` 形参上直接赋值。
- 把普通函数的默认值规则原样套到 `unit interface` 里的 `const` 默认参数上。
- 把匿名函数或 `FindFunction(...)` 返回值默认写成 `f(...)` 直调。
- 把命名参数直接套到二进制函数或系统函数上。
- 在一次调用里先进入命名参数模式,后面又退回位置参数。
代码块身份:反例 / 不可照写
```text
function Add(a, b);
begin
return a + b;
end;
value := Add(1, 2);
```
上面这种混写方式会编译失败,问题不在 `Add` 本身,而在于文件模型混了“函数定义体”和“松散语句”两种写法。
代码块身份:反例 / 不可照写
```text
function Demo(a: integer, b: integer);
begin
return a + b;
end;
```
上面这种写法会编译失败;参数一旦带类型,分隔符应改成分号。
代码块身份:反例 / 不可照写
```text
Pack(a = 1, b = 2)
```
这类写法不要当成命名参数。它虽然可能编译通过,但在我于 `2026-04-09` 的实测里返回结果不对,不能当成可靠的命名参数语法。
代码块身份:反例 / 不可照写
```text
function Demo(a: integer, b: integer = 2): integer;
begin
return a + b;
end;
```
上面这种写法也不对;参数一旦带类型,分隔符仍然应保持分号。
代码块身份:反例 / 不可照写
```text
function TouchDefault(a);
begin
a := 9;
end;
```
不要把上面这种未修饰参数自动理解成“按值传递”。在当前解释器里,它会把调用方实参改掉;如果当前任务要求按值语义,先看 `{$VarByRef-}``in` / `out` 的例子。
代码块身份:反例 / 不可照写
```text
procedure Bad(const x);
begin
x := 2;
end;
```
上面这种写法会编译失败;`const` 形参在当前解释器里不能被重新赋值。
代码块身份:反例 / 不可照写
```text
unit UnitConst;
interface
const CS = 888;
function F(a, b = 100, c = CS);
```
不要把上面这种 `unit interface` 声明直接当成已经等价于普通函数默认参数规则。在当前解释器里,对应的 `F(1)` 实测输出是 `101`,不是按 `CS = 888` 补成的结果;具体边界见 [10_units_and_scope.md](10_units_and_scope.md)。
代码块身份:反例 / 不可照写
```text
a := function(x, y)
begin
return x + y;
end;
WriteLn(a(7, 8));
```
不要把上面这种匿名函数变量的直调写法直接当成可靠规则。在当前解释器里,它会报 `function:a compile error or not found`;稳定写法仍然是 `call(a, 7, 8)``##a(7, 8)`
代码块身份:反例 / 不可照写
```text
WriteLn(IntToStr(value: 200));
```
不要把上面这种写法直接套到二进制函数或系统函数上。当前解释器里,它会报 `named parameter mode can't support here`;如果确实要命名传参,先用 TSL 函数再封一层。
代码块身份:反例 / 不可照写
```text
function Pack(a, b, c);
begin
return a * 100 + b * 10 + c;
end;
begin
WriteLn(Pack(a: 1, 2, c: 3));
end.
```
不要在同一次调用里“先进入命名参数模式,再退回位置参数”。当前这类写法会直接编译失败,错误信息包含 `paramname: not found`
## 跳转指引
- 回看最短骨架:见 [02_quickstart.md](02_quickstart.md)
- 回看流程控制:见 [08_control_flow.md](08_control_flow.md)
- 当函数变多时:见 [10_units_and_scope.md](10_units_and_scope.md)
-`external`、`MakeInstance` 和线程调用:见 [21_external_calls_and_threads.md](21_external_calls_and_threads.md)