C# 试题 31:文件与流
0601 以下代码输出什么?
难度: 实战
var ms = new MemoryStream();
var sw = new StreamWriter(ms);
sw.Write("hello");
Console.WriteLine(ms.Length);
- A.
0,字符还留在 StreamWriter 的内部缓冲区 - B.
5 - C. 抛
ObjectDisposedException - D. 编译失败
查看答案与解析
正确答案A
解析: StreamWriter 默认带 1024 字节缓冲区,Write("hello") 只写入缓冲区,尚未落到 MemoryStream;只有 Flush、Dispose 或缓冲区写满时才会真正写入底层流,因此此时 ms.Length 为 0。
关键点: 包装流的缓冲区不 Flush,底层流看不到数据。
0602 以下代码输出什么?
难度: 实战
var ms = new MemoryStream();
var sw = new StreamWriter(ms);
sw.Write("hello");
sw.Flush();
Console.WriteLine(ms.Length);
- A.
0 - B.
5,Flush 把缓冲区内容写入底层流 - C. 抛
IOException - D. 输出结果不确定
查看答案与解析
正确答案B
解析: Flush() 把 StreamWriter 缓冲区中的 5 字节 UTF-8 数据写入底层 MemoryStream,此时 ms.Length 为 5。
关键点: 需要立即读取底层流时先 Flush,或直接依赖 using 块的 Dispose 隐式刷新。
0603 data.bin 内容只有 3 字节,以下代码输出什么?
难度: 实战
byte[] buf = new byte[1024];
using var fs = File.OpenRead("data.bin");
int n = fs.Read(buf, 0, buf.Length);
Console.WriteLine(n);
- A.
1024 - B.
0 - C.
3,Read 返回实际读到的字节数,到文件末尾则不足请求量 - D. 抛
EndOfStreamException
查看答案与解析
正确答案C
解析: Stream.Read 的返回值是“实际读到的字节数”,可能小于请求值;读到末尾返回 0 而不是抛 EndOfStreamException。EndOfStreamException 只来自 ReadExactly 或 StreamReader 按字节读取等特定 API。
关键点: 循环读流的终止条件是返回值 0,而不是“读到请求数量”。
0604 假设 stream 中只有 5 个字节,以下代码运行结果是什么?
难度: 进阶
byte[] buf = new byte[10];
Console.WriteLine(stream.ReadAtLeast(buf, 8));
- A.
5 - B.
8 - C.
0 - D. 抛
EndOfStreamException,ReadAtLeast 默认在读到 EOF 且不足 minimumBytes 时抛异常
查看答案与解析
正确答案D
解析: Stream.ReadAtLeast(.NET 7+)默认 throwOnEndOfStream: true:流里只剩 5 字节而请求至少 8 字节,读到 EOF 即抛 EndOfStreamException。只有显式传 throwOnEndOfStream: false 才会返回实际读到的 5;ReadExactly 则无条件要求读满并抛 EndOfStreamException。
关键点: ReadAtLeast 的默认行为是“读不够就抛”,与很多人的直觉相反。
0605 以下代码输出什么?
难度: 实战
byte[] data = { 1, 2, 3, 4 };
using var ms = new MemoryStream(data);
ms.WriteByte(9);
Console.WriteLine(data[0]);
- A.
1,MemoryStream 会复制数组 - B.
9,该构造函数直接暴露原数组,写入会修改它 - C. 抛
NotSupportedException - D. 抛
ObjectDisposedException
查看答案与解析
正确答案B
解析: new MemoryStream(byte[]) 直接包住传入的数组而不是复制:流内写入会同步修改原数组。如果希望流拥有独立缓冲区,应改用 ToArray() 或 new MemoryStream() 再写。
关键点: 公开缓冲区构造函数 = 写穿到原数组,容易被误当快照。
0606 以下代码运行结果是什么?
难度: 进阶
byte[] data = { 1, 2, 3 };
using var ms = new MemoryStream(data);
for (int i = 0; i < 4; i++)
{
ms.WriteByte((byte)i);
}
- A. 正常结束
- B. 抛
IOException - C. 第 4 次写入时抛
NotSupportedException,固定缓冲区不可扩展 - D. 抛
OverflowException
查看答案与解析
正确答案C
解析: new MemoryStream(byte[]) 创建的流不可扩展:前三字节写入成功,第 4 次写入超过缓冲区末尾时抛 NotSupportedException(MemoryStream 不可扩展)。用无参构造或 SetLength 之前要确认流是否可扩容。
关键点: 公开数组构造的 MemoryStream 只能覆盖,不能追加扩展。
0607 以下代码输出什么?
难度: 实战
using var src = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 });
src.ReadByte();
using var dst = new MemoryStream();
src.CopyTo(dst);
Console.WriteLine(dst.Length);
- A.
4,CopyTo 从当前 Position 复制到流末尾 - B.
5 - C.
0 - D. 抛
NotSupportedException
查看答案与解析
正确答案A
解析: ReadByte() 已把 Position 推进到 1,CopyTo 从当前位置复制到末尾,因此只复制后 4 字节。先读了一部分再 CopyTo 会丢前面的数据,这是流处理里常见的漏数据原因。
关键点: CopyTo 不是“从 0 复制”,而是“从当前 Position 复制”。
0608 networkStream 是不可寻址的流(CanSeek 为 false),以下代码运行结果是什么?
难度: 进阶
Console.WriteLine(networkStream.Position);
- A.
0 - B.
-1 - C. 抛
IOException - D. 抛
NotSupportedException,不可寻址流不支持读取 Position
查看答案与解析
正确答案D
解析: NetworkStream 等不可寻址流(CanSeek 为 false)的 Position、Seek、Length 都抛 NotSupportedException。写通用流处理代码前先检查 CanSeek,否则会在真实网络流上踩坑。
关键点: 流不是都有 Position;先看 CanSeek 再寻址。
0609 关于 FileStream 的异步 I/O,哪项正确?
难度: 进阶
- A. 只要调用 ReadAsync/WriteAsync 就是操作系统级异步
- B. 需要 FileOptions.Asynchronous(或 useAsync: true)才会在操作系统层执行真正的异步 I/O;未指定时实现可能回退到线程池执行
- C. 该选项只对 Linux 生效
- D. 该选项已被移除,.NET 10 不再支持
查看答案与解析
正确答案B
解析: 官方文档明确:要获得操作系统级异步 I/O,需在 FileStream 构造时指定 useAsync: true 或 options: FileOptions.Asynchronous;不指定时,异步调用仍不会阻塞调用线程,但底层实现可能借道线程池,性能特征不同。
关键点: “异步不阻塞”与“OS 级异步 I/O”是两回事。
0610 以下代码运行结果是什么?
难度: 实战
using (var fs = File.Create("a.txt"))
{
using var fs2 = File.OpenRead("a.txt");
}
- A. 正常运行
- B. fs2 抛
UnauthorizedAccessException - C. fs2 抛
IOException,File.Create 默认使用 FileShare.None 独占文件 - D. 编译失败
查看答案与解析
正确答案C
解析: File.Create(path) 等价于 FileMode.Create + FileAccess.ReadWrite + FileShare.None,即独占打开;在它未释放时再以 File.OpenRead(FileShare.Read)打开同一文件会因共享冲突抛 IOException。
关键点: File.Create 与 File.OpenRead 的默认 FileShare 不同,混用要显式指定共享模式。
0611 以下代码输出什么?
难度: 进阶
using var ms = new MemoryStream(Encoding.UTF8.GetBytes("hi"));
using (var sr = new StreamReader(ms, Encoding.UTF8,
detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true))
{
Console.Write(sr.ReadToEnd());
}
Console.WriteLine(ms.CanRead);
- A.
hi/False - B.
hi/True,leaveOpen: true 让 StreamReader 释放时不关闭底层流 - C. 空 /
True - D. 抛
ObjectDisposedException
查看答案与解析
正确答案B
解析: leaveOpen: true 表示释放 StreamReader 时不释放底层流,因此 ms.CanRead 仍为 True;默认 leaveOpen: false 时释放包装器会一并关闭底层流。需要复用底层流时必须显式传 leaveOpen: true。
关键点: 包装流默认“接管”底层流的生命周期。
0612 以下代码输出什么?
难度: 实战
var ms = new MemoryStream();
using (var writer = new StreamWriter(ms))
{
writer.Write("data");
}
Console.WriteLine(ms.CanWrite);
- A.
True - B. 抛
ObjectDisposedException - C. 编译失败
- D.
False,StreamWriter 默认接管底层流,Dispose 时一并关闭
查看答案与解析
正确答案D
解析: new StreamWriter(ms) 默认 leaveOpen: false:using 块结束时 StreamWriter 先 Flush 再关闭底层 MemoryStream,因此 ms.CanWrite 为 False。想在释放包装器后继续使用底层流,必须用带 leaveOpen: true 的重载。
关键点: 别在 using 块外用已被包装流关闭的底层流。
0613 以下代码输出什么?
难度: 实战
using var ms = new MemoryStream(new byte[] { 42 });
ms.ReadByte();
Console.WriteLine(ms.ReadByte());
- A.
-1,ReadByte 在 EOF 时返回 -1,不抛异常 - B.
0 - C. 抛
EndOfStreamException - D.
42
查看答案与解析
正确答案A
解析: Stream.ReadByte() 读完最后一个字节后再读返回 -1,与 Read 返回 0 表示 EOF 的约定不同。用 BinaryReader.ReadByte 则会抛 EndOfStreamException,两者行为不一致。
关键点: ReadByte 的 EOF 哨兵是 -1,不是 0 也不是异常。
0614 以下代码输出什么?
难度: 进阶
using var fs = new FileStream("t.bin", FileMode.Create);
fs.Write(new byte[100]);
Console.WriteLine(fs.Position);
Console.WriteLine(new FileInfo("t.bin").Length);
- A.
100/100 - B.
100/0,写操作先进入 FileStream 的内部缓冲区,尚未落盘 - C.
0/100 - D.
100/ 抛IOException
查看答案与解析
正确答案B
解析: FileStream 默认有 4096 字节缓冲区:写入 100 字节后 Position 前进到 100,但数据还在缓冲区,磁盘上的文件长度仍是 0,直到 Flush/Dispose。以文件长度判断写入是否生效要小心这一点。
关键点: Position 反映逻辑位置,磁盘 Length 反映已落盘数据。
0615 以下代码输出什么?
难度: 实战
using var ms = new MemoryStream(new byte[] { 1, 2, 3 });
byte[] buf = new byte[4];
int n = await ms.ReadAsync(buf);
Console.WriteLine(n);
- A.
4 - B.
0 - C.
3,ReadAsync 返回实际读取的字节数 - D. 抛
EndOfStreamException
查看答案与解析
正确答案C
解析: ReadAsync(Memory<byte>) 返回 ValueTask<int>,值是实际读取的字节数(这里 3),不会因为缓冲区大就填满,也不因未读满抛异常。异步读取同样要循环直到返回 0。
关键点: ReadAsync 的返回值语义与同步 Read 一致:实际字节数。
0616 以下代码输出什么?
难度: 进阶
var ms = new MemoryStream();
ms.Dispose();
ms.Dispose();
Console.WriteLine("ok");
- A. 输出
ok,Dispose 幂等,重复释放不抛异常 - B. 第二次 Dispose 抛
ObjectDisposedException - C. 第二次 Dispose 抛
InvalidOperationException - D. 编译失败
查看答案与解析
正确答案A
解析: IDisposable.Dispose 的契约是幂等且不抛异常,Stream 及其所有实现都遵循这一点;释放后再释放是无害空操作。这与释放后访问 Read/Write 等成员抛 ObjectDisposedException 不同。
关键点: Dispose 本身必须幂等;“释放后再使用”才抛异常。
0617 关于 Stream.Read 的返回值与 EOF,哪项描述正确?
难度: 进阶
- A. 只有缓冲区填满才算读取成功
- B. 返回 -1 表示已到末尾
- C. Read 保证返回请求的字节数
- D. 返回值可能小于请求字节数,0 表示 EOF;应循环读取直到返回 0
查看答案与解析
正确答案D
解析: Read 是“尽力而为”的:网络流等场景下一次只返回部分数据,且不一定表示 EOF;只有返回 0 才确定到达末尾。正确的读法是用循环累加直到返回 0(或用 ReadExactly/ReadAtLeast)。
关键点: 一次 Read 读不满是常态,不是错误。
0618 以下代码输出什么?
难度: 实战
var ms = new MemoryStream();
using (var sw = new StreamWriter(ms, leaveOpen: true))
{
sw.Write("ok");
}
ms.Position = 0;
Console.WriteLine(new StreamReader(ms).ReadToEnd());
- A. 空字符串
- B.
ok,leaveOpen: true 保留流,Dispose 时缓冲区已刷新 - C. 抛
ObjectDisposedException - D. 抛
IOException
查看答案与解析
正确答案B
解析: leaveOpen: true 使 StreamWriter 释放时不关闭底层流;释放过程仍会先 Flush,所以数据已写入 ms,重置 Position 后能读回 ok。
关键点: leaveOpen 只影响“是否关闭底层流”,不影响“是否刷新缓冲区”。
0619 以下代码输出什么?
难度: 进阶
byte[] bytes = { 0xEF, 0xBB, 0xBF, (byte)'A' };
using var ms = new MemoryStream(bytes);
using var sr = new StreamReader(ms);
Console.WriteLine(sr.ReadToEnd());
- A.
\uFEFFA,BOM 原样保留 - B. 空字符串
- C.
A,StreamReader 默认检测并跳过 UTF-8 BOM - D. 抛
DecoderFallbackException
查看答案与解析
正确答案C
解析: StreamReader 默认 detectEncodingFromByteOrderMarks: true,会识别开头的 EF BB BF 并把 BOM 从结果中去掉;只有显式传 false 才会把 BOM 字节解成 \uFEFF。用默认 StreamReader 读 UTF-8 文件得到的字符串不含 BOM。
关键点: 字符串首字符出现 \uFEFF,多半是绕过了 BOM 检测。
0620 使用 StreamReader/StreamWriter 包装底层流时,关于释放所有权哪项正确?
难度: 实战
- A. 默认 leaveOpen=false,释放包装器会同时释放底层流
- B. 包装器永远不会关闭底层流
- C. 必须显式分别释放两者,否则泄漏
- D. 释放顺序无关紧要
查看答案与解析
正确答案A
解析: 包装流的默认契约是“接管”底层流:Dispose 包装器时(先 Flush)一并 Dispose 底层流。显式传 leaveOpen: true 时才由调用方自行管理底层流生命周期。
关键点: 不要对已被包装流关闭的流二次释放或继续使用。