Swift и случайные числа: магия, которая работает предсказуемо
Каждый разработчик рано или поздно сталкивается с задачей: «Как сгенерировать случайное число?». Казалось бы, что может быть проще? Но в мире Swift эта тема прошла долгий путь эволюции — от древних C-функций до элегантного встроенного API. В этой статье мы разберем все способы генерации случайных чисел в Swift, от простых до промышленных, с живыми примерами и важными нюансами.
Если вы когда-нибудь задавались вопросом, почему ваш arc4random() возвращает странные числа или как сделать настоящий случайный выбор из массива — вы попали по адресу. Поехали!
Краткая история: как мы дошли до жизни такой
До Swift 4.2 (2018 год) разработчики мучительно выбирали между arc4random(), drand48() и костылями вроде rand() % 100. Последний, кстати, был опасен — смещение (bias) в распределении гарантировано, если диапазон не делится нацело на RAND_MAX. С выходом Swift 4.2 Apple подарила нам RandomNumberGenerator протокол и встроенные методы. Теперь генерация случайных чисел стала безопасной, читаемой и единообразной.
Современный способ: RandomNumberGenerator и методы типов
Начиная с Swift 4.2, самый правильный и простой способ — использовать статические методы на типах:
// Случайное целое число от 1 до 100
let randomInt = Int.random(in: 1...100)
// Случайное число с плавающей точкой от 0.0 до 1.0
let randomDouble = Double.random(in: 0.0...1.0)
// Случайное число Float
let randomFloat = Float.random(in: 0.0...1.0)
// Случайный Bool
let randomBool = Bool.random()
Этот API использует системный генератор SystemRandomNumberGenerator, который является криптостойким (но об этом чуть позже). Важно: диапазон задается через closed range (...) или half-open range (..<).
Почему это круто?
- Безопасность типов — компилятор не даст вам сгенерировать Double и присвоить Int.
- Отсутствие смещения — Swift сам заботится о равномерности распределения.
- Универсальность — работает для всех числовых типов: Int, UInt, Int8, Float, Double, CGFloat и даже Bool.
Генерация случайного числа из диапазона: подводные камни
Казалось бы, что может пойти не так? Рассмотрим пример:
let diceRoll = Int.random(in: 1...6) // Кубик: 1,2,3,4,5,6
let coinFlip = Int.random(in: 0..<2) // Монетка: 0 или 1
Все корректно. Но если вы хотите сгенерировать случайное число без верхней границы (например, от 0 до бесконечности) — так не получится. Swift требует явного указания диапазона. Это сделано специально, чтобы избежать ошибок переполнения.
Если вам нужно случайное число от 0 до max (где max — переменная), просто используйте:
let maxValue = 500
let randomUpTo = Int.random(in: 0...maxValue)
Случайный элемент из коллекции (Array, Set, Dictionary)
Генерация случайного числа часто нужна не сама по себе, а для выбора случайного элемента. Swift предоставляет элегантное решение:
let fruits = ["яблоко", "банан", "вишня", "дыня"]
if let randomFruit = fruits.randomElement() {
print("Сегодня едим \(randomFruit)")
} else {
print("Корзина пуста!")
}
Метод randomElement() возвращает опциональное значение — это важно, так как коллекция может быть пустой. Для перемешивания всего массива используйте shuffled():
let shuffledFruits = fruits.shuffled()
print(shuffledFruits) // например: ["банан", "дыня", "яблоко", "вишня"]
Криптостойкая генерация: когда безопасность превыше всего
Стандартный Int.random(in:) использует SystemRandomNumberGenerator, который является криптостойким. Это значит, что он подходит для генерации токенов, паролей и ключей шифрования. Но если вам нужно явно указать генератор (например, для тестирования), можно создать свой:
// Свой генератор (НЕ криптостойкий — только для тестов!)
struct LinearCongruentialGenerator: RandomNumberGenerator {
var state: UInt64 = 1
mutating func next() -> UInt64 {
state = state &* 6364136223846793005 &+ 1442695040888963407
return state
}
}
var generator = LinearCongruentialGenerator()
let testRandom = Int.random(in: 1...100, using: &generator)
На практике вы вряд ли будете писать свой генератор. Но знание этой возможности помогает понять, как Swift абстрагирует генерацию случайных чисел через протокол RandomNumberGenerator.
Старый способ: arc4random и его друзья (для совместимости)
Если вы поддерживаете старый код или работаете с проектом на Swift 3/4.0, вам может встретиться arc4random. Вот как это выглядело раньше:
// Случайное число от 0 до 99 (старый способ)
let oldRandom = Int(arc4random_uniform(100))
// Случайное число от 10 до 99
let oldRangeRandom = Int(arc4random_uniform(90)) + 10
Недостатки:
- Работает только с UInt32 — нужно постоянно приводить типы.
- Неудобно для диапазонов (нужно вычислять смещение).
- Не работает для Float/Double напрямую.
Используйте этот метод только если вы модернизируете legacy-код. Для нового кода — только Int.random(in:).
Случайные числа в SwiftUI и iOS: практический пример
Давайте соберем все вместе и напишем небольшое приложение — генератор случайных паролей на SwiftUI:
import SwiftUI
struct PasswordGeneratorView: View {
@State private var passwordLength: Double = 12
@State private var password: String = ""
var body: some View {
VStack(spacing: 20) {
Text("Генератор паролей")
.font(.title)
Slider(value: $passwordLength, in: 6...32, step: 1)
Text("Длина: \(Int(passwordLength))")
Button("Сгенерировать") {
password = generatePassword(length: Int(passwordLength))
}
.buttonStyle(.borderedProminent)
Text(password)
.font(.system(.body, design: .monospaced))
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
}
.padding()