Result Type — тип-контейнер, който представлява резултата от операция, която може да завърши с успех (Success) или грешка (Failure). За разлика от изключенията, Result предава грешката като обикновена стойност без развиване на стека, което прави обработката на очаквани грешки по-безопасна и композируема. Според Apple Swift Documentation (2026), Result<Success, Failure> в Swift позволява комбиниране на вериги от операции с автоматична обработка на грешки чрез map и flatMap, без прекъсване на изпълнението на програмата.
Основни точки
Result Type е генеричен тип, който капсулира резултата от изпълнението на операция, която може да завърши както с успех, така и с грешка. За разлика от изключенията, където грешката прекъсва нормалния поток на изпълнение и изисква развиване на стека за намиране на 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 — крайъгълният камък на функционалната обработка на грешки в мобилната разработка, алтернатива на try-catch за очаквани сценарии.
Ключовото предимство на Result е композируемостта. Можете да комбинирате множество операции, всяка от които може да върне грешка, в една верига без вложени try-catch. Ако някоя операция във веригата завърши с Failure, цялата верига се прекъсва и връща Failure — без нито едно условие if или блок catch. Това прави кода линеен и четим, особено в сценарии с множество последователни заявки към API или проверки на бизнес правила.
В Swift Result<Success, Failure> е enum с два случая: .success(Success) и .failure(Failure), където Failure е ограничен от протокола Error. Swift Result е пълноценен функционален тип с методи map, flatMap, mapError и get. get е специален метод: връща стойността Success, ако резултатът е успешен, и хвърля грешка, ако е Failure. Това позволява използването на Result като мост между функционалния и базирания на изключения стил: обработвайте чрез map/flatMap във верига, а накрая — get чрез do-catch за интеграция с throws код.
Swift Result е enum с генерични параметри, което позволява на компилатора да проверява изчерпателна обработка чрез switch или do-catch. Ако добавите нов случай към enum NetworkError, компилаторът ще даде грешка във всички switch изрази, където този случай не е обработен. Изчерпателна проверка (exhaustive checking) — основното предимство на Result пред изключенията: компилаторът гарантира, че всички възможни грешки са взети предвид на етапа на компилиране. За разлика от това, изключенията не се проверяват от компилатора в Swift (само throws се декларира, но не и типът на грешката).
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 с изчерпателно покритие — компилаторът проверява дали всички варианти са обработени. Изчерпателен switch — ключовото предимство на Result пред изключенията: компилаторът гарантира, че няма да забравите да обработите .badURL, .requestFailed или .decodingFailed. В случая с изключенията, компилаторът не изисква обработка и нехванат catch(.badURL) лесно може да бъде пропуснат при преглед.
В Kotlin Result<T> е вграден тип от стандартната библиотека, представляващ успех (T) или грешка (Throwable). За разлика от Swift, в Kotlin Result не позволява указване на конкретен тип грешка — само Throwable. Това е направено за простота, но изисква допълнителна проверка на типа грешка чрез израз when. Kotlin Result поддържа функциите fold (обработка на двата случая), getOrNull (успех или null), getOrDefault (успех или стойност по подразбиране), map, recover, andThen. Особеност на Kotlin: Result не е предназначен за директно разпространение през границите на функции — не може да се използва като връщан тип за методи на Android SDK или Kotlin Coroutines API без допълнителна адаптация.
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.
От 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<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 е де факто стандарт.
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 без нито едно вложено if или try-catch. map трансформира успешната стойност: Result.success(x) -> Result.success(f(x)). flatMap (също наричан bind или andThen) — за случаи, когато самата трансформация връща Result: Result.success(x) -> f(x) -> Result<Y>. Ако на която и да е стъпка възникне Failure, следващите операции не се изпълняват — веригата се прекъсва.
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 „Гост". Ако validateToken върне Left (InvalidToken), веригата се прекъсва и се връща „Гост". Ако fetchProfile върне Left (UserNotFound) — също „Гост". Ако и двете операции са успешни — името на профила. flatMap позволява комбиниране на Either функции, всяка от които може да се провали, в една линейна верига. В традиционния стил на изключения, същият код би изисквал два вложени try-catch или проверки за null. getOrElse в края — изходна точка от композицията, която предоставя стойност по подразбиране за случая Failure.
Result и изключенията — не са взаимно изключващи се подходи. Всяко има своето приложно поле и в добре проектирано мобилно приложение се използват и двете. Изборът зависи от това дали грешката е очаквана (expected) или неочаквана (unexpected). Result — за очаквани грешки, които са част от бизнес логиката: невалиден имейл, недостатъчно средства по сметката, надвишен лимит на заявки. Изключения — за неочаквани системни грешки: загуба на мрежа, OutOfMemoryError, NullPointerException (които не трябва да се случват, но се случват).
| Критерий | Result Type | Изключения (Exception/Error) |
|---|---|---|
| Тип грешка | Очаквани (бизнес логика) | Неочаквани (системни) |
| Производителност | Ниска цена (без развиване на стека) | Висока цена (развиване на стека, улавяне на StackTrace) |
| Композиция | Чрез map/flatMap — линейни вериги | Вложени try-catch — трудно за четене |
| Компилатор | Изчерпателна проверка (switch/when) | Само checked exceptions в Java |
| Поток на изпълнение | Не се прекъсва — грешката като стойност | Прекъсва се до най-близкия catch |
| Тестване | Лесно: проверка на резултата, assert isSuccess/isError | Изисква assertThrows и mock обекти |
| Кога да използваме | Бизнес валидация, вериги от заявки, формуляри | Загуба на мрежа, I/O грешки, системни сривове |
Практическо правило: ако грешката е част от нормалния работен поток на приложението (потребителят е въвел невалиден имейл, няма достатъчно права за достъп) — използвайте Result. Ако грешката е изключителна ситуация (сървърът не отговаря, паметта е свършила) — използвайте изключения. В мобилната разработка Result на границите на слоевете (UseCase -> ViewModel) и изключенията вътре в слоевете (API -> Repository) — често срещан модел, който комбинира предимствата на двата подхода.
Миграцията на съществуващия код, базиран на изключения, към Result трябва да бъде постепенна. Започнете от границите на слоевете: обвийте извикванията на throws функции в Result { try ... } (Swift) или runCatching { ... } (Kotlin). След това заменете връщания тип на методите Repository и UseCase с Result/Either, оставяйки вътрешната имплементация на изключения. На последния етап мигрирайте ViewModel: вместо UiState с изключения използвайте sealed class UiState<T> (Loading, Success, Error), където Error съхранява домейн грешката, а не Throwable. Постепенната миграция позволява тестване на всеки слой поотделно без глобален рефакторинг.
Често задавани въпроси
Optional (T?) представлява наличие или липса на стойност — nil означава „няма данни", но не казва защо. Result (Success/Failure) съдържа не само успеха, но и причината за грешката с конкретен тип. Използвайте Optional, когато липсата на стойност е нормална (напр. опционално поле на профил), Result — когато имате нужда от информация за грешката.
В Swift използвайте Result { try throwingFunc() } — конструкторът на Result приема throws затваряне. В Kotlin — runCatching { throwingFunc() }, който връща Result<T>. В Dart — Result<T>.tryCatch(() => throwingFunc()). Това позволява лесна интеграция на код, базиран на изключения, в Result вериги.
Използвайте mapError (Swift) или mapLeft (Either в Dart/Kotlin) за трансформация на типа грешка без промяна на успешната стойност. Ако трябва да обработите и двата случая и да върнете единична стойност — използвайте fold. За логване без прекъсване на веригата използвайте onFailure (Kotlin) или префиксна инспекционна точка.
Да, но внимателно. Kotlin Result<T> не се препоръчва като директен връщан тип на suspend функции поради особености на K2 компилатора и рефлексията. Използвайте собствена sealed class NetworkResult<T> (Success, Error, Loading) за представяне на състояния в корутини. За очаквани грешки в бизнес логиката Either от Arrow е по-мощна алтернатива.
fold — метод, който приема два callback: onSuccess (за успешния случай) и onFailure (за случая на грешка), и връща единична стойност от произволен тип. Това е еквивалентът на switch израз, но под формата на функция от по-висок ред. fold — основната изходна точка от Result вериги, където преобразувате Success/Failure в UiState, низ за потребителя или друг Result.
Обобщение
Ще разработим мобилно приложение под ключ
IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.
Прочетете също