Swift: print() vs println() vs NSLog() — что выбрать для логирования?
Полный разбор: print(), println() и NSLog() в Swift
Отличный вопрос! Давайте разберём каждый инструмент по порядку, а затем я покажу, как правильно организовать логирование в современном Swift-проекте.
1. println() — история
println() существовал только в самых ранних версиях Swift (1.x) и был полностью удалён. В современном Swift (5.x) этот код даже не скомпилируется. Забудьте про него — используйте только print().
2. print() — для быстрой отладки
print() — это базовая функция для вывода в стандартный поток (stdout). Она:
- Не добавляет никаких метаданных (время, процесс).
- Работает синхронно и быстро.
- Выводит объекты, реализующие протокол
CustomStringConvertible. - Поддерживает интерполяцию строк:
print("Value: \(x)").
Пример:
let name = "Alice"
print("Hello, \(name)!") // Вывод: Hello, Alice!
Когда использовать: Только для локальной отладки во время разработки. Никогда не оставляйте print() в продакшен-коде — это создаёт нагрузку на консоль и не даёт никакой структуры для анализа.
3. NSLog() — устаревший, но всё ещё работает
NSLog() — это функция из Foundation, которая:
- Добавляет timestamp, имя процесса, PID и номер потока.
- Автоматически добавляет символ новой строки.
- Является потоко-безопасной (thread-safe).
- Работает медленнее, чем
print(). - В iOS симуляторе выводит в консоль Xcode, на реальном устройстве — в системный лог (через
syslogd).
Пример:
NSLog("User tapped button %@", buttonID) // Вывод: 2025-01-15 10:30:45.123 MyApp[1234:56789] User tapped button btn_ok
Когда использовать: Только если вы поддерживаете очень старый код (iOS 9 и ниже) или вам нужно совместимость с macOS logging tools. В новых проектах NSLog() считается устаревшим.
4. os_log() — современный стандарт (рекомендую)
Начиная с iOS 10/macOS 10.12, Apple представила фреймворк os.log (теперь часть OSLog). Это правильный инструмент для логирования в продакшене:
- Категоризация: можно разделять логи по подсистемам (subsystem) и категориям (category).
- Уровни важности:
default,info,debug,error,fault. - Производительность: логи не формируются, если уровень логирования не активен (ленивая оценка).
- Безопасность: можно скрыть чувствительные данные (privacy).
- Интеграция: работает с Console.app, Instruments и Unified Logging.
Пример использования:
import os.log
let logger = Logger(subsystem: "com.myapp", category: "network")
// Разные уровни
logger.debug("Connecting to server...")
logger.info("Connection established")
logger.error("Failed to fetch data: \(error.localizedDescription)")
// Скрытие чувствительных данных
let userEmail = "user@example.com"
logger.info("User email: \(userEmail, privacy: .private)")
5. Сравнение производительности
Чтобы вы понимали разницу:
print()— ~0.001 мс на вызов (очень быстро).NSLog()— ~0.05-0.1 мс (медленнее из-за форматирования).os_log()— ~0.001 мс, если уровень не активен, и ~0.01 мс, если активен (оптимизировано системой).
В цикле из 10 000 вызовов NSLog() может вызвать заметные лаги, а os_log() — нет.
Рекомендация
В современных проектах используйте такую стратегию:
- Для быстрой отладки в Xcode:
print()— можно оставить временно, но перед коммитом удалить. - Для продакшен-логирования:
Loggerизos.log— структурированно, безопасно и производительно. - Для форматированного вывода сложных объектов: используйте
dump()— он показывает иерархию объекта.
Пример правильного подхода:
import os.log
struct AppLogger {
static let ui = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "UI")
static let network = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Network")
static let data = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Data")
}
// Использование
AppLogger.ui.debug("Button pressed")
AppLogger.network.error("Request failed: \(statusCode)")
Так вы получаете чистый, структурированный лог, который можно анализировать в Console.app или через сторонние инструменты (например, Datadog, Sentry).
<<Альтернативный подход: обёртка для логирования
Если вы хотите универсальное решение, которое легко переключать между print() и os_log(), можно написать простую обёртку:
enum LogLevel: String {
case debug, info, warning, error
}
func log(_ message: String, level: LogLevel = .debug, file: String = #file, function: String = #function, line: Int = #line) {
#if DEBUG
// В debug-сборке используем print для скорости
print("[\(level.rawValue.uppercased())] \(file.components(separatedBy: "/").last ?? ""):\(line) - \(function) -> \(message)")
#else
// В релизной сборке используем os_log
let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: level.rawValue)
switch level {
case .debug: logger.debug("\(message)")
case .info: logger.info("\(message)")
case .warning: logger.warning("\(message)")
case .error: logger.error("\(message)")
}
#endif
}
// Использование
log("User signed in", level: .info)
log("Database error: \(error)", level: .error)
Плюсы:
- Единый API для всех уровней.
- Автоматически переключается между
print()(для отладки) иos_log()(для релиза). - Добавляет имя файла и номер строки для быстрого поиска.
Минусы:
- Не поддерживает приватность данных (privacy) — нужно дорабатывать.
- В debug-режиме нет категоризации по subsystem.
Этот подход хорош для небольших проектов или когда вы только начинаете внедрять логирование. Для серьёзных приложений лучше сразу использовать Logger напрямую.