背景知识
外部排序指的是大文件的排序,即待排序的记录存储在外存储器上,待排序的文件无法一次装入内存,需要在内存和外部存储器之间进行多次数据交换,以达到排序整个文件的目的。外部排序最常用的算法是多路归并排序,即将原文件分解成多个能够一次性装入内存的部分,分别把每一部分调入内存完成排序。然后,对已经排序的子文件进行归并排序。
问题提出
假设我们要写一个外部排序程序。现在要讨论的是对已经排序的子文件进行归并排序。
解决方案1
下面是外部排序归并阶段的代码片段:
01: class ExternalSorting 02: { 03: void Merge(string inputFileName1, string inputFileName2, string outputFileName) 04: { 05: using (var reader1 = new StreamReader(inputFileName1)) 06: { 07: using (var reader2 = new StreamReader(inputFileName2)) 08: { 09: using (var writer = new StreamWriter(outputFileName)) 10: { 11: Merge(reader1, reader2, writer); 12: } 13: } 14: } 15: } 16: 17: void Merge(TextReader reader1, TextReader reader2, TextWriter writer) 18: { 19: var s1 = reader1.ReadLine(); 20: var s2 = reader2.ReadLine(); 21: while (s1 != null || s2 != null) 22: { 23: if (Compare(s1, s2) <= 0) StepIt(ref s1, reader1, writer); 24: else StepIt(ref s2, reader2, writer); 25: } 26: } 27: 28: int Compare(string s1, string s2) 29: { 30: if (s1 == null && s2 == null) throw new ArgumentException("s1 和 s2 不能同时为 null"); 31: if (s1 == null) return 1; 32: if (s2 == null) return -1; 33: return string.Compare(s1, s2); 34: } 35: 36: void StepIt(ref string s, TextReader reader, TextWriter writer) 37: { 38: writer.WriteLine(s); 39: s = reader.ReadLine(); 40: } 41: }
上述代码中的第 05 到 14 行的三个 using 语句逐个嵌套,依次缩进,是不是很难看?
注意,上述代码中第 33 行可以替换为你想要的比较大小的方法,以便按照不同的关键字进行排序。
解决方案2
我们知道,可以将多个对象与 using 语句一起使用,但必须在 using 语句中声明这些对象。因此,我们可以将上述的第 05 到 14 行的代码重构如下:
1: using (TextReader reader1 = new StreamReader(inputFileName1), 2: reader2 = new StreamReader(inputFileName2)) 3: { 4: using (TextWriter writer = new StreamWriter(outputFileName)) 5: { 6: Merge(reader1, reader2, writer); 7: } 8: }
但是还是有两个嵌套的 using 语句,不爽。
解决方案3
我们还知道,C# 编译器实际上将 using 语句转化为 try - finally 块。那么我们继续进行重构:
01: TextReader reader1 = null; 02: TextReader reader2 = null; 03: TextWriter writer = null; 04: try 05: { 06: reader1 = new StreamReader(inputFileName1); 07: reader2 = new StreamReader(inputFileName2); 08: writer = new StreamWriter(outputFileName); 09: Merge(reader1, reader2, writer); 10: } 11: finally 12: { 13: if (reader1 != null) reader1.Dispose(); 14: if (reader2 != null) reader2.Dispose(); 15: if (writer != null) writer.Dispose(); 16: }
这样看起来很不错的样子。注意:
- 如果上述代码片段不是 Merge 方法中仅有的代码块,请使用一对大括号将其括起来,以便为上述三个对象(reader1、reader2 和 writer)创建有限的范围。
- 实际上 C# 编译器对解决方案1和解决方案2生成的 IL 代码是差不多的,都是三个嵌套的 try - finally 块。而不是象解决方案3中那样只有一个 try - finally 块。
解决方案4
我们知道 using 语句只不过是提供能确保正确使用 IDisposable 对象的方便语法。using 语句按照正确的方式调用对象上的 Dispose 方法,并会导致在调用 Dispose 时对象自身处于范围之外。因此,可以重构如下:
1: using (IDisposable reader1 = new StreamReader(inputFileName1), 2: reader2 = new StreamReader(inputFileName2), 3: writer = new StreamWriter(outputFileName)) 4: { 5: Merge(reader1 as TextReader, reader2 as TextReader, writer as TextWriter); 6: }
这是我最喜欢的方案,你们呢?
解决方案5
但是,解决方案4中第 5 行中使用三个 as 进行强制类型转换毕竟不爽。考虑进行以下重构:
1: using (TextReader reader1 = new StreamReader(inputFileName1), 2: TextReader reader2 = new StreamReader(inputFileName2), 3: TextWriter writer = new StreamWriter(outputFileName)) 4: { 5: Merge(reader1, reader2, writer); 6: }
可惜,上述代码片段无法通过编译,出现以下编译错误:
这个编译错误实在是没有道理。实际上在 for、using 和 fixed 语句中能够使用多个类型对程序员来说很有用的,而且 C# 编译器实现这点也没有什么困难。你们以为呢?
解决方案6
好吧,我们换一种方法进行重构:
1: using (var reader1 = new StreamReader(inputFileName1), 2: reader2 = new StreamReader(inputFileName2), 3: writer = new StreamWriter(outputFileName)) 4: { 5: Merge(reader1, reader2, writer); 6: }
非常遗憾,还是无法通过编译。这次的错误信息如下:
这个编译错误也是没有道理的。同样,在隐式类型的局部变量中有多个声明符对程序员来说也是很有用的,对 C# 编译器来说也没有什么实现上困难。你们以为呢?
结论
建议修改 C# 编译器,去掉 CS1044 和 CS0819 编译错误,以便允许解决方案5和解决方案6,造福广大 C# 程序员。
对于目前的 C# 编译器,建议使用解决方案4。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· [AI/GPT/综述] AI Agent的设计模式综述