CodeGym /Kurslar /Swift SELF /Error: enum vasitəsilə oz xətalarımız, kontekst əlavə edi...

Error: enum vasitəsilə oz xətalarımız, kontekst əlavə edirik

Swift SELF
Səviyyə, Dərs
Mövcuddur

1. Niyə xətaları tipə salmaq lazımdır

Proqram böyüdükcə siz demək olar ki, qaçılmaz olaraq bu vəziyyətə gəlib çıxırsınız: “alınmadı” — bu, çox az məlumat deməkdir. Eyni əməliyyat müxtəlif səbəblərə görə yerinə yetirilməyə bilər və səbəbdən asılı olaraq bundan sonra nə etmək lazım olur: istifadəçidən yenidən daxil etməyi xahiş etmək, formatı izah etmək, ssenarini dayandırmaq, yoxsa ehtiyat yolu seçmək. Buna görə də bizə imtina səbəblərini Int və String tiplərini fərqləndirdiyimiz qədər dəqiq fərqləndirməyə imkan verən alət lazımdır.

Təsəvvür edin ki, siz MiniLibrary adlı kiçik tətbiq yazırsınız (yaddaşda saxlanan şərti kitabxana): istifadəçi kitabın adını və ilini daxil edir. Xətalar çox ola bilər: boş ad, ilin rəqəm olmaması, ilin diapazondan kənarda olması, sətirdə artıq simvolların olması. Əgər siz hər şeyi false-a və ya "error" sətrinə çevirsəniz, kod sonradan qəhvə falına çevrilir.

Swift-də “bəlkə alınmadı” fikrini ifadə etməyin bir neçə yolu var. Optional onlardan biridir və səbəb vacib olmadıqda əladır. Amma səbəb vacib olanda (çox vaxt normal UX üçün vacib olur), biz xətalara keçirik: yəni dəqiq nə baş verdiyini təsvir edən dəyərlərə.

Error — bu protokoldur: bu sadə dillə nə deməkdir

“Protokol” sözü sizə artıq “müqavilə” kimi tanışdır: tipin uyğun gəlməli olduğu tələblər toplusu. Error kompilyatora belə deyən protokoldur: “bu tipin dəyərlərini xəta kimi istifadə etmək olar”. Yəni onları “atma”q (throw) və tutmaq (catch) olar — amma atma/tutma mexanizmini növbəti mühazirələrdə tam şəkildə nəzərdən keçirəcəyik. Burada bizim məqsədimiz xətaları modellər kimi layihələndirməyi öyrənməkdir.

Vacibdir: Error klass deyil və sehr deyil, bu, sadəcə “bayraq”dır: tip “xəta kimi uyğundur”. Buna görə çox vaxt sadəcə : Error yazmaq kifayətdir.


import Foundation

enum InputError: Error {
    case empty
    case tooShort
}

Bu, demək olar ki, həddən artıq sadə görünür — və gözəlliyi də bundadır: Swift sizi “ThrowableExceptionOfDoom”dan miras almağa məcbur etmir. Sadəcə səbəblər toplusunu təsvir etməlisiniz.

Niyə enum xətalar üçün demək olar ki, idealdır

Xətaları enum ilə modelləşdirmək rahatdır, çünki adətən xətanın sonlu səbəblər dəsti olur. Boş giriş, səhv format, diapazondan kənar dəyər — bunlar yaxşı məhdudlaşdırılan hallardır. enum isə məhz bunu deyir: “belə variantlar mümkündür və yalnız bunlar”.

İkinci səbəb — switch. Siz artıq görmüsünüz ki, switch sizi dürüst olmağa məcbur edir: ya bütün case-ləri sadalayın, ya da default əlavə edin. Xətalar üçün bu qızıldır, çünki xəta emalı — “bir halı unutduq” probleminin “istifadəçi nə baş verdiyini anlamır” səviyyəli səhvə çevrildiyi yerdir.

Üçüncü səbəb — oxunaqlılıqdır. Müqayisə edin:

// Pis: sətir — müqavilə deyil
let errorMessage = "invalid_year"

və

// Yaxşı: tipə salınmış səbəb
let error: BookInputError = .invalidYearText(input: "19O2")

İkinci versiya sadəcə daha gözəl deyil. O, sizə struktur verir: kodda nə baş verdiyi görünür və kompilyator bunu emal etməyə kömək edir.

Yeri gəlmişkən, real Swift kodunda və böyük sistemlərdə (infrastruktur həlləri də daxil olmaqla) xətalar çox vaxt case-in içində birbaşa kontekst daşıyır, məsələn “belə ID-li naməlum alıcı”. Bu, normal və çox faydalı praktikadır.

2. Xəta dizaynı: tip və case adları

Xətalar kodunuzun API-sinin bir hissəsidir (hətta “API” hələlik yalnız özünüz üçün olsa belə). Buna görə adlar vacibdir. Yaxşı xəbər: qaydalar kifayət qədər sadədir və adi enum adlandırmasına oxşayır.

Adətən xəta tipinə Error suffiksi verilir: InputError, ParseError, BookError, LibraryError. Case-lər isə konkret səbəblərdir: .empty, .invalidFormat, .outOfRange. Səbəb nə qədər konkret olsa, sonra emal yazmaq bir o qədər asan olur.

Budur “adətən necə edirlər” adlı kiçik cədvəl:

Kontekst Tip adı Case nümunələri
İstifadəçi girişi
InputError
empty, tooShort(min:)
Parse etmə
ParseError
notANumber(input:), extraTokens
Domain qaydaları
BookError
emptyTitle, invalidYear(value:)

Diqqət edin: biz tək bir universal AppError.failed yaratmırıq. Bu, sanki “müxtəlif” adlı bir qutu düzəltmək və sonra oradan pasport, batareya və həyatın mənasını tapmağa çalışmaq kimidir.

import Foundation

enum BookInputError: Error {
    case emptyTitle
    case invalidYearText(input: String)
}

Burada artıq görünür: xəta “kitablar” haqqındadır, “hər şey” haqqında deyil və onun iki aydın səbəbi var.

3. Associated values: “data torbası” olmadan kontekst əlavə edirik

Xətanın səbəbinin özü yaxşıdır. Amma bəzən sizə daha konkret “niyə” izahı da lazımdır: hansı dəyər gəlib, hansı sərhədlər gözlənilirdi, sətirdə dəqiq nə səhv idi. Bunun üçün enum-da associated values var — siz onları artıq enum-komandalar və vəziyyətlərdən tanıyırsınız.

Əsas prinsip budur: kontekst “hər ehtimala qarşı” deyil, emal üçün (loqikanı budaqlamaq üçün) və ya istifadəçiyə/loqlamağa normal mesaj vermək üçün lazım olduğu üçün əlavə edilir.

Nümunə: “diapazondan kənar il” kontekstsiz kədərlidir. “3021 ili 1450...2026 diapazonundan kənardadır” — artıq faydalıdır.

import Foundation

enum RangeError: Error {
    case outOfRange(value: Int, min: Int, max: Int)
}

İndi isə mini-tətbiqimizə bir az da yaxınlaşaq:

import Foundation

enum BookInputError: Error {
    case emptyTitle
    case invalidYearText(input: String)
    case yearOutOfRange(value: Int, min: Int, max: Int)
}

Vacib bir detala diqqət edin: biz ora hər şeyi doldurmuruq (məsələn, “hazırkı istifadəçi”, “vaxt”, “OS adı”). Bu, sadəcə səs-küy olar. Xəta səbəbin yığcam modeli olaraq qalmalıdır.

4. Xəta adi dəyərdir

Swift-də ən “aydınlaşdırıcı” məqamlardan biri: xəta mütləq proqramı sındıran bir şey deyil. Data model səviyyəsində xəta istənilən enum kimi işlənən dəyərdir.

Bu, hətta throws/try-dən əvvəl də faydalıdır: siz “ya xəta, ya da nil qaytar” üslubunda funksiyalar qura bilərsiniz ki, səbəbləri ayırd etməyi və normal mesajlar yazmağı məşq edəsiniz.

Gəlin xətanı insan üçün başa düşülən mətnə çevirən kiçik utilit hazırlayaq. Hələlik heç bir gələcək mexanika istifadə etmirik, sadəcə enum ilə işləyirik.

import Foundation

enum BookInputError: Error {
    case emptyTitle
    case invalidYearText(input: String)
    case yearOutOfRange(value: Int, min: Int, max: Int)
}

func message(for error: BookInputError) -> String {
    switch error {
    case .emptyTitle:
        return "Kitabın adı boş ola bilməz."
    case .invalidYearText(let input):
        return "Il '\(input)' rəqəmə oxşamır."
    case .yearOutOfRange(let value, let min, let max):
        return "Il \(value) \(min)...\(max) diapazonunda olmalıdır."
    }
}

İndi isə bunun həqiqətən sadəcə dəyər olduğunu “əllə yoxlayaq”:

import Foundation

let e: BookInputError = .yearOutOfRange(value: 3021, min: 1450, max: 2026)
print(message(for: e)) // Il 3021 1450...2026 diapazonunda olmalıdır.

Bu, xırda şey kimi görünə bilər, amma praktikada bu, normal arxitekturanın əsasıdır: xətalar konsoldakı “səs-küy” deyil, tipə salınmış data olur.

5. Nümunə: MiniLibrary və throws olmadan giriş xətaları

İndi istifadəçinin bir kitab əlavə etdiyi kiçik tətbiq hissəsi yığacağıq. Saxlama, şəbəkə, fayllar və sairəni mürəkkəbləşdirməyəcəyik: bu gün əsas məsələ məhz xəta modeli və kontekst əlavə etmə bacarığıdır. Buna görə sadə BookDraft strukturunu və BookInputError? qaytaran doğrulama funksiyalarını edəcəyik.

Niyə “həqiqi xətalar” yox, BookInputError?? Çünki bu gün biz xəta tiplərinin dizaynına fokuslanırıq. throws/try/do-catch mexanikası ayrıca mövzudur və hər şeyi bir qazana qatmaq yaxşı deyil (hətta plov çox dadlı olsa belə).

import Foundation

struct BookDraft {
    let title: String
    let year: Int
}

İndi başlığın doğrulama funksiyası. Diqqət edin: o, ya nil qaytarır (hər şey yaxşıdır), ya da konkret xəta.

import Foundation

func validateTitle(_ title: String) -> BookInputError? {
    if title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return .emptyTitle
    }
    return nil
}

İl üçün funksiya. Burada iki müxtəlif səbəb var: “rəqəm deyil” və “diapazondan kənardır”.

import Foundation

func validateYearText(_ text: String) -> BookInputError? {
    guard let value = Int(text) else {
        return .invalidYearText(input: text)
    }

    let min = 1450
    let max = 2026
    guard (min...max).contains(value) else {
        return .yearOutOfRange(value: value, min: min, max: max)
    }

    return nil
}

İndi “kitab yaratmaq cəhdi”ni yığaq — amma hələ mürəkkəb arxitekturasız. Sadəcə hər iki sahəni yoxlayacağıq və ya BookDraft qaytaracağıq, ya da xəta. Yeni mövzular açmamaq üçün tuple istifadə edirik ((BookDraft?, BookInputError?)) — bəli, ideal deyil, amma indi bizim üçün əsas məsələ xətaların kodda necə yaşadığını görməkdir.

import Foundation

func makeBookDraft(title: String, yearText: String) -> (BookDraft?, BookInputError?) {
    if let error = validateTitle(title) {
        return (nil, error)
    }
    if let error = validateYearText(yearText) {
        return (nil, error)
    }

    let year = Int(yearText) ?? 0
    return (BookDraft(title: title, year: year), nil)
}

Və nəhayət — interaktivsiz kiçik ssenari “bu necə görünür”:

import Foundation

let (book, error) = makeBookDraft(title: "  ", yearText: "2020")

if let error {
    print(message(for: error)) // Kitabın adı boş ola bilməz.
} else if let book {
    print("Kitab əlavə edəcəyik: \(book.title), \(book.year)") // (icra olunmayacaq)
}

Bu nümunənin mənası “ideal API uydurduq” demək deyil. Burada görməli olduğunuz şey budur: xətalar həqiqətən data modeli kimi layihələndirilir və associated values bizə sətir xaosu olmadan kontekst verir.

6. Xətaya nə qədər kontekst əlavə etməli

İlk vaxtlar ya ümumiyyətlə kontekst əlavə etməmək istəyirsən (“onsuz da aydındır”), ya da həddindən artıq çox əlavə etmək (“bəlkə lazım olar”). Hər iki yanaşma kodu tez bir zamanda narahat edir. Yaxşı çərçivə belədir: xəta içindəki kontekst ən azı iki sualdan birinə cavab verməlidir.

Birinci sual — “necə emal etmək?” Məsələn, əgər sizdə .outOfRange(value:min:max) varsa, value-dan asılı olaraq istifadəçiyə ən yaxın icazə verilən dəyəri təklif edə və ya düzgün formatı izah edə bilərsiniz.

İkinci sual — “necə izah etmək?” Məsələn, .invalidYearText(input:) istifadəçiyə onun daxil etdiyi dəyərin özünü göstərməyə imkan verir, abstrakt “səhv giriş” yox.

Əgər kontekst nə emala, nə də izaha kömək etmirsə, o, demək olar ki, həmişə zibilə çevrilir. Və bu təhlükəlidir: xətalar böyüməyə başlayır, onları gözlə müqayisə etmək çətinləşir, case-lərlə örtmək çətinləşir və dəstəkləmək çətinləşir.

Yeri gəlmişkən, real kodda xətalar üçün associated values nadirlik deyil, normadır. Məsələn, “belə identifikatorlu naməlum alıcı” — kontekstin çox vaxt xarici loq deyil, səbəbin bir hissəsi olduğunu göstərən klassik haldır.

7. enum Error dizaynında tipik səhvlər

Səhv №1: bütün hallar üçün bir .failed case-i.
Əvvəlcə bu rahat görünür: az yazırsan. Amma sonra qəfildən anlayırsan ki, switch səbəbləri ayıra bilmir, deməli normal reaksiya verə bilmirsən. Axırda hər şey sətir mesajlarına və “təxmin oyunu”na çevrilir, yəni tipə salınmış xətanın mənasını itirirsən.

Səhv №2: “səbəblər” case-lərdə deyil, sətirin içində gizlədilir.
Bəzən .invalid(message: String) kimi bir şey yazıb ora "emptyTitle" və ya "yearOutOfRange" qoyurlar. Bu, demək olar ki, həmişə geriyə addımdır: siz kompilyatorun sizə kömək etmək imkanını əlindən alırsınız, budaqlanma yenə də sətirlərin müqayisəsinə çevrilir, burada isə yazı səhvi oyunda yeni “gizli boss” növünə çevrilir.

Səhv №3: kontekst “bəlkə lazım olar” deyə əlavə olunur və tipi şişirdir.
Əgər xətaya hər şeyi doldursanız, o, oxumaq və saxlamaq üçün ağır olur. Bundan əlavə, siz proqramın müxtəlif səviyyələrindən detalları aid olmadığı yerə daşımağa başlayırsınız. Kontekst minimal və faydalı olmalıdır: giriş sətri, faktiki dəyər, gözlənilən sərhədlər.

Səhv №4: xətalar həddən artıq ümumi olur və artıq “öz sahəsi” haqqında danışmır.
AppError-in bütün tətbiq üçün olması cəlbedici görünür, amma demək olar ki, həmişə tezdir. Xətaları mənaya görə qruplaşdıranda yaşamaq daha asan olur: giriş xətaları ayrı, domen qaydaları xətaları ayrı. Onda emal da normal mətn kimi oxunur: “əgər giriş xətasıdırsa — formatı izah et; əgər qayda xətasıdırsa — məhdudiyyəti açıqlay”.

Səhv №5: “istifadəçi üçün xəta” və “tərtibatçı üçün xəta”nı birbaşa modeldə qarışdırmaq.
Əgər siz birbaşa enum içində UI üçün hazır böyük mətnlər saxlamağa çalışırsınızsa (“istifadəçiyə bunu göstər”), siz domen məntiqini təqdimata vaxtından əvvəl bağlayırsınız. İlk addımlarda yuxarıda etdiyimiz kimi switch vasitəsilə sadə mesajlar qaytarmaq olar, amma fikirdə bunu saxlayın: xəta — səbəbdir, gözəl mətn isə artıq həmin səbəbin təqdimatıdır.

1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Şifrə xətaları
Şifrə xətaları
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Boşluqsuz nik
Boşluqsuz nik
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Sifariş hesabı
Sifariş hesabı
1
Tapşırıq
Swift SELF, səviyyə, dərs
Bağlanıb
Robot əmri
Robot əmri
Şərhlər
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION