Числовой тип данных 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.

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

18 Comments

  1. ttork354:

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

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

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

      • zeromance:

        Прошу прощения, что продолжаю дальше эту тему с глупыми вопросами, но не подскажете, а как-то можно замерить время внутренними средствами языка Swift?
        Средствами, которые нашёл в интернете, пришёл к такому решению данного вопроса и не знаю, насколько это корректно и можно ли лучше:

        // Объявление параметров для Decimal
        var walletDec: Decimal = 0
        let productPriceDec: Decimal = 0.01
        // Фиксируем момент запуска цикла Decimal
        let firstDate = Date.init()
        for _ in 1…100000 {
        walletDec += productPriceDec
        }
        // Фиксируем время, сколько цикл Decimal работал
        let decTime = firstDate.distance(to: Date())

        // Объявление параметров для Float
        let productPriceFlt: Float = 0.01
        var walletFlt: Float = 0
        // Фиксируем момент запуска цикла Float
        let secondDate = Date.init()
        for _ in 1…100000 {
        walletFlt += productPriceFlt
        }
        // Фиксируем время, сколько цикл Float работал
        let floatTime = secondDate.distance(to: Date())

        // Определяем, во сколько раз быстрее Float
        let timeDif = decTime / floatTime

        // На небольших числах где-то в 10 раз
        // При проверки с 1_000_000, 73 секунды против 8.5 показало
        // Но там комп начал гудеть и, возможно, из-за этого не в 10 раз различаются

  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:

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

  7. id144428334:

    Василий, добрый день!Книга написана прекрасно. Большое спасибо!

  8. niklevenets:

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

    Так что же делать в этом случае?)

  9. vadamask:

    Написано, что в коде должно присутствовать выражение import Foundation. У меня и с UIKit всё работает. Объясните?

  10. shalimovkirill:

    Спасибо за книгу, материал и доп. задания / информацию. Очень удобно и понятно. Пробовал учить Swift в небезызвестной онлайн школе, не получилось, было непонятно, сейчас по книге и доп. материалам намного яснее. Спасибо ещё раз!

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