Result Type: что это, тип-контейнер Result и как работает в мобильной разработке

Автор: IT Sectr Опубликовано: 2026-05-26 Время чтения: 9 мин

Result Type — тип-контейнер, который представляет результат операции, которая может завершиться успехом (Success) или ошибкой (Failure). В отличие от исключений, Result передаёт ошибку как обычное значение без раскрутки стека, что делает обработку ожидаемых ошибок более безопасной и композируемой. По данным Apple Swift Documentation (2026), Result<Success, Failure> в Swift позволяет объединять цепочки операций с автоматической обработкой ошибок через map и flatMap, не прерывая выполнение программы.

Главное

  • Result Type — generic-контейнер с двумя состояниями: Success (данные) и Failure (ошибка), без раскрутки стека
  • В Swift Result<Success, Failure> — встроенный тип с методами map, flatMap, mapError и get
  • В Kotlin Result<T> представляет успех или Throwable, с функциями getOrNull, getOrDefault, fold
  • В Dart стандартный Result<T> доступен с Dart 3.0, также есть Either из пакета fpdart
  • Композиция через map и flatMap позволяет объединять несколько операций, возвращающих Result, без вложенных проверок

Что такое Result Type?

Result Type — это generic-тип, инкапсулирующий результат выполнения операции, которая может завершиться как успешно, так и с ошибкой. В отличие от исключений, где ошибка прерывает нормальный поток выполнения и требует раскрутки стека для поиска catch, Result передаёт ошибку как обычное значение — вызывающая сторона всегда получает объект и сама решает, как с ним поступить. Это особенно полезно для ожидаемых ошибок: неверный ввод, бизнес-правила, отказ сервера, где исключение было бы слишком тяжёлым механизмом.

Концепция Result восходит к функциональному программированию, где подобные типы называются Either (или Left/Right). В Swift Result стал стандартным типом в Swift 5.0, в Kotlin Result<T> появился в стандартной библиотеке, в Dart 3.0 — встроенный Result<T>. Каждая реализация предоставляет методы для работы с контейнером: map (трансформация успешного значения), flatMap (цепочка Result-функций), mapError (трансформация ошибки), fold (обработка обоих кейсов). Result Type — краеугольный камень functional error handling в мобильной разработке, альтернатива try-catch для ожидаемых сценариев.

Ключевое преимущество Result — композируемость. Вы можете объединять несколько операций, каждая из которых может вернуть ошибку, в одну цепочку без вложенных try-catch. Если любая операция в цепочке завершается Failure, вся цепочка прерывается и возвращает Failure — без единого условия if или блока catch. Это делает код линейным и читаемым, особенно в сценариях с множественными последовательными запросами к API или проверками бизнес-правил.

Result в Swift

В Swift Result<Success, Failure> — это enum с двумя кейсами: .success(Success) и .failure(Failure), где Failure ограничен протоколом Error. Swift Result — полноценный функциональный тип с методами map, flatMap, mapError и get. get — особый метод: он возвращает значение Success, если результат успешный, и выбрасывает ошибку, если Failure. Это позволяет использовать Result как мост между функциональным и exception-based стилями: обрабатывай через map/flatMap в цепочке, а в конце — get через do-catch для интеграции с throws-кодом.

Result enum и exhaustive switch

Swift Result — это enum с generic параметрами, что позволяет компилятору проверять исчерпывающую обработку через switch или do-catch. Если в enum NetworkError добавить новый кейс, компилятор выдаст ошибку во всех switch-выражениях, где этот кейс не обработан. Exhaustive checking — главное преимущество Result перед исключениями: компилятор гарантирует, что все возможные ошибки учтены на этапе сборки. В отличие от этого, исключения не проверяются компилятором в Swift (только throws декларируется, но не тип ошибки).

swift
enum NetworkError: Error {
    case badURL
    case requestFailed(String)
    case decodingFailed
}

func fetchUser(id: Int) -> Result<User, NetworkError> {
    guard let url = URL(string: "https://api.example.com/users/\(id)") else {
        return .failure(.badURL)
    }
    let result = performRequest(url: url)
    switch result {
    case let .success(data):
        if let user = try? JSONDecoder().decode(User.self, from: data) {
            return .success(user)
        }
        return .failure(.decodingFailed)
    case let .failure(error):
        return .failure(.requestFailed(error.localizedDescription))
    }
}

// Использование с switch
let result = fetchUser(id: 42)
switch result {
case .success(let user):
    showUser(user)
case .failure(.badURL):
    logError("Invalid URL")
case .failure(.requestFailed(let msg)):
    showAlert(msg)
case .failure(.decodingFailed):
    logError("Decoding error")
}

Result<Success, Failure> позволяет типизировать ошибку на уровне типа: функция fetchUser возвращает Result<User, NetworkError>, где NetworkError — конкретный enum с тремя кейсами. Вызывающая сторона обрабатывает каждый кейс через switch с исчерпывающим покрытием — компилятор проверяет, что все варианты обработаны. Exhaustive switch — ключевое преимущество Result перед исключениями: компилятор гарантирует, что вы не забудете обработать .badURL, .requestFailed или .decodingFailed. В случае с исключениями компилятор не требует обработки, и незадествованный catch(.badURL) легко пропустить в ревью.

Result в Kotlin

В Kotlin Result<T> — встроенный тип из стандартной библиотеки, представляющий успех (T) или ошибку (Throwable). В отличие от Swift, в Kotlin Result не позволяет указать конкретный тип ошибки — только Throwable. Это сделано для простоты, но требует дополнительной проверки типа ошибки через when-выражение. Kotlin Result поддерживает функции fold (обработка обоих кейсов), getOrNull (успех или null), getOrDefault (успех или default), map, recover, andThen. Особенность Kotlin: Result не предназначен для прямого propagation через границы функций — его нельзя использовать как возвращаемый тип для методов Android SDK или Kotlin Coroutines API без дополнительной адаптации.

kotlin
fun parseJson(input: String): Result<JsonObject> {
    return runCatching {
        JsonParser.parseString(input).asJsonObject()
    }
}

fun validateEmail(email: String): Result<String> {
    return if (email.contains("@")) {
        Result.success(email.trim())
    } else {
        Result.failure(IllegalArgumentException("Invalid email"))
    }
}

data class SignupData(val name: String, val email: String)

fun processSignup(name: String, email: String): SignupResult {
    return validateEmail(email).fold(
        onSuccess = { validateName(name) },
        onFailure = { SignupResult.Error("Invalid email") }
    )
}

runCatching — удобная обёртка в Kotlin, которая перехватывает любое исключение и возвращает Result.failure. validateEmail возвращает Result.success или Result.failure в зависимости от проверки. fold обрабатывает оба кейса компактно: при Success вызывается следующая функция валидации, при Failure — возвращается SignupResult.Error. Важно: Kotlin Result не предназначен для хранения в полях data class или передачи через границы suspend-функций напрямую — используйте собственные sealed class (Success/Error/Loading) для представления состояний UI в Jetpack Compose или MVVM.

Result в Dart и Flutter

С Dart 3.0 в стандартной библиотеке появился встроенный Result<T> — sealed class с двумя конструкторами: T.ok() (успех) и Error.error() (ошибка с Object и StackTrace). До Dart 3.0 разработчики Flutter использовали Either<L, R> из пакета dartz или кастомные sealed class. Встроенный Result в Dart минималистичен: он не предоставляет map/flatMap напрямую — эти функции нужно реализовать через when или использовать extension methods. Для серьёзной функциональной обработки Either из fpdart остаётся более мощным решением с поддержкой map, flatMap, mapLeft, fold, andThen и операторов bind.

Either из fpdart

Either<L, R> — лево-ассоциированный тип из пакета fpdart, где Left — ошибка, Right — успех. В отличие от встроенного Result<T>, Either типизирует ошибку на уровне параметра типа (L), что позволяет различать типы ошибок на этапе компиляции. Пакет fpdart предоставляет полный набор функциональных комбинаторов: map (Right -> Right), mapLeft (Left -> Left), flatMap (bind — вложенные Either), andThen (цепочка без трансформации), fold (выход из Either), getOrElse (значение по умолчанию). Для Flutter-приложений с функциональным подходом Either — стандарт де-факто.

dart
import 'dart:convert';
import 'package:fpdart/fpdart.dart';

class UserService {
    Either<AppError, User> fetchUser(String id) {
        try {
            final response = await http.get(
                Uri.parse('https://api.example.com/users/$id')
            );
            if (response.statusCode == 200) {
                final user = User.fromJson(
                    json.decode(response.body)
                );
                return Either.of(user);
            }
            return Either.left(
                AppError.serverError(response.statusCode)
            );
        } on SocketException catch (e) {
            return Either.left(AppError.networkError(e.message));
        }
    }
}

// Использование с fold
final result = await service.fetchUser('42');
result.fold(
    (left)  => showError(left.message),
    (right) => showUser(right),
);

Either<L, R> из fpdart — лево-ассоциированный тип: Left — ошибка, Right — успех. fetchUser возвращает Either<AppError, User>, где AppError — sealed class с конкретными типами ошибок (serverError, networkError). fold обрабатывает оба кейса: первый callback для Left (ошибка), второй для Right (успех). В Dart 3.0 встроенный Result также поддерживает fold, но не предоставляет map/flatMap. Для цепочек Either из fpdart предоставляет map, flatMap (bind), mapLeft, andThen — полный набор функциональных комбинаторов для композиции ошибок.

Композиция Result через map и flatMap

Главное преимущество Result перед исключениями — композиция. Если у вас есть несколько операций, каждая из которых может вернуть ошибку, вы можете объединить их в цепочку через map и flatMap без единого вложенного if или try-catch. map трансформирует успешное значение: Result.success(x) -> Result.success(f(x)). flatMap (также called bind or andThen) — для случаев, когда трансформация сама возвращает Result: Result.success(x) -> f(x) -> Result<Y>. Если на любом шаге возникает Failure, последующие операции не выполняются — цепочка прерывается.

kotlin
data class UserRequest(val userId: String, val token: String)

sealed class AuthError {
    data object InvalidToken : AuthError()
    data class UserNotFound(val id: String) : AuthError()
}

typealias Outcome<T> = Either<AuthError, T>

fun validateToken(token: String): Outcome<String> =
    if (token.isNotBlank()) Either.right(token)
    else Either.left(AuthError.InvalidToken)

fun fetchProfile(userId: String): Outcome<Profile> =
    if (userId == "42") Either.right(Profile("Alice"))
    else Either.left(AuthError.UserNotFound(userId))

// Композиция через flatMap (andThen в fpdart)
val result = validateToken("abc123")
    .flatMap { fetchProfile("42") }
    .map { it.name }
    .getOrElse { "Guest" }

println(result) // "Alice"

Цепочка: validateToken -> fetchProfile -> map name -> getOrElse "Guest". Если validateToken вернёт Left (InvalidToken), цепочка прерывается и возвращается "Guest". Если fetchProfile вернёт Left (UserNotFound) — тоже "Guest". Если обе операции успешны — имя профиля. flatMap позволяет объединять Either-функции, каждая из которых может упасть, в одну линейную цепочку. В традиционном exception-стиле тот же код потребовал бы двух вложенных try-catch или проверок null. getOrElse в конце — точка выхода из композиции, которая предоставляет значение по умолчанию для Failure-кейса.

Result vs Исключения: когда что выбирать

Result и исключения — не взаимоисключающие подходы. Каждый имеет свою область применения, и в хорошо спроектированном мобильном приложении используются оба. Выбор зависит от того, является ли ошибка ожидаемой (expected) или неожиданной (unexpected). Result — для ожидаемых ошибок, которые являются частью бизнес-логики: неверный email, недостаточно средств на счету, превышен лимит запросов. Исключения — для неожиданных системных ошибок: потеря сети, OutOfMemoryError, NullPointerException (которых не должно быть, но они случаются).

КритерийResult TypeИсключения (Exception/Error)
Тип ошибкиОжидаемые (бизнес-логика)Неожиданные (системные)
ПроизводительностьНизкая стоимость (без раскрутки стека)Высокая стоимость (stack unwinding, захват StackTrace)
КомпозицияЧерез map/flatMap — линейные цепочкиВложенные try-catch — трудно читать
КомпиляторExhaustive checking (switch/when)Только checked exceptions в Java
Поток выполненияНе прерывается — ошибка как значениеПрерывается до ближайшего catch
ТестированиеЛегко: enough результата, assert isSuccess/isErrorТребует assertThrows и mock-объектов
Когда использоватьБизнес-валидация, цепочки запросов, формыПотеря сети, ошибки I/O, падения системы

Практическое правило: если ошибка — часть нормального потока работы приложения (пользователь ввёл неверный email, не хватает прав доступа) — используйте Result. Если ошибка — исключительная ситуация (сервер не отвечает, закончилась память) — используйте исключения. В мобильной разработке Result на границе слоёв (UseCase -> ViewModel) и исключения внутри слоёв (API -> Repository) — распространённый паттерн, сочетающий преимущества обоих подходов.

Стратегия миграции с исключений на Result

Миграция существующего exception-based кода на Result должна быть постепенной. Начните с границ слоёв: оберните вызовы throws-функций в Result { try ... } (Swift) или runCatching { ... } (Kotlin). Затем замените возвращаемый тип методов Repository и UseCase на Result/Either, оставив внутреннюю реализацию на исключениях. На последнем этапе мигрируйте ViewModel: вместо UiState с исключениями используйте sealed class UiState<T> (Loading, Success, Error), где Error хранит доменную ошибку, а не Throwable. Постепенная миграция позволяет тестировать каждый слой отдельно без глобального рефакторинга.

Часто задаваемые вопросы

Чем Result Type отличается от Optional/Option?

Optional (T?) представляет наличие или отсутствие значения — nil означает "нет данных", но не говорит почему. Result (Success/Failure) содержит не только успех, но и причину ошибки с конкретным типом. Используйте Optional, когда отсутствие значения — норма (например, опциональное поле профиля), Result — когда нужна информация об ошибке.

Как преобразовать throws-функцию в Result?

В Swift используйте Result { try throwingFunc() } — конструктор Result принимает throws-замыкание. В Kotlin — runCatching { throwingFunc() }, который возвращает Result<T>. В Dart — Result<T>.tryCatch(() => throwingFunc()). Это позволяет легко интегрировать exception-based код в Result-цепочки.

Как обработать ошибку в Result без потери информации?

Используйте mapError (Swift) или mapLeft (Either в Dart/Kotlin) для трансформации типа ошибки без изменения успешного значения. Если нужно обработать оба кейса и вернуть единое значение — используйте fold. Для логирования без прерывания цепочки используйте onFailure (Kotlin) или префиксный inspection point.

Можно ли использовать Result в Kotlin с корутинами?

Да, но осторожно. Kotlin Result<T> не рекомендуется как возвращаемый тип suspend-функций напрямую из-за особенностей с K2 compiler и рефлексией. Используйте собственный sealed class NetworkResult<T> (Success, Error, Loading) для представления состояний в корутинах. Для ожидаемых ошибок в бизнес-логике Either из Arrow — более мощная альтернатива.

Что такое fold в контексте Result?

fold — метод, который принимает два callback: onSuccess (для успешного кейса) и onFailure (для ошибочного), и возвращает единое значение любого типа. Это эквивалент switch-выражения, но в виде функции высшего порядка. fold — основная точка выхода из Result-цепочек, где вы преобразуете Success/Failure в UiState, строку для пользователя или другой Result.

Итоги

  • Result Type — generic-контейнер (Success/Failure) для безопасной обработки ожидаемых ошибок без раскрутки стека
  • В Swift Result<Success, Failure> с Failure: Error обеспечивает exhaustive checking через switch с type-safe ошибками
  • В Kotlin Result<T> оборачивает успех или Throwable, runCatching — удобный конструктор из throws-кода
  • В Dart встроенный Result<T> (Dart 3.0) и Either<L, R> из fpdart для продвинутой композиции с map/flatMap
  • Композиция через map (трансформация успеха) и flatMap (цепочка Result-функций) заменяет вложенные try-catch
  • Result vs исключения: Result для ожидаемых бизнес-ошибок, исключения для неожиданных системных сбоев
  • Используйте Result на границах слоёв для явной и тестируемой обработки ошибок без прерывания потока выполнения

Мы разработаем мобильное приложение под ключ

IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.

Обсудить проект

Читайте также