首页 / C# 入门教程 / 流与 JSON 序列化

C# 入门教程

流与 JSON 序列化

本教程共 100 篇 · 第 96 篇 · 更新于 2026-07-31 · 约 9 分钟阅读

C#C# 入门教程编程语言StreamFileStreamSystem.Text.Json

96. 流与 JSON 序列化

本节目标:学完能说清 Stream 是什么,用内存流和文件流读写字节,并把对象序列化成 JSON 字符串或从 JSON 还原成对象。

前面各章读写的文本,底层其实都流经一种东西——流(Stream)。理解了流,你就握住了 C# 所有 IO 的总开关。本章再顺手讲清日常最常用的 JSON 序列化。

一、Stream 是什么,为什么这样设计

Stream 是一个抽象基类,代表「一串可以按顺序读写的字节」。它不关心字节来自文件、内存还是网络。常见派生类有:

  • FileStream:字节来自磁盘文件。
  • MemoryStream:字节来自内存里的一块缓冲区。
  • NetworkStream:字节来自网络连接(属网络编程,略过)。
Note

正因为 FileStreamMemoryStream 都继承自 Stream,上层代码可以「面向 Stream 编程」——只认 Stream,不认数据从哪来。换数据源时,调用方一行都不用改。

为什么要做成统一抽象?打个比方:流像一根水管,数据像水。你只管「接水管、放水、收水」,至于水管那一头接着水库还是水塔,你不必关心。这种「只认接口、不认来源」的设计,让代码能自由拼装:比如在 FileStream 外面再包一层压缩流、加密流,就成了一根「会压缩、会加密的水管」,而读写逻辑完全不变。这就是流的真正威力——可组合、可替换。

二、MemoryStream:在内存里折腾字节

MemoryStream 不直接碰磁盘,适合做临时缓冲、单元测试、或把数据在内存里转来转去后再决定去向。

namespace CSharpDemo;

using var ms = new MemoryStream();
using var writer = new StreamWriter(ms);

writer.Write("hello");
writer.Flush(); // 确保数据进了底层缓冲区

Console.WriteLine($"内存流长度:{ms.Length} 字节");

// 回到开头读出来
ms.Position = 0;
using var reader = new StreamReader(ms);
Console.WriteLine(reader.ReadToEnd()); // hello
Note

写入后要把 Position 重置回 0 才能从头读;Flush 保证缓冲数据真正写入底层。这两个细节新手常漏,漏了读出来就是空。

MemoryStream 的典型适用场景:① 临时拼装一段二进制或文本,拼好再一次性落盘;② 单元测试里替代真实文件,跑得快又不污染磁盘;③ 网络收发时先做中间缓冲。一句话:凡是「先在内存里鼓捣,最后才决定去哪」的活,它最合适。

下面演示流的「管道」能力——把一段字节从一个流直接复制到另一个流:

namespace CSharpDemo;

// 把源流的内容复制进目标流(典型管道操作)
using var source = new MemoryStream([1, 2, 3, 4]);
using var dest = new MemoryStream();
source.CopyTo(dest);
Console.WriteLine($"复制了 {dest.Length} 字节");

CopyToStream 自带的方法。正因为它操作的是抽象 Stream,所以「复制」逻辑既适用于内存流,也适用于文件流、网络流,不用为每种来源重写一遍。

三、FileStream:直接读写磁盘文件

FileStream 把流接到磁盘文件,适合需要精确控制读写的场景,比如只改写文件中间一段,或自己管理缓冲与共享锁。

namespace CSharpDemo;

using var fs = new FileStream("data.bin", FileMode.Create);
fs.Write(new byte[] { 1, 2, 3 }); // 写入 3 个字节
Console.WriteLine($"文件长度:{fs.Length}");

FileMode 决定打开方式:Create 覆盖建新、Append 追加到末尾、Open 只读打开、OpenOrCreate 有则开无则建。FileAccess 还能进一步限制只读/只写/读写,FileShare 控制别的进程能否同时访问同一文件。

Warning

FileStream 占用的是真实磁盘句柄。忘了释放(没用 using),文件会被一直锁住,别处删不掉也改不了。养成「凡是 Stream 一律 using」的习惯,最省心。

日常若只是整篇读写文本,第 95 章的 File 类更省事;要精细控制字节、偏移、共享锁,才上 FileStream

四、System.Text.Json:对象变 JSON

JSON 是程序之间交换数据的通用格式,浏览器、接口、配置文件都在用。.NET 内置 System.Text.Json,高性能且无需第三方库。

Note

System.Text.Json 不在默认隐式 using 里。本教程统一用全限定名 System.Text.Json.JsonSerializer(也避免写显式 using System.Text.Json;),保持与第 95 章一致的代码风格。

namespace CSharpDemo;

class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = "";
    public decimal Price { get; set; }
}

var p = new Product { Id = 1, Name = "机械键盘", Price = 9.99m };

// 序列化:对象 → JSON 字符串
string json = System.Text.Json.JsonSerializer.Serialize(p);
Console.WriteLine(json); // {"Id":1,"Name":"机械键盘","Price":9.99}

// 反序列化:JSON 字符串 → 对象
var back = System.Text.Json.JsonSerializer.Deserialize<Product>(json);
Console.WriteLine($"{back?.Name} 售价 {back?.Price}");

序列化(serialization)是把内存里的对象「拍平」成文本;反序列化(deserialization)反过来,把文本还原成对象。为什么需要它?因为对象活在内存里,程序一关就没了;想通过网络发出去、写进文件、或交给别的语言读,就得先变成大家都能懂的 JSON。

想让输出更好看、字段名按 Web 习惯变小写,可用 JsonSerializerOptions

namespace CSharpDemo;

var options = new System.Text.Json.JsonSerializerOptions
{
    WriteIndented = true,                                             // 缩进美化
    PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase // 字段名转 camelCase
};

var p = new Product { Id = 1, Name = "机械键盘", Price = 9.99m };
Console.WriteLine(System.Text.Json.JsonSerializer.Serialize(p, options));
// {
//   "id": 1,
//   "name": "机械键盘",
//   "price": 9.99
// }
Tip

内置的 System.Text.Json 是默认首选,速度快、零依赖。社区里老的 Newtonsoft.Json 功能更全但需额外装包。新手先用内置的,够用且省心。

序列化常见配置

除了缩进和命名策略,JsonSerializerOptions 还有几个常用开关,按需打开:

  • PropertyNamingPolicy = JsonNamingPolicy.CamelCase:字段名转 camelCase,对接 Web 接口时最常用。
  • WriteIndented = true:美化输出、带缩进,便于人眼阅读与排查。
  • DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull:序列化时跳过 null 字段,让 JSON 更干净。
  • IncludeFields = true:默认只序列化属性,设 true 也会带上公共字段。
Tip

给某个字段自定义 JSON 名,可用 [JsonPropertyName("user_name")] 特性,它的优先级高于全局命名策略。需要彻底忽略某字段,用 [JsonIgnore]。这两个特性在对接第三方接口、字段名对不上时特别有用,能让你在「不改类定义」的前提下适配外部格式。

五、直接序列化到流

更常见的做法是不经过字符串,直接读写流,省一次内存拷贝:

namespace CSharpDemo;

class Item { public int Id { get; set; } public string Name { get; set; } = ""; }

var p = new Item { Id = 1, Name = "鼠标" };

using var fs = File.Create("product.json");
System.Text.Json.JsonSerializer.Serialize(fs, p); // 直接写进文件流

using var readFs = File.OpenRead("product.json");
var loaded = System.Text.Json.JsonSerializer.Deserialize<Item>(readFs);
Console.WriteLine(loaded?.Name);

六、流、字节数组、字符串,怎么选

三者都能装数据,别搞混:

形态特点适用
byte[] 字节数组一次性在内存,随机访问快数据小、要反复改
string 字符串文本、不可变纯文本、拼报文
Stream顺序读写、可来自任意源大文件、网络、管道

简单说:数据小又爱改,用数组;纯文本,用字符串;数据大、来源杂、要边读边写,用流。它们也能互转:stream.ToArray() 把流变成字节数组,Encoding.UTF8.GetString(bytes) 把字节变成字符串。

七、新手踩坑

  1. MemoryStream 写完后忘了 Flush 或重置 Position,读出来是空的。
  2. 以为 System.Text.Json 默认可用,结果编译报「找不到 JsonSerializer」。记得用全限定名 System.Text.Json.JsonSerializer,或加 using System.Text.Json;
  3. 反序列化返回 null 时直接访问属性,抛 NullReferenceException。用 ?. 安全访问。
  4. 属性名默认按原样输出。想自定义 JSON 字段名,用 [JsonPropertyName("name")] 特性。
  5. FileStream 当文本读写却用错编码。文本场景优先用 StreamReader/StreamWriter,它们默认 UTF-8。
  6. 忘了 using 释放流,导致文件被锁。所有流都套 using(或 using var 声明)。
Warning

流式读写是「顺序」的。读了一半又把 Position 随便挪来挪去,极易读错位置。需要随时随机访问,别用流,改用 byte[]File.ReadAllBytes

八、本节小结

Stream 是 C# IO 的统一抽象:MemoryStream 玩内存、FileStream 落磁盘,都套着 StreamReader/StreamWriter 读写文本。对象与 JSON 之间用 System.Text.Json 互转,还能直接对接流省去中间字符串。把「流、编码、using 释放」三件事记牢,IO 就不再是拦路虎。下一章跳出单文件,看整个项目的组织方式。