Деление чисел на ноль в языке C#

BKeaton

Green Team
18.07.2018
204
340
BIT
158
Деление на ноль типов с плавающей точкой в языке C#
В очередной раз, наткнувшись на видеоурок, в котором создается калькулятор, я обратил внимание на то, что авторы почему-то для демонстрации основных арифметических операций используют только целые числа, при этом дробные числа не рассматриваются, хотя на практике и с теми и другими типами сталкиваешься очень часто. Поэтому хотелось бы восполнить этот момент и рассказать об одной особенности, которую очень часто забывают упомянуть авторы таких курсов.


Деление целых чисел на ноль
Описывать процесс создания калькулятора я не буду, т.к. процесс, по сути, ни чём не отличается от калькулятора, в котором используются только целые числа, а сразу перенесёмся в конец таких уроков, когда начинают разбирать допущенные ошибки. Среди которых самая распространенная, это необработанное исключение, которое возникает в результате деления целого числа на ноль, например:
C#:
static void Main (string [] args)
{
int a = 0;
int b = 5 / a;
int c = b;
Console.Write(c);
Console.ReadLine();
}
В данном примере при делении целого числа на ноль возникает исключение DivideByZeroException, после чего выполнение программы прекращается.
1575743062737.png

Обычно в такой ситуации нам предлагают использовать блоки try catch, чтобы отловить исключение, а затем выдать сообщение об ошибке, либо предложить какую-то иную логику, например:
C#:
static void Main (string [] args)
{
try
{
int a = 0;
int b = 5 / a;
int c = b;
Console.Write(c); 
}
catch (DivideByZeroException ex)
{
Console.WriteLine(ex.Message);
}
}
В этот раз при возникновении исключения попадаем в блок catch.
1575743079669.png

Посмотрев этот видеоурок, начинающие программисты очень часто делают для себя вывод, что блоки try catch не зависимо от того, делите вы на ноль целое или дробное число, являются универсальным средством решения данной проблемы. Но, так ли это на самом деле?


Деление дробных чисел на ноль
Предлагаю взглянуть на следующий код:
C#:
static void Main( string [] args)
{
try
{
int a = 0;
float b = 5.2F / a;
float c = b;
Console.Write(c);
Console.ReadLine(); 
}
catch (DivideByZeroException ex)
{
Console.Write(ex.Message);
}
}
Казалось бы, всё учтено, в коде добавлены блоки try catch, и теперь можно делить на ноль любые числа и ничего не боятся. Но, давайте разберём этот пример. Поставим точку остановы, и посмотрим, чему будет равно значение переменной b после операции деления на ноль. Многие скажут, что и так всё понятно возникнет исключение, которое будет обработано блоком catch. Но давайте всё-таки взглянем на результат.
1575743166273.png

продолжим выполнение программы
1575743178830.png

И так что мы видим. Главное это то, что ожидаемого выброса исключения при делении дробного числа на ноль не происходит, вместо этого переменной b было присвоено значение Infinity (бесконечность), после чего программа успешно продолжила свое выполнение. Установленные же блоки try catch не помогли нам выявить деление на ноль, потому что их работа связана с обработкой возникающих исключений, которых в данном случае — нет, как в более раннем примере с целыми числами. Поэтому в качестве результата получаем следующую картинку:
1575743199680.png

Судя по коду, программист явно не ожидал такого результата и не предполагал, что дальнейшие расчеты будут происходить со значением бесконечность. Хотя при этом, казалось бы, все сделано так, как объясняли в уроке с целыми числами, в чём же проблема?


Стандарт IEEE 754
На самом деле всё просто. Для арифметических операций с плавающей точкой используется стандарт IEEE 754 (IEEE floating point) или (The IEEE Standard for Floating-Point Arithmetic), в котором описываются: правила округления, арифметические форматы, операции, обработка исключений и многое другое.

Возникновение исключения, при делении дробного числа на ноль, так же описано в стандарте и попадает в список пяти исключений, при возникновении которых вместо exception возвращается значение по умолчанию. В данном случае таким значением является ±Infinity (которое и было получено ранее в примере). Дополнительная обработка таких ситуаций так же не требуется, поэтому добавленные блоки try catch, хоть и указаны в коде, но никакой роли не играют.

Вывод: при делении на ноль типов с плавающей точкой никогда не генерируется исключение, вместо этого всегда возвращается значение, по умолчанию описанное в стандарте, то есть ±Infinity, но так же возможно значение NaN.


Как определить, было ли выполнено деление на ноль?
Например, можно воспользоваться следующим способом: cначала позволим программе выполнить деление на ноль, в результате получаем +-infinity, а затем добавим проверку с методом float.IsInfinity() либо double.IsInfinity() в зависимости от типа. Проверка ловит, как минус, так и плюс бесконечность.

например:
C#:
static void Main (string [] args)
{
int a = 0;
float b = 5.2F / a;     //Получаем Infinity
if (float.IsInfinity(b))
{
//будет выведено это сообщение
Console.Write("деление на ноль!");
}
else
{
float c = b;
Console.Write(c);
}
}
Но, здесь есть один нюанс, текст сообщения:
C#:
Console.Write("деление на ноль!");
Данное сообщение может быть не верным, в ситуации арифметического переполнения переменной b, возникновение которого так же описано в списке пяти исключений стандарта IEEE754, когда вместо выброса исключения возвращают значение по умолчанию, которым опять же является ±Infinity.

например:
C#:
float a = 0.1F;
//переполнение
float b = float.MinValue / a;
Console.Write(b);
Console.ReadLine();     //-Infinity
Установленная ранее проверка float.IsInfinity() успешно отработает и на консоль будет выведено сообщение:деление на ноль, что, конечно же, не верно.


NaN
Ещё один нюанс возникает, если переменная b будет иметь значение 0.

было:
C#:
float b = 5,2F / a;
стало:
C#:
float b = 0.0F / a;
То есть в данном примере будем делить ноль на ноль. Всё остальное оставим как есть, нажимаем F5
1575743220435.png

Как видно на картинке в результате деления переменная b содержит NaN (Not-a-Number) или (значение, не являющееся числом). Это ещё одно значение, которое можно получить в результате операции деления дробного числа на ноль. При этом никаких исключений не выбрасывается. Добавленная проверка float.IsInfinity() в данном случаи выдает false, а значит код в блоке else выполнится успешно.
1575743239781.png

Чтобы отловить этот момент в коде, можно воспользоваться методом float.IsNaN()
C#:
if (float.IsInfinity(b) | float.IsNaN(b))
{
Console.Write(b); //+-Infinity либо NaN
}
На этом всё. Надеюсь, это статья поможет Вам избежать подобных ошибок в вашем коде.

Добавлено:
В языке C# существует только два типа с плавающей точкой (IEEE floating point) это float и double которые реализованы на основе стандарта IEEE 754. Поэтому при делении на ноль числа типа Decimal будет выброшено исключение DivideByZeroException
 
Мы в соцсетях:

Обучение наступательной кибербезопасности в игровой форме. Начать игру!