playbook/docs/tsl/syntax/04_values_and_literals.md

15 KiB
Raw Permalink Blame History

Basic Types And Values

文档类型:语法主线 是否可直接用于生成代码:是 是否含已验证可执行示例:是 是否含已验证反例:是 遇到不确定时跳转到:05_variables_and_constants.md07_expressions_and_operators.md20_strings_and_text.md

手册位置:第 4 篇,共 32 篇。上一篇:03_core_model.md。下一篇:05_variables_and_constants.md

这一篇整理基本类型、字面量、数组、字符串与基础值模型,避免把值规则分散在函数或金融示例里。

这一篇解决什么问题

回答“基本类型怎么写、数组和字符串怎么索引、哪些值规则属于语言级事实”。

必须记住的规则

  • 最先掌握的几类值是:整数、实数、字符串、布尔和 array
  • 普通字符串既可以用双引号,也可以用单引号。
  • 同类引号本身可以通过连续写两个同类引号放进字符串里。
  • \\\"\n 这类基础转义已验证可用。
  • \t\xNN 这类转义也已验证可用。
  • \r\r\n\a\b\f\v 这些经典转义也已验证可用。
  • 当前解释器接受 %% 原始字符串;开头的 %%%%tag 后面必须先跟一个空白分隔符,正文才开始。
  • %% 原始字符串里的反斜杠不做转义处理,并且支持多行内容。
  • array(...) 既可以写顺序数组,也可以写字符串键表。
  • 顺序数组下标从 0 开始。
  • Binary(...) 创建的二进制缓冲区,当前也用 [] 访问,并且下标从 0 开始。
  • 字符串下标从 1 开始。
  • s[0] 在运行时会越界,不要把字符串当成 0 基下标。
  • 字符串取子串用 s[start:end],并且 end 是包含在结果里的。
  • 字符串替换子串用 s[start:end] := "..."
  • 字符串删除子串,本质上就是 s[start:end] := ""
  • 字符串插入子串用 s[index:0] := "..."
  • 当前解释器接受 L""U"" 前缀字符串,但当前手册默认仍优先普通字符串。
  • 当前解释器也接受 L%% ...%%U%% ...%% 前缀原始字符串。
  • 当前最小验证表明:从 IfWString(...) 观察,混合拼接时结果是否为宽串,取决于左操作数是否为宽串;U"" 在这里仍按非宽串处理。
  • 当前最小验证表明:可以用 String(...) 把宽串显式转回普通串,也可以用 WideString(...) 把普通串显式转成宽串。
  • 当前最小验证表明:U"" 可以和 UnicodetoUTF8(...)UTF8ToUnicode(...)UTF8ToAnsi(...)AnsiToUTF8(...) 组成一条清晰的 UTF8 转换链;U"" 本身不等同于宽串。
  • 当前最小验证表明:\uXXXX 在普通字符串里不要直接当成“单个字符宽串”;例如 Length("\u0041")2,而 Length(L"\u0041")Length(U"\u0041") 都为 1
  • 当前最小验证表明:字符串可以包含 ASCII 0 字符;\0#0 都能构造这个字符,并且不会把字符串截断。
  • #number 可以按字符码直接拼进字符串里;#13#10 这类写法可直接形成换行。

已验证语法

最基础的值写法:

代码块身份:已验证可执行示例

count := 1;
price := 12.5;
name := "ABC";
flag1 := true;
flag2 := false;
items := array(1, 2, 3);

其中已经验证:

  • true 可以直接写成布尔值。
  • false 可以直接写成布尔值。
  • 运行输出里,true / false 分别表现为 1 / 0

普通字符串字面量:

代码块身份:已验证可执行示例

WriteLn("ABC");
WriteLn('XYZ');

已验证运行结果:

  • 依次输出 ABCXYZ

字符串内引号与基础转义:

代码块身份:已验证可执行示例

WriteLn("A""B");
WriteLn('A''B');
WriteLn("A\\B");
WriteLn("A\"B");
WriteLn("A\nB");

已验证运行结果:

  • 依次输出 A"BA'BA\B
  • "A\nB" 会分成两行输出 AB

更多基础转义:

代码块身份:已验证可执行示例

WriteLn("A\tB" = "A"#9"B");
WriteLn("A\x30B" = "A0B");

已验证运行结果:

  • 两行都输出 1
  • 说明 \t 可以表示制表符,\x30 这类写法可以按 ANSI 字节值插入字符

经典控制字符转义:

代码块身份:已验证可执行示例

program test;
begin
    WriteLn("\r" = #13);
    WriteLn("\r\n" = #13#10);
    WriteLn(Length("\r\n"));
    WriteLn("\a" = #7);
    WriteLn("\b" = #8);
    WriteLn("\f" = #12);
    WriteLn("\v" = #11);
end.

已验证运行结果:

  • 依次输出 1121111
  • 说明 \r / \r\n 可以直接表示回车和回车换行
  • 说明 \a\b\f\v 也已经在当前解释器里拿到正向最小结果

L"" / U"" 前缀的最小已验证事实:

代码块身份:已验证可执行示例

WriteLn(L"A" = L"\u0041");
WriteLn(U"Hello" = "Hello");

已验证运行结果:

  • 依次输出 11
  • 说明当前解释器接受 L""U"" 和 ASCII 范围的 \u 表达
  • 但在当前控制台里直接 WriteLn(L"Hello") 会显示成带空字节的宽串输出,因此当前手册不把 L"" 作为默认示例写法

非 ASCII 宽串的最小已验证事实:

代码块身份:已验证可执行示例

WriteLn(L"\u5929\u8F6F" = L""#0x5929#0x8F6F);

已验证运行结果:

  • 输出 1
  • 说明当前解释器接受非 ASCII 宽串的 \uXXXX#0xXXXX 这两种构造方式
  • 但当前手册仍优先把它们当作边界写法,而不是默认入门写法

原始字符串 %%

代码块身份:已验证可执行示例

s := %% ABC%%;
WriteLn(s);

已验证运行结果:

  • 输出 ABC
  • 说明 %% 开头后面的第一个空白只是分隔符,不计入最终字符串内容

原始字符串不会处理转义:

代码块身份:已验证可执行示例

s := %% a\r\nb%%;
WriteLn(s = "a\\r\\nb");

已验证运行结果:

  • 输出 1
  • 说明 %% 原始字符串里的 \r\n 只是普通字符,不会变成回车换行

带标识符的原始字符串:

代码块身份:已验证可执行示例

s := %%tag A"B'C%%tag;
WriteLn(s);

已验证运行结果:

  • 输出 A"B'C
  • 说明 %%tag ...%%tag 的首尾标识符可以配对使用,正文里可直接放单双引号

多行原始字符串:

代码块身份:已验证可执行示例

s := %%m
A
B%%m;
WriteLn(s);

已验证运行结果:

  • 输出两行 AB
  • 说明 %% 原始字符串支持直接跨行

带前缀的原始字符串:

代码块身份:已验证可执行示例

WriteLn(L%% ABC%% = L"ABC");
WriteLn(U%% ABC%% = "ABC");

已验证运行结果:

  • 依次输出 11
  • 说明 L%% ...%%U%% ...%% 在当前解释器里都可用

前缀字符串与混合拼接的最小类型观察:

代码块身份:已验证可执行示例

program test;
begin
    WriteLn(IfWString("A" + L"B"));
    WriteLn(IfWString(L"A" + "B"));
    WriteLn(IfWString(U"A"));
    WriteLn(IfWString(U"A" + L"B"));
    WriteLn(IfWString(L"A" + U"B"));
end.

已验证运行结果:

  • 依次输出 01001
  • 说明从 IfWString(...) 的观察角度看,混合拼接是否得到宽串,取决于左操作数是否为宽串
  • 也说明 U"" 前缀字符串在这里仍按非宽串处理,不等同于 L""

显式字符串类型转换:

代码块身份:已验证可执行示例

program test;
begin
    a := String(L"abcd");
    b := WideString("abcd");
    WriteLn(a = "abcd");
    WriteLn(IfWString(a));
    WriteLn(b = L"abcd");
    WriteLn(IfWString(b));
end.

已验证运行结果:

  • 依次输出 1011
  • 说明 String(...) 可以把宽串显式转回普通串
  • 说明 WideString(...) 可以把普通串显式转成宽串

U"" 与 UTF8 显式转换:

代码块身份:已验证可执行示例

program test;
begin
    utf8_s := U"\u5929\u8F6F";
    utf8_s2 := UnicodetoUTF8(L"\u5929\u8F6F");
    wide_s := UTF8ToUnicode(utf8_s);
    ansi_s := UTF8ToAnsi(utf8_s);
    WriteLn(IfWString(utf8_s));
    WriteLn(utf8_s = utf8_s2);
    WriteLn(wide_s = L"\u5929\u8F6F");
    WriteLn(ansi_s = String(L"\u5929\u8F6F"));
end.

已验证运行结果:

  • 依次输出 0111
  • 说明 U"" 在当前解释器里不是宽串
  • 说明 U"\u5929\u8F6F"UnicodetoUTF8(L"\u5929\u8F6F") 可以对应到同一个 UTF8 串
  • 说明 UTF8ToUnicode(...) 可以把 UTF8 串转回宽串,UTF8ToAnsi(...) 可以把 UTF8 串转回普通串

ANSI / UTF8 / 宽串的最小往返:

代码块身份:已验证可执行示例

program test;
begin
    ansi_s := String(L"\u5929\u8F6F");
    utf8_s := AnsiToUTF8(ansi_s);
    ansi_back := UTF8ToAnsi(utf8_s);
    wide_s := UTF8ToUnicode(utf8_s);
    WriteLn(IfWString(ansi_s));
    WriteLn(IfWString(utf8_s));
    WriteLn(IfWString(wide_s));
    WriteLn(ansi_back = ansi_s);
    WriteLn(wide_s = L"\u5929\u8F6F");
end.

已验证运行结果:

  • 依次输出 00111
  • 说明 AnsiToUTF8(...) 产物在这里仍按非宽串处理
  • 说明 UTF8ToAnsi(...)UTF8ToUnicode(...) 都已经在当前解释器里拿到了正向最小结果

\uXXXX 在普通字符串、宽串和 U 串里的最小边界:

代码块身份:已验证可执行示例

program test;
begin
    WriteLn(Length("\u0041"));
    WriteLn(Length(L"\u0041"));
    WriteLn(Length(U"\u0041"));
end.

已验证运行结果:

  • 依次输出 211
  • 因此不要把普通字符串里的 "\u0041" 直接理解成和 L"\u0041" 一样的“单字符宽串”语义

ASCII 0 字符不会截断字符串:

代码块身份:已验证可执行示例

program test;
begin
    s1 := "A\0B";
    s2 := "A"#0"B";
    WriteLn(Length(s1));
    WriteLn(Length(s2));
    WriteLn(s1 = s2);
    WriteLn(s1[2] = #0);
end.

已验证运行结果:

  • 依次输出 3311
  • 说明 \0#0 都可以把 ASCII 0 放进字符串
  • 也说明当前字符串不是遇到 #0 就自动截断的 C 风格零结尾串

带 ASCII 0 的字符串切片仍然保留该字符:

代码块身份:已验证可执行示例

program test;
begin
    s := "A"#0"B";
    t := s[1:3];
    WriteLn(Length(t));
    WriteLn(t = s);
    u := s[2:2];
    WriteLn(Length(u));
    WriteLn(u = #0);
end.

已验证运行结果:

  • 依次输出 3111
  • 说明字符串切片和单字符读取对 ASCII 0 仍然稳定,不会在中间被截断

字符码拼接:

代码块身份:已验证可执行示例

WriteLn("A"#48"B");
WriteLn("A"#13#10"B");

已验证运行结果:

  • 第一行输出 A0B
  • 第二组输出会分成两行 AB
  • 说明 #48 这类写法会按字符码直接参与字符串拼接,#13#10 可直接表示换行

代码块身份:已验证可执行示例

arr := array(10, 20, 30);
hash := array("Code": "0001", "Price": 12.3);
s := "ABC";
WriteLn("ARR0=", arr[0]);
WriteLn("ARR1=", arr[1]);
WriteLn("HASH=", hash["Code"]);
WriteLn("STR1=", s[1]);
WriteLn("STR2=", s[2]);
WriteLn("STR3=", s[3]);

已验证运行结果对应关系:

  • arr[0] = 10
  • arr[1] = 20
  • hash["Code"] = "0001"
  • s[1] = "A"
  • s[2] = "B"
  • s[3] = "C"

二进制缓冲区:

代码块身份:已验证可执行示例

program test;
begin
    b := Binary("30");
    WriteLn(IfBinary(b));
    WriteLn(b[0] = "3");
    WriteLn(b[1] = "0");
    b[1] := 0x31;
    WriteLn(b[1] = "1");
end.

已验证运行结果:

  • 依次输出 1111
  • 说明 Binary(...) 当前确实得到二进制缓冲区
  • 说明二进制缓冲区的 [] 下标从 0 开始
  • 也说明二进制缓冲区项可以直接按单字节读写

字符串子串读取:

代码块身份:已验证可执行示例

s := "ABCDE";
WriteLn(s[2:4]);

已验证运行结果:

  • 输出 BCD
  • 这说明字符串区间 2:4 会包含结束位 4

字符串替换子串:

代码块身份:已验证可执行示例

s := "ABCDE";
s[2:4] := "XYZ";
WriteLn(s);

已验证运行结果:

  • 输出 AXYZE

字符串删除子串:

代码块身份:已验证可执行示例

s := "ABCDE";
s[2:4] := "";
WriteLn(s);

已验证运行结果:

  • 输出 AE

字符串插入子串:

代码块身份:已验证可执行示例

s := "AAABBB";
s[4:0] := "111";
WriteLn(s);

已验证运行结果:

  • 输出 AAA111BBB
  • 这说明 s[index:0] := ... 是在指定位置插入,而不是删除或替换区间

最小可编译示例

如果你只是要抓住“基本类型 + array”的第一层用这个最短例子

代码块身份:已验证可执行示例

count := 1;
price := 12.5;
name := "ABC";
flag := true;
items := array(1, 2, 3);

常见误写

  • s[0] 访问字符串首字符。
  • 误以为 array(...) 只能写顺序数组,不能写字符串键表。
  • 误以为字符串区间和数组一样从 0 开始。
  • s[index:0] := ... 误解成“替换到第 0 位”。
  • 把二进制缓冲区也按字符串的 1 基下标去理解。
  • 在普通字符串示例里默认切到 L"",却没有先确认下游场景真的需要宽串。
  • U"" 误解成和 L"" 一样的宽串。
  • 把普通字符串里的 "\uXXXX" 直接当成和 L"\uXXXX" 一样的单字符写法。
  • 把带 #0 / \0 的字符串当成会自动截断的 C 风格零结尾串。
  • %%ABC%% 当成合法的原始字符串起手式。

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

s := "ABC";
WriteLn(s[0]);

上面这类写法在运行时会报字符串下标越界。

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

s := "ABCDE";
WriteLn(s[1:3]);

不要把上面这个区间访问类比成数组切片的半开区间。当前解释器里它输出的是 ABC,不是 AB;字符串区间的结束位会被包含进结果。

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

s := %%ABC%%;

上面这种写法在当前解释器里会报 missing or invalid characters%% 原始字符串开头后面必须先跟一个空白分隔符,或者写成 %%tag 这种“标识符 + 空白”形式。

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

WriteLn(Length("\u0041"));
WriteLn(Length(L"\u0041"));

不要把上面这两行想成同一件事。当前解释器里前者输出 2,后者输出 1;普通字符串里的 \uXXXX 不能直接按宽串单字符去理解。

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

WriteLn(IfWString(U"\u5929\u8F6F"));
WriteLn(IfWString(L"\u5929\u8F6F"));

不要把 U""L"" 当成同一种“宽串前缀”。当前解释器里前者输出 0,后者输出 1

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

s := "A"#0"B";
WriteLn(Length(s));

不要把上面这类字符串想成遇到 #0 就结束。当前解释器里这里输出 3,说明 ASCII 0 是字符串内容的一部分。

跳转指引