Возможно, вам нужны не числа с плавающей точкой

2023.02.26. Теги: Golang

В языках программирования есть много возможностей, которые чреваты проблемами и рекомендуются к использованию с осторожностью. Один из самых знаменитых примеров — оператор goto. К таким возможностям стоит относить и числа с плавающей запятой.

Почему?

Не буду перечислять все возможные проблемы чисел с плавающей запятой: скорее всего, со многими из них вы уже знакомы, а если нет, рекомендую почитать следующие статьи:

Такое поведение создаёт дополнительную когнитивную нагрузку на разработчика и пространство для ошибок. Многие ли программисты могут, особо не думая, сказать для какого-нибудь случая минимальный шаг изменения числа?

Не поймите меня неправильно: посыл статьи не в том, что числа с плавающей запятой не стоит использовать. Они — инструмент со своей областью применения. Эта статья о том, в каких случаях лучше использовать другие инструменты.

И что делать?

Если в каком-то месте хочется использовать число с плавающей запятой, можно вместо них рассмотреть следующие возможности:

1. Целые числа

Идентификаторы, счётчики, порядковые номера — самые очевидные примеры значений, которые лучше представлять как целые числа. Но существуют и многие другие случаи, в которых, если подумать дважды, целые числа оказываются лучшим решением.

Так, например, если вы хотите хранить дробное количество километров, то, возможно, стоит представить их не как километры, а как метры / сантиметры / миллиметры:

// ❌ здесь не ровно 17.529 километров
distanceKilometers := 17.529

// 👍 так лучше!
distanceMeters := 17529

Аналогично можно поступать и с другими физическими величинами.

2. Десятичные числа

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

В Python, например, в стандартной библиотеке есть модуль decimal:

>>> my_salary = 10.1
>>> print("{:.16f}".format(my_salary))
10.0999999999999996

>>> from decimal import *
>>> my_salary = Decimal('10.1')
>>> print("{:.16f}".format(my_salary))
10.1000000000000000

3. Рациональные числа

Иногда дробные числа делятся на некоторое известное число, которое может быть не кратно десяти:

import "fmt"

func main() {
    fullPageWidth := 1000
    blocksAmount := 3
    blockWidth := fullPageWidth / blocksAmount

    if (blockWidth * blocksAmount) != fullPageWidth {
        fmt.Println("We need a perfect fit!") 
    } else {
        fmt.Println("Perfect!")
    }
}

//=> We need a perfect fit!

В таких случаях можно использовать рациональные числа:

import (
    "fmt"
    "math/big"
)

func main() {
    fullPageWidth := new(big.Rat).SetInt64(1000)
    blocksAmount := new(big.Rat).SetInt64(3)
    blockWidth := new(big.Rat).Quo(fullPageWidth, blocksAmount)

    if new(big.Rat).Mul(blockWidth, blocksAmount).Cmp(fullPageWidth) != 0 {
        fmt.Println("We need a perfect fit!")
    } else {
        fmt.Println("Perfect!")
    }
}

//=> Perfect!

4. Специализированные типы для величин

Во многих базах данных и некоторых языках уже существуют предопределённые типы для таких величин, как время и деньги. Стоит изучить их наличие и возможности: у таких типов уже продуман метод хранения и все нужные операции с данными.

Заключение

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

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

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