Числовой тип данных Decimal и точность вычислений в Swift

Дополнительный раздел к главе 5

Когда вы работаете с целочисленными типами, для вас и ваших программ нет никаких проблем с точностью вычисления. Число 2 помноженное на 99 всегда будет 198, а 10 деленное на 2 – однозначно равняется 5. Но рано или поздно вы придете к необходимости применения чисел с плавающей запятой, т.е. чисел с дробной частью. И в некоторых случаях это может привести к неожиданным проблемам. Рассмотрим пример из листинга 1.1.

Листинг 1.1

var wallet: Float = 0
let productPrice: Float = 0.01

Переменная wallet описывает ваш кошелёк, а productPrice – стоимость товара, который вы продаёте.

Что будет, если вы продадите 100 единиц товара? Конечно же в вашем кошельке появится 1 рубль (листинг 1.2).

В следующем листинге используется еще не рассмотренная нами конструкция for-in. Это так называемый цикл, который позволяет выполнить вложенные в него выражения необходимое количество раз. Далее мы подробно рассмотрим широкие возможности for-in.

Листинг 1.2

for _ in 1...100 {
wallet += productPrice
}
wallet // 0.9999999993

Но что мы видим? По какой-то причине вместо числа 1.0 мы получили очень приближенное к нему, но отличающееся от задуманного, значение 0.9999999993.

Причина этому в особенностях работы компьютера с дробными числами. Как вы знаете, любое число в конечном счёте рассматривается компьютером, как совокупность 0 и 1. Так число 2 – это 10, число 13 – это 1101 и т.д. компьютеру очень просто и удобно оперировать с такими числами, перемножать их, вычитать, делить, ну и собственно, проводить любые математические (и логические) операции.

Когда число представлено в форме 0 и 1 говорят, что оно имеет базу 2, то есть два числа, с помощью которых представляется. Так числа в разных системах счисления имеют различную базу: у шестнадцатиричной – база 16, у десятиричной – база 10 и т.д. Но какие бы вы не использовали системы счисления в своих программах – для компьютера они будут выглядеть, как числа из 0 и 1, т.е. будет иметь базу два.

Ошибки в точности вычислений возникают, когда вы работаете с числами с плавающей запятой. В некоторых случаях компьютер не может точно представить число в двоичной форме, и использует максимально близкое, приближенное по значению. Так было и в случае с productPrice. Нет числа в двоичной форме, которое бы представило без ошибок 0,01. И таких примеров практически бесконечное множество.

О переводе чисел с плавающей точкой из десятичной в двоичную систему счисления вы можете прочитать самостоятельно. В сети полно примеров с подробным объяснением процесса.

Теперь представьте, что вы разрабатываете биржу криптовалюты. Пользователи проводят операции с биткоином, ошибка накапливается, и внезапно вы становитесь аферистом, мошенником, и все потому что просто использовали не правильный тип данных.

Не правильный тот тип? – спросите вы – А что существует правильный тип?

Да, в Swift есть числовой тип данных, который позволяет проводить операции с дробными числами без потери точности вычислений. Он называется Decimal. Попробуем поменять тип параметров из листингов выше с Float на Decimal, и посмотрим на результат (листинг 1.3)

Листинг 1.3

import Foundation
var wallet: Decimal = 0
let productPrice: Decimal = 0.01
for _ in 1...100 {
    wallet += productPrice
    
}
wallet // 1
Обратите внимание, что в коде должно присутствовать выражение import Foundation. О его предназначении мы поговорим позже, но если коротко, то с его помощью обеспечивается подключение библиотеки функций, в состав которой входят многие дополнительные типы данных и функции. Тип Decimal не является фундаментальным, для его работы необходима дополнительная библиотека (Foundation).

Как вы можете видеть, проблема ушла. Количество денег в кошельке ровно то, какое мы и ожидали.

Decimal отличается от Float и Double тем, что с его помощью можно оперировать и проводить операции с числами с базой 10. Данный тип обслуживается не просто аппаратной частью вашего компьютера, где из десятичного числа получается двоичное (и вместе с этим преобразованием возникают и ошибки в точности). В интересах Decimal функционируют специальные программные компоненты, которые обеспечивают точность. Конечно на нижнем уровне (в процессоре) числа все равно состоят из 0 и 1, но сложная логика работы этих специальных компонентов компенсирует все возникающие ошибки.

Но у данного решения существует и обратная сторона – тип Decimal работает значительно медленнее, чем Float или Double. Не стоит использовать Decimal повсеместно. Но где именно стоит его применять?

В случаях, когда значения могут быть измерены (например физические величины) применяйте Float и Double. В случаях, когда значения могут быть сосчитаны (например деньги) – используйте Decimal.

Будьте осторожны с физическими величинами, для которых критически важна точность вплоть до стотысячных, миллионных долей и т.д.

11 Comments

  1. ttork354:

    Как измерить на сколько медленнее работает Decimal чем Float? Я правда не знаю, зачем мне это знать сейчас, но когда читаешь доп. материал и постоянно видишь, это мы дальше пройдем это дальше, это вы узнаете позже – невольно возникает желание задавать тупые вопросы)))))

    • Я рад, что вы читаете данный материал, значит он написан не просто так)
      Для измерений можете составить цикл, производящий к примеру миллион операций над числами конкретного типа, замерить время перед его выполнение и после. Повторить для второго типа. Сравнить результаты.

      Задавайте вопросы, это хорошо! Так же можете воспользоваться нашим чатом в Telegram https://swiftme.ru/telegramchat

  2. Boda:

    var decimalNum: Decimal = 0.23

    В качестве результата выдает 0.2300000000000000512.
    Выходит, не такая уж и точность?

    • Тип данных значения 0.23 в первой строке – Double.
      Ваш код равносилен
      let doubleNum: Double = 0.23
      var decimalNum: Decimal = doubleNum

      Число 0.23 так же имеет проблемы с переводом в двоичный вид (имеется ошибка точности).

      Но, если избежать Double:

      1) Попробуйте к примеру перевести String в Decimal
      Decimal(string: "0.23")

      2) или провести 23 операции сложения с 0.01
      var result: Decimal = 0
      let decimalNum: Decimal = 0.01
      for _ in 1...23 {
      result += decimalNum
      }

      и проверьте итоговое значение

      Оно будет 0.23

  3. sergegroup:

    Здравствуйте.

    Я Сергей Хватюк – 30 минут назад приступил к изучению SWIFT. Ваш блог помог мне разобраться с типом Decimal.
    Я благодарю Вас за Вашу работу и желаю Вам баланса и благополучия.

  4. Олеся Украинская:

    Всем привет ✌️Я новенькая 😊Меня зовут Олеся и я очень хочу научится классно кодить ☝️😁

  5. Дмитрий Ахмеров:

    Спасибо за материал! Продолжаем учиться 🙂

  6. Shevchenko_d:

    Благодарю Автора за подробные пояснения. Пока очень доходчиво!

Добавить комментарий