12.5 try-finally Yapısı
Her try bloğuna karşılık gelen bir catch bloğu vardır. Program akışı try bloğunu hangi şekilde terk ederse etsin, finally bloğu içindeki kod her zaman çalıştırılır. Böylece bir istisna fırlatılsa veya try bloğu içinde return deyimi yürütülse dahi, finally bloğunun çalıştırılması garanti edilir.
|
Eğer try bloğu yürütülürken, CLR beklenmedik şekilde sonlandırıldı ise, örneğin Windows Görev Yöneticisi ile programı durdurursak, finally bloğu içindeki kod yürütülmez.
|
finally bloğunun temel yapısı aşağıdaki gibidir:
Try
{
// Some code that could or could not cause an exception
}
finally
{
// Code here will always execute
}
|
Her try bloğu en az sıfır yada daha fazla catch bloğu ve en fazla bir adet finally bloğu içerebilir. Aynı try-catch-finally yapısı içinde birden fazla catch bloğunun ve bir adet finally bloğunun olması da mümkündür.
Try
{
some code
}
catch (…)
{
// Code handling an exception
}
catch (…)
{
// Code handling another exception
}
finally
{
// This code will always execute
}
|
12.5.1 try-finally Ne Zaman Kullanılmalıdır?
Birçok uygulamanın kaynakları programın dışındadır. Dış kaynaklar için örnek verecek olursak, dosyalar, ağ bağlantıları, grafik elemanları, oluklar (pipe) ve farklı donanım aygıtları için sürekli ileti akışları (yazıcılar, kart okuyucu ve diğerleri gibi) sayılabilir. Dış kaynaklar ile iletişim kurulacağı zaman, kaynak artık gerekli olmadığında, mümkün olan en erken zamanda kaynakları serbest bırakmak kritik öneme sahiptir. Örneğin bir dosyayı okumak için açtığımızda (.JPG resmi yükleyecek olursak), içeriğini okuduktan sonra dosyayı hemen kapatmalıyız. Eğer dosyayı açık bırakırsak, işletim sistemi, diğer kullanıcıları ve uygulamaları, dosya üzerinde bazı işlemler yapmamaları için engeller. Belki çalışan bir işlem tarafından kullanılmakta olduğu için, bir dizini yada dosyayı silemediğiniz zaman böyle bir durumla karşı karşıya kalmışsınızdır.
finally bloğu dış kaynakları serbest bırakmak ve herhangi başka temizlikler için ihtiyacımız olduğunda paha biçilemez ve değerlidir. finally bloğu beklenmeyen bir istisna nedeniyle yada return, continue veya break yürütülmesiyle, temizleme işlemlerinin yanlışlıkla atlanmayacağını garanti eder.
Doğru kaynak yönetimi programlamada önemli bir kavram olduğu için, bu konuya biraz daha detaylı bakacağız.
12.5.2 Kaynak Temizleme – Sorunu Tanımlama
Örneğimizde, bir dosya okumak istiyoruz. Bunu yapmak için, dosya okunduktan sonra kapatılması gereken bir reader okuyucu değişkenimiz var. Bunu başarmanın en iyi yolu okuyucuyu kullanan satırları bir try-finally bloğu içinde çevrelemektir. Örnek kodumuz aşağıda verilmiştir:
static void ReadFile(string fileName)
{
TextReader reader = new StreamReader(fileName);
string line = reader.ReadLine();
Console.WriteLine(line);
reader.Close();
}
|
Bu kod ile sorun nedir? Kodun şunu yapması gerekiyor: Bir dosya okuyucusu açılsın, veri okunsun ve metot dönmeden önce okuyucu kapatılsın. Metot aşağıda anlatıldığı gibi birçok farklı şekilde bitirilebileceği için, bu son bölüm problemli olabilir:
-
(Eğer dosya eksikse) okuyucu başlatıldığında bir istisna fırlatılmış olabilir.
-
Dosya okuma sırasında bir istisna ortaya çıkabilir (uzak ağ aygıtında bulunan bir dosya okuma esnasında çevrimdışı kalabilir).
-
Okuyucu kapatılmadan önce return deyimi yürütülebilir (örneğimizde bu açıktır, ancak her zaman bu kadar belirgin olmayabilir).
-
Her şey beklendiği gibi gider ve metot normal olarak yürütülür.
Yukarıdaki örnekte yazıldığı gibi metotumuzun önemli bir kusuru vardır: okuyucu sadece son senaryoda kapatılır. Diğer tüm durumlarda, okuyucuyu kapatan kod yürütülmez. Ve eğer bu kod bir döngü içinde ise, işler daha da karmaşık hale gelir, bu durumda continue ve break işleçleri de dikkate alınmalıdır.
12.5.3 Kaynak Temizleme – Problemin Çözülmesi
Önceki bölümde verilen çözümün temel hatasını açıkladık. 'Dosya Aç oku kapat'. Eğer dosya açma veya okuma sırasında bir hata ortaya çıkarsa, dosyayı açık bırakacağız.
Bunu çözmek için, try-finally yapısını kullanabiliriz. Önce sadece bir kaynağın (bu durumda bir dosya) serbest bırakılarak temizleneceği durumu göz önüne alacağız. Sonra iki veya daha fazla kaynak için örnek vereceğiz.
Dosya akışının kapanışı da aşağıdaki kalıba uygun olarak yapılabilir:
static void ReadFile(string fileName)
{
TextReader reader = null;
try
{
reader = new StreamReader(fileName);
string line = reader.ReadLine();
Console.WriteLine(line);
}
finally
{
// Always close "reader" (if it was opened)
if (reader != null)
{
reader.Close();
}
}
}
|
Bu örnekte ilkin reader (okuyucu) değişkeni bildiriliyor ve sonra bir try bloğu içinde TextReader başlatılıyor. Sonra finally bloğunun okuyucuyu kapatıyoruz. TextReader başlatılması yada okuma sırasında hangi hata olursa olsun, dosyanın kapatılışı garanti altına alınmıştır. Eğer okuyucunun başlatılmasında (dosyanın eksik olması nedeniyle) bir sorun oluşursa, o zaman okuyucu boş (null) kalacaktır, bu nedenle Close() çağrılmadan önce finally bloğu içinde null kontrolü yaparız. Eğer gerçekten değer null ise, okuyucu başlatılmamıştır ve bunu kapatmak için hiçbir gerek yoktur. Yukarıdaki kod eğer dosya açılmış ise metot hangi şekilde sonlanırsa sonlansın dosyanın kapatılacağını garanti etmektedir.
Yukarıdaki örnek, prensip olarak, okuyucunun başlatılması ve açılması sırasında ortaya çıkabilecek tüm istisnaları (FileNotFoundException istisnası gibi) düzgün bir şekilde işlemektedir. Örneğimizde, bu istisnalar yakalanmamıştır ve ele alınması yalnızca çağıran metota bırakılmıştır. Örneğimizde serbest bırakılan kaynaklar dosya akışlarıdır, ama temizleme gerektiren tüm kaynaklar için aynı ilke geçerlidir. Bunlar uzak bağlantılar, işletim sistemi kaynakları, veritabanı bağlantıları vb. olabilir.
12.5.4 Kaynak Temizleme – Daha İyi Bir Çözüm
Yukarıdaki çözüm doğru olsa da, gereksiz yere karmaşıktır. Daha basit bir versiyona bakalım:
static void ReadFile(string fileName)
{
TextReader reader = new StreamReader(fileName);
try
{
string line = reader.ReadLine();
Console.WriteLine(line);
}
finally
{
reader.Close();
}
}
|
Bu kodun avantajı daha basit ve daha kısa olmasıdır. Reader (okuyucu) değişkeninin ön bildiriminden ve finally bloğu içindeki null kontrolünden vazgeçiyoruz. Okuyucunun başlatılması try bloğunun dışında olduğu için null kontrolü artık gerekli değildir ve eğer başlatma sırasında bir istisna oluşursa, finally bloğu hiç çalıştırılmaz.
Bu kod daha temiz, daha kısa ve daha nettir ve "ortadan kaldırma (dispose) kalıbı" olarak bilinir. Ancak, unutmayın ki bu şekilde istisna ReadFile(…) metotunu çağıran metota kadar gidecektir.
12.5.5 Birden Fazla Kaynak Temizleme
Bazen birden fazla serbest kaynağa ihtiyacımız olur. Kaynakların oluşturulma sırasına göre sondan başa doğru serbest bırakılması iyi bir uygulamadır.
Yukarıda özetlenen aynı yaklaşımı iç içe try-finally blokları için şöyle kul4lanabiliriz:
static void ReadFile(string filename)
{
Resource r1 = new Resource1();
try
{
Resource r2 = new Resource2();
try
{
// Use r1 and r2
}
finally
{
r2.Release();
}
}
finally
{
r1.Release();
}
}
|
Başka bir seçenek de önceden tüm kaynakları bildirmek ve sonra tek bir finally bloğu içinde gerekli null kontrolleri yaparak temizliği gerçekleştirmektir.
static void ReadFile(string filename)
{
Resource r1 = null;
Resource r2 = null;
try
{
Resource r1 = new Resource1();
Resource r2 = new Resource2();
// Use r1 and r2
}
finally
{
if (r1 != null)
{
r1.Release();
}
if (r2 != null)
{
r2.Release();
}
}
}
|
Bu seçeneklerin her ikisi de doğrudur ve her iki durum da programcının tercihine bağlı olarak uygulanır. İkinci yaklaşım biraz daha risklidir, çünkü eğer bir istisna, finally bloğunun içinde ortaya çıkarsa, bazı kaynaklar temizlenmeyecektir. Yukarıdaki örnekte, r1.Release() sırasında bir istisna fırlatılırsa, r2 temizlenmeyecektir. Eğer ilk seçeneği kullanırsak, böyle bir sorun çıkmaz, ancak kod biraz daha uzun olacaktır.
12.6 Idisposable ve "using" Deyimi
C# kaynaklarını serbest bırakmanın daha kısa ve basitleştirilmiş yolları vardır. Şimdi bu program yapılarını sunmak için zamanımızı kullanacağız. Hangi kaynakların bu program yapılarını kullanabileceğini anlatacağız.
12.6.1 IDisposable
IDisposable arabirimi kaynakları serbest bırakmak için kullanılır. .NET ağ kaynaklarına örnekler pencere kolları, dosyalar, akışlar ve diğerleridir. Arayüzler hakkında “Nesne Yönelimli Programlamanın İlkeleri” Bölümü’nde bilgi vereceğiz. Verilen her nesne türü (örneğin dosya okumak için açılan akışlar), belirli sayıda işlemleri destekler (örneğin akışın kapatılması ve kaynakların serbest bırakılması). Şimdilik arayüzleri bu desteğin bir göstergesi olarak düşünmelisiniz.
Çok daha derin düşünmemizi gerektiren çöp toplayıcının nasıl çalıştığını açıklamak zorunluluğu nedeniyle, şimdilik IDisposable gerçekleştiriminin detaylarına, yıkıcılar ve yönetilmeyen kaynakların kullanımına değinmeyeceğiz.
IDisposable arayüzünün en önemli metotu Dispose() metotudur. Bilmemiz gereken en önemli şey bu metotu uygulayan sınıf, kaynaklarını serbest bırakır. Kaynaklara verilecek örnekler akış, okuyucu yada dosyalar ise, bunları IDisposable arayüzünün Dispose() metotunu çağırarak serbest bırakabiliriz. Bu metot da ilgili Close() yöntemini çağıracaktır. Bu yöntem onları kapatır ve kaynaklarını serbest bırakır. Böylece, bir akışı kapatmak için yapılması gerekenler aşağıda verilmiştir:
StreamReader reader = new StreamReader(fileName);
try
{
// Use the reader here
}
finally
{
if (reader != null)
{
reader.Dispose();
}
}
|
12.6.2 “using” Anahtar Sözcüğü
Bir önceki örnek, C# using anahtar sözcüğü ile aşağıdaki örnekte gösterildiği gibi daha kısa şekilde yazılabilir:
using (StreamReader reader = new StreamReader(fileName))
{
// Use the reader here
}
|
Yukarıdaki basitleştirilmiş kod “kalıbı”nı yazması basit, kullanımı ve okuması kolaydır, ve using ifadesiyle parantez içinde belirtilmiş kaynakların doğru olarak serbest bırakılmasını garanti eder.
Kaynakların serbest bırakılmasında try-finally bloğunu kullanmak veya herhangi bir metotu açıktan çağırmak muhakkak gerekli olmaz. Using bloğundan ayrıldıktan sonra, derleyici otomatik olarak try-finally bloğunu işletecek ve kullanılan kaynaklar Dispose() metotunun çağrılmasıyla serbest bırakılacaktır.
İlerleyen “Metin Dosyaları” Bölümü’nde metin dosyalarını düzgün şekilde okumak ve yazmak için using ifadesini yaygın olarak kullanacağız.
12.6.3 İç içe “using” İfadeleri
Birden fazla using ifadesi iç içe geçerek kullanılabilir:
using (ResourceType r1 = …)
using (ResourceType r2 = …)
…
using (ResourceType rN = …)
statements;
|
Üstteki örnek aşağıdaki kodla yeniden yazılabilir:
using (ResourceType r1 = …, r2 = …, …, rN = …)
{
statements;
}
|
Using ifadesinin istisna ve hata işleme ile ilgili olmadığını belirtmek önemlidir. Tek amacı kaynakları istisna hallerinde veya değil, ne olursa olsun serbest bırakmaktır. İstisna işlemekte kullanılmaz.
12.6.4 “using” İfadesi Ne Zaman Kullanılır?
.NET sınıfları ile “using” deyimini kullanmak için basit bir kural vardır:
|
IDisposable arabirimini uygulayan tüm sınıflar için using ifadesini kullanın. MSDN kütüphanesinde IDisposable anahtar sözcüğünü arayın.
|
Bir sınıf IDisposable arabirimini uyguladığında, bu sınıfın yaratıcısı using ifadesini kullanabilir anlamına gelir ve öylece bırakılmaması gereken önemli kaynakları içeriyor diye bilinir. Aynı zamanda, IDisposable uygulayan bir sınıf kullanıldıktan sonra hemen serbest bırakılmalıdır. C# dilinde bunu yapmanın en kolay yolu using deyimini kullanmaktır.
12.7 İstisnaları Kullanmanın Avantajları
Şimdiye kadar, istisnaların özelliklerini ve nasıl kullanılacaklarını detaylarıyla gözden geçirdik. Şimdi neden programcıya tanıtıldığını ve bu kadar yaygın kullanıldığına değinelim.
12.7.1 İstisna İşleme Kodunun Ayrılması
İstisnaları kullanmak, programın normal yürütülmesi için gerekli kod ile beklenmedik yürütülmesi için gerekli hata işleme kodunun birbirinden ayrılmasına olanak tanır. Aşağıdaki örnekte bu ayrılık kavramı anlatacağız:
void ReadFile()
{
OpenTheFile();
while (FileHasMoreLines)
{
ReadNextLineFromTheFile();
PrintTheLine();
}
CloseTheFile();
}
|
Örneği adım adım inceleyelim. Aşağıdaki işlemleri gerçekleştirir:
- Dosyayı açar;
- Dosyayı daha fazla satır varken:
- Dosyadan bir sonraki satırı okur;
- Satırı yazdırır;
- Dosyayı kapatır.
Bu metot iyi görünüyor, ama yakından bakmak bazı soruları beraberinde getiriyor:
- Eğer bu dosya mevcut değilse ne olacak?
- Dosya açılamıyor ise ne olacak?
- Eğer bir satırı okuma başarısız olursa ne olacak?
- Dosya kapatılamaz ise ne olacak?
12.7.1.1 İstisnasız Hata İşleme
Bu soruları aklımızda tutarak metotu, istisnaları kullanmayacak şekilde yeniden yazalım. Her metotun döndüreceği ve doğru yürütülüp yürütülmediğini belirten bir int değer olan hata kodlarını kullanacağız. Hata kodlarını kullanmak yordam yönelimli programlamada hata işlemek için standart bir yoldur. 0 (sıfır) hata kodu her şeyin doğru olduğu anlamına gelir. Herhangi bir başka kod ise hata anlamına gelir. Farklı farklı hatalar farklı farklı kodlara sahiptir (genellikle negatif bir sayı).
int ReadFile()
{
errorCode = 0;
openFileErrorCode = OpenTheFile();
// Check whether the file is open
if (openFileErrorCode == 0)
{
while (FileHasMoreLines)
{
readLineErrorCode = ReadNextLineFromTheFile();
if (readLineErrorCode == 0)
{
// Line has been read properly
PrintTheLine();
}
else
{
// Error during line reading
errorCode = -1;
break;
}
}
closeFileErrorCode = CloseTheFile();
if (closeFileErrorCode != 0 && errorCode == 0)
{
errorCode = -2;
}
else
{
errorCode = -3;
}
}
else if (openFileErrorCode == -1)
{
// File does not exist
errorCode = -4;
}
else if (openFileErrorCode == -2)
{
// File can't be open
errorCode = -5;
}
return errorCode;
}
|
Sonuç olarak, anlaması zor ve parçalanması kolay bir “spagetti” koda sahip olduk. Burada program mantığı ve hata işleme mantığı birbirine karışmıştır. Kodun büyük bir parçası hata işleme için yazılan kurallardan ibarettir. Hatalar türüne, açıklamasına göre yığıt izlencesinde ayrıştırılmamıştır, ve farklı hata kodlarının ne anlama geldiği belirsizdir.
12.7.1.2 İstisnalı Hata İşleme
Yukarıdaki spagetti kodu sadece istisnaları kullanarak engelleyebiliriz. İstisnaları kullandığımızda, aynı metot aşağıdaki gibi görünür:
void ReadFile()
{
try
{
OpenTheFile();
while (FileHasMoreLines)
{
ReadNextLineFromTheFile();
PrintTheLine();
}
}
catch (FileNotFoundException)
{
DoSomething();
}
catch (IOException)
{
DoSomethingElse();
}
finally
{
CloseTheFile();
}
}
|
Aslında istisnalar, hataları bulmak ve işlemek konusunda bizi ekstra çabadan kurtarmaz, ancak bunu daha zarif, kısa, açık ve etkili bir şekilde yapmamızı sağlar.
12.7.2 Farklı Hata Türlerini Gruplandırma
İstisnaların hiyerarşik yapısı sayesinde, tüm istisna grupları bir seferde yakalanabilir ve bunları işleyebilirsiniz. Catch kullanırken verilen istisna türünü sadece yakalamakla kalmıyoruz, ama bildirilen türün içinde kalan istisna türlerinin bütün hiyerarşisini de yakalıyoruz.
catch (IOException e)
{
// Handle IOException and all its descendants
}
|
Yukarıdaki örnekte sadece IOException yakalanmayacak, ancak onun soyundan gelen tüm FileNotFoundException, EndOfStreamException, PathTooLongException ve diğerleri de dahil olmak üzere yakalanacaktır. Aynı zamanda UnauthorizedAccessException ve OutOfMemoryException gibi istisnalar yakalanmayacaktır, çünkü onlar IOException’dan devralmamıştır. Hangi istisnaları yakayabileceğinizi merak ediyorsanız, MSDN kütüphanesinde istisna hiyerarşilerini araştırabilirsiniz.
İyi bir yöntem değildir, ama mümkün olan tüm istisnaları da yakalayabilirsiniz.
catch (Exception e)
{
// A (too) general exception handler
}
|
Exception sınıfına ait tüm istisnaları yakalamak iyi bir alışkanlık değildir, IOException gibi daha spesifik istisna gruplarını, yada FileNotFoundException gibi sadece bir tür istisnayı yakalamak daha iyi bir uygulamadır.
12.7.3 İstisnaları Yakalamak için En Uygun Yer Seçimi
Dostları ilə paylaş: |