第 3 章 / 共 58 章 卷一 起步

基本类型与字面量全形态

数据是程序的原料。这一章把 xlang 的每一种基本类型、每一种字面量写法全部过一遍。

本章导读

第 2 章搭好了程序的骨架,这一章往里填"数据"。任何程序处理的无非是数字、文字、真假、时间这几类东西,xlang 为它们各自准备了哪些类型、字面上怎么写,就是本章要一次讲透的内容。

本章的写法是"全形态":不是挑常用的讲,而是把每一种合法写法都列出来并实测——包括十六进制整数字母必须小写、raw 字符串不转义、f-string 里能写任意表达式这类容易踩坑的细节。读完本章,你看到任何 xlang 字面量都不会陌生。

本章学习目标

认识 xlang 全部基本类型;掌握整数、浮点、字符串、时间、布尔、空值的全部字面量形态;理解 var/auto 的区别;会用 type() 查看任何值的类型。

3.1 类型系统第一印象

xlang 是静态类型语言:每个变量在声明时就有确定的类型,编译器在运行前就会检查类型错误。声明采用 C 风格类型前置:

类型全景
bool flag = true
int count = 42
float pi = 3.14159
string name = "xlang"
time today = #2026-07-18#
var anything = "动态"     // 动态类型
auto inferred = 100       // 编译期推断为 int

想随时确认一个值"到底是什么类型",用全局函数 type():

> xl run types.xl # println(type(flag), type(count), ...) bool int float string time string int

xlang 的基本类型全集(本章逐个讲):

类别类型
空值与布尔null、bool
整数int、int8 uint8 int16 uint16 int32 uint32 int64 uint64
浮点float、float32、float64
文本string
时间time
动态与推断var、auto、type
指针标签point、point<T>(C 互操作用,本章只建立概念)

容器类型(array/list/set/dict)和用户定义类型(class/union)不在本章——它们分别是卷四和卷五的主角。

3.2 布尔与空值

bool 只有两个取值:true 和 false,全部小写:

bool 与 null
bool ok = true
bool bad = false
println(ok, bad)          // 输出: true false

var empty = null
println(empty == null)   // 输出: true

null 表示"什么都没有"。它不是 0,不是空字符串,不是 false——它是一个独立的值,表示"此处无值"。判断一个值是否为 null 用 == 比较(判断类型是否为空用 is null,第 7 章详解)。

新手常问:能不能写 if empty then 来判断?第 8 章分支结构会讲条件表达式的类型要求,这里先记住用 == null 是最明确的写法。

3.3 整数全族

整数字面量的四种进制

int_literals.xl
int a = 123          // 十进制
int b = 1_000_000    // 下划线分组,仅便于阅读
int c = 0x2A         // 十六进制 = 42
int d = 0o52         // 八进制 = 42
int e = 0b101010     // 二进制 = 42
println(a, b, c, d, e)
> xl run int_literals.xl 123 1000000 42 42 42

三个细节:

  • 下划线 _ 可以插在数字中间做分组,编译器直接忽略,1_000_000 就是 1000000;
  • 进制前缀只认小写 0x、0o、0b。大写 0X 是语法错误(见常见错误坑 1);VB 风格的 &H、&O、&B 在 xlang 中不存在;
  • int 不带后缀时是 64 位整数(深入细节拾遗 1 有实测证据)。

八种定宽整数类型

需要精确控制宽度时(二进制协议、硬件寄存器、文件格式),xlang 提供 8/16/32/64 位、各有符号和无符号共八种定宽类型:

定宽整数
int8 i8 = 127                  // -128 ~ 127
int16 i16 = 32767              // -32768 ~ 32767
int32 i32 = 2147483647
int64 i64 = 9223372036854775807
uint8 u8 = 255                 // 0 ~ 255
uint16 u16 = 65535
uint32 u32 = 4294967295
uint64 u64 = 18446744073709551615
println(i8, i16, i32, i64)   // 127 32767 2147483647 9223372036854775807

字面量超出声明类型的范围时,编译期就会被拦下,例如 int8 a = 200 会得到明确的报错(见常见错误坑 3)。

RC0 已知问题:无符号整型不能直接打印

写作本章时实测:println(u8) 这类直接打印无符号整型的写法会触发运行时链接错误(详见常见错误坑 4)。在修复版本发布前,请用 println(string(u8)) 或先装入 var 再打印。本书后续示例会主动避开这个缺陷。

3.4 浮点数

float_literals.xl
float a = 1.25       // 小数形式
float b = 1e3        // 科学计数法 = 1000.0
float c = 1.2e-3     // = 0.0012
println(a, b, c)
float32 f32 = 3.14
float64 f64 = 2.718281828459045
println(f32, f64)
> xl run float_literals.xl 1.25 1000.0 0.0012 3.140000104904175 2.718281828459045

float 不带后缀时是 64 位双精度(等价 float64);float32 是 32 位单精度。注意上面第二行输出:float32 存不下 3.14 的精确值,打印出来是 3.140000104904175——这不是 bug,而是 IEEE 754 单精度浮点的固有精度(拾遗 2 展开)。

规则:e 或 E 表示"×10 的多少次方",1e3 = 1×10³。没有小数点也没有 e 的字面量是整数;想让整数当浮点用,写 1.0 或用转换函数(第 7 章)。

3.5 字符串六种形态

xlang 有六种字符串写法,应对六种场景。

① 双引号 ② 单引号——普通字符串

普通字符串与转义
string a = "hello"
string b = 'world'       // 单双引号完全等价
string e = "换行\n制表\t引号\"反斜杠\\"

普通字符串支持转义序列:\n 换行、\t 制表、\" 引号、\\ 反斜杠等。选择单引号还是双引号的唯一理由:少写转义——字符串里有双引号就用单引号包,反之亦然。

③④ raw 字符串 r"..." / r'...'——不处理转义

raw 字符串
println(r"a\nb")              // 输出: a\nb(\n 是字面两个字符)
println(r"c:\new\file.txt")   // 输出: c:\new\file.txt

前缀 r 表示 raw(原样):反斜杠不再有任何特殊含义。写 Windows 路径、正则表达式时,raw 字符串能省掉成倍的反斜杠。

⑤⑥ 三引号 """...""" / '''...'''——多行字符串

多行字符串
string a = """
第一行
第二行
"""
string b = '''
单引号三引号
也可以
'''

三引号之间的换行原样保留,适合大段文本、SQL、HTML 模板。注意上面变量 a 的值以换行开头、以换行结尾——三引号"原样保留"包括首尾换行。

3.6 f-string 插值

前缀 f 让字符串支持 {表达式} 插值——这是拼接文本最顺手的方式:

fstr.xl
string name = "xlang"
int ver = 6
println(f"hello {name}")          // hello xlang
println(f"1 + 2 = {1 + 2}")       // 1 + 2 = 3 —— 任意表达式
println(f"{name} DEMO{ver}")      // xlang DEMO6
println(f"字面量大括号 {{ }}")     // 字面量大括号 { }
println(rf"c:\tmp\{name}.txt")   // c:\tmp\xlang.txt

要点:

  • {...} 里可以放任意表达式,不只变量名;
  • 想输出字面的大括号,写双份:{{ 和 }};
  • rf"..." 是 raw + f-string 的组合:反斜杠不转义,但插值仍然生效——Windows 路径插值的完美组合;
  • 空的 {} 是解析错误(见常见错误坑 5),大括号必须配对。
f-string 与 println 多参数输出的选择:多参数适合快速调试打印;f-string 适合需要精确控制格式的正式输出。第 19 章会讲更完整的格式化能力。

3.7 time 字面量

时间是 xlang 的内置类型 time,字面量用 # 包裹,三种形态:

time_literals.xl
time d = #2026-06-18#             // 纯日期
time t = #12:30:45#               // 纯时间
time dt = #2026-06-18 12:30:45#   // 日期时间
println(d)
println(t)
println(dt)
> xl run time_literals.xl 2026-06-18 00:00:00 0-01-01 12:30:45 2026-06-18 12:30:45

三行输出都值得读:纯日期的时间部分是 00:00:00;纯时间的日期部分显示为 0-01-01("第 0 年第 1 天"的零值日期,拾遗 3 展开)。

两条边界规则:时间字面量内部不能换行;忘记写结尾的 # 会得到专门的报错"未闭合的时间字面量"(常见错误坑 2)。时间的加减、比较、格式化是第 20 章的内容。

3.8 var、auto、type 与 point

var——动态类型

var 是"什么都能装"的动态类型,同一个变量先后可以装不同类型的值:

var 动态类型
var v = 123
println(type(v))     // int
v = "现在变字符串"
println(type(v))     // string

var 适合容器里的混合数据、脚本快速原型。代价是类型错误要到运行时才暴露——第 15 章会专门讨论何时该用 var、何时该用静态类型。

auto——编译期推断,推断完就是静态类型

auto 推断
auto n = 42       // 推断为 int
auto s = "hi"    // 推断为 string
println(type(n), type(s))   // int string

关键区别:auto 推断发生在编译期,推断完成后变量就是静态类型,之后不能再装别的类型;var 是运行时动态类型,可以随时换装。一句话:auto 是"帮我写类型",var 是"不要类型"。

type——类型本身也是值

type(expr) 返回表达式的类型值,类型值之间可以比较。本章已经多次用它验证类型。类型系统的完整玩法(typedef、类型比较、泛型)在第 15 章。

point——给 C 互操作准备的指针标签

point 声明
point p
println(type(p))   // point

point 是零开销的静态不透明指针标签,对应 C 的 void*;point<T> 是带标签的版本,不同标签之间不能隐式转换。它不面向日常编程——你在调用 C 库、处理原生句柄时才会用到它(第 32 章详解)。现在只需知道:基本类型全家福里有它一个位置。

深入细节

拾遗 1:int 默认是 64 位

很多语言的 int 是 32 位,xlang 不是。实测:int a = 2147483648(超出 32 位上限 1)声明成功,int 可以一直用到 9223372036854775807(2⁶³−1)。工程建议:日常写代码用 int 即可,不必纠结宽度;只有在对二进制布局有精确要求时(读写文件格式、网络协议、C 结构体映射)才使用 int8/int16/int32 等定宽类型——定宽类型的价值是"布局确定",不是"省内存"。

拾遗 2:float32 为什么存不下 3.14

3.4 节实测 float32 f32 = 3.14 打印出 3.140000104904175。原因:IEEE 754 单精度浮点只有 23 位尾数(约 7 位十进制有效数字),3.14 在二进制下是无限循环小数,只能存最近似值。双精度 float64 有 52 位尾数(约 15–16 位十进制有效数字),日常计算足够。推论:不要用 == 直接比较浮点数,金额计算不要用二进制浮点——这是所有 IEEE 754 语言的共同课题,第 6 章讲比较运算时还会提醒。

拾遗 3:纯时间字面量的日期为什么是"0-01-01"

#12:30:45# 打印为 0-01-01 12:30:45:time 是"日期+时间"的统一类型,纯时间字面量只填时间部分,日期部分保持零值,显示出来就是第 0 年 1 月 1 日。做时间比较和计算时这个零值日期通常无害,但把时间值格式化输出给用户看时,应只取时间部分(第 20 章的时间格式化会讲)。

⚠️ 常见错误

以下报错均为真实输出。

坑 1:十六进制前缀写成大写 0X
# bad.xl 内容:int a = 0X2A > xl run bad.xl bad.xl:1:10: [parse] 语法错误

原因:xlang 只保留小写规范形式 0x/0o/0b。词法器读到 0 就按十进制整数收尾,后面的 X2A 无法接续,于是在第 10 列报语法错误。历史语言里的大写 0X、VB 风格 &H 在 xlang 中一律不存在。正确做法:统一小写 0x2A。

坑 2:字符串或时间字面量忘记闭合
> xl run bad1.xl # string s = "abc bad1.xl:1:12: [parse] 未闭合的字符串 > xl run bad2.xl # time t = #2026-06-18 bad2.xl:1:10: [parse] 未闭合的时间字面量

原因:词法器对这三类字面量(字符串、时间、多行注释)给出了专属错误,而不是笼统的"语法错误"。报错列号指向字面量开始扫描后"发现收不了尾"的位置。正确做法:补上配对的引号或 #;普通字符串不能跨行,要跨行请用三引号。

坑 3:类型不匹配与字面量超范围
> xl run bad3.xl # int a = "text" bad3.xl:1:9: [sem] 初始化表达式类型和声明类型不匹配: target int, value string > xl run bad4.xl # int8 a = 200 bad4.xl:1:10: [sem] 整数字面量超出声明类型的表示范围

原因:两条都是静态类型检查([sem])在编译期拦下的问题。第一条:xlang 不会偷偷把字符串转成整数;第二条:200 超出 int8 上限 127,编译器宁可报错也不静默截断。正确做法:类型不一致时用显式转换(int("123") 等,第 7 章);定宽类型声明前先确认取值范围。

坑 4:直接打印无符号整型(RC0 已知缺陷)
# u.xl 内容: # uint8 a = 255 # println(a) > xl run u.xl tcc: tcc: error: unresolved reference to 'xl6_xrt_u64_to_string'

原因:这不是你的代码有问题——实测确认 RC0 中 println 打印任何无符号整型(uint8/16/32/64)都会因为缺少对应的运行时转换函数而链接失败,是编译器/运行时的已知缺陷。正确做法(修复前的变通):println(string(a)) 先转成字符串,或 var v = a 装入动态类型再打印,两种写法均已实测可用。有符号整型(含 int64)不受影响。

坑 5:f-string 写了空的或没闭合的插值
> xl run bad5.xl # string s = f"{}" bad5.xl:1:12: [parse] f-string 插值表达式不能为空 > xl run bad6.xl # string s = f"{1+2" bad6.xl:1:12: [parse] 未闭合的 f-string 插值表达式

原因:{} 必须成对且内部必须是合法表达式。正确做法:插值写完整;想要字面大括号就写 {{ 和 }}。

本章小结

知识点速查
类型声明C 风格类型前置 int a = 1;静态类型,编译期检查。
bool / nulltrue/false 全小写;null 表示无值,用 == null 判断。
整数字面量十进制、1_000_000 下划线、0x/0o/0b 仅小写;int 默认 64 位。
定宽整数int8/16/32/64 与 uint8/16/32/64;字面量超范围编译期报错;RC0 打印无符号需 string() 或 var 变通。
浮点1.25、1e3、1.2e-3;float = float64;float32 仅约 7 位有效数字。
字符串六种"" '' 普通(支持 \n \t \" \\);r"" r'' 原样;"""...""" '''...''' 多行。
f-stringf"{任意表达式}";{{ }} 字面大括号;rf"" 原样+插值。
time 字面量#日期# #时间# #日期 时间#;内部不可换行;纯时间日期部分为零值。
var / autovar 运行时动态、可换装;auto 编译期推断、推断后静态。
type()type(expr) 返回类型值,随时验证类型。
point静态不透明指针标签,对应 C void*,C 互操作专用(第 32 章)。

复习题

✍️ 先遮住答案想一想
  1. int、int32、uint8 三个类型的取值范围分别是什么?什么场景才需要后两个?
  2. r"c:\new" 和 "c:\\new" 的值是否相同?raw 字符串的适用场景是什么?
  3. var 和 auto 都"不用写类型",它们的本质区别是什么?
  4. 为什么 float32 f = 3.14 打印出来不是 3.14?由此得出什么工程结论?
  5. f"{{count}} = {count}" 中,哪对花括号会被求值、哪对会原样输出?

编程练习

练习 1:进制转换小工具

编写 bases.xl:声明同一个数值(例如 42)的十进制、十六进制、八进制、二进制四种字面量形式,用 println 输出验证它们相等;再用 f-string 输出一行形如 0x2A = 42 的说明。

参考思路:四种前缀的字面值相同则打印结果相同;f-string 里 0x2A 是普通文本,直接写即可,注意前缀小写。

练习 2:类型侦探

编写 detective.xl:分别用 type() 打印以下表达式的类型——42、42.0、"42"、true、null、1e3、#2026-01-01#。先把你的预测写在注释里,再运行对照。

参考思路:42.0 和 1e3 都是 float;"42" 是 string 不是数字。预测错的地方往往就是理解盲区,值得回到对应小节重读。

练习 3:个人名片生成器

编写 card.xl:声明姓名(string)、年龄(int)、生日(time)、是否学生(bool)四个变量,用三引号多行字符串 + f-string 输出一张带边框的文本名片。

参考思路:三引号保留换行,f-string 负责插值,但二者不能直接合并前缀——可以先 f 插值出各行再拼接,或直接在 f-string 里用 \n。边框用 - 和 | 字符即可。