第 5 章 / 共 58 章 卷一 起步

输入输出与 print 家族

程序与外界对话的通道。print 家族四个函数的确切差异,一次讲清。

本章导读

从第 1 章到现在,我们已经用了几十次 print 和 println,但一直是"会用"而非"懂"。这一章把输出这件事彻底讲透:print 家族四个函数的精确差异、各种类型打印出来长什么样、命令行参数怎么进程序,以及一个诚实的现状说明——xlang 当前版本如何"读"输入。

卷一至此收官:装好工具(第 1 章)、看懂结构(第 2 章)、认识数据(第 3 章)、学会存取(第 4 章)、能进能出(本章)。从卷二开始,我们让程序"动起来":运算与控制。

本章学习目标

精确区分 print/println/write/writeln;掌握各类型的打印格式;会用 arg 接收命令行参数;了解当前版本读取输入的两条现实途径(命令行参数、文件)。

5.1 四个函数一张表

print 家族四个全局函数,分两组:

函数参数参数间空格末尾换行值格式化
print(...)任意数量、任意类型自动加一个空格不加有
println(...)任意数量、任意类型自动加一个空格加(\n)有
write(text)恰好一个 string无(只有一个参数)不加无,原样输出
writeln(text = "")恰好一个 string无加(\r\n)无,原样输出

一句话记忆:print 组管"值",write 组管"字"。print 组接收任意值并负责把它们变成文本(值格式化);write 组只接受已经是字符串的文本,原样写出,不做任何加工。

注意一个实测差异:println 追加的换行是 \n,而 writeln 追加的是 \r\n(Windows 风格的回车换行)——深入细节拾遗 1 有字节级证据。

5.2 print 与 println

值输出两兄弟的唯一区别是末尾换不换行。看一组实测(输出用字节形式展示):

p1.xl
print("a")
print("b")
println("c")
println("d", "e")
print(1, 2.5, true)
println()
# 实际输出(\n 表示换行) abc\nd e\n1 2.5 true\n

逐行读:print("a") 和 print("b") 不换行,所以 ab 连在一起,直到 println("c") 才把这一行收尾;多个参数之间自动插入一个空格(d e、1 2.5 true);println() 空参数调用只输出一个换行——这是打印空行的惯用法。

新手最常犯的错:以为 print 会自动换行,结果输出全挤在一行(见常见错误坑 1)。需要换行就用 println,或显式在字符串里写 \n。

5.3 write 与 writeln

write 组是"原样写出"的纯文本通道:

w.xl
write("no")
write("space")
writeln("!")
writeln()            // 等价于 writeln(""),输出空行
# 实际输出 nospace!

与 print 组的三条硬差异:

  • 只接受一个 string 参数。传两个参数报"实参数量不匹配",传非字符串报"实参类型不匹配"(见常见错误坑 2、3);
  • 不做值格式化、不加空格——写什么就是什么;
  • writeln 的参数有默认值 "",所以 writeln() 合法且只输出换行。

那 write 组有什么用?它和 f-string 是黄金搭档:先用 f-string 把值精确拼成想要的文本,再用 write 原样输出,不引入任何额外的空格或格式:

f-string + write
int n = 42
write(f"n={n}")       // 输出 n=42,无多余空格、无换行
writeln(f"n={n}")     // 输出 n=42 并换行

更完整的格式化能力(对齐、位数控制等)是第 19 章的主题。

5.4 打印各种类型

print 组的"值格式化"把各种类型变成合理文本。实测一屏看全:

p2.xl
println(42, 3.14, true, null, #2026-07-18#)
println([1, 2, 3])
println({"a": 1})
> xl run p2.xl 42 3.14 true null 2026-07-18 00:00:00 [1,2,3] {"a":1}

格式约定:

  • 数字按十进制原样输出;true/false、null 直接输出字面;
  • time 输出为 年-月-日 时:分:秒(纯日期的时间部分是 00:00:00,第 3 章讲过零值日期);
  • 容器输出为 JSON 风格的紧凑文本:数组 [1,2,3]、字典 {"a":1}——调试时一眼看清内容(容器的正式学习在卷四)。
提醒:uint 打印缺陷

第 3 章坑 4 讲过的 RC0 已知问题在本章依然适用:println 直接打印无符号整型(uint8/16/32/64)会触发运行时链接错误。变通:println(string(u)) 或先装入 var。详见常见错误坑 4。

5.5 命令行参数 arg

命令行参数是程序最常用的一种"输入"。第 2 章已经见过机制,这里做一次完整复盘:

args.xl
func main(...) as int
    if arg.length < 2 then
        println("用法: xl run args.xl -- 名字")
        return 1
    end
    println(f"你好, {arg[1]}!")
    return 0
end
> xl run args.xl -- 小明 你好, 小明! > xl run args.xl 用法: xl run args.xl -- 名字 #(退出码为 1)

要点回顾:-- 之后的参数进入 arg 数组;arg[0] 是脚本路径本身,第一个用户参数是 arg[1];arg.length 取参数个数;下标 arg[1] 取具体参数(数组下标的完整规则见第 16 章)。配合退出码,一个行为规范的命令行工具就成型了。

5.6 读取输入的现状

讲了半天"输出","输入"呢?这里要诚实地说明当前版本(DEMO6 RC0)的状况:

标准库目前没有"从控制台读一行"这类交互输入函数。process 命名空间里只有 process.stdin/stdout/stderr 三个标准流编号常量(用于子进程管道,第 41 章前后才会用到),没有面向终端用户的 readline 式 API。所以当前程序获取输入的现实途径有两条:

  • 命令行参数(5.5 节)——一次性传入,适合工具和脚本;
  • 读文件——标准库 file.readText 读取整个文件为字符串:
read_file.xl(input.txt 内容为两行文本)
string text = file.readText("input.txt")
print(text)
> xl run read_file.xl 第一行 第二行

文件读写(file 命名空间)与更丰富的数据通道将在卷四之后展开;控制台交互输入如果进入后续版本标准库,本书会在卷四补充。现在写练习程序时,请用命令行参数或读文件的方式"喂数据"。

深入细节

拾遗 1:println 输出 \n,writeln 输出 \r\n

把 println("x") 和 writeln("y") 的输出重定向后用字节工具查看,实测结果是 78 0A 79 0D 0A——即 x、\n、y、\r\n。两个函数的换行约定不同:print 组追加 Unix 风格 \n,writeln 追加 Windows 风格 \r\n(回车+换行)。Windows 记事本等老工具只认 \r\n,现代编辑器和终端两者都认。写文本文件或对接老式 Windows 工具时,这个差异值得记住。

拾遗 2:print 的实现不经过 C 标准库

通常语言打印最终落到 C 的 printf/fwrite。xlang 不是:生成代码中的 xl6_xrt_print 不使用 C FILE*、stdout,Windows 下直接通过标准输出句柄调用 WinAPI 写入,POSIX 下直接 write(1, ...)。这样做砍掉了对 C 标准库 stdio 的依赖(与全书反复出现的"零外部依赖"设计一致),也让输出行为在所有平台上完全可控。值到文本的转换(整数、浮点、时间、容器的 JSON 风格文本)则由 XRT 的转换能力完成。

拾遗 3:为什么容器打印成 JSON 风格

[1,2,3]、{"a":1} 这种输出不是巧合:print 的值格式化复用了 XRT 的 JSON/XSON 序列化能力。这意味着打印结果往往就是合法的 JSON——调试时可以直接把输出贴进 JSON 工具里检查。但注意它是紧凑格式(无空格无缩进),仅供调试阅读;正式的 JSON 序列化 API 在第 52 章(JSON 解析器项目)前后会详细使用。

⚠️ 常见错误

以下报错与输出均为真实结果。

坑 1:以为 print 会换行,输出全挤在一起
# 代码:print("结果:") print(42) print("完毕") # 期望:三行;实际输出: 结果:42完毕

原因:print 不追加换行,三个调用的输出首尾相接;参数之间的空格是"同一调用内多参数"才有的。正确做法:每个独立一行用 println;或在字符串里显式写 \n。

坑 2:给 write 传多个参数
# w.xl 内容:write("no", "space") > xl run w.xl w.xl:1:6: [sem] 函数调用实参数量和重载签名不匹配: expected 1, actual 2

原因:write 只接受一个参数——多参数逗号分隔是 print 组的能力。报错里 expected 1, actual 2 把签名和实参数量说得明明白白。正确做法:多值输出用 print/println;要精确拼接先 f-string 成一个字符串再 write。

坑 3:给 write 传非字符串
# w.xl 内容:write(42) > xl run w.xl w.xl:1:7: [sem] 函数调用实参类型和参数类型不匹配: target string, value int

原因:write 不做值格式化,这是它与 print 的分工。正确做法:write(f"{42}") 或 write(string(42));或者干脆用 print(42)。

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

原因:第 3 章坑 4 已登记的编译器缺陷——println 对 uint 全族的值格式化缺少运行时函数。这不是你的代码错误。正确做法(变通):println(string(port)) 或 var v = port; println(v),两种写法均实测可用;有符号整型不受影响。

本章小结

知识点速查
print(...)任意数量任意类型,参数间一个空格,不换行。
println(...)同 print,末尾加 \n;println() 打空行。
write(text)恰好一个 string,原样输出,不换行不加空格。
writeln(text)同 write,末尾加 \r\n;参数默认 ""。
选型口诀print 组管"值",write 组管"字";精确格式用 f-string + write。
类型打印数字原样、true/false/null 字面、time 全格式、容器 JSON 紧凑风格。
uint 打印RC0 缺陷:需 string(u) 或装入 var 变通。
命令行参数xl run p.xl -- a b;arg[0] 是脚本路径,用户参数从 arg[1] 起;arg.length 计数。
读取输入RC0 无控制台交互输入函数;用命令行参数或 file.readText(path) 读文件。
格式化f-string 当前主力;完整格式化第 19 章。

复习题

✍️ 先遮住答案想一想
  1. print 和 println 的区别是什么?write 和 writeln 呢?两对答案一样吗?
  2. println("a", "b") 与 write("a", "b") 各自的命运如何?为什么?
  3. 要在输出中精确得到 n=42(无空格无换行),有哪些写法?
  4. xl run tool.xl -- report.csv 中,程序里 arg[0]、arg[1]、arg.length 分别是什么?
  5. 当前版本想给程序"喂"一批数据,有哪些现实途径?

编程练习

练习 1:四函数对照实验

编写 family.xl:用 print、println、write、writeln 各输出同一句话两次,运行后把输出重定向到文件(xl run family.xl > out.txt),用编辑器查看,逐字节解释每处换行与空格来自哪个函数的哪条规则。

参考思路:重点观察 println 与 writeln 换行的不同(\n vs \r\n,Windows 下可用 VS Code 状态栏或 od -c 查看)。

练习 2:命令行计算器(雏形)

编写 calc.xl:接收两个命令行参数作为操作数,打印它们的和。提示:参数是字符串,用 int(arg[1]) 转换(第 7 章正式讲转换,先用起来);参数不足两个时打印用法并 return 1。

参考思路:xl run calc.xl -- 3 4 应输出 7。这个小程序把 arg、类型转换、退出码串在了一起,是命令行工具的完整雏形。

练习 3:文件回声

编写 echo_file.xl:从命令行参数接收一个文件路径,用 file.readText 读出内容,再原样打印到控制台。用本书任意一章的 .xl 源文件做测试。

参考思路:路径来自 arg[1];打印用 print(文件内容自带换行,用 println 会多一个空行——试一次就理解为什么)。