Возможно, вам нужны не числа с плавающей точкой
2023.02.26. Теги: Golang
В языках программирования есть много возможностей, которые чреваты проблемами и рекомендуются к использованию с осторожностью. Один из самых знаменитых примеров — оператор goto. К таким возможностям стоит относить и числа с плавающей запятой.
Почему?
Не буду перечислять все возможные проблемы чисел с плавающей запятой: скорее всего, со многими из них вы уже знакомы, а если нет, рекомендую почитать следующие статьи:
- Что нужно знать про арифметику с плавающей запятой
- Examples of floating point problems
- Ordering Numbers, How Hard Can It Be?
Такое поведение создаёт дополнительную когнитивную нагрузку на разработчика и пространство для ошибок. Многие ли программисты могут, особо не думая, сказать для какого-нибудь случая минимальный шаг изменения числа?
Не поймите меня неправильно: посыл статьи не в том, что числа с плавающей запятой не стоит использовать. Они — инструмент со своей областью применения. Эта статья о том, в каких случаях лучше использовать другие инструменты.
И что делать?
Если в каком-то месте хочется использовать число с плавающей запятой, можно вместо них рассмотреть следующие возможности:
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. Специализированные типы для величин
Во многих базах данных и некоторых языках уже существуют предопределённые типы для таких величин, как время и деньги. Стоит изучить их наличие и возможности: у таких типов уже продуман метод хранения и все нужные операции с данными.
Заключение
Если вы хотите использовать дробные числа, возможно, числа с плавающей запятой — не то, что вам нужно.
Числа с плавающей запятой стоит использовать в случаях, когда точность не принципиальна и приблизительные значения вполне подходят.
В других случаях вместо них можно использовать целые числа, десятичные числа, рациональные числа и специализированные типы для величин, таких как деньги и время.