65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的算术溢出——一些细节

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (13投票s)

2016年5月5日

CPOL

8分钟阅读

viewsIcon

42097

downloadIcon

145

让我们深入探讨整数数据类型中的一些细节,以及当其算术限制被超出时,幕后可能发生什么

引言

本文将带您了解 C# .NET 框架中整数和长整型数据类型的算术溢出和下溢的基础知识。了解这些算术溢出和下溢的关键细节可以为您节省大量挫败感和潜在的静默故障。让我们开始吧。

必备组件

阅读本文需要 C# 语言和 Microsoft .NET Framework 的基础知识。

关于附加代码的说明

要在您的计算机上运行附加代码,您需要以下软件:

  1. Visual Studio 2012 或更高版本

背景

这篇文章的灵感来自于我办公室里一位非常优秀的程序员朋友给我出的一个非常简单的问题。问题是打印出 C# .NET Framework 中整数(别名 `Int32`)数据类型所能容纳的最大数的后继数。随之而来的是一系列的意外。

基本原理

本文中我们将只讨论 `Integer` 数据类型,但它同样适用于 `long` 数据类型,没有任何例外。

注意:在我的文章中,我将 `int`、`Integer` 和 `Int32` 互换使用。`int` 是 C# 编译器为整数数据类型提供的关键字便捷方式,因为它是一种原始数据类型。因此,在代码中编写 `int`、`Int32` 和 `System.Int32` 是等效的。对于 `System.Int64` 数据类型,关键字是 `long`。

让我们快速回答一些可能现在出现在您脑海中的基本问题:

什么是整数数据类型? `Int32` 或 `int` 或 `System.Int32` 是 .NET Framework 支持的数据类型,用于处理数字计算。它只能包含整数值,如 `0`、`1`、`2`、`-1`、`-2`、`-3`。整数数据类型的变量不能存储分数。即使您尝试这样做,分数部分也会被自动截断。内部存储大小为 32 位,正如其名称 `Int32` 所暗示的那样。 `Int32` 类型变量可能存储的最大值为 `2147483647`,最小值为 `-2147483648`。

什么是算术溢出? 如果您尝试将一个整数值赋给一个 `integer` 类型变量,而该值超出了 `-2147483648` 和 `2147483647` 之间的范围,则会导致算术溢出错误。当赋给整数变量的值大于 `2147483647` 或小于 `-2147483648` 时,由于结果值(包括其符号 +/-)无法容纳分配给变量的 32 位内存空间,就会发生内存溢出。

.NET Framework 类库有一个内置的异常类 `System.ArithmeticException`,用于捕获代码中出现的此溢出情况。

以下是从附加源代码中生成的、会导致 `ArithemticException` 的代码片段。在此示例中,异常是因为 `Int32` 数据类型的负数极限发生的算术溢出而引发的。

代码片段 #1

        //Trivial code has been removed for brevity and clarity.
        //Please download attached code for complete reference.        
        private static void CreateArithmeticOverflowForInteger()
        {
            try
            {
                var myNumber = int.MinValue;
                var predecessorNumber = myNumber - 1;
            }
            catch (ArithmeticException ex)
            {
                Console.WriteLine(ex.ToString());
            }

        }

Using the Code

回到我最初的问题陈述,打印出整数数据类型所能容纳的最大数的后继数,这是我编写的程序:

代码片段 #2

       //Trivial code has been removed for brevity and clarity. 
       //Please download attached code for complete reference.

        static void Main(string[] args)
        {
            PrintSuccessor(int.MaxValue);
        }

        private static void PrintSuccessor(int number)
        {
            Console.WriteLine(number + 1);
        }

控制台打印出以下内容:-2147483648

`Int32` 数据类型可以存储的最高值是 `2147483647`。这里打印出的数字是 `-2147483648`。这是怎么回事?肯定有问题,因为数学上 `2147483647`(`Int32` 数据类型可存储的最大值)的后继数是 `2147483648`。另外,如果您注意到控制台上打印出的这个值实际上是 `Int32` 数据类型可以存储的最小值。不用担心,这一切都是因为 C# 项目的一个默认未启用的设置导致的这种异常行为。让我们检查一下。

逃避算术异常的项目设置

打开解决方案资源管理器。右键单击项目文件“`ArithmeticCalculationsNumericDataTypes`”。在上下文菜单中选择“属性”。这将打开项目属性窗口。您也可以在解决方案资源管理器中选择项目文件后按 `Alt + Enter` 来打开此窗口。现在,在项目属性窗口中转到“生成”选项卡。点击“高级”按钮。检查“检查算术溢出/下溢”复选框的值。当前,它将处于未选中状态,如下所示:

C# Project Settings for arithmetic overflow

现在,这个未选中的设置是您当前观察到的异常行为的根本原因。此项目设置指示编译器生成会忽略算术溢出异常的 MSIL。当整数变量的值增加到超出 `Int32` 数据类型的正数极限时,该值会发生回绕,并被设置为 `Int32` 数据类型的负数极限,即 `-2147483648`。不会引发任何运行时异常。只是一次静默的失败,没人能注意到。:) 我把这段源代码发给了我的朋友,结果却因为产生错误的结果而感到尴尬。

一个恼人的限制:一旦在项目属性中设置,我就无法在运行时更改这种算术溢出的行为。如果项目属性中选中了此设置,则在发生算术溢出时总是会引发异常。如果项目属性中未选中此设置,则在发生算术溢出时将始终捕获该异常,并根据情况悄悄地回绕到数据类型的下限或上限。有什么办法? C# 编译器设计者都是聪明人。:) 让我们看看他们为我们准备了什么。

Checked 和 Unchecked 编程构造

为了在运行时修改/控制算术溢出的行为,C# 提供了两个编程构造,即 `checked` 和 `unchecked` 关键字。让我们看看它们的实际应用。假设我们决定在运行时启用算术溢出异常。以下是一个快速片段,展示了它的工作原理:

        //Trivial code has been removed for brevity and clarity.
        //Please download attached code for complete reference.
        private static void CheckedProgrammingConstruct()
        {
            checked
            {
                var myNumber = int.MaxValue;
                //Exception will be raised here as we are inside checked block
                var successorNumber = myNumber + 1;
            }
        }

因此,您可以清楚地看到,在最后一行中,将会发生算术溢出。它将导致算术异常,因为它会超出 `Int32` 数据类型的上限,而与您项目属性中的算术溢出设置状态无关,因为这段代码块被包含在 `checked` 块中。

现在让我们看看 `unchecked` 编程构造:

        //Trivial code has been removed for brevity and clarity.
        //Please download attached code for complete reference.        
        private static void UncheckedProgrammingConstruct()
        {
            unchecked
            {
                var myNumber = int.MaxValue;
                //Exception will NOT be raised here as we are inside unchecked block
                var successorNumber = myNumber + 1;
            }
        }

因此,您再次可以看到,在最后一行代码中,存在算术溢出的可能性,但它不会导致任何算术异常,即使它会超出 `Int32` 数据类型的上限。它将悄悄地跳转到 `Int32` 数据类型的负数极限,而与您项目属性中的算术溢出设置状态无关,因为这段代码块被包含在 `unchecked` 块中。

谁能解决我的后继数问题?

获得正确结果的唯一方法是使用 `Int64`(`long`)数据类型而不是 `Int32`(`int`)数据类型。这是解决了我的朋友最初问题的代码:

        //Trivial code has been removed for brevity and clarity.  
        //Please download attached code for complete reference.

        static void Main(string[] args)
        {
            PrintSuccessorWithoutError(int.MaxValue);
        }

        private static void PrintSuccessorWithoutError(long number)
        {
            Console.WriteLine(number + 1);
        }

FCL 类是否尊重您的项目设置?

这是给您最重要的启示,也是我写这篇文章的原因,因为这个发现让我有些惊讶,并给了我思考的素材。假设您在项目属性中将“检查算术溢出/下溢”设置保持为未选中状态。那么以下代码片段将如何表现。`List` 是一个框架类库(FCL)类,随 .NET Framework 一起提供。在这里,我们将两个整数存储在一个列表中,然后要求 `List` 类的 `Sum` API 对它们进行求和。我选择了两个数字,以便求和过程会导致算术溢出。

        //Trivial code has been removed for brevity and clarity.
        //Please download attached code for complete reference.        
        private static void BehaviorOfFclClasses()
        {
            var listOfNumbers = new List<int>();
            listOfNumbers.Add(Int32.MaxValue);
            listOfNumbers.Add(1);
            //This sum operation is done by the FCL class internally and returned back to you.
            var totalSumOfListItems = listOfNumbers.Sum();
        }

此代码会导致算术溢出异常,而与项目属性中的算术溢出设置无关。这确实对程序员有利。问题是,如果框架类库开始根据您的项目设置隐藏算术溢出异常,情况可能会变得非常混乱。如果发生这种情况,它们将返回不正确的 `sum` 等结果,而不会引发异常。因此,FCL 类在其实现中始终使用 `checked` 关键字,只要涉及到算术运算。所以,下次如果您看到 FCL 类不遵循您项目中的算术溢出设置时,请不要感到困惑。:) 它们是为了您的最大利益而这样做的。

因此,您已经准备好深入算术运算的世界了。

建议

  1. 始终遵循算术溢出的默认项目设置(未选中状态)。通过项目设置在整个应用程序中启用算术溢出检查一次性会降低性能。如果您有特定情况,认为必须启用它,那么应该通过 `checked` 关键字在运行时仅针对特定代码块进行设置。

关注点

  • 我有一个 `Int32` 变量。假设我给它赋了 `Int32` 数据类型的最小值。现在您必须考虑对该变量进行什么算术运算,如果本文中提到的项目设置为未选中,将导致 `Int32` 数据类型的下限跳到 `Int32` 数据类型的上限。
  • 如何将执行算术计算的 FCL 类 API 封装在 `unchecked` 关键字中?观察它的行为。它会引发算术异常还是悄悄地忽略它?
            unchecked

           {
                var listOfNumbers = new List<int>();
                listOfNumbers.Add(Int32.MaxValue);
                listOfNumbers.Add(1);
                //This sum operation is done by the FCL class internally and returned back to you.
                var totalSumOfListItems = listOfNumbers.Sum();        
            }

历史

  • 2016 年 5 月 5 日 - 首次发布
  • 2016 年 5 月 9 日 - 更新了文章,删除了下溢的概念。下溢的概念与整数数据类型无关。整数在其正数和负数极限处都会发生溢出。
© . All rights reserved.