第17章 IO类

统一理解标准与文件I/O、line/chunk读写、position、text/binary mode、buffer,以及process、open-uri和StringIO适配边界。

学习目标

  • 能解释标准输入输出与文件输入输出在“第17章 IO类”中的责任边界
  • 能围绕“怎样区分流能力、文件位置、缓冲、文本模式与外部进程生命周期?”运行正常与故障轨迹并定位首个分岔
  • 能用“第17章 IO类的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”证明“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”

来源、版次与运行边界

“第17章 IO类”以作者维护的第 5 版支持页核定 2016 年 3 月 12 日首刷、两位作者、松本行弘监修以及四部分 23 章目录;逐章程序清单练习答案勘误只作为公开支持材料,不被冒充为原书全文。

对“第17章 IO类”而言,中文解释、示例、交互、练习和答案均为独立教学重写;站内中文章名是与官方 23 章顺序对应的课程映射,不宣称是日文小节的逐字翻译,也不从公开程序清单复制整段实现。

“第17章 IO类”固定在 Ruby 2.3 语境;Ruby 2.3.0 官方文档稳定版发布说明用于核对当时可用的语言和标准库行为。现代 Ruby 的差异只能另列迁移说明,不能静默改变本页示例的版本结论。

围绕“怎样区分流能力、文件位置、缓冲、文本模式与外部进程生命周期?”,本页验收“第17章 IO类的输入样本、接收者与方法、关键状态前后值、正常与失败输出、异常或退出状态,以及复位后的再次运行记录。”。先预测正常轨迹,再只注入“写入后未刷新或关闭就读取文件大小,把缓冲状态误判为数据丢失”;若无法定位第一处状态分岔,就拒绝当前解释。

从“IO都能read/write所以可以互换”开始纠正

IO-shaped objects共享readwriteeach_line等messages,但file、pipe、terminal、socket和StringIO在seekability、timeout、durability、encoding与ownership上不同。决定可替换范围。

先预测:把StringIO测试通过能否证明socket reader不会挂死?不能。StringIO没有network timeout、partial read、backpressure和disconnect。Contract tests共享数据处理,transport integration tests验证真实边界。

输入/输出的种类:标准输入输出与文件输入输出

$stdin/STDIN、$stdout/STDOUT、$stderr/STDERR是process标准streams。Normal machine/user output写stdout,diagnostic写stderr,使shell pipeline只接收协议数据。不能靠终端混合画面验收。

File I/O由open mode决定read/write/append、truncate/create和text/binary。Creator通常拥有close;被注入的IO由caller拥有,method不应擅自close。Block-form File.open把owner固定在method。

def copy_lines(input, output)
  input.each_line do |line|
    output.write(line)
  end
end
 
File.open("source.txt", "r:UTF-8") do |input|
  File.open("copy.txt", "w:UTF-8") do |output|
    copy_lines(input, output)
  end
end

业务core只依赖input/output protocol,path/open/close留在adapter。Open mode和permissions是data safety policy,覆盖文件前还要atomic replace/backup策略。

基本的输入操作与输出操作

gets读一条record并在EOF返回nil;read读全部或指定length;readpartial适合streaming但EOF/short read语义需处理。each_line以enumeration隐藏重复gets。防止unbounded read。

def count_bytes(input, chunk_size: 16 * 1024)
  total = 0
  loop do
    chunk = input.read(chunk_size)
    break if chunk.nil? || chunk.empty?
    total += chunk.bytesize
  end
  total
end

File#read(length)在EOF返回nil/empty的具体状态按目标API验证;loop同时防两种。Network stream可能short read,不可假定一次read填满length。Protocol若要求exact bytes,循环累积并对early EOF报错。

write返回写入bytes数量,print不自动换行,puts按对象/数组添加换行,printf按format输出。Machine protocol优先write明确bytes;partial writes在低层transport需循环/专用API。

文件指针

pos/tell返回byte offset,seek(offset, whence)移动,rewind回到开头。不能把character index直接seek。

File.open("data.bin", "rb") do |file|
  header = file.read(4)
  payload_offset = file.pos
  file.seek(payload_offset, IO::SEEK_SET)
  payload = file.read
  p header: header, bytes: payload.bytesize
end

Pipe/socket通常不可seek,会抛Errno::ESPIPE等。Text transcoding下byte position与character boundary可能不对齐;需要随机访问文本时建byte-offset index并从合法边界decode,或用higher-level record store。

二进制模式与文本模式

Text mode把bytes按external encoding解码为String并可能做newline转换;binary mode保留bytes,String encoding常为ASCII-8BIT。不允许中途猜编码。

digest_input = File.binread("asset.dat")
p digest_input.encoding
 
File.open("names.txt", "r:UTF-8") do |file|
  file.each_line { |line| p line.chomp }
end

Image/protocol/archive必须binary;配置/source是text并验证encoding。Binary String可含NUL且length/bytesize语义不同,不能随意puts或Regexp处理。第19章会展开Encoding。

缓冲

Ruby/OS/device可能多层buffer。flush把Ruby user-space buffer推向OS,不等于disk durable;fsync等才请求更强durability,仍受filesystem/hardware语义限制。避免“日志已flush所以不会丢”。

Terminal输出常line-buffered,file/network策略不同;sync = true降低buffering但增加syscalls。Benchmark throughput与latency,关键transaction使用database/atomic file protocol,而非只开sync。

Buffer还影响交互:向child process写request后不flush,双方可能互等。Reader需处理producer长期不换行,不能只依赖gets。

与命令进行交互

IO.popen可连接child stream,但同时处理stdin/stdout/stderr与status更适合Open3。防注入与hang。

require "open3"
 
stdout, stderr, status = Open3.capture3("ruby", "-e", "puts RUBY_VERSION")
raise "child failed: #{stderr}" unless status.success?
puts stdout

Capture3会把output全收内存;大输出用popen3并并发drain stdout/stderr,避免pipe deadlock。Production加timeout、process group cleanup与redaction,argument vector禁止拼接shell。

open-uri库

Open-uri让URI表现为IO,但隐藏redirect、DNS、TLS、proxy、timeout和response size。是SSRF与资源安全边界。

不可信URL不能直接open;解析后allowlist scheme/host,防DNS rebinding/private IP,限制redirect并stream到size cap。现代项目常用显式HTTP client获取status/headers/body,open-uri只适合受控简化场景。

stringio库

StringIO用String实现许多IO methods,适合注入测试。限定证据强度。

require "stringio"
 
input = StringIO.new("a\nb\n")
output = StringIO.new
copy_lines(input, output)
 
raise "copy mismatch" unless output.string == "a\nb\n"

StringIO position、encoding与close semantics可测试core;另外用faulting fake模拟read/write异常,再用真实tempfile/pipe integration覆盖OS行为。

正式节点与章专属证据

  • :第 1 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 2 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 3 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 4 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 5 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 6 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 7 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 8 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。
  • :第 9 个正式节点要能回到“读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。”,并说明故障发生前后的第一处差异。

对象与状态模型

从输入、接收者到可观察证据

怎样区分流能力、文件位置、缓冲、文本模式与外部进程生命周期?

输入与接收者

固定标准输入输出所需的原始值、Ruby 版本和调用入口。

状态变化

在执行前记录接收者身份,并声明文件输入输出的允许状态。

观察证据

保存第17章 IO类的初值、参数、编码或资源位置。

正式节点:标准输入输出、文件输入输出、输入操作与输出操作、文件指针、二进制模式与文本模式、缓冲、与命令进行交互、open-uri库、stringio库

控制与消息轨迹

在相同初值下定位首个分岔

  1. 01固定标准输入输出的输入和接收者
  2. 02执行文件输入输出并记录状态
  3. 03观察输入操作与输出操作的返回或副作用
  4. 04用stringio库核对不变量并复位

运行不变量:读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。

边界故障探针

一次只破坏一个前提

基线满足:读写前确认流状态,资源所有者覆盖关闭、刷新和子进程退出。

本章回顾:统一stream操作,不抹平transport差异

  1. STDIN/STDOUT/STDERR与File有不同protocol和owner,normal data与diagnostic分流。
  2. Line/chunk read明确EOF、short read、blocking与size;creator关闭resource。
  3. Position是byte offset且只对seekable stream;text/binary决定decode与String semantics。
  4. Flush不等于durability,subprocess必须管理三条stream、status、timeout和reap。
  5. Open-uri有network security policy,StringIO只证明core stream logic,不证明transport behavior。

练习与答案

练习

  1. 问题 1:建立正常轨迹。 回答“怎样区分流能力、文件位置、缓冲、文本模式与外部进程生命周期?”,并写出四步执行记录。
  1. 问题 2:注入单一故障。 只制造“写入后未刷新或关闭就读取文件大小,把缓冲状态误判为数据丢失”,应从哪里开始定位?
  1. 问题 3:覆盖正式节点。 用一个证据包串联标准输入输出、文件输入输出、输入操作与输出操作、文件指针、二进制模式与文本模式、缓冲、与命令进行交互、open-uri库、stringio库,说明为什么结论可由另一位读者独立复核。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

标准输入输出

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

文件输入输出

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

输入操作与输出操作

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

文件指针

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

二进制模式与文本模式

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

缓冲

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

与命令进行交互

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

open-uri库

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

stringio库

“第17章 IO类”中的正式节点;必须说明它接收什么、改变什么,以及用什么结果复核。

资料与写作方式声明

本章以Ruby 基础教程第 5 版作者支持资料合法公开试读核定可见范围,并以目录限定未公开部分,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…