当前位置: 首页 > news >正文

C# MemoryStream 使用详解

总目录


前言

在.NET开发中,流(Stream)是一个用于处理输入和输出的抽象类,MemoryStream是流的一个具体实现,它允许我们在内存中读写数据,就像操作文件一样,而无需涉及磁盘 I/O 操作。尤其适合需要快速读写、转换或传输数据的场景。本文将详细讲解MemoryStream的使用。


一、什么是 MemoryStream?

1. 定义

MemoryStreamSystem.IO 命名空间中的一个类,它允许我们在内存中创建可读写的流。与文件流或网络流不同,MemoryStream的数据存储在内存中,它不需要依赖物理文件,因此读写速度非常快,适合处理临时数据(如网络传输、临时缓存、序列化对象等)。但会占用一定的内存资源。

📌MemoryStreamSystem.IO 命名空间中的一个类,它实现了 Stream 抽象类,提供了一系列用于操作数据流的属性和方法。

2. 继承关系

在这里插入图片描述

2. 核心特性

  • 内存高效:数据直接存储在内存中,无需磁盘 I/O,读写速度快。
  • 灵活操作:支持读写、重置位置、转换为字节数组等数据处理操作。
  • 轻量级:无需文件句柄,适合小到中等规模的数据。

3. 用途

  • 处理大量数据,如图像、音频和视频文件等二进制数据。
  • 临时存储数据,如网络传输过程中的数据缓冲。
  • 实现自定义数据流逻辑,例如加密或压缩数据。

4. 为什么需要 MemoryStream?

在数据处理场景中,频繁的磁盘IO操作(如读写文件)会显著降低程序性能,尤其是面对海量数据或高频读写需求时。MemoryStream作为C#中的内存流,将数据存储在内存而非硬盘中,避免了磁盘IO瓶颈,读写速度更快。它适用于网络数据传输、临时缓存、二进制数据处理等场景,是实现高性能代码的利器!

二、基础用法

1. 创建 MemoryStream 对象

MemoryStream有多个构造函数,可以根据需要选择合适的构造函数来初始化MemoryStream

1)无参构造函数

使用无参构造函数可以创建一个空白的 MemoryStream 对象,其初始容量为 0,随着数据写入自动扩展。

using System.IO;

MemoryStream memoryStream = new MemoryStream();

2)带参构造函数

使用带参构造函数可以根据指定的容量创建 MemoryStream 对象,或者从一个字节数组创建。

▶ 指定初始容量的构造函数
MemoryStream memoryStream = new MemoryStream(1024);

创建一个初始容量为1024字节的MemoryStream

▶ 使用字节数组初始化的构造函数
byte[] buffer = new byte[] { 72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33 };
MemoryStream ms = new MemoryStream(buffer);

使用现有的字节数组初始化MemoryStream

2. 写入数据

MemoryStream 提供了多种方法来写入数据,最常用的是 Write 方法和 WriteByte 方法。

1)写入字节数组

使用 Write 方法可以将一个字节数组写入 MemoryStream

byte[] data = new byte[] { 72, 101, 108, 108, 111 }; 
memoryStream.Write(data, 0, data.Length);

2)写入字符串

MemoryStream 写入一个字符串,需要将字符串转换为字节数组。

string text = "Hello, World!";
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
memoryStream.Write(data, 0, data.Length);

3)使用 WriteByte 方法

WriteByte 方法可以逐字节写入数据。

string text = "Hello, World!";
byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
foreach (byte b in data)
{
    memoryStream.WriteByte(b);
}

3. 读取数据

MemoryStream 提供了多种方法来读取数据,最常用的是 Read 方法和 ReadByte 方法。

1)读取字节数组

MemoryStream 读取一定数量的字节到字节数组中。

byte[] buffer = new byte[11];
int bytesRead = memoryStream.Read(buffer, 0, buffer.Length);

2)读取字符串

MemoryStream 读取一定数量的字节,然后将其转换为字符串。

byte[] buffer = new byte[11];
int bytesRead = memoryStream.Read(buffer, 0, buffer.Length);
string text = System.Text.Encoding.UTF8.GetString(buffer, 0, bytesRead);

3)使用 ReadByte 方法

ReadByte 方法可以逐字节读取数据。

List<byte> byteList = new List<byte>();
while (memoryStream.Position < memoryStream.Length)
{
    byteList.Add((byte)memoryStream.ReadByte());
}
string text = System.Text.Encoding.UTF8.GetString(byteList.ToArray());

4. 常用属性

1)Capacity

获取或设置分配给MemoryStream 的字节数

MemoryStream memoryStream = new MemoryStream(1024);
int capacity = memoryStream.Capacity;	// 输出:1024

2)Length

获取MemoryStream中实际使用的数据长度。

MemoryStream memoryStream = new MemoryStream(1024);
long length = memoryStream.Length;	// length = 0

3)Position

获取或设置MemoryStream的当前读写位置。

memoryStream.Position = 0; // 定位到流的开头

4)CanRead、CanWrite、CanSeek

bool canRead = ms.CanRead;
bool canWrite = ms.CanWrite;
bool canSeek = ms.CanSeek;

表示MemoryStream是否支持读取、写入和定位操作。对于MemoryStream,这些属性通常返回true

5. 常用辅助方法

1)SetLength 设置长度

使用 SetLength 方法可以设置 MemoryStream 的长度,如果新长度小于当前长度,数据将被截断;如果新长度大于当前长度,数据将被扩展。

memoryStream.SetLength(50);

2)Seek 设置当前读写位置

ms.Seek(0, SeekOrigin.Begin);

移动MemoryStream的当前读写位置。

3)ToArray 转换为字节数组

使用 ToArray 方法可以将 MemoryStream 的内容转换为字节数组。

byte[] allBytes = memoryStream.ToArray();

4)GetBuffer 获取底层缓冲区的字节数组

byte[] buffer = memoryStream.GetBuffer();

GetBuffer方法返回底层缓冲区的完整字节数组(包含未使用的空间),ToArray方法返回仅包含有效数据的数组(排除未使用的空间)。

关于 ToArray 和 GetBuffer 方法的区别,详见:C# MemoryStream 中 ToArray 和 GetBuffer 的区别

6. Position 注意事项

1)写入数据后的 Position 自动前进

// 创建空的 MemoryStream
using (MemoryStream ms = new MemoryStream())
{
    // 写入字符串
    byte[] data = System.Text.Encoding.UTF8.GetBytes("Hello, MemoryStream!");
    ms.Write(data, 0, data.Length);
    
    // 写入后的位置自动前进
    Console.WriteLine($"当前位置:{ms.Position}"); // 输出:20(假设 UTF-8 编码)
}

2)读取数据 必须重置 Position

using (MemoryStream ms = new MemoryStream())
{
    // 写入数据后重置位置到开头
    ms.Write(data, 0, data.Length);
    ms.Position = 0; // 必须重置位置才能读取

    // 读取数据
    byte[] buffer = new byte[ms.Length];
    ms.Read(buffer, 0, (int)ms.Length);
    string result = System.Text.Encoding.UTF8.GetString(buffer);
    Console.WriteLine(result); // 输出:Hello, MemoryStream!
}

Tips
除了使用 Position 属性重置位置外,读写前还可用Seek()调整指针位置,如 stream.Seek(0, SeekOrigin.Begin)

7. 示例代码

下面是一个完整的示例,演示了如何使用 MemoryStream

public class Program
{
    public static void Main(string[] args)
    {
        // 创建 MemoryStream
        MemoryStream memoryStream = new MemoryStream();

        // 获取当前读写的位置
        Console.WriteLine($"MemoryStream Position: {memoryStream.Position}");// 输出:MemoryStream Position: 0


        string text = "Hello, World!";
        byte[] data = System.Text.Encoding.UTF8.GetBytes(text);
        // 写入数据
        memoryStream.Write(data, 0, data.Length);

        // 转换为字节数组
        Console.WriteLine(BitConverter.ToString(memoryStream.ToArray()));   //输出:48-65-6C-6C-6F-2C-20-57-6F-72-6C-64-21

        // 设置长度
        memoryStream.SetLength(50);

        // 获取长度
        Console.WriteLine($"MemoryStream length: {memoryStream.Length}"); // 输出:MemoryStream length: 50

        // 获取当前读写的位置
        Console.WriteLine($"MemoryStream Position: {memoryStream.Position}");// 输出:MemoryStream Position: 13

        // 读取数据
        byte[] buffer = new byte[5];
        int bytesRead = memoryStream.Read(buffer, 0, buffer.Length);
        // 输出结果
        Console.WriteLine(BitConverter.ToString(buffer)); // 输出:00-00-00-00-00

        // 定位
        memoryStream.Position = 0;

        // 再次读取数据
        bytesRead = memoryStream.Read(buffer, 0, buffer.Length);
        Console.WriteLine(BitConverter.ToString(buffer)); // 输出:48 65 6C 6C 6F
        Console.WriteLine(Encoding.UTF8.GetString(buffer)); //输出:Hello

        // 清空 MemoryStream
        memoryStream.SetLength(0);
        memoryStream.Position = 0;

        // 检查是否清空
        Console.WriteLine("MemoryStream length after clear: " + memoryStream.Length); // 输出:MemoryStream length after clear: 0
    }
}

通过这个示例,我们可以看到 MemoryStream 在处理内存中的数据流时是多么灵活和有用。它不仅可以用于临时存储数据,还可以用于实现复杂的数据处理逻辑。

三、MemoryStream的高级使用

1. 数据交互

1)与文本数据交互

使用 StreamReader/StreamWriter

public class Program
{
    public static void Main(string[] args)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            // 创建一个StreamWriter,用于向MemoryStream写入字符串
            using (StreamWriter sw = new StreamWriter(ms, Encoding.UTF8, 1024, leaveOpen: true))
            {
                // leaveOpen: true 的作用:
                // 防止 StreamWriter 关闭时连带关闭底层的 MemoryStream,确保后续 StreamReader 可正常操作流
                sw.WriteLine("Hello, World!");
                sw.WriteLine("This is a test.");
            }
            // 将MemoryStream的位置重置到开头
            ms.Seek(0, SeekOrigin.Begin);

            // 创建一个StreamReader,用于从MemoryStream读取字符串
            using (StreamReader sr = new StreamReader(ms, Encoding.UTF8))
            {
                string line;
                while ((line = sr.ReadLine()) != null)
                {
                    Console.WriteLine(line);
                }
            }
        }
    }
}

在这个例子中,我们首先创建了一个MemoryStream实例,然后使用StreamWriterMemoryStream写入了两行字符串。写入完成后,我们将MemoryStream的位置重置到开头,接着使用StreamReaderMemoryStream读取字符串并打印到控制台。

2)与二进制数据交互

使用 BinaryReader/BinaryWriter

public class Program
{
    public static void Main(string[] args)
    {
        using (MemoryStream ms = new MemoryStream())
        {
            // 创建一个BinaryWriter,用于向MemoryStream写入二进制数据
            using (BinaryWriter writer = new BinaryWriter(ms,Encoding.UTF8,leaveOpen:true))
            {
                // leaveOpen: true 的作用:
                // 防止 StreamWriter 关闭时连带关闭底层的 MemoryStream,确保后续 StreamReader 可正常操作流
                
                writer.Write(32);// 写入整数
                writer.Write(12.3f);// 写入单精度浮点数
                writer.Write("This is a test.");// 写入字符串
            }

            // 将MemoryStream的位置重置到开头
            ms.Seek(0, SeekOrigin.Begin);

            // 创建一个 BinaryReader,用于从MemoryStream读取二进制数据
            using (BinaryReader reader = new BinaryReader(ms, Encoding.UTF8))
            {
                Console.WriteLine(reader.ReadInt32());  //输出:32
                Console.WriteLine(reader.ReadSingle()); //输出:12.3
                Console.WriteLine(reader.ReadString()); //输出:This is a test.
            }
        }
    }
}

3)与网络流交互

将内存流作为网络传输的缓冲区:

// 服务端接收数据
NetworkStream ns = client.GetStream();
MemoryStream ms = new MemoryStream();
ns.CopyTo(ms); // 将网络流复制到内存流
byte[] buffer = ms.ToArray();

2. 高级技巧

1)及时释放资源

使用using语句:确保流对象及时释放,避免内存泄漏。

using (MemoryStream stream = new MemoryStream()) { /*...*/ }

2)设置初始容量

预分配容量:若已知数据大小,初始化时指定Capacity减少动态扩容开销。

频繁写入数据时,指定初始容量可避免内存频繁扩容:

// 预分配 1MB 内存
using (MemoryStream ms = new MemoryStream(1024 * 1024))
{
    // 写入大量数据
}

3)重置流以复用内存

通过 SetLength(0)Seek 方法或 设置Position重置流:

using (MemoryStream ms = new MemoryStream())
{
    ms.Write(data, 0, data.Length);
    
    // 重置流并清空内容
    ms.SetLength(0); 
    ms.Position = 0; //或 ms.Seek(0, SeekOrigin.Begin);
    
    // 重新写入新数据
    ms.Write(newData, 0, newData.Length);
}

3. 实际应用示例

1)使用MemoryStream序列化和反序列化对象

using System;
using System.Text;
using System.Text.Json;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main()
    {
        Person person = new Person { Name = "John Doe", Age = 30 };
        // 序列化对象到MemoryStream
        using (MemoryStream ms = new MemoryStream())
        {
            JsonSerializer.Serialize(ms, person);
            // 将MemoryStream的位置重置到开头
            ms.Seek(0, SeekOrigin.Begin);
            // 反序列化对象从MemoryStream
            Person deserializedPerson = JsonSerializer.Deserialize<Person>(ms);
            Console.WriteLine("Name: " + deserializedPerson.Name);  // 输出:Name: John Doe
            Console.WriteLine("Age: " + deserializedPerson.Age);    // 输出:Age: 30
        }
    }
}

2)使用MemoryStream作为临时缓冲区

using System;
using System.IO;
public class MemoryStreamExample
{
    public static void Main()
    {
        // 创建一个MemoryStream作为临时缓冲区
        using (MemoryStream ms = new MemoryStream())
        {
            // 写入一些数据到MemoryStream
            byte[] data = new byte[] { 1, 2, 3, 4, 5 };
            ms.Write(data, 0, data.Length);
            // 将MemoryStream作为参数传递给其他方法
            ProcessData(ms);
        }
    }
    public static void ProcessData(MemoryStream ms)
    {
        // 将MemoryStream的位置重置到开头
        ms.Seek(0, SeekOrigin.Begin);
        // 读取数据 from MemoryStream
        byte[] buffer = new byte[ms.Length];
        ms.Read(buffer, 0, buffer.Length);
        Console.WriteLine("Received data:");
        foreach (byte b in buffer)
        {
            Console.Write(b + " ");
        }
    }
}

3)高效处理复杂场景

案例:日志文件关键字筛选

假设需要从多个.log文件中提取含特定关键字的行,传统方法可能导致内存暴涨。

优化方案

  1. 逐行读取文件:使用StreamReader避免一次性加载大文件。
  2. 内存流缓存匹配行:将匹配的行暂存至MemoryStream,减少磁盘IO次数。
  3. 批量写入结果:最后将内存流数据一次性写入目标文件。
    List<string> matchedLines = new List<string>();
    foreach (var file in Directory.GetFiles("logs", "*.log")) 
    {
        using (var reader = new StreamReader(file)) 
        {
            while (!reader.EndOfStream) 
            {
                string line = reader.ReadLine();
                if (Regex.IsMatch(line, "keyword")) 
                {
                    matchedLines.Add(line);
                }
            }
        }
    }
    // 使用MemoryStream合并数据并写入文件
    using (MemoryStream ms = new MemoryStream()) 
    {
        byte[] buffer = Encoding.UTF8.GetBytes(string.Join("\n", matchedLines));
        ms.Write(buffer, 0, buffer.Length);
        File.WriteAllBytes("result.txt", ms.ToArray());
    }
    
    实测性能提升显著

四、常见问题与解决方案

1. 读取时超出容量

// 错误示例:未重置位置导致读取失败
using (MemoryStream ms = new MemoryStream())
{
    ms.Write(data, 0, data.Length);
    byte[] buffer = new byte[ms.Length];
    ms.Read(buffer, 0, buffer.Length); // 抛出异常,因为 Position 已在末尾
}

// 正确做法:重置位置
ms.Position = 0;
ms.Read(buffer, 0, buffer.Length);

2. 处理大文件时的内存问题

当数据量超过内存限制时,改用 FileStream

// 替代方案:使用文件流
using (FileStream fs = new FileStream("temp.bin", FileMode.Create))
{
    // 写入数据到文件流
}

3. 异步操作

通过 ToArray() 获取字节数组后,可异步处理:

public async Task ProcessAsync()
{
    using (MemoryStream ms = new MemoryStream())
    {
        // 写入数据
        byte[] data = ms.ToArray();
        
        // 异步发送到网络
        await client.SendAsync(data);
    }
}

五、MemoryStream的优缺点

优点
  • 内存中操作速度快:由于数据存储在内存中,读写速度非常快。
  • 容量灵活MemoryStream的容量可以动态增长,以适应数据量的变化。
  • 支持读写定位操作:支持CanReadCanWriteCanSeek属性,便于灵活操作。
缺点
  • 内存占用高:处理大量数据时,可能会占用大量的内存资源。
  • 不适合持久化存储:数据存储在内存中,程序关闭后数据会丢失。

六、最佳实践总结

  • 资源管理
    始终使用 using 语句确保流正确释放:

    using (MemoryStream ms = new MemoryStream()) { ... }
    
  • 位置重置
    写入后读取前必须重置 Position0

  • 性能优化

    • 指定初始容量:在创建MemoryStream时,尽量指定初始容量,以减少动态增长的次数,提高性能。
    • 处理大量数据时谨慎使用:处理大量数据时,考虑使用文件流或其他适合的流类型,避免内存占用过高。(对大文件使用 FileStream 替代。)

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
C# MemoryStream流的详解与示例
C# Stream 和 byte[] 之间的转换(文件流的应用)
C# Stream篇(五) – MemoryStream

相关文章:

  • 爬虫的第三天——爬动态网页
  • ubuntu服务器进程启动失败的原因分析
  • LabVIEW医疗设备故障智能诊断系统
  • 智能网联交通加速落地,光路科技TSN技术助推车路云一体化发展
  • 电脑连不上手机热点会出现的小bug
  • vs2022+QT6.7.3打包程序流程
  • 推荐《人工智能算法》卷1、卷2和卷3 合集3本书(附pdf电子书下载)
  • 详细介绍WideCharToMultiByte()
  • MTK 后端初探
  • 如何正确地在 Postman 中添加认证 Token?
  • 3PL EDI:SA Piper Logistics EDI需求分析
  • AWS API Gateway Canary部署实战:Lambda到ECS的平滑迁移指南
  • 上位机知识篇---Linux中pythonpipapt
  • 【SpringCloud】Eureka的使用
  • Qt下载模板到本地文件内容丢失问题
  • 软件项目管理课程之第4讲:软件需求管理
  • 重温:时间窗口与滑动步长的概念
  • 【算法】动态规划:子序列问题、回文子串问题、两个数组的dp
  • C++ 变量类型
  • Qt 信号和槽
  • 中保协发布《保险机构适老服务规范》,全面规范保险机构面向老年人提供服务的统一标准
  • 哲学新书联合书单|远离苏格拉底
  • 一海南救护车在西藏无任务拉警笛开道,墨脱警方:已处罚教育
  • 巴基斯坦军方:印度导弹袭击巴首都附近空军基地
  • 国家主席习近平会见斯洛伐克总理菲佐
  • 上海“世行对标改革”的税务样本:设立全国首个税务审判庭、制定首个税务行政复议简易程序