1. Явные преобразования
Если писать программы только про «сложили два целых — получили целое», жизнь действительно проста. Но реальный ввод пользователя быстро ломает эту идиллию: цены, проценты, скидки, средние значения, деление “на всех”. В таких задачах вы почти всегда встречаете смесь Int и Double, а ещё — необходимость «привести результат к виду, который понимает человек». Человек хочет “19.99”, а не “19.990000000000002”.
Представим, что мы делаем небольшой консольный калькулятор заказа. Пользователь вводит цену, скидку и число людей, а программа считает итоговую сумму и «сколько с каждого». Для вычислений удобен Double, а для некоторых частей логики (например, “сколько людей”) нужен Int. Значит, нам понадобятся преобразования типов — и аккуратное округление.
Swift — язык, который предпочитает, чтобы вы явно сказали, что именно хотите сделать. Поэтому выражение вида “целое умножить на дробное” без подготовки часто не компилируется: компилятор не угадывает, хотите ли вы расширить Int до Double, или наоборот сузить Double до Int.
Посмотрим на мини‑пример. Допустим, у нас есть количество людей (Int) и коэффициент скидки (Double):
import Foundation
let people: Int = 3
let discountFactor: Double = 0.9
let result = Double(people) * discountFactor
print(result) // 2.7
Здесь важный момент: мы сами сделали Double(people). Swift не делает это автоматически — и это хорошо, потому что для программы “случайно не потерять данные” обычно важнее, чем “сэкономить 5 символов”.
Тот же принцип работает и для целых типов разной ширины:
import Foundation
let a: Int = 42
let b: Int64 = 42
let sum = Int64(a) + b
print(sum) // 84
Это продолжение идеи из той лекции про целые типы: если типы разные, договоритесь, в каком типе вы считаете.
2. Когда преобразование может не получиться: if let
Когда вы пишете:
let age = Int("25")
всё работает — строка "25" превращается в число 25.
Но если строка не число?
let age = Int("сорок два")
Swift не может превратить "сорок два" в число. Поэтому функция Int(...) не возвращает число напрямую. Она возвращает опциональное значение (Optional<Int>): либо число, либо nil.
Самый простой способ работать с таким результатом — конструкция if let. Она означает: “если значение получилось — положи его в переменную и выполни код внутри if.”
import Foundation
let text = "42"
if let number = Int(text) { //number имеет тип Int. Не Int?
print("Число:", number)
} else {
print("Это не число")
}
Если преобразование удалось, переменная number содержит результат. Если нет — выполняется ветка else.
Внутри if переменная уже имеет обычный тип (Int), поэтому её можно использовать как обычное число.
import Foundation
let text = "10"
if let value = Int(text) {
print(value * 2) // 20
}
3. Double → Int: отбрасывание дроби и движение к нулю
Когда вы делаете Int(someDouble), вы можете ожидать «округления». Но Swift не читает ваши мысли (и слава богу). Преобразование Double → Int делает не округление, а отбрасывание дробной части — причём не “вниз”, а именно к нулю.
Это важно, потому что на отрицательных числах многие интуитивно путаются.
import Foundation
let a: Double = 19.99
let b: Double = -19.99
print(Int(a)) // 19
print(Int(b)) // -19
Обратите внимание: -19.99 превратилось в -19, а не в -20. Это и есть «движение к нулю».
Если вы делаете финансовые вычисления или расчёты скидок/налогов, такой эффект может внезапно “съесть” единицу, и вы начнёте спорить с калькулятором (обычно калькулятор выигрывает, потому что у него нет бага).
4. rounded() и round(): округление как операция
Чтобы именно округлить, в Swift есть rounded() и round(). Это часть API протокола FloatingPoint, и она специально сделана как отдельная операция: «сначала округли, потом (если надо) переведи в целое». В стандартной библиотеке для этого есть правила округления, описанные через FloatingPointRoundingRule.
Базовый вариант — просто rounded():
import Foundation
let x: Double = 19.49
let y: Double = 19.50
let z: Double = 19.51
print(x.rounded()) // 19.0
print(y.rounded()) // 20.0
print(z.rounded()) // 20.0
Заметьте: результат по‑прежнему Double. Это логично: округление — это про дробное число, но тип остаётся дробным.
Если вам нужен Int, делайте это явно:
import Foundation
let value: Double = 19.99
let roundedInt = Int(value.rounded())
print(roundedInt) // 20
Такой двухшаговый стиль читается честно: «я округляю, потом превращаю в целое».
5. Правила округления: .up, .down, .towardZero
Когда вы начинаете округлять не “до ближайшего”, а по правилам “вверх” или “вниз”, важно понимать, что речь идёт о движении по числовой прямой. Swift даёт несколько правил округления через FloatingPointRoundingRule.
Давайте соберём это в небольшую таблицу — так мозгу проще:
| Правило | Что делает интуитивно | Пример для 2.3 | Пример для -2.3 |
|---|---|---|---|
|
“вниз” (к −∞), аналог floor | |
|
|
“вверх” (к +∞), аналог ceil | |
|
|
к нулю (как Int(...) по смыслу) | |
|
|
от нуля | |
|
Обратите внимание на отрицательные числа: именно там чаще всего появляются «почему оно округлилось не так?!».
Посмотрим это в коде:
import Foundation
let v: Double = -2.3
print(v.rounded(.down)) // -3.0
print(v.rounded(.up)) // -2.0
print(v.rounded(.towardZero)) // -2.0
print(v.rounded(.awayFromZero)) // -3.0
И вот теперь Int(v) перестаёт быть загадкой: это по смыслу примерно “округление к нулю”, только сразу с изменением типа.
6. floor() и ceil() и связь с rounded(.down/.up)
Иногда в задачах вам прямо хочется сказать: округлить вниз (floor) или округлить вверх (ceil). Например, “сколько целых коробок нужно, если в коробке 12 банок, а я покупаю 25 банок” — это ceil(25/12).
В Swift исторически floor() и ceil() приходят из C‑мира (через Foundation/Darwin/Glibc), но концептуально это то же самое, что rounded(.down) и rounded(.up). Даже в обсуждении дизайна стандартной библиотеки прямо отмечалось, что rounded(.up)/rounded(.down) эквивалентны ceil()/floor().
Давайте сравним:
import Foundation
let x: Double = 2.3
print(floor(x)) // 2.0
print(ceil(x)) // 3.0
print(x.rounded(.down)) // 2.0
print(x.rounded(.up)) // 3.0
Почему тогда вообще нужны floor/ceil, если есть rounded(.down/.up)? В учебном коде часто удобнее показать “классические” названия (особенно если вы переходите из других языков). А в Swift‑стиле часто приятнее держать всё в виде “методов числа”: x.rounded(...). Выбирайте вариант, который делает ваш код более читаемым именно вам и вашей команде.
7. Округление до N знаков: масштабирование
С округлением до целого всё понятно. Но пользователи обычно хотят округление до 2 знаков (деньги), до 1 знака (температура), до 3 знаков (какие‑то измерения). В базовом Swift (без форматтеров и без будущих тем) самый простой и понятный паттерн — масштабирование.
Идея такая: если вам нужно 2 знака после запятой, умножаете на 100, округляете до целого, делите обратно.
import Foundation
let raw: Double = 19.999
let scaled = raw * 100
let roundedScaled = scaled.rounded()
let result = roundedScaled / 100
print(result) // 20.0
Это выглядит чуть многословно, зато прозрачно: вы явно управляете шагами, и любой новичок может отладить это по print.
Если хочется компактнее, можно в одну строку (но не злоупотребляйте “умными” строками, они любят становиться простынёй):
import Foundation
let raw: Double = 19.994
let rounded2 = (raw * 100).rounded() / 100
print(rounded2) // 19.99
Смысл тот же: масштабировали — округлили — вернули масштаб.
8. Где появляются потери: три типичных места
Потери — это не обязательно “ошибка языка”. Чаще это честная цена за то, что мы переводим число из одной модели мира в другую. Проблема начинается, когда мы теряем данные неосознанно.
Потеря дробной части при Double → Int
Это самая очевидная потеря: Int(19.99) становится 19. Тут не «сломалось», вы сами попросили сделать целое число, а целое число не умеет хранить дробь.
import Foundation
let price: Double = 19.99
let whole = Int(price)
print(whole) // 19
Если вам нужно округление, не делайте вид, что Int(...) обязан быть “умным”. Сначала округлите.
Погрешности Double: «почему 0.1 не живёт нормально»
Вещественные числа приближённы, и иногда это проявляется не только в сравнении, но и в округлении “по дороге”. Например, вы можете умножить цену на 100, а там окажется не 1999.0, а 1998.9999999997. Поэтому паттерн с (x * 100).rounded() — не глупость, а практическая защита.
Большие целые и Double: предел около 2^53
Есть более тонкая потеря, о которой полезно знать хотя бы как о факте: Double не может точно представить все большие целые числа. Внутри у Double ограниченная точность мантиссы, и после определённой границы целые начинают «слипаться» (между соседними целыми числами просто нет представимых значений). В обсуждениях стандартной библиотеки эта граница часто всплывает как около 2^53.
Практическое следствие: если вы возьмёте очень большое целое, превратите в Double, а потом обратно в Int, вы можете получить не то же самое значение.
Покажем идею на примере (числа намеренно большие):
import Foundation
let big: Int64 = 9_007_199_254_740_993 // 2^53 + 1 (примерно)
let asDouble = Double(big)
let back = Int64(asDouble)
print(big) // 9007199254740993
print(back) // может стать 9007199254740992 (эффект потери точности)
В зависимости от платформы/реализации вы можете увидеть разницу прямо в выводе. Важно не то, какое именно число “сломается”, а сам принцип: Double — не контейнер для бесконечно больших целых без потерь. Для большинства бытовых задач (цены, проценты, расстояния) вы не упираетесь в такие значения, но знать про это полезно, чтобы не делать “ID‑шники” через Double.
9. Преобразования без потерь: init?(exactly:)
Иногда вы хотите не просто “преобразовать”, а “преобразовать без потерь — иначе отказаться”. В Swift есть семейство failable‑инициализаторов init?(exactly:), которые возвращают nil, если точное представление невозможно. Это было добавлено именно как более безопасная модель числовых преобразований.
На практике это выглядит так: вы пробуете сделать преобразование, а если нельзя — получаете nil и обрабатываете как обычно (через if let, как мы уже делали с readLine()).
Пример: хотим превратить Double в Int, но только если дробной части нет:
import Foundation
let x: Double = 10.0
let y: Double = 10.5
let a = Int(exactly: x)
let b = Int(exactly: y)
print(a) // Optional(10)
print(b) // nil
И типичный “джуновский, но рабочий” стиль обработки:
import Foundation
let value: Double = 10.5
if let exactInt = Int(exactly: value) {
print("Exact:", exactInt)
} else {
print("Not an exact integer")
}
// Not an exact integer
Это отличный приём, когда вы не хотите “случайно потерять дробь” и потом ловить странные баги в логике.
10. Практика: считаем чек и округляем до копеек
Теперь давайте аккуратно соберём небольшой, но интересный сценарий в наш учебный CLI‑скрипт: пользователь вводит сумму заказа и процент чаевых, а мы считаем итог. Затем покажем три варианта округления: до копеек “по ближайшему”, “всегда вверх” и “всегда вниз”. Это полезно, потому что разные системы ведут себя по‑разному: где‑то округляют в пользу клиента, где‑то в пользу магазина, а где‑то “как получится”.
Сделаем ввод максимально простым: одна строка, два числа через пробел. Мы уже умеем split, а Double(...) и ?? у нас тоже есть.
import Foundation
print("Enter sum and tip percent (e.g. 19.99 10):", terminator: " ")
let parts = (readLine() ?? "").split(separator: " ")
let sum = Double(parts.first ?? "0") ?? 0
let tipPercent = Double(parts.dropFirst().first ?? "0") ?? 0
let total = sum + sum * tipPercent / 100
print("Raw total:", total)
Дальше — округление до 2 знаков тремя способами.
Округление “до ближайшего”:
import Foundation
let total: Double = 21.989
let nearest2 = (total * 100).rounded() / 100
print(nearest2) // 21.99
Округление “вверх до копейки” (полезно, если вы хотите гарантировать, что не “недоберёте”):
import Foundation
let total: Double = 21.981
let up2 = (total * 100).rounded(.up) / 100
print(up2) // 21.99
Округление “вниз до копейки” (иногда используют для скидок, чтобы не завысить):
import Foundation
let total: Double = 21.989
let down2 = (total * 100).rounded(.down) / 100
print(down2) // 21.98
Здесь особенно хорошо видно, что .down — это «пол вниз», а не «к нулю»: для положительных чисел это одно и то же, а вот для отрицательных — уже нет.
Деньги в центах: Int вместо Double
Мы не уходим сегодня в специальные денежные типы (это отдельная тема, и у неё есть свои тонкости), но базовую идею полезно увидеть: иногда удобнее хранить деньги как целое число минимальных единиц. Например, $19.99 хранить как 1999 центов (Int).
Тогда округление происходит ровно один раз — в момент перевода из Double в “центы”, и дальше вы работаете с целыми числами.
import Foundation
let total: Double = 21.989
let cents = Int((total * 100).rounded())
print(cents) // 2199
А показать обратно человеку можно так:
import Foundation
let cents: Int = 2199
let dollars = Double(cents) / 100
print(dollars) // 21.99
Это очень приятная модель для задач “суммировать/делить/сравнивать деньги”, потому что в основной логике вы избегаете вещественных ошибок. Но помните: перевод туда‑сюда — это как переезд. Лучше делать его реже, а не каждые две строки.
11. Типичные ошибки
Ошибка №1: ожидать, что Int(3.9) округлит до 4.
Такое ожидание появляется из бытового смысла слова «привести к целому». Но Int(...) не округляет, а отбрасывает дробную часть (к нулю). Если вам нужно округление, сначала делайте rounded(), и только потом — Int(...). Код станет длиннее на несколько символов, зато короче на несколько часов отладки.
Ошибка №2: путать .down с “к нулю”, особенно на отрицательных числах.
На положительных значениях rounded(.down) и “отбросить дробь” дают одинаковый результат, и мозг привыкает. А потом появляется -2.3, и внезапно .down превращает его в -3.0, потому что “вниз” — это к минус бесконечности. Если вы хотите “просто убрать дробь”, мыслите правилом .towardZero или преобразованием Int(...).
Ошибка №3: округлять до двух знаков через floor(x * 100) / 100 и удивляться странностям.
Формула кажется разумной, но из‑за приближённости Double выражение x * 100 может получиться чуть меньше ожидаемого (например, 1998.9999999997). Тогда floor даст на 1 цент меньше. Поэтому практический паттерн — использовать rounded() для “до копейки”, а floor/ceil применять только когда вы действительно хотите “всегда вниз” или “всегда вверх”.
Ошибка №4: надеяться, что Double безопасно хранит любые большие целые числа.
В большинстве учебных примеров всё нормально, и это расслабляет. Но у Double есть ограничение точности, и после больших значений целые числа перестают представляться точно (примерно после области порядка 2^53). Если вы работаете с большими идентификаторами, счётчиками или временем в наносекундах, не превращайте это в Double “просто потому что так удобнее делить”.
Ошибка №5: делать преобразование “как получится”, когда по смыслу нужно “только без потерь”.
Иногда логика требует: «это число должно быть целым». Если вы просто сделаете Int(x), вы молча потеряете дробь и программа продолжит работу в неправильном состоянии. В таких ситуациях лучше использовать Int(exactly: x) и обработать nil. Эта возможность как раз существует для того, чтобы конверсия могла честно провалиться, если точность сохранить нельзя.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ