Почему результат разделения отличается от типа броска?

Вот часть кода, которую я не понимаю:

byte b1 = (byte)(64 / 0.8f); // b1 is 79
int b2 = (int)(64 / 0.8f); // b2 is 79
float fl = (64 / 0.8f); // fl is 80

Почему первые два вычисления отключены одним? Как я должен выполнять эту операцию, поэтому ее быстро и правильно?

EDIT: мне понадобится результат в байте

Ответ 1

РЕДАКТИРОВАТЬ: Не совсем правильно, см. Почему результат разделения отличается в зависимости от типа приведения? (Followup)

Проблема округления: путем преобразования в byte/int вы обрезаете десятичные знаки.

Но 64 / 0.8 не должно приводить к десятичным разрядам? Неправильно: из-за характера чисел с плавающей запятой 0.8f нельзя представить точно так же, как в памяти; он хранится как нечто близкое к 0.8f (но не точно). См. Примеры неточности с плавающей запятой или аналогичные потоки. Таким образом, результат вычисления не равен 80.0f, но 79.xxx, где xxx близок к 1, но все еще не точно один.

Вы можете проверить это, введя следующее в окно Immediate в Visual Studio:

(64 / 0.8f)
80.0
(64 / 0.8f) - 80
-0.0000011920929
100 * 0.8f - 80
0.0000011920929

Вы можете решить это, используя округление:

byte b1 = (byte)(64 / 0.8f + 0.5f);
int b2 = (int)(64 / 0.8f + 0.5f);
float fl = (64 / 0.8f);

Ответ 2

Я боюсь, что быстрые и правильные противоречия в таких случаях.

Двоичная арифметика с плавающей запятой почти всегда создает небольшие ошибки из-за базового представления в наших архитектурах процессора. Таким образом, в вашем первоначальном выражении вы фактически получаете значение немного меньше, чем математически корректное. Если вы ожидаете целое число в результате конкретной математической операции и получаете что-то очень близкое к ней, вы можете использовать метод Math.Round(Double, MidpointRounding) для правильного округления и компенсации небольших ошибок (и убедитесь, что вы выбрали стратегию MidpointRounding, которую вы ожидаете).

Простое приведение результата к типу типа byte или int не делает округления - он просто отсекает дробную часть (даже 1.99999f станет 1, когда вы просто применяете его к этим типам).

Десятичная арифметика с плавающей запятой работает медленнее и интенсивнее, но не вызывает этих ошибок. Чтобы выполнить его, используйте decimal литералы вместо float литералов (например, 64 / 0.8m).

Эмпирическое правило:

  • Если вы имеете дело с точными количествами (как правило, искусственными, как деньги), используйте decimal.
  • Если вы имеете дело с неточными величинами (такими как дробные физические константы или иррациональные числа, такие как π), используйте double.
  • Если вы имеете дело с неточными количествами (как указано выше), и некоторая точность может быть дополнительно принесена в жертву за скорость (например, при работе с графикой), используйте float.

Ответ 3

Чтобы понять проблему, вам нужно понять основы представления и операций с плавающей запятой.

0.8f не может быть точно представлен в памяти с использованием числа с плавающей запятой.

В математике 64/0.8 равно 80. В арифметике с плавающей запятой 60/0.8 равно приблизительно 80.

Когда вы создаете float для целого числа или байта, сохраняется только целая часть числа. В вашем случае неточный результат деления с плавающей запятой немного меньше 80, следовательно, преобразование в целое число дает 79.

Если вам нужен целочисленный результат, я предлагаю вам округлить результат, а не отбрасывать его. Один из способов сделать это - использовать следующую функцию, которая преобразует в целое число округлением до ближайшего целого числа:

Convert.ToInt32(64/0.8f);