若不使用MemoryMappedFile,谁负责文件内存映射?及自定义DiskMappedList问询
嘿,我来帮你拆解这两个问题~
关于实现继承IList的磁盘持久化列表
你想搞一个继承IList<T>的包装类,完全靠文件流做持久化,替代MemoryMappedFile的自动映射逻辑,这个思路完全可行——相当于手动实现一套“磁盘版列表”,把内存列表的操作都转成磁盘IO。
先把你的示例代码格式化一下,顺便补点关键逻辑的示例:
public class DiskMappedList<T> : IList<T> { private int _count; private FileStream _dataStream; // 如果是可变大小元素,建议加个索引文件来记录每个元素的位置 private FileStream _indexStream; public DiskMappedList(string directory) { _count = 0; if (!Directory.Exists(directory)) { Directory.CreateDirectory(directory); } // 生成数据文件和索引文件 var dataFileName = $"{Guid.NewGuid()}.data"; var indexFileName = $"{Guid.NewGuid()}.index"; _dataStream = File.Create(Path.Combine(directory, dataFileName)); _indexStream = File.Create(Path.Combine(directory, indexFileName)); } // 以Add方法为例,实现元素的持久化 public void Add(T item) { // 这里推荐用Protobuf/System.Text.Json替代过时的BinaryFormatter var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(item); // 先记录元素在数据文件中的起始位置和长度到索引文件 var elementOffset = _dataStream.Position; var elementLength = jsonBytes.Length; _indexStream.Write(BitConverter.GetBytes(elementOffset)); _indexStream.Write(BitConverter.GetBytes(elementLength)); // 写入数据文件 _dataStream.Write(jsonBytes); _count++; // 强制刷新到磁盘,避免缓存丢失 _dataStream.Flush(true); _indexStream.Flush(true); } // 索引器实现:读取指定位置的元素 public T this[int index] { get { if (index < 0 || index >= _count) throw new ArgumentOutOfRangeException(nameof(index)); // 从索引文件中读取对应元素的偏移和长度 _indexStream.Seek(index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin); var elementOffset = BitConverter.ToInt64(_indexStream.ReadBytes(sizeof(long))); var elementLength = BitConverter.ToInt32(_indexStream.ReadBytes(sizeof(int))); // 从数据文件中读取元素字节并反序列化 _dataStream.Seek(elementOffset, SeekOrigin.Begin); var elementBytes = _dataStream.ReadBytes(elementLength); return JsonSerializer.Deserialize<T>(elementBytes); } set { // 类似读取逻辑,先定位覆盖写入数据,再更新索引(如果元素长度变化的话) var jsonBytes = JsonSerializer.SerializeToUtf8Bytes(value); _indexStream.Seek(index * (sizeof(long) + sizeof(int)), SeekOrigin.Begin); var elementOffset = BitConverter.ToInt64(_indexStream.ReadBytes(sizeof(long))); _dataStream.Seek(elementOffset, SeekOrigin.Begin); _dataStream.Write(jsonBytes); // 如果新元素长度和旧的不一样,需要更新索引文件的长度值 _indexStream.Seek(index * (sizeof(long) + sizeof(int)) + sizeof(long), SeekOrigin.Begin); _indexStream.Write(BitConverter.GetBytes(jsonBytes.Length)); _dataStream.Flush(true); _indexStream.Flush(true); } } // 辅助扩展方法:简化字节读取 private byte[] ReadBytes(this Stream stream, int length) { var buffer = new byte[length]; stream.Read(buffer, 0, length); return buffer; } // 别忘了实现IList<T>的其他成员:Remove、IndexOf、Clear等 public bool Remove(T item) { // 实现逻辑:找到元素索引,删除对应索引的索引记录,然后整理数据文件(或者标记为删除,后续做碎片整理) throw new NotImplementedException(); } // 其他成员... }
这里给你几个关键提醒:
- 别用BinaryFormatter:它已经被标记为过时,存在安全漏洞,优先选
System.Text.Json、MessagePack或者Protobuf,性能和安全性都更靠谱。 - 索引文件很重要:如果你的元素是可变大小的,单靠数据文件定位会慢到爆炸,用索引文件记录每个元素的偏移和长度,能大幅提升查询效率。
- 并发安全不能省:你说省略了锁逻辑,但实际生产环境一定要加——用
lock或者ReaderWriterLockSlim处理多线程读写,不然很容易搞坏文件。 - 缓存优化:频繁读写磁盘性能肯定差,可以自己加个内存缓存,把常用的元素存在内存里,定期同步到磁盘,平衡性能和持久化可靠性。
不使用MemoryMappedFile时,谁负责文件到内存的映射?
一句话:完全由你的代码来负责,操作系统不会帮你做“把文件内容直接映射成可操作的内存对象”这件事。
当你用FileStream直接读写文件时,操作系统确实有自己的页缓存——它会把经常访问的文件内容临时存在内存里,但这是操作系统层面的透明操作,你没法直接操控这块缓存,也不能把缓存的内存地址当成对象来访问。
如果你想实现“像操作内存列表一样操作磁盘数据”的体验,就得自己搞定这些:
- 把文件里的字节数据反序列化成内存中的对象,供你的代码操作;
- 当对象被修改后,再把它序列化回文件,持久化到磁盘;
- 如果要提升性能,得自己维护内存缓存,处理缓存和磁盘数据的同步逻辑。
简单说,MemoryMappedFile帮你做了“把文件映射到进程地址空间,让你直接用指针操作内存”的脏活;而你自己用FileStream实现的话,所有“内存-磁盘”的数据转换、位置定位、缓存管理都得自己写代码搞定。
内容的提问来源于stack exchange,提问作者Sergey Kljopov
相关产品推荐
相关产品推荐

