C#从数据库提取Blob存为文件时BinaryWriter.Write报越界错误
问题根因
你遇到的报错由明确的逻辑错误触发,除此之外代码中还存在多个会导致文件损坏、资源泄漏的隐藏问题:
- 直接触发异常的错误:最后写入剩余字节的代码多写了无意义的
-1运算。当最后一次调用reader.GetBytes返回的读取字节数retval为0(即Blob内容已经全部读完,无剩余字节)时,(int)retval - 1的计算结果为-1,给BinaryWriter.Write传入负数的count参数会直接触发你看到的参数越界异常;哪怕retval不为0,这个减1操作也会丢失每个文件的最后1个字节,导致导出的文件损坏无法打开。 - 读取偏移量未重置:你把记录Blob读取位置的
startIndex变量定义在了所有文件循环的外层,处理完第一个文件后这个变量不会归零,后续处理其他文件时会从错误的偏移位置开始读取,要么读不到内容要么读出乱码。 - 其他隐藏问题:
- 手动调用
Close()方法释放资源的写法不可靠,一旦执行过程中抛出异常,文件流、数据库连接、阅读器都不会被正确释放,会出现文件被占用、数据库连接池耗尽的问题 - 直接用字符串加号拼接文件路径,没有处理路径分隔符,若传入的保存目录末尾不带路径分隔符,会导致目录名和文件名拼接错误
- 给SqlParameter传值时把Guid转成了字符串,会让数据库执行时发生隐式类型转换,可能导致查询性能下降甚至查不到结果
- 缓冲区大小设置为100字节太小,大文件导出效率极低
- 手动调用
修正后的实现代码
public static void ExtractFiles(string connectionString, List<Guid> itemDetailIds, List<string> filenames, string fileLocation) { Directory.CreateDirectory(fileLocation); // 缓冲区调整为更合理的8KB大小 const int bufferSize = 8192; byte[] outbyte = new byte[bufferSize]; // 提前清理所有非法文件名字符 for (var i = 0; i < filenames.Count; i++) { filenames[i] = CleanFileName(filenames[i]); } const string sql = @"select did.FileExtension, did.Blob From ItemView v inner join DocumentItemDetail did on did.ItemDetailID = v.ItemDetailId Where did.ItemDetailID = @ItemDetailId AND v.Type='Document' AND did.Blob IS NOT NULL"; // 用using包裹所有实现IDisposable的资源,自动释放,不需要手动调用Close using var cnn = new SqlConnection(connectionString); using var sqlCommand = new SqlCommand(sql, cnn); // 提前定义参数,循环中仅修改参数值,无需每次清空参数集合 var itemIdParam = sqlCommand.Parameters.Add("@ItemDetailId", SqlDbType.UniqueIdentifier); cnn.Open(); for (var i = 0; i < itemDetailIds.Count; i++) { // 每个文件读取前重置偏移量为0 long startIndex = 0; itemIdParam.Value = itemDetailIds[i]; using var reader = sqlCommand.ExecuteReader(CommandBehavior.SequentialAccess); while (reader.Read()) { string fileExt = reader.GetString(0); // 用Path.Combine拼接路径,自动处理路径分隔符 string savePath = Path.Combine(fileLocation, $"{filenames[i]}{fileExt}"); using var fs = new FileStream(savePath, FileMode.OpenOrCreate, FileAccess.Write); using var bw = new BinaryWriter(fs); long retval = reader.GetBytes(1, startIndex, outbyte, 0, bufferSize); while (retval == bufferSize) { bw.Write(outbyte); bw.Flush(); startIndex += bufferSize; retval = reader.GetBytes(1, startIndex, outbyte, 0, bufferSize); } // 移除错误的-1运算,仅当有剩余字节时写入 if (retval > 0) { bw.Write(outbyte, 0, (int)retval); } bw.Flush(); } } }
修正点说明
- 所有实现了
IDisposable的对象都用using包裹,无论是否抛出异常都会自动释放资源,不会出现资源泄漏 - 偏移量
startIndex放在每个文件的循环内部定义,处理每个文件时都会从0位置开始读取 - 用
Path.Combine处理路径拼接,不需要手动关心目录末尾有没有路径分隔符 - 参数类型直接指定为
UniqueIdentifier对应C#的Guid类型,不需要转字符串,避免隐式类型转换带来的性能问题 - 最后写入剩余字节前先判断retval是否大于0,避免写入0字节的无效操作,同时去掉了错误的减1逻辑,不会丢失字节也不会传入负数count触发异常
- 缓冲区大小调整为8KB,大文件导出速度会有明显提升
内容的提问来源于stack exchange,提问作者lross15
相关产品推荐
相关产品推荐

