谜题26: 在循环中
下面的程序计算了一个循环的迭代次数,并且在循环终止时打印这个计数值。那么,它打印的是什么呢?
class InTheLoop
{
const int END = int.MaxValue;
const int START = END - 100;
static void Main()
{
int count = 0;
for (int i = START; i <= END; i++)
count++;
System.Console.WriteLine(count);
}
}
解惑26: 在循环中
如果没有非常仔细地查看这个程序,你可能会认为它将打印100,因为END比START大100。稍微仔细一点,你可能会发现该程序没有使用典型的循环惯用法。大多数的循环会在循环索引小于终止值时持续运行,而这个循环则是在循环索引小于或等于终止值时持续运行。所以它会打印101,对吗?嗯,根本不对。如果你运行该程序,就会发现它压根就什么都没有打印。更糟的是,它会持续运行直到撤销为止。它没有机会打印count,因为在打印语句之前插入的是一个无限循环。
问题在于这个循环会在循环索引(i)小于或等于int.MaxValue时持续运行,但是所有的int变量都是小于或等于int.MaxValue的。因为它被定义为int数值中的最大值。当i达到int.MaxValue,并且再次执行增量操作时,它就又绕回到了int.MinValue。
如果需要的循环会迭代到int数值的边界附近,最好使用一个long变量作为循环索引。只需将循环索引的类型从int改变为long就可以解决该问题,从而使程序打印我们所期望的101:
for (long i = START; i <= END; i++)
更一般地讲,这里的教训就是int不能表示所有的整数。无论你在何时使用了一个整数类型,都要意识到其边界条件。如果其数值下溢或是上溢了,会怎么样呢?所以通常最好是使用一个取值范围更大的类型。
不使用long类型的循环索引变量也可以解决该问题,但是它看起来不那么漂亮:
int i = START;
do {
count++;
} while (i++ != END);
如果清晰性和简洁性占据了极其重要的地位,那么在这种情况下使用一个long类型的循环索引几乎总是最佳方案,但是有一个例外:如果你在所有的(或者几乎所有的)int数值上迭代,那么使用int类型的循环索引的速度大约可以提高一倍。下面是将f函数作用于所有40多亿个int数值上的惯用法:
// Apply the function f to all four billion int values
int i = int.MinValue;
do {
f(i);
} while (i++ != int.MaxValue);
该谜题对语言设计者的教训与谜题3相同:可能真的值得考虑,应该对那些在产生溢出时不会不抛出异常的算术运算提供支持。同时,可能还值得去考虑,应该对那些在整数值范围之上进行迭代的循环进行特殊设计,就像许多其他语言所做的那样。
(注:在C#的checked上下文中将进行溢出检查[C#语言规范 7.5.12])
C#解惑总目录
【推荐】国内首个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的设计模式综述