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> المدمج. توفر كل implementación دوال للعمل مع الحاوية: 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. Result في Swift هو نوع وظيفي كامل مع دوال map و flatMap و mapError و get. get هي دالة خاصة: تعيد قيمة Success إذا كانت النتيجة ناجحة، وتلقي الخطأ إذا كانت Failure. هذا يسمح لـ Result بالعمل كجسر بين الأنماط الوظيفية والقائمة على الاستثناءات: المعالجة عبر map/flatMap في سلسلة، وفي النهاية استخدام get مع do-catch للتكامل مع كود throws.
Result في Swift هو enum مع معاملات عامة، مما يسمح للمترجم بالتحقق من المعالجة الشاملة عبر switch أو do-catch. إذا أضفت حالة جديدة إلى enum NetworkError، سينتج المترجم خطأ في جميع تعبيرات switch حيث لم تتم معالجة تلك الحالة. التحقق الشامل هو الميزة الرئيسية لـ 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، لا يسمح Result في Kotlin بتحديد نوع خطأ ملموس — فقط Throwable. يتم ذلك للبساطة ولكنه يتطلب تحققاً إضافياً من نوع الخطأ عبر تعبير when. يدعم Result في Kotlin دوال fold (معالجة كلتا الحالتين)، getOrNull (نجاح أو null)، getOrDefault (نجاح أو قيمة افتراضية)، map، recover، andThen. خصوصية Kotlin: Result غير مصمم للانتشار المباشر عبر حدود الدوال — لا يمكن استخدامه كنوع إرجاع لدوال Android SDK أو واجهة برمجة Kotlin Coroutines دون تكييف إضافي.
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. مهم: Result في Kotlin غير مصمم للتخزين في حقول data class أو النقل المباشر عبر حدود دوال suspend — استخدم classes المختومة المخصصة (Success/Error/Loading) لتمثيل حالات واجهة المستخدم في Jetpack Compose أو MVVM.
منذ Dart 3.0، تتضمن المكتبة القياسية Result<T> مدمج — class مختوم بمنشئين: T.ok() (نجاح) و Error.error() (خطأ مع Object و StackTrace). قبل Dart 3.0، كان مطورو Flutter يستخدمون Either<L, R> من حزمة dartz أو classes المختومة المخصصة. Result المدمج في Dart بسيط: لا يوفر map/flatMap مباشرة — يجب تنفيذ هذه الدوال عبر when أو باستخدام دوال التمديد. للمعالجة الوظيفية الجادة، يظل 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 هو class مختوم بأنواع خطأ ملموسة (serverError، networkError). fold يعالج كلتا الحالتين: الاستدعاء الأول لـ 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 والاستثناءات ليسا نهجين متنافيين. لكل منهما مجاله الخاص، وفي تطبيق محمول جيد التصميم، يتم استخدام كليهما. يعتمد الاختيار على ما إذا كان الخطأ متوقعاً أم غير متوقع. Result للأخطاء المتوقعة التي هي جزء من منطق العمل: بريد إلكتروني غير صالح، أموال غير كافية، تجاوز حد الطلبات. الاستثناءات للأخطاء النظامية غير المتوقعة: فقدان الشبكة، OutOfMemoryError، NullPointerException (التي لا ينبغي أن تحدث ولكنها تحدث).
| المعيار | Result Type | الاستثناءات (Exception/Error) |
|---|---|---|
| نوع الخطأ | متوقعة (منطق العمل) | غير متوقعة (نظامية) |
| الأداء | تكلفة منخفضة (بدون فك تكدس) | تكلفة عالية (فك تكدس، التقاط StackTrace) |
| التركيب | عبر map/flatMap — سلاسل خطية | try-catch متداخلة — صعبة القراءة |
| المترجم | تحقق شامل (switch/when) | فقط الاستثناءات المفحوصة في Java |
| تدفق التنفيذ | لا ينقطع — الخطأ كقيمة | ينقطع حتى أقرب catch |
| الاختبار | سهل: التحقق من النتيجة، تأكيد isSuccess/isError | يتطلب assertThrows وكائنات mock |
| متى تستخدم | التحقق من العمل، سلاسل الطلبات، النماذج | فقدان الشبكة، أخطاء الإدخال/الإخراج، أعطال النظام |
قاعدة عملية: إذا كان الخطأ جزءاً من سير العمل الطبيعي للتطبيق (أدخل المستخدم بريداً إلكترونياً غير صالح، صلاحيات غير كافية) — استخدم Result. إذا كان الخطأ حالة استثنائية (الخادم لا يستجيب، نفاد الذاكرة) — استخدم الاستثناءات. في تطوير التطبيقات المحمولة، Result على حدود الطبقات (UseCase -> ViewModel) والاستثناءات داخل الطبقات (API -> Repository) هو نمط شائع يجمع مزايا كلا النهجين.
يجب أن يكون ترحيل الكود الحالي القائم على الاستثناءات إلى Result تدريجياً. ابدأ بـ حدود الطبقات: غلف استدعاءات دوال throws في Result { try ... } (Swift) أو runCatching { ... } (Kotlin). ثم استبدل نوع الإرجاع لدوال Repository و UseCase بـ Result/Either، مع إبقاء التنفيذ الداخلي على الاستثناءات. في الخطوة الأخيرة، قم بترحيل ViewModel: بدلاً من UiState مع الاستثناءات، استخدم class مختوم UiState<T> (Loading، Success، Error)، حيث يخزن Error خطأ المجال، وليس Throwable. الترحيل التدريجي يسمح باختبار كل طبقة على حدة دون إعادة هيكلة شاملة.
الأسئلة الشائعة
Optional (T?) يمثل وجود أو غياب قيمة — nil يعني «لا توجد بيانات» لكنه لا يشرح لماذا. Result (Success/Failure) يحتوي ليس فقط على النجاح ولكن أيضاً على سبب الخطأ بنوع ملموس. استخدم Optional عندما يكون الغياب طبيعياً (مثلاً، حقل ملف شخصي اختياري)، و Result عندما تحتاج إلى معلومات الخطأ.
في Swift، استخدم Result { try throwingFunc() } — منشئ Result يأخذ إغلاقاً يرمي. في Kotlin، استخدم runCatching { throwingFunc() }، الذي يعيد Result<T>. في Dart، استخدم Result<T>.tryCatch(() => throwingFunc()). هذا يسمح بدمج سهل للكود القائم على الاستثناءات في سلاسل Result.
استخدم mapError (Swift) أو mapLeft (Either في Dart/Kotlin) لتحويل نوع الخطأ دون تغيير قيمة النجاح. إذا كنت بحاجة لمعالجة كلتا الحالتين وإرجاع قيمة واحدة، استخدم fold. للتسجيل دون مقاطعة السلسلة، استخدم onFailure (Kotlin) أو نقطة فحص بادئة.
نعم، ولكن بحذر. لا يُوصى باستخدام Result<T> من Kotlin كنوع إرجاع لدوال suspend مباشرة بسبب خصائص مترجم K2 والانعكاس. استخدم class المختوم الخاص بك NetworkResult<T> (Success، Error، Loading) لتمثيل الحالات في coroutines. للأخطاء المتوقعة في منطق العمل، Either من Arrow هو بديل أكثر قوة.
fold هو دالة تأخذ استدعاءين: onSuccess (لحالة النجاح) و onFailure (لحالة الخطأ)، وتعيد قيمة واحدة من أي نوع. إنه مكافئ لتعبير switch لكن كدالة ذات ترتيب أعلى. fold هو نقطة الخروج الرئيسية من سلاسل Result، حيث تحول Success/Failure إلى UiState أو سلسلة للمستخدم أو Result آخر.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا