WWDC 2015’de duyurulan Swift 2’nin getirdiği en kapsamlı özellik yeni bir “error handling” mekanizması. Programda beklenmeyen durumları yakalayıp gerekli şeyleri yaptığımız “error handling”‘i kaba bir şekilde Türkçe’ye “hata yönetimi” veya “hata yakalama” olarak çevirebiliriz. Swift 2 öncesi yazdığınız programın uç durumlarda doğru çalıştığından emin olmak istiyorsanız izleyebileceğiniz iki yol vardı; bunların ilki Apple’ın önerdiği NSError sınıfını kullanmak. Bu yöntem ile şöyle bir kod yazmanız gerekliydi;
var hata: NSError? let dosya = NSFileManager.defaultManager().contentsOfDirectoryAtPath("/Users/Koddit", error: &hata) if let hata = hata { // Hata oluştu } else { // Sıkıntı yok, içeriği kullan. }
Bu yaklaşımdaki en büyük sorun NSError nesnesinin kontrol edilip edilmediğini derleyicinin takip edemiyor olması. Programcı basit bir kontrolü atladığında daha önce testlerde yaşanmayan ve pek beklenmeyen bir durumda uygulamanın patlaması çok olası. Apple Swift 2 ile bunun önüne geçmek için NSError‘ün bu kullanımı kaldırarak daha bilindik try-catch yapısını dile ekliyor. Aynı kodu Swift 2 ile yazarsak;
do { let dosya = try NSFileManager.defaultManager().contentsOfDirectoryAtPath("/Users/Koddit") // Sıkıntı yok, dosyayı kullan. } catch { // Hata oluştu }
Böylece hem kod kısa kısaldı hem de programcı olarak hata yapma olasılığınız neredeyse ortadan kalktı. Ancak ve lakin başta da belirttiğim gibi NSError tek yöntem değil. Çoğu kişi kendi kodunda NSError ile uğraşmak yerine dönen değeri kontrol etmeyi veya daha karışık ve projeye özel yapılar kullanmayı tercih ediyor ki bu herkes için tam bir kaos. Özellikle başkasının kodunu kullandığınız durumlarda yanlış şeyler yapmanız veya bir şeyleri atlamanız çok olası. Pek kullanışlı olmayan ancak basit bir örneğe göz atarsak;
func böl (bölünen: Int, bölen: Int) -> Int? { if (bölen == 0 ) { return nil; } return bölünen / bölen; } if let sonuç = böl (3, bölen: 1 ){ // Sonucu kullan }
Fonksiyon eğer bölen 0 ise nil, değilse bölme işleminin sonucunu döndürüyor. Ancak geriye nil dönmesi kodu ilk kez kullanan kişi açısından pek yararlı değil, eğer dokümantasyon da yoksa niye nil döndüğünü anlaması için kodu okuması şart. Elbette geriye hata mesajı döndürmek vs. gibi olaylara girilebilir ancak bunlar da kodun iyice dağılmasına ve karışık hale gelmesine sebep olabilir. Swift 2 neyse ki bizi bu tarz dertlerden kurtarıyor;
enum BölmeHatası: ErrorType { case BölenSıfır; } func böl (bölünen: Int, bölen: Int) throws -> Int { if (bölen == 0) { throw BölmeHatası.BölenSıfır; } return bölünen / bölen; } do { let sonuç = try böl (3, bölen: 1); // Sonucu kullan } catch BölmeHatası.BölenSıfır { // Bölen sıfır olduğu için hata oluştu. }
ErrorType Swift 2 ile gelen yeni bir protokol, catch ile yakaladığımız hataların bu protokolü desteklemesi gerekiyor. Bölme sırasındaki olabilecek hataları tanımladıktan sonra fonksiyonu “throws” olarak tanımlıyoruz ve hataların oluştuğu noktalarda “throw” kelimesi ile ilgili hataları fırlatıyoruz. Önceki örneğe göre biraz daha fazla kod içerse de bu şekilde yazmak hem daha temiz hem daha değişikliklere açık.
O zaman temel yapıdan bahsettiğimize göre yeni yöntemin yararlarını anlayabilmek için önce daha karışık bir örnekten yola çıkarak koda önce klasik hata düzeltme yöntemlerini eklemeyi deneyelim ve daha sonra Swift 2’ye dönüştürelim;
func suParasınıYatır (miktar miktar: Int) -> Int? { if (!sorguBaşlat()) { return nil; } if let kullanıcıBorcu = borçSorgula() where kullanıcıBorcu <= miktar { if (!işlemiOnayla ()) { return nil; } sorguBitir(); // False dönse de işlem bitti. return miktar - kullanıcıBorcu; // Para üstü } sorguBitir(); // False dönse de işlem zaten iptal. return nil; } func sorguBaşlat () -> Bool { // Bu fonksiyonun database e bağlantı kurmak / session başlatmak //gibi işlemler yaptığını hayal edin. // Bunları yapamadığında geriye false dönüyor. return false; } func sorguBitir () -> Bool { // Bu fonksiyonun database le olan bağlantıyı kapatmak / session'ı //sona erdirmek gibi işlemler yaptığını hayal edin. // Bunları yapamadığında geriye false dönüyor. return false; } func borçSorgula () -> Int? { // Eğer fatura ödenmediyse miktarı döndür. if (sonFaturaÖdenmişMi()) { return nil; } return 150; // Bunun sistemden döndüğünü hayal edin. } func sonFaturaÖdenmişMi () -> Bool { // Sisteme son borcu sor return false; // Bunun sistemden döndüğünü hayal edin. } func işlemiOnayla () -> Bool { // Sisteme parayı aktardığını ve borcu sildiğini hayal edin. // Bunları yapamadığında geriye false dönüyor. return true; } if let sonuç = suParasınıYatır(miktar: 50) { // Para başarıyla yatırıldı. } else { // Para yatırılamadı. }
Bu hayali tahsilat programında kullanıcı su parasını ödemek istediğinde öncelikle suParasınıYatır fonksiyonu çalışıyor ve gerekli bağlantıları oluşturuyor, ardından sisteme kullanıcının borç miktarını soruyor ve işlemi onaylıyor, hepsi sona erince de bağlantıları bitirip para üstünü geriye döndürüyor. Herhangi bir sıkıntı oluştuğu anda ise tüm fonksiyonlar nil döndürüyor ki bu büyük bir problem, kullanıcıya bu durumda neyin yanlış gittiğini aktarmanız veya programcı olarak kullanıcıya çaktırmadan bunları halletmeniz mümkün değil.
Eğer koda Swift 2 öncesindeki gibi bir hata yönetimi yaparsak yaklaşık şöyle bir kod elde ederiz;
var birşeylerTersGitti = false; // Sahte hata oluşturmak için, yok sayabilirsiniz. func suParasınıYatır (miktar miktar: Int) -> (paraÜstü: Int, hataMesajı: String?) { var hata: NSError?; var paraÜstü: Int = 0, hataMesajı: String?; sorguBaşlat(&hata); if (hata != nil) { hataMesajı = hata!.localizedDescription; } else { let borçMiktarı = borçSorgula(&hata); if (hata != nil) { hataMesajı = hata!.localizedDescription; } else if borçMiktarı <= miktar { işlemiOnayla (&hata); if (hata != nil) { hataMesajı = hata!.localizedDescription; } else { paraÜstü = miktar - borçMiktarı; } } else { let eksik = borçMiktarı - miktar; // Türkçe karakter sorunu yüzünden sonraki satırda " " içerisinde değil, orada kullanabilirsiniz. hataMesajı = "\(eksik)" + " TL eksik."; } sorguBitir(&hata); if (hata != nil) { // Yeniden dene? // PS: İşlem zaten sona erdi o yüzden hatayı göndermeye gerek yok. } } return (paraÜstü, hataMesajı); } func sorguBaşlat (error: NSErrorPointer) { // Bu fonksiyonun database e bağlantı kurmak / session başlatmak // gibi işlemler yaptığını hayal edin. if (birşeylerTersGitti) { hataOluştur(error, hataMesajı: "Sorgu başlatılırken zaman aşımı oldu.", kod: 101) } } func sorguBitir (error: NSErrorPointer) { // Bu fonksiyonun database le olan bağlantıyı kapatmak / session'ı //sona erdirmek gibi işlemler yaptığını hayal edin. if (birşeylerTersGitti) { hataOluştur(error, hataMesajı: "Sorgu sonlandırılırken zaman aşımı oldu.", kod: 101) } } func borçSorgula (error: NSErrorPointer) -> Int { // Eğer fatura ödenmediyse miktarı döndür. if (sonFaturaÖdenmişMi()) { hataOluştur(error, hataMesajı: "Ödenmemiş borç bulunamadı.", kod: 102) return 0; } return 150; // Bunun sistemden döndüğünü hayal edin. } func sonFaturaÖdenmişMi () -> Bool { // Sisteme son borcu sor return false; // Bunun sistemden döndüğünü hayal edin. } func işlemiOnayla (error: NSErrorPointer) { // Sisteme parayı aktardığını ve borcu sildiğini hayal edin. // Bunları yapamadığında geriye false dönüyor. if (birşeylerTersGitti) { hataOluştur(error, hataMesajı: "İşlem onaylanamadı, sistemde arıza var.", kod: 103) } } func hataOluştur (error: NSErrorPointer, hataMesajı: String, kod: Int) { let userInfo = [NSLocalizedDescriptionKey: hataMesajı]; if error != nil { error.memory = NSError(domain: "Koddit", code: kod, userInfo: userInfo); } } let sonuç = suParasınıYatır(miktar: 50); if sonuç.hataMesajı != nil { print (sonuç.hataMesajı!); // Para yatırılamadı. } else { // Para yatırıldı. }
Artık neyin ters gittiğini bilsek de sürekli olarak NSError ile uğraşmak zorundayız, özellikle suParasınıYatır fonksiyonunun içi korkunç. Hele hele daha farklı hata mesajları veya hata mesajına göre işlemler yapmak istediğinizde kodun iyice içinden çıkılamaz bir hale gelmesi çok olası. Elbette bu kod en ideali değil ve iyileştirmek için bir şeyler yapılabilir ancak Swift 2 öncesi genel olarak takip edeceğiniz yol da bu. (NSError kullanmazsanız her fonksiyondan hata kodu veya nil döndürüp onları kontrol edebilirsiniz veya kendi hata sınıflarınızı oluşturabilirsiniz). Peki Swift 2’de bu çok mu farklı? try-catch’in o kadar ciddi bir katkısı var mı? Kendiniz karar verin;
var birşeylerTersGitti = false; // Sahte hata oluşturmak için, yok sayabilirsiniz. /// 1 - Enumlar enum SistemHatası: ErrorType { case ZamanAşımı, SistemArızası } enum ÖdemeHatası: ErrorType { case BorçYok, ParaYetersiz (Int) } /// 3 - Ana Fonksiyon func suParasınıYatır (miktar miktar: Int) throws -> Int { /// 5 - Defer defer { do { try sorguBitir (); } catch { // Yeniden dene? // PS: İşlem zaten sona erdi o yüzden hatayı göndermeye gerek yok. } } try sorguBaşlat (); let borçMiktarı = try borçSorgula(); if (borçMiktarı > miktar) { throw ÖdemeHatası.ParaYetersiz(borçMiktarı - miktar); } return miktar - borçMiktarı; } /// 2 - Yan Fonksiyonlar func sorguBaşlat () throws { // Bu fonksiyonun database e bağlantı kurmak / session başlatmak // gibi işlemler yaptığını hayal edin. if (birşeylerTersGitti) { throw SistemHatası.ZamanAşımı; } } func sorguBitir () throws { // Bu fonksiyonun database le olan bağlantıyı kapatmak / session'ı // sona erdirmek gibi işlemler yaptığını hayal edin. if (birşeylerTersGitti) { throw SistemHatası.ZamanAşımı; } } func borçSorgula () throws -> Int { // Eğer fatura ödenmediyse miktarı döndür. if (sonFaturaÖdenmişMi()) { throw ÖdemeHatası.BorçYok; } return 150; // Bunun sistemden döndüğünü hayal edin. } func sonFaturaÖdenmişMi () -> Bool { // Sisteme son borcu sor return false; // Bunun sistemden döndüğünü hayal edin. } func işlemiOnayla () throws { // Sisteme parayı aktardığını ve borcu sildiğini hayal edin. // Bunları yapamadığında geriye false dönüyor. if (birşeylerTersGitti) { throw SistemHatası.SistemArızası; } } ///4 - Fonksiyonun kullanımı do { try suParasınıYatır (miktar: 50); } catch ÖdemeHatası.BorçYok { print ("Borç yok"); } catch ÖdemeHatası.ParaYetersiz (let miktar) { print ("\(miktar)" + " TL eksik"); } catch is SistemHatası { print ("Sistemde sorun oluştu"); // Tekrar dene ? }
- Enumlar: Farklı hata tipleri için farklı enumlarımız var, böylece hata nedenini takip etmemiz kolaylaşıyor.
- Yan Fonksiyonlar: Hata çıkartma ihtimali olan tüm fonksiyonlar throws olarak tanımlı ve hata oluştuğu anda alakalı hata tipini fırlatıyorlar.
- Ana Fonksiyon: Bu örnek için hatalarla suParasınıYatır fonksiyonunun değil onu çağıran kod parçacığının ilgilenmesini istiyoruz, o yüzden suParasınıYatır kendisine gelen tüm hataları ilettiği gibi ödeme sırasında para miktarı yetmezse hata fırlatıyor.
- Fonksiyonun Kullanımı: Fonksiyonu do – try – catch yapısı içerisinde kullanıp gelen hataları yakalıyoruz. Her hata tipine farklı farklı tanım yapabileceğimiz gibi (BorçYok– ParaYetersiz) bir hata tipindeki tüm durumlara aynı muameleyi yapabiliyoruz (SistemHatası). Gerektiği noktada enum’ların esnekliğinden yararlanarak ParaYetersiz durumunda olduğu gibi enuma çeşitli değerler ilişkilendirmemiz de mümkün.
- Defer: Bazı durumlarda fonksiyon nasıl sona ererse ersin belli işlemleri yapmak isteriz. Örneğin bu örnek için sorguBitir fonksiyonunu çalıştırarak arkamızı temiz bırakmak istiyoruz. defer tam olarak bu işe yarıyor, fonksiyonun nasıl bittiğinden bağımsız olarak tanımlanan kodu her seferinde çalıştırıyor. Burada not etmek gereken bir detay var, defer içerisinde return veya throw kullanmanız mümkün değil, ancak oluşabilecek hataları yine de yakalayabilir ve duruma göre gerekli olan şeyleri bu örnekte olduğu gibi yapabilirsiniz.
Peki hata düzeltmeyle ilgili bilinmesi gereken başka neler var?
- Bazı durumlarda (özellikle hata ayıklarken) programın çalışmaya devam etmesindense kendini imha edip kapatmasını tercih edebilirsiniz. Swift’de bunu hali hazırda assert fonksiyonu ile yapmak mümkün, ancak try-catch blokları için daha hızlı bir çözüm getirilmiş, try yerine try! kullandığınızda hata oluştuğu durumlarda uygulama otomatik olarak sonlanıyor. Örneğin ilk örneğimizi do-catch olmadan try! ile kullanırsak uygulama dosyayı bulamadığında otomatik olarak kapanır;
let dosyaİçeriği = try! NSFileManager.defaultManager().contentsOfDirectoryAtPath("/Users/Koddit")
- Swift 2’nin yeni özelliklerinden olan guard hata yakalayacağınız durumlarda epey kullanışlı. Bu yazıda iyice her şeyi birbirine sokmamak için kasten es geçsem de ufak bir örnek vermiş olayım;
enum DeğerHatası: ErrorType { case İsim, Adres; } struct Mağaza { var isim: String!; var adres: String!; } func mağazaOluştur (veri: [String: String]) throws -> Mağaza { guard let isim = veri ["isim"] else { throw DeğerHatası.İsim; } guard let adres = veri ["adres"] else { throw DeğerHatası.Adres; } return Mağaza(isim: isim, adres: adres); } do { var mağaza = try mağazaOluştur (["içerik" : "yiyecek"]); } catch DeğerHatası.İsim { print ("İsim eksk"); } catch DeğerHatası.Adres { print ("Adres eksik"); }
- Son olarak belirtmiş olayım, Objective-C’deki NSException ile Swift’in try-catch yapısı uyumlu değil, eğer aynı projede hem Swift hem Objective-C kullanıyorsanız bunu aklınızdan çıkartmamalısınız.
Son not: Yazıdaki ilki hariç tüm kod parçalarını Xcode 7 Playgrounds ile deneyebilirsiniz. Ayrıca Koddit’in kod editörü Swift için Türkçe karaktere izin vermediği için kod örneklerinin çoğunu Swift yerine XHTML ile işaretlemek durumunda kaldım, renksiz oldu biraz kusura bakmayın.