Result Type — ชนิดคอนเทนเนอร์ที่แสดงถึงผลลัพธ์ของการดำเนินการที่สามารถเสร็จสมบูรณ์ด้วยความสำเร็จ (Success) หรือข้อผิดพลาด (Failure) ต่างจาก exceptions ตรงที่ Result ส่งผ่านข้อผิดพลาดเป็นค่าปกติโดยไม่ต้องคลายสแต็ก ทำให้การจัดการข้อผิดพลาดที่คาดไว้ปลอดภัยและประกอบได้มากขึ้น ตาม Apple Swift Documentation (2026), Result<Success, Failure> ใน Swift ช่วยให้สามารถเชื่อมโยงการดำเนินการกับการจัดการข้อผิดพลาดอัตโนมัติผ่าน map และ flatMap โดยไม่ขัดจังหวะการทำงานของโปรแกรม
ประเด็นสำคัญ
Result Type เป็นชนิดทั่วไปที่ห่อหุ้มผลลัพธ์ของการดำเนินการที่อาจสำเร็จหรือล้มเหลว ต่างจาก exceptions ที่ข้อผิดพลาดขัดจังหวะการไหลปกติและต้องคลายสแต็กเพื่อค้นหาบล็อก catch Result ส่งผ่านข้อผิดพลาดเป็นค่าปกติ — ผู้เรียกจะได้รับอ็อบเจ็กต์เสมอและตัดสินใจว่าจะจัดการอย่างไร ซึ่งมีประโยชน์โดยเฉพาะสำหรับข้อผิดพลาดที่คาดไว้: อินพุตไม่ถูกต้อง กฎทางธุรกิจ การปฏิเสธจากเซิร์ฟเวอร์ ซึ่ง exceptions จะเป็นกลไกที่หนักเกินไป
แนวคิดของ Result มาจากการเขียนโปรแกรมเชิงฟังก์ชัน ซึ่งชนิดที่คล้ายกันเรียกว่า Either (หรือ Left/Right) ใน Swift Result กลายเป็นชนิดมาตรฐานใน Swift 5.0; ใน Kotlin Result<T> ปรากฏในไลบรารีมาตรฐาน; ใน Dart 3.0 มีการแนะนำ Result<T> ในตัว แต่ละการimplement จัดเตรียมเมธอดสำหรับทำงานกับคอนเทนเนอร์: 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 ทำหน้าที่เป็นสะพานเชื่อมระหว่างสไตล์เชิงฟังก์ชันและแบบ exceptions: จัดการผ่าน map/flatMap ในห่วงโซ่ และในตอนท้ายใช้ get กับ do-catch เพื่อรวมกับโค้ด throws
Result ใน Swift เป็น enum ที่มีพารามิเตอร์ทั่วไป ซึ่งช่วยให้คอมไพเลอร์บังคับการจัดการที่ครอบคลุมผ่าน switch หรือ do-catch หากคุณเพิ่มเคสใหม่ใน enum NetworkError คอมไพเลอร์จะสร้างข้อผิดพลาดในนิพจน์ switch ทั้งหมดที่ไม่ได้จัดการเคสนั้น การตรวจสอบที่ครอบคลุม เป็นข้อได้เปรียบหลักของ Result เหนือ exceptions: คอมไพเลอร์รับประกันว่าข้อผิดพลาดที่เป็นไปได้ทั้งหมดได้รับการพิจารณาในเวลาสร้าง ในทางตรงกันข้าม exceptions ไม่ได้รับการตรวจสอบโดยคอมไพเลอร์ใน 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 เหนือ exceptions: คอมไพเลอร์รับประกันว่าคุณจะไม่ลืมจัดการ .badURL, .requestFailed หรือ .decodingFailed สำหรับ exceptions คอมไพเลอร์ไม่ต้องการการจัดการ และ 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 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 เป็น wrapper ที่สะดวกใน Kotlin ซึ่งดักจับ exceptions ใด ๆ และคืนค่า Result.failure validateEmail คืนค่า Result.success หรือ Result.failure ขึ้นอยู่กับการตรวจสอบ fold จัดการทั้งสองกรณีอย่างกะทัดรัด: เมื่อ Success ฟังก์ชันตรวจสอบถัดไปจะถูกเรียก; เมื่อ Failure จะคืนค่า SignupResult.Error สำคัญ: Result ใน Kotlin ไม่ได้ออกแบบมาสำหรับการจัดเก็บในฟิลด์ 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 โดยตรง — ฟังก์ชันเหล่านี้ต้องimplement ผ่าน when หรือใช้เมธอดส่วนขยาย สำหรับการจัดการเชิงฟังก์ชันที่จริงจัง Either จาก fpdart ยังคงเป็นโซลูชันที่ทรงพลังกว่าพร้อมรองรับ map, flatMap, mapLeft, fold, andThen และตัวดำเนินการ bind
Either<L, R> เป็นชนิดเอนเอียงซ้ายจากแพ็คเกจ fpdart โดยที่ Left คือข้อผิดพลาด และ Right คือความสำเร็จ ต่างจาก Result<T> ในตัว Either กำหนดชนิดข้อผิดพลาดในระดับพารามิเตอร์ชนิด (L) ซึ่งช่วยให้แยกแยะชนิดข้อผิดพลาดในเวลาคอมไพล์ แพ็คเกจ fpdart จัดเตรียมชุด combinators เชิงฟังก์ชันที่สมบูรณ์: 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 จัดการทั้งสองกรณี: คอลแบ็กแรกสำหรับ Left (ข้อผิดพลาด), คอลแบ็กที่สองสำหรับ Right (ความสำเร็จ) ใน Dart 3.0 Result ในตัวก็รองรับ fold เช่นกัน แต่ไม่ได้ให้ map/flatMap สำหรับห่วงโซ่ Either จาก fpdart ให้ map, flatMap (bind), mapLeft, andThen — ชุด combinators เชิงฟังก์ชันที่สมบูรณ์สำหรับการประกอบข้อผิดพลาด
ข้อได้เปรียบหลักของ Result เหนือ exceptions คือการประกอบ หากคุณมีการดำเนินการหลายอย่าง แต่ละอย่างอาจล้มเหลว คุณสามารถเชื่อมโยง它们โดยใช้ 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 ซึ่งแต่ละอย่างอาจล้มเหลว เข้าเป็นห่วงโซ่เชิงเส้นเดียว ในสไตล์ exceptions แบบดั้งเดิม โค้ดเดียวกันจะต้องใช้ try-catch ที่ซ้อนกันสองอันหรือการตรวจสอบ null getOrElse ในตอนท้ายเป็นจุดออกจากการประกอบ ซึ่งให้ค่าเริ่มต้นสำหรับกรณี Failure
Result และ exceptions ไม่ใช่แนวทางที่แยกจากกันโดยสิ้นเชิง แต่ละอย่างมีโดเมนของตัวเอง และในแอปพลิเคชันมือถือที่ออกแบบมาอย่างดี ทั้งสองอย่างถูกใช้ การเลือกขึ้นอยู่กับว่าข้อผิดพลาดนั้นคาดไว้หรือไม่คาดคิด Result สำหรับข้อผิดพลาดที่คาดไว้ซึ่งเป็นส่วนหนึ่งของตรรกะทางธุรกิจ: อีเมลไม่ถูกต้อง เงินไม่เพียงพอ เกินขีดจำกัดอัตรา Exceptions สำหรับข้อผิดพลาดของระบบที่ไม่คาดคิด: สูญเสียเครือข่าย OutOfMemoryError NullPointerException (ซึ่งไม่ควรเกิดขึ้นแต่เกิดขึ้น)
| เกณฑ์ | Result Type | Exceptions (Exception/Error) |
|---|---|---|
| ชนิดข้อผิดพลาด | คาดไว้ (ตรรกะทางธุรกิจ) | ไม่คาดคิด (ระบบ) |
| ประสิทธิภาพ | ต้นทุนต่ำ (ไม่ต้องคลายสแต็ก) | ต้นทุนสูง (คลายสแต็ก, จับ StackTrace) |
| การประกอบ | ผ่าน map/flatMap — ห่วงโซ่เชิงเส้น | try-catch ที่ซ้อนกัน — อ่านยาก |
| คอมไพเลอร์ | การตรวจสอบที่ครอบคลุม (switch/when) | เฉพาะ checked exceptions ใน Java |
| การไหลของการทำงาน | ไม่ถูกขัดจังหวะ — ข้อผิดพลาดเป็นค่า | ถูกขัดจังหวะจนถึง catch ที่ใกล้ที่สุด |
| การทดสอบ | ง่าย: ตรวจสอบผลลัพธ์ ยืนยัน isSuccess/isError | ต้องใช้ assertThrows และอ็อบเจ็กต์ mock |
| เมื่อใดควรใช้ | การตรวจสอบธุรกิจ ห่วงโซ่คำขอ ฟอร์ม | สูญเสียเครือข่าย ข้อผิดพลาด I/O ระบบล่ม |
กฎปฏิบัติ: หากข้อผิดพลาดเป็นส่วนหนึ่งของเวิร์กโฟลว์ปกติของแอปพลิเคชัน (ผู้ใช้ป้อนอีเมลไม่ถูกต้อง สิทธิ์ไม่เพียงพอ) — ใช้ Result หากข้อผิดพลาดเป็นสถานการณ์พิเศษ (เซิร์ฟเวอร์ไม่ตอบสนอง หน่วยความจำหมด) — ใช้ exceptions ในการพัฒนาแอปมือถือ Result ที่ขอบเขตของเลเยอร์ (UseCase -> ViewModel) และ exceptions ภายในเลเยอร์ (API -> Repository) เป็นรูปแบบทั่วไปที่รวมข้อดีของทั้งสองแนวทาง
การย้ายโค้ดที่มีอยู่บนพื้นฐาน exceptions ไปยัง Result ควรทำแบบค่อยเป็นค่อยไป เริ่มต้นด้วย ขอบเขตของเลเยอร์: ห่อหุ้มการเรียกฟังก์ชัน throws ใน Result { try ... } (Swift) หรือ runCatching { ... } (Kotlin) จากนั้นแทนที่ชนิดคืนค่าของเมธอด Repository และ UseCase ด้วย Result/Either โดยคงการimplement ภายในไว้บน exceptions ในขั้นตอนสุดท้าย ย้าย ViewModel: แทนที่จะใช้ UiState ที่มี exceptions ให้ใช้ sealed class UiState<T> (Loading, Success, Error) โดยที่ Error จัดเก็บข้อผิดพลาดโดเมน ไม่ใช่ Throwable การย้ายแบบค่อยเป็นค่อยไปช่วยให้ทดสอบแต่ละเลเยอร์แยกกันได้โดยไม่ต้องปรับโครงสร้างทั้งระบบ
คำถามที่พบบ่อย
Optional (T?) แทนการมีอยู่หรือไม่มีค่าของค่า — nil หมายถึง “ไม่มีข้อมูล” แต่ไม่ได้อธิบายว่าทำไม Result (Success/Failure) มีไม่เพียงแค่ความสำเร็จ แต่ยังมีสาเหตุของข้อผิดพลาดพร้อมชนิดที่เป็นรูปธรรม ใช้ Optional เมื่อการไม่มีเป็นเรื่องปกติ (เช่น ฟิลด์โปรไฟล์ที่เป็นทางเลือก) และ Result เมื่อคุณต้องการข้อมูลข้อผิดพลาด
ใน Swift ใช้ Result { try throwingFunc() } — คอนสตรัคเตอร์ Result รับ closure ที่ throws ใน Kotlin ใช้ runCatching { throwingFunc() } ซึ่งคืนค่า Result<T> ใน Dart ใช้ Result<T>.tryCatch(() => throwingFunc()) ซึ่งช่วยให้รวมโค้ดที่ใช้ exceptions เข้ากับห่วงโซ่ Result ได้ง่าย
ใช้ mapError (Swift) หรือ mapLeft (Either ใน Dart/Kotlin) เพื่อแปลงชนิดข้อผิดพลาดโดยไม่เปลี่ยนค่าความสำเร็จ หากคุณต้องจัดการทั้งสองกรณีและคืนค่าเดียว ใช้ fold สำหรับการบันทึกโดยไม่ขัดจังหวะห่วงโซ่ ใช้ onFailure (Kotlin) หรือจุดตรวจสอบคำนำหน้า
ได้ แต่ด้วยความระมัดระวัง Kotlin Result<T> ไม่แนะนำให้ใช้เป็นชนิดคืนค่าโดยตรงสำหรับฟังก์ชัน suspend เนื่องจากลักษณะเฉพาะของคอมไพเลอร์ K2 และการสะท้อน ใช้ sealed class ของคุณเอง NetworkResult<T> (Success, Error, Loading) เพื่อแทนสถานะใน coroutines สำหรับข้อผิดพลาดที่คาดไว้ในตรรกะทางธุรกิจ Either จาก Arrow เป็นทางเลือกที่ทรงพลังกว่า
fold เป็นเมธอดที่รับคอลแบ็กสองตัว: onSuccess (สำหรับกรณีสำเร็จ) และ onFailure (สำหรับกรณีข้อผิดพลาด) และคืนค่าเดียวของชนิดใดก็ได้ มันเทียบเท่ากับนิพจน์ switch แต่ในรูปแบบของฟังก์ชันลำดับสูง fold เป็นจุดออกหลักจากห่วงโซ่ Result ที่คุณแปลง Success/Failure เป็น UiState สตริงสำหรับผู้ใช้ หรือ Result อื่น
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม