üye ol Üye Girişi Söyle Sözünü Anasayfa Yukari Çik
Bilgi Paylaştıkça Büyür

C#’ta Şifreleme

XOR Operatörü ile Temel Bir Şifreleme Algoritması

Şifreleme günümüzde güvenli iletişim için çok önemli bir konuma gelmiştir, uzun yıllardan beri çok fazla şifreleme algortiması geliştirilmiştir. Bu şifreleme algoritmalarının bir çoğu .NET sınıf kütüphanesinde zaten varsayılan olarak bulunmaktadır, bu yazıda ise kendi şifreleme algortimalarımızı nasıl oluşturabileceğimiz görmek açısından temel bir şifreleme algoritmasını sizlere göstereceğim.

Bir mesajın yada metnin şifrelenmesi genellikle şifrelenecek mesajın çeşitli operatörler yardımıyla farklı mesajlara dönüştürülmesi ile olmaktadır. Burada bilmemiz gereken nokta şudur : şifrelenecek mesaj ile şifrelenmiş mesajın aynı alfabeden sözcükleri içermesidir. Örneğin ikili(binary) sayılardan oluşturulan bir mesaj şifrelendiği takdirde yine ikili bir sayı olacaktır. Şifreleme yapılırken genellikle anahtar dedğimiz yardımcı bir mesajdan faydalanır. Mesajın şifrelenmesi bu anahtar ile gerçekleşmektedir. Aynı şekilde şifrelenmiş mesajın çözülmesinde de bu anahtar kullanılmaktadır. Şifreleme işlemi ise bir yada daha fazla operatör sayesinde yapılmaktadır. Buradaki operatörler tekil bir operatör olabileceği gibi kullanıcının tanımlayacağı karmaşık değişkenli operatörler de olabilir.

Şifreleme programını yazanlar genellikle şifreyi çözen programıda yazmak zorunda kalırlar. Nede olsa şifreler çözülmek içindir. Çözülemeyen şifreli mesajların pek bir anlamı olmayacağı açıktır. Her ne kadar şifreleme ve şifre çözme programları birbirnin tersi de olsa iki farklı program yazmak yinede zaman kaybettirir. Aynı programın hem şifreleyici hemde şifre çözücü olduğu bir sistem herhalde hepimizin ilgisini çekecektir. Bu yazıda hem şifre çözücü hemde şifreleme işine yarayacak özel bir operatör olan XOR(Bitsel Özel Veya) operatörünü ve bu operatörü kullanarak nasıl şifreleyici ve aynı zamanda şifre çözücü bir programı geliştirebileceğimizi inceleyeceğiz.

XOR(Bitsel Özel Veya) Operatörü

“Özel veya” operatörü iki operandı olan bir operatördür. Özel veya operatörü aldığı operandlarının bütün bitlerini karşılıklı olarak “özel veya(XOR)” işlemine tutar. İsterseniz birçoğumzun matematik derslerinden hatırlayacağı “özel veya” yani XOR işleminin tanımını görelim. Özel veya operatörü iki operand aldığı için dört farklı durum sözkonusudur. Bu durumlar ve sonuçları aşağıdaki tabloda belirtilmiştir.

Operand 1

Operand 2

Sonuç

1

1

0

1

0

1

0

1

1

0

0

0

Tablodan da görüldüğü özere XOR operatörünün sonucu ancak ve ancak her iki operand da birbirine eşitse 1 değerini almaktadır. Bu sonuç bize şifreleme algoritmasında büyük bir kolaylık sağlayacaktır. XOR operatörü bitsel bir operatör olduğu için her iki operandın da ikili bir sayı olması gerekir. C#’taki veri türleri ile XOR operatörü kullanıldığında veriyi oluşturan her bir değişkenin bütün bitleri karşılıklı olarak XOR işlemine tabi tutulur. Örneğin byte türünden 1 sayısı ile yine byte türünden 2 sayının XOR işlemin sonra hangi değeri oluşturacağını görelim.

Öncelikle 1 ve 2 sayısının bitsel açılımını yazalım :

Not : 1 byte büyüklüğünün 8 bite denk düştüğünü hatırlayalım.

1 –> 0 0 0 0   0 0 0 1

2 –> 0 0 0 0   0 0 1 0

——————— 1 ^ 2 (Not : XOR operatörünün simgesi ^ karakteridir.)

3 –> 0 0 0 0   0 0 1 1

Dolayısıyla 1 ve 2 değerini XOR işlemine tabi tutarsak 3 değerini elde ederiz. Bu sonucu programlama yoluyla elde etmek için bir konsol uygulaması açın ve aşağıdaki ifadeyi ekrana yazdırın.

Console.WriteLine((1^2));

XOR operatörünün diğer önemli bir özelliği ise geri dönüşümlü bir operatör olmasıdır. Yani bir sayıyı “özel veya” işlemine tabi tuttuktan sonra sonucu yine aynı sayı ile “özel veya” işlemine tabi tutarsak başlangıçtaki sonucu elde ederiz. Örneğin 3 sayısını 1 ile “özel veya” işlemine tabi tutarsak 2 sayısını, 2 ile “özel veya” işlemine tabi tutarsak bu sefer 1 sayısını elde ederiz. Bu özelliği bir formül ile gösterirsek;

x = z ^ b;

y = x ^ b;

ise

z = y dir.

XOR işleminin bu özelli yazdığımız programa hem şifre çözücü hemde şifreleyici olma özelliği katacaktır.

Şifreleyici ve Şifre Çözücü Program

Bu bölümde şifre çözücü ve aynı zamanda şifreleyeci programı XOR operatörünü kullanarak geliştireceğiz. Program bir dosya şifreleyicisi ve şifre çözücüsü olarak kullanılacaktır. Şifrelenecek dosya bir metin dosyası, çalıştırılabilir exe dosyası olabileceği gibi bir video ve resim dosyasıda olabilir. Çünkü XOR işlemini dosyayı oluşturan byte’lar düzeyinde gerçekleştireceğiz. Şifreleme işlemi yaparken dosyadaki her bir byte sırayla kullanıcının gireceği bir anahtardan elde edilen sayı ile XOR işlemine tabi tutulacaktır. XOR işlemi sayesinde yazdığımız program aynı zamanda bir şifre çözücü program olarak ta çalışacaktır. İlk olarak programımızın en temel halini yazalım ardından programız üzerinde iyileştirme çalışması yapacağız.

Kaynak kodları aşağıda verilen programı yazın ve derleyin.

using System;
using System.IO;namespace XOR
{
   class csharpnedir
   {
      static void Main(string[] args)
      {
         if(args.Length != 2)
         {
            Console.WriteLine(“Hatalı kullanım”);
            Console.WriteLine(“Örnek kullanım : Sifrele xx.text anahtar”);
            return ;
         }

         string kaynakDosya = args[0];
         string hedefDosya = args[1];
         string anahtar = “”;

         Console.Write(“Anahtarı girin :”);
         anahtar = Console.ReadLine();

         int XOR = 0;

         for(int i = 0; i
            XOR = XOR + (int)(anahtar[i]);

         FileStream fsKaynakDosya = new FileStream(kaynakDosya,FileMode.Open);
         FileStream fsHedefDosya = new FileStream(hedefDosya,FileMode.CreateNew | FileMode.CreateNew,FileAccess.Write);

         int kaynakByte;//(3 byte’lık 0 dizisi + kaynakByte)
         byte hedefByte;

         while((kaynakByte = fsKaynakDosya.ReadByte()) != -1)
         {
            hedefByte = (byte)((int)kaynakByte ^ XOR);
            fsHedefDosya.WriteByte(hedefByte);
         }

         fsHedefDosya.Close();
         fsKaynakDosya.Close();
      }
   }
}

Hemen programın sonucunu görelim :

Aşağıdaki gibi gizlilik derecesi yüksek olan bir metin dosyası oluşturun.

Not : Şifrelenecek dosyanın metin tabanlı olması zorunlu değildir. Çünkü şifreleme işlemini karakter tabanlı değil byte düzeyinde yapmaktayız. Ama sonuçlarını daha iyi görebilmek için örneği metin tabanlı dosya üzerinde gösteriyorum.

Programı aşağıdaki gibi komut satırından çalıştırın.

Programı çalıştırdıktan sonra oluşturulan Sifreli isimli dosyayı Notepad programında görüntülediğimizde aşağıdaki gibi bir ekran ike karşılaşırız.

Dikkat edin, şifreleme işlemini byte düzeyinde yaptığımız için şifreli dosya artık metin dosyası değil binary bir dosya haline gelmiştir.

Şifrelenmiş dosyayı tekrar eski haline getirmek için tek yapmamız gereken komut satırından şifreleme programını diğer bir deyişle şifre çözücü programını çalıştırmamız gerekir. Anahtar olarak ta tabiki şifrelemede kullandığımız anahtar kullanmamız gerekir. Komut satırından aşağıdaki gibi programı çalıştırdığımızda orjinal metin dosyasını elde edebiliriz.

XOR SifreliMesaj OrjinalMesaj.txt
Anahtarı Girin : XkuksAh

Gördüğünz gibi programımız hem şifreleyici hemde şifre çözücü olarak kullanılabilmektedir.

Sonuçlar

Dikkat ederseniz mesaj dosyasının her byte değeri sabit bir değerle karşılıklı olarak XOR işlemine tabi tutulmuştur. XOR işlemine tabi tutulan değer kullanıcı tarafından girilen anahtardan oluşturulmuştur. Anahtar değerin her bi karakterinin ASCII karşılığı toplanarak elde edilen değer XOR işleminin sabit operandı olarak ele alınmıştır. Ancak programımızda ufak bir sorun var. Çünkü şifrelemek için girilen anahtar değerini oluşturan karakterlerin hepsini içerecek şekilde oluşturulan bütün kombinasyonlar şifrelenmiş dosyayı çözecektir. Örneğin şifrelemek için kullanılan anahtar değerin “AxyHMnK2″ olduğunu düşünelim. Bu durumda “xynAHMNK2″ ve “2MnKHyxA” gibi kombinasyonlar dosyanın çözülmesini sağlayacaktır.

Yukarıda bahsi geçen kısıtı engellemek için XOR işlemine tabi tutulacak operandı anahtar değerden elde ederlen farklı bir yöntem kullanılır. Bu operandı aşağıdaki gibi yeniden elde edebiliriz.

int XOR = 0;for(int i = 0; i
     XOR = XOR + (int)(anahtar[i] * 10);

Yukarıdaki düzenlmeye rağmen şifreyi çözecek anahtar tek değildir. Çünkü farklı karakter kombinasyonlarının toplamı çok düşük bir ihtimalde olsa orjinal XOR değerine eşit olabilir. Ancak bu durum şifreleme tekniğinin güvenirliğini azaltmaz. Çünkü orjinal XOR değerinin tahmin etme olsaılığı çok azdır.

Gelelim diğer bir kısıta : Dikkat ederseniz şifreleme yaparken dosyadaki her bir byte değerini sabit bir değerle XOR işlemine tabi tuttuk. Bir byte değişkenin sınırları 0- 255 arası olduğu için şifreleme programını çözmek için en fazla 256 ihtimal vardır. Tabi burada anahtar değerden XOR işlemine tabi tutulacak değerin nasıl elde edildiğinin bilindiği varsayılmaktadır. Eğer bu yöntem bilinmiyorsa şifrenin çözülme olasılığı neredeyse imkansızdır. XOR operandının elde edilme yönteminin bilindiği varsayımı altında 256 sayısını yani şifrenenin çözülme olasılığını azaltmak için yapmamız gereken XOR işlemini 1 byte’lık bloklar yerine daha büyük bloklar ile yapmaktır. Örneğin XOR işlemini 4 byte lık veri blokları ile yaptığımızda XOR işleminin operandı 4.294.967.296 ihtimalden birisidir. Eğer XOR işlemine sokulan veri bloğu artırılırsa operandın alabileceği değerler üstel bir biçimde artacaktır. Bu arada XOR işlemine sokulacak veri bloklarının sayısı arttıkça xor işlemindeki operandın değerini belirlemek için farklı yöntemler kullanılmalıdır. Çünkü eğer aşağıdaki yöntemde elde edilen XOR operandını kullanırsak 1 byte yada 4 byte’lık verilerle çalışmanın çok önemli bir farkı olmayacaktır. (Burada fark, girilen anahtara göre belirlenir. Örneğin oluşturulan xor operandı 256 değerinden küçük ise hiç bir fark meydana gelmeyecektir.)

int XOR = 0;for(int i = 0; i
     XOR = XOR + (int)(anahtar[i]);

Bu yöntemle geliştirilecek bir şifreleme programını daha etkili hale getirmek için bir yöntem daha vardır. Programı incelerseniz her bir byte bloğunu sabit bir değerle xor işlemine soktuk. Bu aslında biraz risklidir. Zira büyük bir şifreli metnin çok küçük bölümünün çözülmesi tamamının çözülmesi anlamına gelir. Bu yüzden her bir byte bloğunu farklı bir değerle xor işlemine tabi tutarsak şifreli metnin her bir şifreli bloğu bir diğerinden bağımsız hale gelir. Yani çözülmüş bir şifreli blok diğer bloğun çözülmesine kesin bir bilgi vermez. Dolayısıyla şifre krıcı programların lineer bir yöntem izlemesi engellenmiş olur.

Bu tür bir şifreleme yönteminin devlet düzeyinde güvenli olması gereken mesajlarda kullanılması uygun olmasada mesajların başkaları tarafından açıkca görülmeden haberleşme sistemlerinden geçirilmesi için uygun bir yöntemdir. Elbetteki daha basit yöntemlerle de bu işlemi gerçekleştirebiliriz ancak bu yöntemin en önemli özelliği hem şifreleme hemde şifre çözücü olarak kullanılabilmesidir.

Bu yazının kendi şifreleme algortimalarınızı oluşturmada size yol gösterebileceğini umuyor iyi çalışmalar diliyorum.

 

 

 

C# ile Çok Kanallı(Multithread) Uygulamalar – 4

Bundan önceki üç makalemizde iş parçacıkları hakkında bilgiler vermeye çalıştım, bu makalemde ise işimize yarayacak tarzda bir uygulama geliştirecek ve bilgilerimizi pekiştireceğiz. Bir iş parçacığının belkide en çok işe yarayacağı yerlerden birisi veritabanı uygulamalarıdır. Bazen programımız çok uzun bir sonuç kümesi döndürecek sorgulara veya uzun sürecek güncelleme ifadeleri içeren sql cümlelerine sahip olabilir. Böyle bir durumda programın diğer öğeleri ile olan aktivitemizi devam ettirebilmek isteyebiliriz. Ya da aynı anda bir den fazla iş parçacığında, birden fazla veritabanı işlemini yaptırarak bu işlemlerin tamamının daha kısa sürelerde bitmesini sağlıyabiliriz. İşte bu gibi nedenleri göz önüne alarak bu gün birlikte basit ama faydalı olacağına inandığım bir uygulama geliştireceğiz.

Olayı iyi anlayabilmek için öncelikle bir milat koymamız gerekli. İş parçacığından önceki durum ve sonraki durum şeklinde. Bu nedenle uygulamamızı önce iş parçacığı kullanmadan oluşturacağız. Sonrada iş parçacığı ile. Şimdi programımızdan kısaca bahsedelim. Uygulamamız aşağıdaki sql sorgusunu çalıştırıp, bellekteki bir DataSet nesnesinin referans ettiği bölgeyi, sorgu sonucu dönen veri kümesi ile dolduracak.

SELECT Products.* From [Order Details] Cross Join Products

Bu sorgu çalıştırıldığında, Sql sunucusunda yer alan Northwind veritabanı üzerinden, 165936 satırlık veri kümesi döndürür. Elbette normalde böyle bir işlemi istemci makinenin belleğine yığmamız anlamsız. Ancak sunucu üzerinde çalışan ve özellikle raporlama amacı ile kullanılan sorguların bu tip sonuçlar döndürmeside olasıdır. Şimdi bu sorguyu çalıştırıp sonuçları bir DataSet’e alan ve bu veri kümesini bir DataGrid kontrolü içinde gösteren bir uygulama geliştirelim. Öncelikle aşağıdaki formumuzu tasarlayalım.

 
Şekil 1. Form Tasarımımız.

Şimdide kodlarımızı yazalım.

DataSet ds;public void Bagla()
{

            dataGrid1.DataSource=ds.Tables[0];
}

public void Doldur()
{

            SqlConnection conNorthwind=new SqlConnection(“data source=localhost;initial catalog=Northwind;integrated security=sspi”);

          conNorthwind.Open();

            SqlDataAdapter daNorthwind=new SqlDataAdapter(“SELECT Products.* From [Order Details] Cross Join Products”,conNorthwind);

            ds=new DataSet();

          daNorthwind.Fill(ds);

          conNorthwind.Close();

          MessageBox.Show(“DataTable dolduruldu…”);

}

private void btnKapat_Click(object sender, System.EventArgs e)
{
            Close();
}

private void btnCalistir_Click(object sender, System.EventArgs e)
{

            Doldur();
}

private void btnGoster_Click(object sender, System.EventArgs e)
{

            Bagla();
}

Yazdığımız kodlar gayet basit. Sorgumuz bir SqlDataAdapter nesnesi ile, SqlConnection’ımız kullanılarak çalıştırılıyor ve daha sonra elde edilen veri kümesi DataSet’e aktarılıyor. Şimdi uygulamamızı bu haliyle çalıştıralım ve sorgumuzu Çalıştır başlıklı buton ile çalıştırdıktan sonra, textBox kontrolüne mouse ile tıklayıp bir şeyler yazmaya çalışalım.

 
Şekil 2. İş parçacığı olmadan programın çalışması.

Görüldüğü gibi sorgu sonucu elde edilen veri kümesi DataSet’e doldurulana kadar TextBox kontrolüne bir şey yazamadık. Çünkü işlemcimiz satır kodlarını işletmek ile meşguldü ve bizim TextBox kontrolümüze olan tıklamamızı ele almadı. Demekki buradaki sorgumuzu bir iş parçacığı içinde tanımlamalıyız. Nitekim programımız donmasın ve başka işlemleride yapabilelim. Örneğin TextBox kontrolüne bir şeyler yazabilelim (bu noktada pek çok şey söylenebilir. Örneğin başka bir tablonun güncellenmesi gibi). Bu durumda yapmamız gereken kodlamayı inanıyorumki önceki makalelerden edindiğiniz bilgiler ile biliyorsunuzdur. Bu nedenle kodlarımızı detaylı bir şekilde açıklamadım. Şimdi gelin yeni kodlarımızı yazalım.

DataSet ds;public void Bagla()
{
            if(!t1.IsAlive)
            {
                        dataGrid1.DataSource=ds.Tables[0];
            }
}

public void Doldur()
{

            SqlConnection conNorthwind=new SqlConnection(“data source=localhost;initial catalog=Northwind;integrated security=sspi”);

            conNorthwind.Open();

            SqlDataAdapter daNorthwind=new SqlDataAdapter(“SELECT Products.* From [Order Details] Cross Join Products”,conNorthwind);

            ds=new DataSet();

            daNorthwind.Fill(ds);

            conNorthwind.Close();

            MessageBox.Show(“DataTable dolduruldu…”);
}

ThreadStart ts1;

Thread t1;

private void btnKapat_Click(object sender, System.EventArgs e)
{
          if(!t1.IsAlive)
          {
                   Close();
          }

          else
          {
                        MessageBox.Show(“Is parçacigi henüz sonlandirilmadi…Daha sonra tekrar deneyin.”);
          }
}

private void btnCalistir_Click(object sender, System.EventArgs e)
{
            ts1=new ThreadStart(Doldur);

            t1=new Thread(ts1);

            t1.Start();
}

private void btnIptalEt_Click(object sender, System.EventArgs e)
{
            t1.Abort();
}

private void btnGoster_Click(object sender, System.EventArgs e)
{
            Bagla();
}

Şimdi programımızı çalıştıralım.

 
Şekil 3. İş Parçacığının sonucu.

Görüldüğü gibi bu yoğun sorgu çalışırken TextBox kontrolüne bir takım yazılar yazabildik. Üstelik programın çalışması hiç kesilmeden. Şimdi Göster başlıklı butona tıkladığımızda veri kümesinin DataGrid kontrolüne alındığını görürüz.

Şekil 4. Programın Çalışmasının Sonucu.

Geldik bir makalemizin daha sonuna. İlerliyen makalelerimizde Thred’leri daha derinlemesine incelemeye devam edeceğiz. Hepinize mutlu günler dilerim.

Arayüz(Interface) Kullanımına Giriş

Bugünkü makalemizde, nesneye dayalı programlamanın önemli kavramlarından birisi olan arayüzleri incelemeye çalışacağız. Öncelikle, arayüz’ün tanımını yapalım.

Bir arayüz, başka sınıflar için bir rehberdir. Bu kısa tanımın arkasında, deryalar gibi bir kavram denizi olduğunu söylemekte yarar buluyorum. Arayüzün ne olduğunu tam olarak anlayabilmek için belkide asıl kullanım amacına bakmamız gerekmektedir.

C++ programlama dilinde, sınıflar arasında çok kalıtımlılık söz konusu idi. Yani bir sınıf, kalıtımsal olarak, birden fazla sınıftan türetilebiliyordu . Ancak bu teknik bir süre sonra kodların dahada karmaşıklaşmasına ve anlaşılabilirliğin azalmasına neden oluyordu. Bu sebeten ötürü değerli Microsoft mimarları, C# dilinde, bir sınıfın sadece tek bir sınıfı kalıtımsal olarak alabileceği kısıtlmasını getirdiler. Çok kalıtımlık görevini ise anlaşılması daha kolay arayüzlere bıraktılar. İşte arayüzleri kullanmamızın en büyük nedenlerinden birisi budur.

Diğer yandan, uygulamalarımızın geleceği açısından da arayüzlerin çok kullanışlı olabileceğini söylememiz gerekiyor. Düşününkü, bir ekip tarafından yazılan ve geliştirilen bir uygulamada görevlisiniz. Kullandığınız nesnelerin, türetildiği sınıflar zaman içerisinde, gelişen yeniliklere adapte olabilmek amacıyla, sayısız yeni metoda, özelliğe vb.. sahip olduklarını farzedin. Bir süre sonra, nesnelerin türetildiği sınıflar içerisinde yer alan kavram kargaşısını, “bu neyi yapıyordu?, kime yapıyordu? , ne için yapıyordu?” gibi soruların ne kadar çok sorulduğunu düşünün. Oysa uygulamanızdaki sınıfların izleyeceği yolu gösteren rehber(ler) olsa fena mı olurdu? İşte size arayüzler. Bir arayüz oluşturun ve bu arayüzü uygulayan sınıfların hangi metodları, özellikleri vb kullanması gerektiğine karar verin. Programın gelişmesimi gerekiyor? Yeni niteliklere mi ihtiyacın var? İster kullanılan arayüzleri, birbirlerinden kalıtımsal olarak türetin, ister yeni arayüzler tasarlayın. Tek yapacağınız sınıfların hangi arayüzlerini kullanacağını belirtmek olucaktır.

Bu açıklamalar ışığında bir arayüz nasıl tanımlanır ve hangi üyelere sahiptir bundan bahsedelim.Bir arayüz tanımlanması aşağıdaki gibi yapılır. Yazılan kod bloğunun bir arayüz olduğunu Interface anahtar sözcüğü belirtmektedir. Arayüz isminin başında I harfi kullanıldığına dikkat edin. Bu kullanılan sınıfın bir arayüz olduğunu anlamamıza yarayan bir isim kullanma tekniğidir. Bu sayede, sınıfların kalıtımsal olarak aldığı elemanların arayüz olup olmadığını daha kolayca anlayabiliriz.

public inteface IArayuz
{}

Tanımlama görüldüğü gibi son derece basit. Şimdi arayüzlerin üyelerine bir göz atalım. Arayüzler, sadece aşağıdaki üyelere sahip olabilirler:

Arayüz Üyeleri

A. Özellikler (properties)
B. Metodlar (methods)
C. Olaylar (events)
D. İndeksleyiciler (indexers)

Tablo 1. Arayüzlerin sahip olabileceği üyeler

Diğer yandan, arayüzler içerisinde aşağıdaki üyeler kesinlikle kullanılamazlar:

Arayüzlerde Kullanılamayan Üyeler
i. Yapıcılar (constructors)
ii. Yokediciler (destructors)
iii. Alanlar (fields)

Tablo 2. Arayüzlerde kullanılamayan üyeler.

Arayüzler Tablo1 deki üyelere sahip olabilirler. Peki bu üyeler nasıl tanımlanır. Herşeyden önce arayüzler ile ilgili en önemli kural onun bir rehber olmasıdır. Yani arayüzler sadece, kendisini rehber alan sınıfların kullanacağı üyeleri tanımlarlar. Herhangi bir kod satırı içermezler. Sadece özelliğin, metodun, olayın veya indeksleyicinin tanımı vardır. Onların kolay okunabilir olmalarını sağlayan ve çoklu kalıtım için tercih edilmelerine neden olan sebepte budur. Örneğin;

public interface IArayuz
{
            /* double tipte bir özellik tanımı. get ve set anahtar sözcüklerinin herhangibir blok {} içermediğine dikkat edin. */       double isim
    {
         get;
         set;
     }

         /* Yanlız okunabilir (ReadOnly) string tipte bir özellik tanımı. */   
   string soyisim
   {
            get ;
   }

         /* integer değer döndüren ve ili integer parametre alan bir metod tanımı. Metod tanımlarındada metodun dönüş tipi, parametreleri, ismi dışında herhangibir kod satırı olmadığına dikkat edin. */

         int topla(int a, int b);

         /* Dönüş değeri olmayan ve herhangibir parametre almayan bir metod tanımı. */

         void yaz();

         /* Bir indeksleyici tanımı */
         string this [ int index]
         {
              get;
              set;
         }
}

Görüldüğü gibi sadece tanımlamalar mevcut. Herhangibir kod satırı mevcut değil. Bir arayüz tasarlarken uymamız gereken bir takım önemli kurallar vardır. Bu kurallar aşağıdaki tabloda kısaca listelenmiştir.

1

Bir arayüz’ün tüm üyeleri public kabul edilir. Private, Protected gibi belirtiçler kullanamayız. Bunu yaptığımız takdirde örneğin bir elemanı private tanımladığımız takdirde, derleme zamanında şu hatayı alırız. “The modifier ‘private’ is not valid for this item”

2

Diğer yandan bir metodu public olarakta tanımlayamayız. Çünkü zaten varsayılan olarak bütün üyeler public tanımlanmış kabul edilir. Bir metodu public tanımladığımızda yine derleme zamanında şu hatayı alırız. “The modifier ‘public’ is not valid for this item”

3

Bir arayüz, bir yapı(struct)’dan veya bir sınıf(class)’tan kalıtımla türetilemez. Ancak, bir arayüzü başka bir arayüzden veya arayüzlerden kalıtımsal olarak türetebiliriz.

4

Arayüz elemanlarını static olarak tanımlayamayız.

5

Arayüzlerin uygulandığı sınıflar, arayüzde tanımlanan bütün üyeleri kullanmak zorundadır.

Tablo 3. Uyulması gereken kurallar.

Şimdi bu kadar açıklamadan sonra konuyu daha iyi anlayabilmek için basit ve açıklayıcı bir örnek geliştirelim. Önce arayüzümüzü tasarlayalım:

public interface IArayuz
{
            void EkranaYaz();
            int Yas
            {
                        get;
                        set;
            }            string isim
            {
                 get;
                 set;
            }
}

Şimdide bu arayüzü kullanacak sınıfımızı tasarlayalım.

public  class Kisiler:IArayuz
{}

Şimdi bu anda uygulamayı derlersek, IArayuz’ündeki elemanları sınıfımız içinde kullanmadığımızdan dolayı aşağıdaki derleme zamanı hatalarını alırız.

  • Interfaces1.Kisiler’ does not implement interface member ‘Interfaces1.IArayuz.EkranaYaz()’
  • Interfaces1.Kisiler’ does not implement interface member ‘Interfaces1.IArayuz.isim’
  • Interfaces1.Kisiler’ does not implement interface member ‘Interfaces1.IArayuz.Yas’

Görüldüğü gibi kullanmadığımız tüm arayüz üyeleri için bir hata mesajı oluştu. Bu noktada şunu tekrar hatırlatmak istiyorum,

Arayüzlerin uygulandığı sınıflar, arayüzde(lerde) tanımlanan tüm üyeleri kullanmak, yani kodlamak zorundadır.

Şimdi sınıfımızı düzgün bir şekilde geliştirelim:

public  class Kisiler:IArayuz /* Sınıfın kullanacağı arayüz burada belirtiliyor.*/
{            private int y;
            private string i;

            /* Bir sınıfa bir arayüz uygulamamız, bu sınıfa başka üyeler eklememizi engellemez. Burada örneğin sınıfın yapıcı metodlarınıda düzenledik. */

            public Kisiler()
            {
                        y=18;
                        i=”Yok”;
            }

            /* Dikkat ederseniz özelliğin herşeyi, arayüzdeki ile aynı olmalıdır. Veri tipi, ismi vb… Bu tüm diğer arayüz üyelerinin, sınıf içerisinde uygulanmasında da geçerlidir. */

            public Kisiler(string ad,int yas)
            {

                        y=yas;
                        i=ad;
            }

            public int Yas
            {
                        get
                        {
                                    return y;
                        }

                        set
                        {
                                    y=value;
                        }
            }

            public string Isim
            {
                        get
                        {
                                    return i;
                        }

                        set
                        {
                                    i=value;
                        }
           }

            public void EkranaYaz()
            {
                        Console.WriteLine(“Adım:”+i);
                        Console.WriteLine(“Yaşım:”+y);
            }
}

Şimdi oluşturduğumuz bu sınıfı nasıl kullanacağımıza bakalım.

class Arayuz_Deneme

          {
                        Kisiler kisi=new Kisiler(“Burak”,27);
                        Console.WriteLine(“Yaşım “+kisi.Yas.ToString());
                        Console.WriteLine(“Adım “+kisi.Isim);
                        Console.WriteLine(“———–”);
                        kisi.EkranaYaz();
            }
}

Uygulamamızı çalıştırdığımızda aşağıdaki sonucu elde ederiz.

 
Şekil 1. Uygulamanın Çalışması Sonucu.

Bu makalemizde arayüzlere kısa bir giriş yaptık. Bir sonraki makalemizde ise, bir sınıfa birden fazla arayüzün nasıl uygulanacağını inceleyeceğiz. Hepinize mutlu günler dilerim.

 

Temsilci(Delegate) Kavramına Giriş

Bugünkü makalemizde, C# programlama dilinde ileri seviye kavramlardan biri olan Temsilcileri(delegates) incelemeye başlayacağız. Temsilciler ileri seviye bir kavram olmasına rağmen, her seviyden C# programcısının bilmesi gereken unsurlardandır. Uygulamalarımızı temsilciler olmadan da geliştirebiliriz. Ancak bu durumda, yapamıyacaklarımız, yapabileceklerimizin önüne geçecektir. Diğer yandan temsilcilerin kullanımını gördükçe bize getireceği avantajları daha iyi anlayacağımız kanısındayım. Bu makalemizde temsilcileri en basit haliyle anlamaya çalışıcağız.

Temsilci (delegate), program içerisinde bir veya daha fazla metodu gösteren(işaret eden), referans türünden bir nesnedir. Programlarımızda temsilciler kullanmak istediğimizde, öncelikle bu temsilcinin tanımını yaparız. Temsilci tanımları, arayüzlerdeki metod tanımlamaları ile neredeyse aynıdır. Tek fark delegate anahtar sözcüğünün yer almasıdır. Bununla birlikte, bir temsilci tanımlandığında, aslında işaret edebileceği metod(ların) imzalarınıda belirlemiş olur. Dolayısıyla, bir temsilciyi sadece tanımladığı metod imzasına uygun metodlar için kullanabiliceğimizi söyleyebiliriz. Temsilci tanımları tasarım zamanında yapılır. Bir temsilciyi, bir metodu işaret etmesi için kullanmak istediğimizde ise, çalışma zamanında onu new yapılandırıcısı ile oluşturur ve işaret etmesini istediğimiz metodu ona parametre olarak veririz. Bir temsilci tanımı genel haliyle, aşağıdaki şekildeki gibidir.
Şekil 1. Temsilci tanımlaması.

Şekildende görüldüğü gibi, temsilciler aslında bir metod tanımlarlar fakat bunu uygulamazlar. İşte bu özellikleri ile arayüzlerdeki metod tanılamalarına benzerler. Uygulamalarımızda, temsilci nesneleri ile göstermek yani işaret etmek istediğimiz metodlar bu imzaya sahip olmalıdır. Bildiğiniz gibi metod imzaları, metodun geri dönüş tipi ve aldığı parametreler ile belirlenmektedir.

Bir temsilcinin tanımlanması, onu kullanmak için yeterli değildir elbette. Herşeyden önce bir amacımız olmalıdır. Bir temsilciyi çalışma zamanında oluşturabiliriz ve kullanabiliriz. Bir temsilci sadece bir tek metodu işaret edebileceği gibi, birden fazla metod için tanımlanmış ve oluşturulmuş temsilcileride kullanabiliriz. Diğer yandan, tek bir temsilcide birden fazla temsilciyi toplayarak bu temsilcilerin işaret ettiği, tüm metodları tek bir seferde çalıştırma lüksünede sahibizdir. Ancak temsilciler gerçek anlamda iki amaçla kullanılırlar. Bunlardan birincisi olaylardır(events). Diğer yandan, bugünkü makalemizde işleyeceğimiz gibi, bir metodun çalışma zamanında, hangi metodların çalıştırılacağına karar vermesi gerektiği durumlarda kullanırız. Elbette bahsetmiş olduğumuz bu amacı, herhangibir temsilye ihtiyaç duymadan da gerçekleştirebiliriz. Ancak temsilcileri kullanmadığımızda, bize sağladığı üstün programlama tekniği, kullanım kolaylığı ve artan verimliliğide göz ardı etmiş oluruz.

Şimdi dilerseniz bahsetmiş olduğumuz bu amaçla ilgili bir örnek verelim ve konuyu daha iyi kavramaya çalışalım. Örneğin, personelimizin yapmış olduğu satış tutarlarına göre, prim hesabı yapan ve ilgili yerlere bu değişiklikleri yazan bir projemiz olsun. Burada primlerin hesaplanması için değişik katsayılar, yapılan satışın tutarına göre belirlenmiş olabilir. Örneğin bu oranlar düşük, orta ve yüksek olarak tanımlanmış olsun. Personel hangi gruba giriyorsa, metodumuz ona uygun metodu çağırsın. İşte bu durumda karar verici metodumuz, çalıştırabileceği metodları temsil eden temsilci nesnelerini parametre olarak alır. Yani, çalışma zamanında ilgili metodlar için temsilci nesneleri oluşturulur ve karar verici metoda , hangi metod çalıştırılacak ise onun temsilcisi gönderilir. Böylece uygulamamız çalıştığında, tek yapmamız gereken hangi metodun çalıştırılması isteniyorsa, bu metoda ilişkin temsilcinin, karar verici metoda gönderilmesi olacaktır.

Oldukça karışık görünüyor. Ancak örnekleri yazdıkça daha iyi kavrayacağınıza inanıyorum. Şimdiki örneğimizde, temsilcilerin tasarım zamanında nasıl tanımlandığını, çalışma zamanında nasıl oluşturulduklarını ve karar verici bir metod için temsilcilerin nasıl kullanılacağını incelemeye çalışacağız.

using System;namespace Delegates1
{
    public class Calistir
   {
        public static int a; 
        public delegate void temcilci(int deger); /* Temsilci tanımlamamızı yapıyoruz. Aynı zamanda temsilcimiz , değer döndürmeyen ve integer tipte tek bir parametre alan bir metod tanımlıyor. Temsilcimizin adı ise temsilci.*/

        * Şimdi bu temsilciyi kullacanak bir metod yazıyoruz. İşte karar verici metodumuz budur. Dikkat ederseniz metodumuz parametre olarak, temsilci nesnemiz tipinden bir temsilci(Delegate) alıyor. Daha sonra metod bloğu içinde, parametre olarak geçirilen bu temsilcinin işaret ettiği metod çağırılıyor ve bu metoda parametre olarak integer tipte bir değer geçiriliyor. Kısaca, metod içinden, temsilcinin işaret ettiği metod çağırılıyor. Burada, temsilci tanımına uygun olan metodun çağırılması garanti altına alınmıştır. Yani, programın çalışması sırasında, new yapılandırıcısı kulllanarak oluşturacağımız bir temsilci(delegate), kendi metod tanımı ile uyuşmayan bir metod için yaratılmaya çalışıldığında bir derleyici hatası alacağızdır. Dolayısıyla bu, temsilcilerin yüksek güvenlikli işaretçiler olmasını sağlar. Bu , temsilcileri, C++ dilindeki benzeri olan işaretçilerden ayıran en önemli özelliktir. */
    
        public void Metod1(Calistir.temcilci t)
        {
            t(a);
       }
    }
    
    class Class1
    { 

        /* IkıKat ve UcKat isimli metodlarımız, temsilcimizin programın çalışması sırasında işaret etmesini istediğimiz metodlar. Bu nedenle imzaları, temsilci tanımımızdaki metod imzası ile aynıdır. */

        public static void IkiKat(int sayi)
        {
            sayi=sayi*2;
            Console.WriteLine(“IkiKat isimli metodun temsilcisi tarafindan çagirildi.”+sayi.ToString());
        }
    
        public static void UcKat(int sayi)    
        {
            sayi=sayi*3;
            Console.WriteLine(“UcKat isimli metodun temsilcisi tarafindan çagirildi.”+sayi.ToString());
        }

        static void Main(string[] args)
        {

            /* Temsilci nesnelerimiz ilgili metodlar için oluşturuluyor. Burada, new yapılandırıcısı ile oluşturulan temsilci nesneleri parametre olarak, işaret edecekleri metodun ismini alıyorlar. Bu noktadan itibaren t1 isimli delegate nesnemiz IkiKat isimli metodu, t2 isimli delegate nesnemizde UcKat isimli metodu işaret ediceklerdir. */
    
                Calistir.temcilci t1=new Delegates1.Calistir.temcilci(IkiKat);

                Calistir.temcilci t2=new Delegates1.Calistir.temcilci(UcKat);

                Console.WriteLine(“1 ile 20 arası değer girin”);

                Calistir.a=System.Convert.ToInt32(Console.ReadLine());

                Calistir c= new Calistir();

                /* Kullanıcının Console penceresinden girdiği değer göre, Calistir sınıfının a isimli integer tipteki değerini 10 ile karşılaştırılıyor. 10 dan büyükse, karar verici metodumuza t1 temsilcisi gönderiliyor. Bu durumda Metod1 isimli karar verici metodumuz, kendi kod bloğu içinde t1 delegate nesnesinin temsil ettiği IkıKat metodunu, Calistir.a değişkeni ile çağırıyor. Aynı işlem tarzı t2 delegate nesnesi içinde geçerli.*/

                  if(Calistir.a>=10)
                {
                   c.Metod1(t1);
                }
                else
               {
                    c.Metod1(t2);
                }
            }
        }
s}

Uygulamamızı çalıştıralım ve bir değer girelim.


Şekil 2. Programın çalışmasının sonucu.

Bu basit örnek ile umarım temsilciler hakkında biraz olsun bilgi sahibi olmuşsunuzdur. Şimdi temsilciler ile ilgili kavramlarımıza devam edelim. Yukarıdaki örneğimiz ışığında temsilcileri programlarımızda temel olarak nasıl kullandığımızı aşağıdaki şekil ile daha kolay anlayabileceğimizi sanıyorum.


Şekil 3. Temsilcilerin Karar Verici metodlar ile kullanımı.

Yukarıdaki örneğimizde, her bir metod için tek bir temsilci tanımladık ve temsilcileri teker teker çağırdık. Bu Single-Cast olarak adlandırılmaktadır. Ancak programlarımız da bazen, tek bir temsilciye birden fazla temsilci ekleyerek, birden fazla metodu tek bir temsilci ile çalıştırmak isteyebiliriz. Bu durumda Multi-Cast temsilciler tanımlarız. Şimdi multi-cast temsilciler ile ilgili bir örnek yapalım. Bu örneğimizde t1 isimli temsilcimiz, multi-cast temsilcimiz olucak.

using System;
namespace Delegates2
{
     public class temsilciler
     {
          public delegate void dgTemsilci(); /* Temsilcimiz tanımlanıyor. Geri dönüş değeri olmayan ve parametre almayan metodları temsil edebilir. */          /* Metod1, Metod2 ve Metod3 temsilcilerimizin işaret etmesini istediğimiz metodlar olucaktır.*/
          public static void Metod1()
          {
               Console.WriteLine(“Metod 1 çalıştırıldı.”);
          }
     
          public static void Metod2()
          {
               Console.WriteLine(“PI değeri 3.14 alınsın”);
          }

          public static void Metod3()
          {
               Console.WriteLine(“Mail gönderildi…”);
          }

          /* Temsilcilerimizi çalıştıran metodumuz. Parametre olarak gönderilen temsilciyi, dolayısıyla bu temsilcinin işaret ettiği metodu alıyor. */

          public static void TemsilciCalistir(temsilciler.dgTemsilci dt)
          {
               dt(); /* Temsilcinin işaret ettiği metod çalıştırılıyor.*
          }
     }
     
     class Class1
     {
          static void Main(string[] args)
          {
               /* Üç metodumuz içinde temsilci nesnelerimiz oluşturuluyor .*/

                    temsilciler.dgTemsilci t1=new Delegates2.temsilciler.dgTemsilci(temsilciler.Metod1);

                    temsilciler.dgTemsilci t2=new Delegates2.temsilciler.dgTemsilci(temsilciler.Metod2);

                    temsilciler.dgTemsilci t3=new Delegates2.temsilciler.dgTemsilci(temsilciler.Metod3);

                    Console.WriteLine(“sadece t1″);

                    temsilciler.TemsilciCalistir(t1);

                    Console.WriteLine(“—”);

                                    /* Burada t1 temsilcimize, t2 temsilcisi ekleniyor. Bu durumda, t1 temsilcimiz hem kendi metodunu hemde, t2 temsilcisinin işaret ettiği metodu işaret etmeye başlıyor. Bu halde iken TemsilciCalistir metodumuza t1 temsilcisini göndermemiz her iki temsilcinin işaret ettiği metodların çalıştırılmasına neden oluyor.*/

                      t1+=t2;

                      Console.WriteLine(“t1 ve t2″);

                      temsilciler.TemsilciCalistir(t1);

                      Console.WriteLine(“—”);

                      t1+=t3; /* Şimdi t1 temsilcimiz hem t1, hem t2, hem de t3 temsilcilerinin işaret ettiği metodları işaret etmiş olucak.*/

                      Console.WriteLine(“t1,t2 ve t3″);

                      temsilciler.TemsilciCalistir(t1);

                      Console.WriteLine(“—”);

                      t1-=t2; /* Burada ise t2 metodunu t1 temsilcimizden çıkartıyoruz. Böylece, t1 temsilcimiz sadece t1 ve t3 temsilcilerini içeriyor. */
                      Console.WriteLine(“t1 ve t3″);

                      temsilciler.TemsilciCalistir(t1);

                      Console.WriteLine(“—”);
               }
          }
}

Uygulamamızı çalıştırdığımızda aşağıdaki sonucu elde ederiz.

Şekil 4. Multi-Cast temsilciler.

Geldik bir makalemizin daha sonuna. Bir sonraki makalemizde temsilcilerin kullanılıdığı olaylar(events) kavramına gireceğiz. Hepinize mutlu günler dilerim.

 

 

Huffman Veri Sıkıştırma Algoritması ve Uygulaması

Bu makalede bilgisayar bilimlerinin önemli konularından biri olan veri sıkıştırma algoritmalarından Huffman algoritmasını inceledikten sonra uygulamasını gerçekleştirip sonuçlarını göreceğiz.

Sayısal haberleşme tekniklerinin önemli ölçüde arttığı günümüzde, sayısal verilen iletilmesi ve saklanması bir hayli önem kazanmıştır. Sayısal veriler çeşitli saklayıcılarda saklanırken hedef daima minimum alanda maksimum veriyi saklamadır. Veriler çeşitli yöntemlerle sıkıştırılarak kapladığı alandan ve iletim zamanından tasarruf edilir. Sayısal iletişim(digital communication) kuramında veriler çok çeşitli yöntemlerle sıkıştırılabilir. Bu yöntemlerden en çok bilineni David Huffman tarafından öne sürülmüştür. Bu yazıda bu teknik “Huffman algoritması” olarak adlandırılacaktır. Bu yazıda Huffman Algoritması detaylı olarak açıklandıktan sonra bu algoritmanın C# dili ile ne şekilde uygulanacağı gösterilecektir.

Sıkıştırma algoritmaları temel olarak iki kategoride incelenir. Bunlar, kayıplı ve kayıpsız sıkıştırma algoritmalarıdır. Kayıplı algoritmalarda sıkıştırılan veriden orjinal veri elde edilemezken kayıpsız sıkıştırma algoritmalarında sıkıştırılmış veriden orjinal veri elde edilebilir. Kayıplı sıkıştırma tekniklerine verilebilecek en güzel örnekler MPEG ve JPEG gibi standartlarda kullanılan sıkıştırmalardır. Bu tekniklerde sıkıştırma oranı artırıldığında orjinal veride bozulmalar ve kayıplar görülür. Örneğin sıkıştırılmış resim formatı olan JPEG dosyalarının kaliteli yada az kaliteli olmasının nedeni sıkıştırma katsayısıdır. Yani benzer iki resim dosyasından daha az disk alanı kaplayan daha kötü kalitededir deriz. Kayıpsız veri sıkıştırmada durum çok farklıdır. Bu tekniklerde önemli olan orjinal verilerin aynen sıkıştırılmış veriden elde edilmesidir. Bu teknikler daha çok metin tabanlı verilen sıkıştırılmasında kullanılır. Bir metin dosyasını sıkıştırdıktan sonra metindeki bazı cümlelerin kaybolması istenmediği için metin sıkıştırmada bu yöntemler kullanılır.

Bu yazının konusu olan Huffman sıkıştırma algoritması kayıpsız bir veri sıkıştırma tekniğini içerir. Bu yüzden bu yöntemin en elverişli olduğu veriler metin tabanlı verilerdir. Bu yazıda verilecek örnek programdaki hedef metin tabanlı verilerin sıkıştırılması olacaktır.

Huffman algoritması, bir veri kümesinde daha çok rastlanan sembolü daha düşük uzunluktaki kodla, daha az rastlanan sembolleri daha yüksek uzunluktaki kodlarla temsil etme mantığı üzerine kurulmuştur. Bir örnekten yola çıkacak olursak : Bilgisayar sistemlerinde her bir karakter 1 byte yani 8 bit uzunluğunda yer kaplar. Yani 10 karakterden oluşan bir dosya 10 byte büyüklüğündedir. Çünkü her bir karakter 1 byte büyüklüğündedir. Örneğimizdeki 10 karakterlik veri kümesi “aaaaaaaccs” olsun. “a” karakteri çok fazla sayıda olmasına rağmen “s” karakteri tektir. Eğer bütün karakterleri 8 bit değilde veri kümesindeki sıklıklarına göre kodlarsak veriyi sembolize etmek için gereken bitlerin sayısı daha az olacaktır. Söz gelimi “a” karakteri için “0″ kodunu “s” karakteri için “10″ kodunu, “c” karakteri için “11″ kodunu kullanabiliriz. Bu durumda 10 karakterlik verimizi temsil etmek için

(a kodundaki bit sayısı)*(verideki a sayısı) + (c kodundaki bit sayısı)*(verideki c sayısı) + (s kodundaki bit sayısı)*(verideki s sayısı) = 1*7 + 2*2 + 2*1 = 12 bit

gerekecektir. Halbuki bütün karakterleri 8 bit ile temsil etseydik 8*10 = 80 bite ihtiyacımız olacaktı. Dolayısıyla %80 ‘in üzerinde bir sıkıştırma oranı elde etmiş olduk. Burada dikkat edilmesi gereken nokta şudur : Veri kümesindeki sembol sayısına ve sembollerin tekrarlanma sıklıklarına bağlı olarak Huffman sıkıştırma algoritması %10 ile %90 arasında bir sıkıştırma oranı sağlayabilir. Örneğin içinde 2000 tane “a” karakteri ve 10 tane “e” karakteri olan bir veri kümesi Huffman tekniği ile sıkıştırılırsa %90′lara varan bir sıkıştırma oranı elde edilir.

Huffman tekniğinde semboller(karakterler) ASCII’de olduğu gibi sabit uzunluktaki kodlarla kodlanmazlar. Her bir sembol değişken sayıda uzunluktaki kod ile kodlanır.

Bir veri kümesini Huffman tekniği ile sıkıştırabilmek için veri kümesinde bulunan her bir sembolün ne sıklıkta tekrarlandığını bilmemiz gerekir. Örneğin bir metin dosyasını sıkıştırıyorsak her bir karakterin metin içerisinde kaç adet geçtiğini bilmemiz gerekiyor. Her bir sembolün ne sıklıkta tekrarlandığını gösteren tablo frekans tablosu olarak adlandırılmaktadır. Dolayısıyla sıkıştırma işlemine geçmeden önce frekans tablosunu çıkarmamız gerekmektedir. Bu yönteme Statik Huffman tekniği de denilmektedir. Diğer bir teknik olan Dinamik Huffman tekniğinde sıkıştırma yapmak için frekans tablosuna önceden ihtiyaç duyulmaz. Frekans tablosu her bir sembolle karşılaştıkça dinamik olarak oluşturulur. Dinamik Huffman tekniği daha çok haberleşme kanalları gibi hangi verinin geleceği önceden belli olmayan sistemlerde kullanılmaktadır. Bilgisayar sistemlerindeki dosyaları sıkıştırmak için statik huffman metodu yeterlidir. Nitekim bir dosyayı baştan sona tarayarak herbir sembolün hangi sıklıkla yer aldığını tespit edip frekans tablosunu elde etmemiz çok basit bir işlemdir.

Huffman sıkıştırma tekniğinde frekans tablosunu elde etmek için statik ve dinamik yaklaşımlarının olduğunu söyledik. Eğer statik yöntem seçilmişse iki yaklaşım daha vardır. Birinci yaklaşım, metin dosyasının diline göre sabit bir frekans tablosunu kullanmaktır. Örneğin Türkçe bir metin dosyasında “a” ve “e” harflerine çok sık rastlanırken “ğ” harfine çok az rastlanır. Dolayısıyla “ğ” harfi daha fazla bitle “a” ve “e” harfi daha az bitle kodlanır. Frekans tablosunu elde etmek için kullanılan diğer bir yötem ise metni baştan sona tarayarak her bir karakterin frekansını bulmaktır. Sizde takdir edersinizki ikinci yöntem daha gerçekçi bir çözüm üretmekle beraber metin dosyasının dilinden bağımsız bir çözüm üretmesi ile de ön plandadır. Bu yöntemin dezavantajı ise sıkıştırılan verilerde geçen sembollerin frekansının da bir şekilde saklanma zorunluluğunun olmasıdır. Sıkıştırılan dosyada her bir sembolün frekansıda saklanmalıdır. Bu da küçük boyutlu dosyalarda sıkıştırma yerine genişletme etkisi yaratabilir. Ancak bu durum Huffman yönteminin kullanılabililiğini zedelemez. Nitekim küçük boyutlu dosyaların sıkıştırılmaya pek fazla ihtiyacı yoktur zaten.

Frekans tablosunu metin dosyasını kullanarak elde ettikten sonra yapmamız gereken “Huffman Ağacını” oluşturmaktır. Huffman ağacı hangi karakterin hangi bitlerle temsil edileceğini(kodlanacağını) belirlememize yarar. Birazdan örnek bir metin üzerinden “Huffman Ağacını” teorik olarak oluşturup algoritmanın derinliklerine ineceğiz. Bu örneği iyi bir şekilde incelediğinizde Huffman algoritmasının aslında çok basit bir temel üzerine kurulduğunu göreceksiniz.

Huffman Ağacının Oluşturulması

Bir huffman ağacı aşağıdaki adımlar izlenerek oluşturulabilir.

Bu örnekte aşağıdaki frekans tablosu kullanılacaktır.

Sembol(Karakter)

Sembol Frekansı

a

50

b

35

k

20

m

10

d

8

ğ

4

Bu tablodan çıkarmamız gereken şudur : Elimizde öyle bir metin dosyası varki “a” karakteri 50 defa, “b” karakteri 35 defa …. “ğ” karakteri 2 defa geçiyor. Amacımız ise her bir karakteri hangi bit dizileriyle kodlayacağımızı bulmak.

1 – Öncelikle “Huffman Ağacını” ndaki en son düğümleri(dal) oluşturacak bütün semboller frekanslarına göre aşağıdaki gibi küçükten büyüğe doğru sıralanırlar.

2 – En küçük frekansa sahip olan iki sembolün frekansları toplanarak yeni bir düğüm oluşturulur. Ve oluşturulan bu yeni düğüm diğer varolan düğümler arasında uygun yere yerleştirilir. Bu yerleştirme frekans bakımından küçüklük ve büyüklüğe göredir. Örneğin yukarıdaki şekilde “ğ” ve “d” sembolleri toplanarak “12″ frakansında yeni bir “ğd” düğümü elde edilir. “12″ frekanslı bir sembol şekilde “m” ve “k” sembolleri arasında yerleştirilir. “ğ” ve “d” düğümleri ise yeni oluşturulan düğümün dalları şeklinde kalır. Yeni dizimiz aşağıdaki şekilde olacaktır.

3 – 2.adımdaki işlem tekrarlanarak en küçük frekanslı iki düğüm tekrar toplanır ve yeni bir düğüm oluşturulur. Bu yeni düğümün frekansı 22 olacağı için “k” ve “b” düğümleri arasına yerleşecektir. Yeni dizimiz aşağıdaki şekilde olacaktır.

4 – 2.adımdaki işlem tekrarlanarak en küçük frekanslı iki düğüm tekrar toplanır ve yeni bir düğüm oluşturulur. Bu yeni düğümün frekansı 42 olacağı için “b” ve “a” düğümleri arasına yerleşecektir. Yeni dizimiz aşağıdaki şekilde olacaktır. Dikkat ederseniz her dalın en ucunda sembollerimiz bulunmaktadır. Dalların ucundaki düğümlere özel olarak yaprak(leaf) denilmektedir. Sona yaklaştıkça Bilgisayar bilimlerinde önemli bir veri yapısı olan Tree(ağaç) veri yapısına yaklaştığımızı görüyoruz.

5 – 2.adımdaki işlem tekrarlanarak en küçük frekanslı iki düğüm tekrar toplanır ve yeni bir düğüm oluşturulur. Bu yeni düğümün frekansı 77 olacağı için “a” düğümünden sonra yerleşecektir. Yeni dizimiz aşağıdaki şekilde olacaktır. Dikkat ederseniz her bir düğümün frekansı o düğümün sağ ve sol düğümlerinin frekanslarının toplamına eşit olmaktadır. Aynı durum düğüm sembolleri içinde geçerlidir.

6 – 2.adımdaki işlem en tepede tek bir düğüm kalana kadar tekrar edilir. En son kalan düğüm Huffman ağacının kök düğümü(root node) olarak adlandırılır. Son düğümün frekansı 127 olacaktır. Böylece huffman ağacının son hali aşağıdaki gibi olacaktır.

Not : Dikkat ederseniz Huffman ağacının son hali simetrik bir yapıda çıktı. Yani yaprak düğümler hep sol tarafta çıktı. Bu tamamen seçtiğimiz sembol frekanslarına bağlı olarak rastlantısal bir sonuçtur. Bu rastlantının oluşması oluşturduğumuz her bir yeni düğümün yeni dizide ikinci sıraya yerleşmesinden kaynaklanmaktadır. Sembol frekanslarında değişiklik yaparak ağacın şeklindeki değişiklikleri kendinizde görebilirsiniz.

7 – Huffman ağacının son halini oluşturduğumuza göre her bir sembolün yeni kodunu oluşturmaya geçebiliriz. Sembol kodlarını oluşturuken Huffman ağacının en tepesindeki kök düğümden başlanır. Kök düğümün sağ ve sol düğümlerine giden dala sırasıyla “0″ ve “1″ kodları verilir. Sırası ters yönde de olabilir. Bu tamamen seçime bağlıdır. Ancak ilk seçtiğiniz sırayı bir sonraki seçimlerde korumanız gerekmektedir. Bu durumda “a” düğümüne gelen dal “0″, “bkmğd” düğümüne gelen dal “1″ olarak seçilir. Bu işlem ağaçtaki tüm dallar için yapılır. Dalların kodlarla işaretlenmiş hali aşağıdaki gibi olacaktır.

 

8 – Sıra geldi her bir sembolün hangi bit dizisiyle kodlanacağını bulmaya. Her bir sembol dalların ucunda bulunduğu için ilgili yaprağa gelene kadar dallardaki kodlar birleştirilip sembollerin kodları oluşturulur. Örneğin “a” karakterine gelene kadar yalnızca “0″ dizisi ile karşılaşırız. “b” karakterine gelene kadar önce “1″ dizisine sonra “0″ dizisi ile karşılaşırız. Dolayısıyla “b” karakterinin yeni kodu “10″ olacaktır. Bu şekilde bütün karakterlerin sembol kodları çıkarılır. Karakterlerin sembol kodları aşağıda bir tablo halinde gösterilmiştir.

Frekans

Sembol(Karakter)

Bit Sayısı

Huffman Kodu

50

a

1

0

35

b

2

10

20

k

3

110

10

m

4

1110

8

d

5

11111

4

ğ

5

11110

Sıkıştırılmış veride artık ASCII kodları yerine Huffman kodları kullanılacaktır. Dikkat ederseniz hiçbir Huffman kodu bir diğer Huffman kodunun ön eki durumunda değildir. Örneğin ön eki “110″ olan hiç bir Huffman kodu mevcut değildir. Aynı şekilde ön eki “0″ olan hiç bir Huffman kodu yoktur. Bu Huffman kodları ile kodlanmış herhangi bir veri dizisinin “tek çözülebilir bir kod” olduğunu göstermektedir. Yani sıkıştırılmış veriden orjinal verinin dışında başka bir veri elde etme ihtimali sıfırdır. (Tabi kodlamanın doğru yapıldığını varsayıyoruz.)

Şimdi örneğimizdeki gibi bir frekans tablosuna sahip olan metnin Huffman algoritması ile ne oranda sıkışacağını bulalım :

Sıkıştırma öncesi gereken bit sayısını bulacak olursak : Her bir karakter eşit uzunlukta yani 8 bit ile temsil edildiğinden toplam karakter sayısı olan (50+35+20+10+8+4) = 127 ile 8 sayısını çarpmamız lazım. Orjinal veriyi sıkıştırmadan saklarsak 127*8 = 1016 bit gerekmektedir.

Huffman algoritmasını kullanarak sıkıştırma yaparsak kaç bitlik bilgiye ihtiyaç duyacağımızı hesaplayalım : 50 adet “a” karakteri için 50 bit, 35 adet “b” karakteri için 70 bit, 20 adet “k” karakteri için 60 bit….4 adet “ğ” karakteri için 20 bite ihtiyaç duyarız. (yukarıdaki tabloya bakınız.) Sonuç olarak gereken toplam bit sayısı = 50*1 + 35*2 + 20*3 + 10*4 + 8*5 + 4*5 = 50 + 70 + 60 + 40 + 40 + 20 = 280 bit olacaktır.

Sonuç : 1016 bitlik ihtiyacımızı 280 bite indirdik. Yani yaklaşık olarak %72 gibi bir sıkıştırma gerçekleştirmiş olduk. Gerçek bir sistemde sembol frekanslarınıda saklamak gerektiği için sıkıştırma oranı %72′ten biraz daha az olacaktır. Bu fark genelde sıkıştırılan veriye göre çok çok küçük olduğu için ihmal edilebilir.

Huffman Kodunun Çözülmesi

Örnekte verilen frekans tablosuna sahip bir metin içerisindeki “aabkdğmma” veri kümesinin sıkıştırılmış hali her karakter ile karakterin kodu yer değiştirilerek aşağıdaki gibi elde edilir.

a a b    k     d         ğ        m       m      a
0 0 10  110  11111  11110  1110   1110  0    –> 00101101111111110111011100

Eğer elimizde frekans tablosu ve sıkıştırılmış veri dizisi varsa işlemlerin tersini yaparak orjinal veriyi elde edebiliriz. Şöyleki; sıkıştırılmış verinin ilk biti alnır. Eğer alınan bit bir kod sözcüğüne denk geliyorsa, ilgili kod sözcüğüne denk düşen karakter yerine koyulur, eğer alınan bit bir kod sözcüğü değilse sonraki bit ile birlikte ele alınır ve yeni dizinin bir kod sözcüğü olup olmadığına bakılır. Bu işlem dizinin sonuna kadar yapılır ve huffman kodu çözülür. Huffman kodları tek çözülebilir kod olduğu için bir kod dizisinden farklı semboller elde etmek olanaksızdır. Yani bir huffman kodu ancak ve ancak bir şekilde çözülebilir. Bu da aslında yazının başında belirtilen kayıpsız sıkıştırmanın bir sonucudur.

C# ile Huffman Algoritmasının Gerçekleştirilmesi

Huffman algoritması bilgisayar bilimlerinde genellikle metin dosyalarının sıkıştırılmasında kullanılır. Bu yazıda örnek bir program ile Huffman algoritmasının ne şekilde uygulanacabileceğini de göreceğiz. Dil olarak tabiki C#’ı kullanılacaktır.

Huffman algoritmasının uygulanmasındaki ilk adım frekans tablosunun bulunmasıdır. Bu yüzden bir dosyayı sıkıştırmadan önce dosyadaki her bir karakterin ne sıklıkla yer aldığını bulmak gerekir. Örnek programda sembol frekanslarını bellekte tutmak için System.Collections isim alanıdan bulunan Hashtable koleksiyon yapısı kullanılmıştır. Örneğin “c” karakterinin frekansı 47 ise,

CharFrequencies["c"] = 47

şeklinde sembolize edilir. Örnek programda frekans tablosu aşağıdaki metot ile elde edilebilir. Sıkıştırılacak dosya baştan sona taranarak frekanslar hashtable nesnesine yerleştirilir.

[Not : Örnek uygulamada kullandığım değişken ve metot isimlerini bazı özel nedenlerden dolayı İngilizce yazmak zorunda kaldığım için tüm okurlardan özür dilerim.]

private Hashtable CharFrequencies = new Hashtable();private void MakeCharFrequencies()
{
       FileStream fs = File.Open(file,FileMode.Open,FileAccess.Read);

       StreamReader sr = new StreamReader(fs,System.Text.Encoding.ASCII);

       int iChar;
       char ch;
       while((iChar = sr.Read()) != -1)
       {
              ch = (char)iChar;
              if(!CharFrequencies.ContainsKey(ch.ToString()))
              {
                     CharFrequencies.Add(ch.ToString(),1);
              }
              else
              {
                     int oldFreq = (int)CharFrequencies[ch.ToString()];
                     int newFreq;
                     newFreq = oldFreq + 1;
                     CharFrequencies[ch.ToString()] = newFreq;
              }
       }

       sr.Close();
       fs.Close();
       sr = null;
       fs = null;
}

Frekans tablosunu yukarıdaki metot yardımıyla oluşturduktan sonra huffman algoritmasının en önemli adımı olan huffman ağacının oluşturulmasına sıra geldi. Huffman ağacının oluşturulması en önemli ve en kritik noktadır. Huffman ağacı oluşturulurken Tree(ağaç) veri yapısından faydalanılmıştır. Ağaçtaki her bir düğümü temsil etmek için bir sınıf yazılmıştır. Huffman ağacındaki düğümleri temsil etmek için kullanılabilecek en basit düğüm sınıfının yapısı aşağıdaki gibidir.

Yukarıdaki düğüm yapısı algoritmayı uygulamak için gereken en temel düğüm yapısıdır. Bu yapı istenildiği gibi genişletilebilir. Önemli olan minimum gerekliliği sağlamaktır. Düğüm sınıfındaki SagDüğüm ve SolDüğüm özellikleri de düğüm türündendir. Başlangıçtaki düğümler için bu değerler null dır. Her bir yeni düğümm oluşturulduğunda yeni düğümün sağ ve sol düğümleri oluşur. Ancak unutmamak gerekir ki sadece sembollerimizi oluşturan yaprak düğümlerde bu özellikler null değerdedir.

Örnek uygulamadaki HuffmanNode sınıfında ayrıca ilgili düğümün yaprak olup olmadığını gösteren IsLeaf ve ilgili düğümün ana(parent) düğümünü gösteren HuffmanNode türünden ParentNode özellikleri bulunmaktadır.

HuffmanNode düğüm sınıfının yapısı aşağıdaki gibidir.

using System;namespace Huffman
{
     public class HuffmanNode
     {
          private HuffmanNode leftNode;
          private HuffmanNode rightNode;

          private HuffmanNode parentNode;

          private string symbol;
          private int frequency;
          private string code = “”;

          private bool isLeaf;

          public HuffmanNode LeftNode
          {
               get{return leftNode;}
               set{leftNode = value;}
          }

          public HuffmanNode RightNode
          {
               get{return rightNode;}
               set{rightNode = value;}
          }

          public HuffmanNode ParentNode
          {
               get{return parentNode;}
               set{parentNode = value;}
          }

          public string Symbol
          {
               get{return symbol;}
               set{symbol = value;}
          }

          public string Code
          {
               get{return code;}
               set{code = value;}
          }

          public int Frequency
          {
               get{return frequency;}
               set{frequency = value;}
          }

          public bool IsLeaf
          {
               get{return isLeaf;}
               set{isLeaf = value;}
          }
          public HuffmanNode()
          {

          }
     }

     public class NodeComparer : IComparer
     {
          public NodeComparer()
          {
          }

          public int Compare(object x, object y)
          {
               HuffmanNode node1 = (HuffmanNode)x;
               HuffmanNode node2 = (HuffmanNode)y;

               return node1.Frequency.CompareTo(node2.Frequency);
          }
     }
}

Başlangıçta sembol sayısı kadar HuffmanNode nesnesi yani huffman düğümü oluşturmamız gerekecektir. Oluşturulan bu düğümler Arraylist koleksiyon yapısında tutulmaktadır. Her bir yeni düğüm oluşturulduğunda Arraylist kolaksiyonun yeni bir düğüm eklenir ve iki düğüm çıkarılır. Yeni düğümün sağ ve sol düğümleri çıkarılan düğümler olacaktır. İlk oluşturulan düğümlerin sağ ve sol düğümlerinin null olduğunu hatırlayınız. Yeni düğüm eklendiğinde Arraylist sınıfının Sort() metodu yardımıyla düğümler frekanslarına göre küçükten büyüğe doğru sıralanır. Yani Arraylist elemanını ilk elemanı en küçük frekanslı düğümdür. Sort() metodu IComparer arayüzünü kullanan NodeComparer sınıfını kullanmaktadır. Yani bu sınıftaki Compare() metodu yukarıdaki kodda belirtildiği gibi düğüm nesnelerinin neye göre sıralancağını tanımlar.

Yukarıdaki tanımlar ışığında Huffman ağacı aşağıdaki metot yardımıyla bulunabilir. Dikkat ettiyseniz, metodun icrası bittiğinde Nodes koleksiyonunda tek bir düğüm nesnesi kalacaktır, bu da huffman ağacındaki kök düğümden(root node) başkası değildir.

private ArrayList Nodes;public void MakeHuffmanTree()
{
     Nodes = new ArrayList();

     //Başlangıç düğümleri oluşturuluyor.
     foreach(Object Key in CharFrequencies.Keys)
     {
          HuffmanNode node = new HuffmanNode();
          node.LeftNode = null;
          node.RightNode = null;
          node.Frequency = (int)(CharFrequencies[Key]);
          node.Symbol = (string)(Key);
          node.IsLeaf = true;
          node.ParentNode = null;

          Nodes.Add(node);
     }

     //Düğümlerin neye göre sıralanacağı NodeComparer sınıfındaki Compare metodunda tanımlıdır.
      Nodes.Sort(
new NodeComparer());

     while(Nodes.Count > 1)
     {
          HuffmanNode node1 = (HuffmanNode)Nodes[0];
          HuffmanNode node2 = (HuffmanNode)Nodes[1];

          HuffmanNode newnode1 = new HuffmanNode();
          newnode1.Frequency = node1.Frequency + node2.Frequency;
          newnode1.IsLeaf = false;
          newnode1.LeftNode = node1;
          newnode1.RightNode = node2;
          newnode1.Symbol = node1.Symbol + node2.Symbol;

          node1.ParentNode = newnode1;
          node2.ParentNode = newnode1;

          Nodes.Add(newnode1);

          Nodes.Remove(node1);
          Nodes.Remove(node2);

            Nodes.Sort(new NodeComparer());
     }
}

Algoritmanın son adımı ise Huffman kodlarının oluşturulmasıdır. Programatik olarak kodlamanın en zor olduğu bölüm burasıdır. Zira burada huffman ağacı öz-yineli(recursive) olarak taranarak her bir düğümün huffman kodu atanacaktır. Performansı artırmak için semboller ve kod sözcükleri ayrıca bir Hashtable koleksiyonunda saklanmıştır. Böylece her bir kodun çözülmesi sırasında ağaç yapısı taranmayacak, bunun yerine Hashtable’dan ilgili kod bulunacaktır. Kodlamayı yapan metot aşağıdaki gibidir.

private void FindCodeWords(HuffmanNode Node)
{
     HuffmanNode leftNode = Node.LeftNode;
     HuffmanNode rightNode = Node.RightNode;     if(leftNode == null && rightNode == null)
          Node.Code = “1″;

     if(Node == this.RootHuffmanNode)
     {
          if(leftNode != null)
               leftNode.Code = “0″;

          if(rightNode != null)
               rightNode.Code= “1″;
     }
     else
     {
          if(leftNode != null)
               leftNode.Code = leftNode.ParentNode.Code + “0″;

          if(rightNode != null)
               rightNode.Code= rightNode.ParentNode.Code + “1″;
     }
     if(leftNode != null)
     {
          if(!leftNode.IsLeaf)
                  FindCodeWords(leftNode);
          else
          {
               CodeWords.Add(leftNode.Symbol[0].ToString(),leftNode.Code);
          }
     }

     if(rightNode != null)
     {
          if(!rightNode.IsLeaf)
                  FindCodeWords(rightNode);
          else
          {
               CodeWords.Add(rightNode.Symbol[0].ToString(),rightNode.Code);
          }
     }
}

Huffman algoritmasının temel basamakları bitmiş durumda. Bundan sonraki kodlar elde edilen sıkıştırılmış verinin ne şekilde dosyaya kaydedileceği ve sıkıştırılmış dosyanın nasıl tekrar geri elde edileceği ile ilgilidir. Bu noktada uygulamamız için bir dosya formatı belirlememiz gerekmektedir. Hatırlanacağı üzere sıkıştırılmış veriyi tekrar eski haline getirmek için frekans tablosuna ihtiyacımız vardır. Dolayısıyla sıkıştırılmış veriyi dosyaya yazmadan önce dosya formatımızın kurallarına göre sembol frekanslarını dosyaya yazmamız gerekir.

Benim programı yazarken geliştirdiğim dosya formatı aşağıdaki gibidir.

dosya.huff
———–
1- ) İlk 4 byte –> Orjinal verideki toplam sembol sayısı.(ilk byte yüksek anlamlı byte olacak şekilde)
2 -) Sonraki 1 byte –> Data bölümünde bulunan son byte’taki ilk kaç bitin data’ya dahil olduğunu belirtir. (Data bölümündeki bitlerin sayısı 8′in katı olmayabilir.)
3 -) 2 byte Sembol + 4 byte sembol frekansı (bu bölüm ilk 4 byte’ta belirtilen sembol sayısı kadar tekrar edecektir.-ilk byte yüksek anlamlı byte olacak şekilde-)
4 -) Son bölümde ise sıkıştırılmış veri bit dizisi şeklinde yer alır.

Aşağıdaki metot yukarıda belirlenen kurallara göre sıkıştırılmış veriyi dosyaya .huff uzantılı olarak kaydedir.

[Not : Yazılacak programa göre bu dosya formatı değişebilir. Örneğin birden fazla dosyayı aynı anda sıkıştıran bir program için bu format tamamen değişecektir.]

public void WriteCompressedData()
{
     /// 4 byte ->(uint) Sembol Sayısı(K adet)
     /// 1 byte ->(uint) Data bölümünde bulunan son byte’taki ilk kaç bitin data’ya dahil olduğunu belirtir.
     ///
     /// ——————SEMBOL FREKANS BÖLÜMÜ——————————————
     ///
     /// 2 byte Sembol + 4 byte Sembol Frekansı
     /// .
     /// . K adet
     /// .
     /// 2 byte Sembol + 4 byte Sembol Frekansı
     ///
     ///
     /// ——————DATA BÖLÜMÜ—————————————————-
     ///
     /// Bu bölümde Sıkıştırılmış veriler byte dizisi şeklinde yer alır.
     ///
     /// Data bölümü (6*K + 6) numaralı byte’tan itibaren başlar.
     ///     int K = CharFrequencies.Keys.Count;

     /*K = 4 ise
     *
     * byte[0] = 0 0 0 0 0 1 0 0
     * byte[1] = 0 0 0 0 0 0 0 0
     * byte[2] = 0 0 0 0 0 0 0 0
     * byte[3] = 0 0 0 0 0 0 0 0
     *
     * K(bits) = 00000100 00000000 00000000 00000000
     * */

     byte[] byteK = BitConverter.GetBytes(K);

     FileStream fs = File.Open(this.NewFile,FileMode.Create,FileAccess.ReadWrite);

     byte[] CompressedByteStream = GenerateCompressedByteStream();
     WriteByteArrayToStream(byteK,fs);
     fs.WriteByte(RemainderBits);
     
     foreach(string sembol in CharFrequencies.Keys)
     {
          byte[] byteC = BitConverter.GetBytes((char)sembol[0]);
          byte[] bytesFreqs = BitConverter.GetBytes((int)CharFrequencies[sembol]);

          WriteByteArrayToStream(byteC,fs);
          WriteByteArrayToStream(bytesFreqs,fs);
     }

     WriteByteArrayToStream(CompressedByteStream,fs);

     fs.Flush();
     fs.Close();
}

Sıkıştırılan bir veri geri elde edilemediği sürece sıkıştırmanın bir anlam ifade etmeyeceği düşünülürse sıkıştırma işleminin tersinide yazmamız gerekmektedir. Sıkıştırma işlemi yaparken yaptığımız işlemlerin tersini yaparak orjinal veriyi elde edebiliriz. Sıkıştırılmış bir dosyayı açmak için aşağıdaki adımlar izlenir.

1 -) .huff uzantılı dosyadan sembol frekansları okunur ve Hashtable nesnesine yerleştirilir.
2 -) Frekans tablosu kullanılarak Huffman ağacı oluşturulur.
3 -) Huffman ağacı kullanılarak Huffman kodları oluşturulur.
4 -) .huff uzantılı dosyanın data bölümünden sıkıştırılmış veri dizisi okunarak huffman kodları ile karşılaştırılarak orjinal metin bulunur ve .txt uzantılı dosyaya yazılır.

2. ve 3. adımlardaki işlemler sıkıştırma yapıldığında kullanılan işlemler ile aynıdır. Burada ayrıca değenmeye gerek yoktur. Bu aşamada önemli olan verilen okunması ve yazılmasında izlenen yoldur. Verilerin okunmasında ve tekrar yazılmasında StringBuilder sınıfı kullanılmıştır. Bu sınıf string nesnesine göre oldukça iyi performans göstermektedir. Örneğin aynı işlemi string ile yaptığımda 75 K’lık bir dosya açma işlemi 30 sn sürerken StringBuilder kullandığımda 2 sn sürmektedir.

1.aşamdaki işlemi yapacak metot aşağıdaki gibidir.

private System.Text.StringBuilder CompressedData = new System.Text.StringBuilder(“”);private void ReadCompressedFile()
{
     FileStream fs = File.Open(file,FileMode.Open,FileAccess.Read);

     //İlk önce sembol sayısını bulalım.
     byte[] byteK = new byte[4];

     for(int i=0 ; i<4 ; i++)
     {
         byteK[i] = (byte)fs.ReadByte();
     }

     int SymbolCount = BitConverter.ToInt32(byteK,0);

     int RemainderBitCount = (byte)fs.ReadByte();

     for(int k = 0; k < SymbolCount ; k++)
     {
          //Sembollerin elde edilmesi(2 byte)
          byte[] symbolB = new byte[2];
          symbolB[0] = (byte)fs.ReadByte();
          symbolB[1] = (byte)fs.ReadByte();

          char cSymbol = BitConverter.ToChar(symbolB,0);

          //Frekans bilgisinin elde edilmesi(4 byte)
          byte[] freqB = new byte[4];
          for(int j=0 ; j<4 ; j++)
          {
               freqB[j] = (byte)fs.ReadByte();
          }

          int freq = BitConverter.ToInt32(freqB,0);

          CharFrequencies.Add(cSymbol.ToString(),freq);
     }     

     //Data bölümünün okunması
     int readByte;
     while((readByte = fs.ReadByte()) != -1)
     {
          byte b = (byte)readByte;

          bool[] bits = new bool[8];
          bits = GetBitsOfByte(b);
          BitArray ba = new BitArray(bits);

          for(int m=0 ; m < ba.Length ; m++)
          {
               if(ba[m])
                    CompressedData.Append(“1″);
               else
                    CompressedData.Append(“0″);
          }
     }

     //Son byte’taki fazla veriler atılıyor
     int removingBits = 8 – RemainderBitCount;

     if(RemainderBitCount != 0)
     {
          CompressedData.Remove(CompressedData.Length – removingBits, removingBits);
     }

     fs.Flush();
     fs.Close();
}

Dosyayı açarken son yapılması gereken adım okunan verilerin dosyaya yazılmasıdır. Bir önceki adımda okunan veriler aşağıdaki metot aracılığıyla yeni bir dosyaya kaydedilir.

System.Text.StringBuilder OriginalData = new System.Text.StringBuilder(“”);private void WriteOrginalData()
{
     FileStream fs = File.Open(this.NewFile,FileMode.Create,FileAccess.ReadWrite);

     StreamWriter sw = new StreamWriter(fs,System.Text.Encoding.ASCII);

     System.Text.StringBuilder sb = new System.Text.StringBuilder(“”);

     //Hashtable’ın performansından faydalanmak için kod sözcükleri ters çevrilip yeni bir hashtable oluşturuluyor.
     ReverseCodeWordsHashtable();

     for(int i=0 ; i < CompressedData.Length ; i++)
     {
          sb.Append(CompressedData[i]);

          string symbol = “”;
          bool found = false;

          if(CodeWords.ContainsValue(sb.ToString()))
          {
               symbol = (string)ReversedCodeWords[sb.ToString()];
               found = true;
          }

          if(found)
          {
               OriginalData.Append(symbol);
               sb.Remove(0,sb.Length);
          }
     }

     sw.Write(OriginalData.ToString());

     sw.Flush();
     fs.Flush();
     sw.Close();
     fs.Close();
}

Yukarıdaki metotların tamamı Huffman isimli bir metotta toplanmıştır. Dosya sıkıştırma ve dosya açma için farklı nesnelerin kullanılması gerekmektedir. Huffman sınıfının örnek kullanımı aşağıda verilmiştir.

//Sıkıştırma
Huffman hf1 = new Huffman(“deneme.txt”);
hf1.Compress();//Açma
Huffman hf2 = new Huffman(“deneme.huff”);
hf2.Decompress();

Uygulamayı 75 K’lık bir İngilizce metin tabanlı dosya üzerinde çalıştırdığımda dosyanın 43 K’ya düştüğünü aşağıdaki gibi gözlemledim.

Umarım faydalı bir prgram ve faydalı bir yazı olmuştur.

Uygulamanın bütün kodlarını ve proje dosyasını indirmek için tıklayınız.

 

Not : Bu makalenin son cümlesini yazdığımda programın açma modülünde Türkçe karakterler ile ilgili bir bug’ın olduğunu farkettim. Yani şu anda herhangi bir türkçe karakter içeren dosyayı sıkıştırıp dosyayı tekrar açtığınızda Türkçe karakterler yerine “?” karakterini görecekseniz. En kısa zamanda bug’ı düzeltip kaynak kodları yeniden yükleyeceğim. Bug’ın nedenini bulup düzelten kişiye süpriz bir ödül vereceğimide belirtmek isterim.

 

 

 

 

 

 

 

 

Bir Arayüz, Bir Sınıf ve Bir Tablo

Bugünkü makalemizde, bir arayüzü uygulayan sınıf nesnelerinden faydalanarak, bir Sql tablosundan nasıl veri okuyacağımızı ve değişiklikleri veritabanına nasıl göndereceğimizi incelemeye çalışacağız. Geliştireceğimiz örnek, arayüzlerin nasıl oluşturulduğu ve bir sınıfa nasıl uygulandığını incelemekle yetinmeyecek, Sql veritabanımızdaki bir tablodaki belli bir kayda ait verilerin bu sınıf nesnelerine nasıl aktarılacağını da işleyecek. Kısacası uygulamamız, hem arayüzlerin hem sınıfların hemde Sql nesnelerinin kısa bir tekrarı olucak.

Öncelikle uygulamamızın amacından bahsedelim. Uygulamamızı bir Windows uygulaması şeklinde geliştireceğiz. Kullanacağımız veri tablosunda arkadaşlarımızla ilgili bir kaç veriyi tutuyor olacağız. Kullanıcı, Windows formunda, bu tablodaki alanlar için Primary Key niteliği taşıyan bir ID değerini girerek, buna karşılık gelen tablo satırına ait verilerini elde edicek. İstediği değişiklikleri yaptıktan sonra ise bu değişiklikleri tekrar veritabanına gönderecek. Burada kullanacağımız teknik makalemizin esas amacı olucak. Bu kez veri tablosundan çekip aldığımız veri satırının programdaki eşdeğeri, oluşturacağımız sınıf nesnesi olucak. Bu sınıfımız ise, yazmış olduğumuz arayüzü uygulayan bir sınıf olucak. Veriler sınıf nesnesine, satırdaki her bir alan değeri, aynı isimli özelliğe denk gelicek şekilde yüklenecek. Yapılan değişiklikler yine bu sınıf nesnesinin özelliklerinin sahip olduğu değerlerin veri tablosuna gönderilmesi ile gerçekleştirilecek.

Uygulamamızda, verileri Sql veritabanından çekmek için, SqlClient isim uzayında yer alan SqlConnection ve SqlDataReader nesnelerini kullanacağız. Hatırlayacağınız gibi SqlConnection nesnesi ile , bağlanmak istediğimiz veritabanına, bu veritabanının bulunduğu sunucu üzerinden bir bağlantı tanımlıyoruz. SqlDataReader nesnemiz ile de, sadece ileri yönlü ve yanlız okunabilir bir veri akımı sağlayarak, aradığımız kayda ait verilerin elde edilmesini sağlıyoruz. Şimdi uygulamamızı geliştirmeye başlayalım. Öncelikle vs.net ortamında bir Windows Application oluşturalım. Burada aşağıdaki gibi bir form tasarlayalım.

 

Şekil 1. Form tasarımımız.

Kullanıcı bilgilerini edinmek istediği kişinin ID’nosunu girdikten sonra, Getir başlıklı butona tıklayarak ilgili satırın tüm alanlarına ait verileri getirecek. Ayrıca, kullanıcı veriler üzerinde değişiklik yapabilecek ve bunlarıda Güncelle başlıklı butona tıklayarak Sql veritabanındaki tablomuza aktarabilecek. Sql veritabanında yer alan Kisiler isimli tablomuzun yapısı aşağıdaki gibidir.

 

Şekil 2. Tablomuzun yapısı.

Şimdi gelelim işin en önemli ve anahtar kısımlarına. Program kodlarımız. Öncelikle arayüzümüzü tasarlayalım. Arayüzümüz, sonra oluşturacağımız sınıf için bir rehber olucak. Sınıfımız, veri tablomuzdaki alanları birer özellik olarak taşıyacağına göre arayüzümüzde bu özellik tanımlarının yer alması gerektiğini söyleyebiliriz. Ayrıca ilgili kişiye ait verileri getirecek bir metodumuzda olmalıdır. Elbette bu arayüze başka amaçlar için üye tanımlamalarıda ekleyebiliriz. Bu konuda tek sınır bizim hayal gücümüz. İşin gerçeği bu makalemizde hayal gücümü biraz kısdım konunun daha fazla dağılmaması amacıyla :) . (Ama siz, örneğin kullanıcının yeni girdiği verileri veritabanına yazıcak bir metod tanımınıda bir üye olarak ekleyebilir ve gerekli kodlamaları yapabilirsiniz.) İşte arayüzümüzün kodları.

public interface IKisi{

     /* Öncelikle tablomuzdaki her alana karşılık gelen özellikler için tanımlamalarımızı yapıyoruz.*/

     int KisiID /* KisiID, tablomuzda otomatik artan ve primary key olan bir alandır. Dolayısıyla programcının var olan bir KisiID’sini değiştirmemesi gerekir. Bu nedenle sadece okunabilir bir özellik olarak tanımlanmasına izin veriyoruz. */

     {

          get;

     }

     string Ad /* Tablomuzdaki char tipindeki Ad alanımız için string tipte bir alan.*/

     {

          get;set;

     }

     string Soyad

     {

          get;set;

     }

     DateTime DogumTarihi /* Tablomuzda, DogumTarihi alanımız datetime tipinde olduğundan, DateTime tipinde bir özellik tanımlanmasına izin veriyoruz.*/

     {

          get;set;

     }

     string Meslek

     {

          get;set;

     }

     void Bul(int KID); /* Bul metod, KID parametresine göre, tablodan ilgili satıra ait verileri alıcak ve alanlara karşılık gelen özelliklere atayacak metodumuzdur.*/

}

Şimdide bu arayüzümüzü uygulayacağımız sınıfımızı oluşturalım. Sınıfımız IKisi arayüzünde tanımlanan her üyeyi uygulamak zorundadır. Bu bildiğiniz gibi arayüzlerin bir özelliğidir.

public class CKisi:IKisi /* IKisi arayüzünü uyguluyoruz.*/{

     /* Öncelikle sınıftaki özelliklerimiz için, verilerin tutulacağı alanları tanımlıyoruz.*/

     private int kisiID;

     private string ad;

     private string soyad;

     private DateTime dogumTarihi;

     private string meslek;

     /* Arayüzümüzde yer alan üyeleri uygulamaya başlıyoruz.*/

     public int KisiID

     {

          get{ return kisiID;}

     }

     public string Ad

     {

          get{return ad;}set{ad=value;}

     }

     public string Soyad

     {

          get{return soyad;}set{soyad=value;}

     }

     public DateTime DogumTarihi

     {

          get{return dogumTarihi;}set{dogumTarihi=value;}

     }

     public string Meslek

     {

          get{return meslek;}set{meslek=value;}

     }

     public void Bul(int KID)

     {

          /* Öncelikle Sql Veritabanımıza bir bağlantı açıyoruz.*/

          SqlConnection conFriends=new SqlConnection(“data source=localhost;integrated security=sspi;initial catalog=Friends”);

          /* Tablomuzdan, kullanıcının bu metoda parametre olarak gönderdiği KID değerini baz alarak, ilgili KisiID’ye ait verileri elde edicek sql kodunu yazıyoruz.*/

          string sorgu=”Select * From Kisiler Where KisiID=”+KID.ToString();

          /* SqlCommand nesnemiz yardımıyla sql sorgumuzu çalıştırılmak üzere hazırlıyoruz.*/

          SqlCommand cmd=new SqlCommand(sorgu,conFriends);

          SqlDataReader rd;/* SqlDataReader nesnemizi yaratıyoruz.*/

          conFriends.Open(); /* Bağlantımızı açıyoruz. */

          rd=cmd.ExecuteReader(CommandBehavior.CloseConnection); /* ExecuteReader ile sql sorgumuzu çalıştırıyoruz ve sonuç kümesi ile SqlDataReader nesnemiz arasında bir akım(stream) açıyoruz. CommandBehavior.CloseConnection sayesinde, SqlDataReader nesnemizi kapattığımızda, SqlConnection nesnemizinde otomatik olarak kapanmasını sağlıyoruz.*/

          while(rd.Read())

          {

               /* Eğer ilgili KisiID’ye ait bir veri satırı bulunursa, SqlDataReader nesnemizin Read metodu sayesinde, bu satıra ait verileri sınıfımızın ilgili alanlarına aktarıyoruz. Böylece, bu alanların atandığı sınıf özellikleride bu veriler ile dolmuş oluyor.*/

               kisiID=(int)rd["KisiID"];

               ad=rd["Ad"].ToString();   

               soyad=rd["Soyad"].ToString();

               dogumTarihi=(DateTime)rd["DogumTarihi"];

               meslek=rd["Meslek"].ToString();

          }

          rd.Close();

     }

     public CKisi()

     {

     }

}

Artık IKisi arayüzünü uygulayan, CKisi isimli bir sınıfımız var.Şimdi Formumuzun kodlarını yazmaya başlayabiliriz. Öncelikle module düzeyinde bir CKisi sınıf nesnesi tanımlayalım.

CKisi kisi=new CKisi();

Bu nesnemiz veri tablosundan çektiğimiz veri satırına ait verileri taşıyacak. Kullanıcı Getir başlıklı button kontrolüne bastığında olucak olayları gerçekleştirecek kodları yazalım.

private void btnGetir_Click(object sender, System.EventArgs e){

     int.ToInt32(txtKisiID.Text.ToString()); /* Kullanıcının TextBox kontrolüne girdiği ID değeri Convert sınıfının ToInt32 metodu ile Integer’a çeviriyoruz.*/

     kisi.Bul(id); /* Kisi isimli CKisi sınıfından nesne örneğimizin Bul metodunu çağırıyoruz.*/

     Doldur(); /* Doldur Metodu, kisi nesnesinin özellik değerlerini, Formumuzdaki ilgili kontrollere alarak, bir nevi veri bağlama işlemini gerçekleştirmiş oluyor.*/

}

Şimdide Doldur metodumuzun kodlarını yazalım.

public void Doldur(){

     txtAd.Text=kisi.Ad.ToString(); /* txtAd kontrolüne, kisi nesnemizin Ad özelliğinin şu anki değeri yükleniyor. Yani ilgili veri satırının ilgili alanı bu kontrole bağlamış oluyor.*/

     txtSoyad.Text=kisi.Soyad.ToString();

     txtMeslek.Text=kisi.Meslek.ToString();

     txtDogumTarihi.Text=kisi.DogumTarihi.ToShortDateString();

     lblKisiID.Text=kisi.KisiID.ToString();

}

Evet görüldüğü gibi artık aradığımız kişiye ait verileri formumuzdaki kontrollere yükleyebiliyoruz. Şimdi TextBox kontrollerimizin TextChanged olaylarını kodlayacağız. Burada amacımız, TextBox’larda meydana gelen değişikliklerin anında, CKisi sınıfından türettiğimiz Kisi nesnesinin ilgili özelliklerine yansıtılabilmesi. Böylece yapılan değişiklikler anında nesnemize yansıyacak. Bu nedenle aşağıdaki kodları ekliyoruz.

/* Metodumuz bir switch case ifadesi ile, aldığı ozellikAdi parametresine göre, CKisi isimli sınıfımıza ait Kisi nesne örneğinin ilgili özelliklerini değiştiriyor.*/public void Degistir(string ozellikAdi,string veri)

{

     switch(ozellikAdi)

     {

          case “Ad”:

          {

               kisi.Ad=veri;

               break;

          }

          case “Soyad”:

          {

               kisi.Soyad=veri;

               break;

          }

          case “Meslek”:

          {

               kisi.Meslek=veri;

               break;

          }

          case “DogumTarihi”:

          {

               kisi.DogumTarihi=Convert.ToDateTime(veri);

               break;

          }

     }

}

private void txtAd_TextChanged(object sender, System.EventArgs e)

{

     Degistir(“Ad”,txtAd.Text);

}

private void txtSoyad_TextChanged(object sender, System.EventArgs e)

{

     Degistir(“Soyad”,txtSoyad.Text);

}

private void txtDogumTarihi_TextChanged(object sender, System.EventArgs e)

{

     Degistir(“DogumTarihi”,txtDogumTarihi.Text.ToString());

}

private void txtMeslek_TextChanged(object sender, System.EventArgs e)

{

     Degistir(“Meslek”,txtMeslek.Text);

}

private void btnGuncelle_Click(object sender, System.EventArgs e)

{

     int id;

    .ToInt32(lblKisiID.Text.ToString());

     Guncelle(id);

}

Görüldüğü gibi kodlarımız gayet basit. Şimdi güncelleme işlemlerimizi gerçekleştireceğimiz kodları yazalım. Kullanıcımız, TextBox kontrollerinde yaptığı değişikliklerin veritabanınada yansıtılmasını istiyorsa Guncelle başlıklı button kontrolüne tıklayacaktır. İşte kodlarımız.

private void btnGuncelle_Click(object sender, System.EventArgs e){

     /* Güncelleme işlemi, şu anda ekranda olan Kişi için yapılacağından, bu kişiye ait KisiID sini ilgili Label konrolümüzden alıyoruz ve Guncelle isimli metodumuza parametre olarak gönderiyoruz. Asıl güncelleme işlemi Guncelle isimli metodumuzda yapılıyor. */

     int id;

    .ToInt32(lblKisiID.Text.ToString());

     Guncelle(id);

}

public void Guncelle(int ID)

{

     /* Sql Server’ımıza bağlantımızı oluşturuyoruz.*/

     SqlConnection conFriends=new SqlConnection(“data source=localhost;integrated security=sspi;initial catalog=Friends”);

     /* Update sorgumuzu oluşturuyoruz. Dikkat edicek olursanız alanlara atanacak değerler, kisi isimli nesnemizin özelliklerinin değerleridir. Bu özellik değerleri ise, TextBox kontrollerinin TextChanged olaylarına ekldeğimiz kodlar ile sürekli güncel tutulmaktadır. En ufak bir değişiklik dahi buraya yansıyabilecektir.*/

     string sorgu=”Update Kisiler Set Ad=’”+kisi.Ad+”’,Soyad=’”+kisi.Soyad+”’,Meslek=’”+kisi.Meslek+”’,DogumTarihi=’”+kisi.DogumTarihi.ToShortDateString()+”’ Where KisiID=”+ID;

     SqlCommand cmd=new SqlCommand(sorgu,conFriends); /* SqlCommand nesnemizi sql cümleciğimiz ve geçerli bağlantımız ile oluşturuyoruz. */

     conFriends.Open(); /* Bağlantımızı açıyoruz.*/

     try

     {

          cmd.ExecuteNonQuery(); /* Komutumuzu çalıştırıyoruz.*/

     }

     catch

     {

          MessageBox.Show(“Başarısız”);

     }

     finally /* Update işlemi herhangibir neden ile başarısız olsada, olmasada sonuç olarak(finally) açık olan SqlConnection bağlanıtımızı kapatıyoruz. */

     {

          conFriends.Close();

     }

}

İşte uygulama kodlarımız bu kadar. Şimdi gelin uygulamamızı çalıştırıp deneyelim. Öncelikle KisiID değeri 1000 olan satıra ait verileri getirelim.

 

Şekil 3. KisiID=1000 Kaydına ait veriler Kisi nesnemize yüklenir.

Şimdi verilerde bir kaç değişiklik yapalım ve güncelleyelim. Ben Ad alanında yer alan “S.” değerini “Selim” olarak değiştirdim. Bu durum sonucunda yapılan değişikliklerin veritabanına yazılıp yazılmadığını ister programımızdan tekrar 1000 nolu satırı getirerek bakabiliriz istersekde Sql Server’dan direkt olarak bakabiliriz. İşte sonuçlar.

 

Şekil 4. Güncelleme işleminin sonucu.

Programımız elbette gelişmeye çok, ama çok açık. Örneğin kodumuzda hata denetimi yapmadığımız bir çok ölü nokta var. Bunların geliştirilmesini siz değerli okurlarımıza bırakıyorum. Bu makalemizde özetle, bir arayüzü bir sınıfa nasıl uyguladığımızı, bu arayüzü nasıl yazdığımızı hatırlamaya çalıştık. Ayrıca, sınıfımıza ait bir nesne örneğine, bir tablodaki belli bir veri satırına ait verileri nasıl alabileceğimizi, bu nesne özelliklerinde yaptığımız değişiklikleri tekrar nasıl veri tablosuna gönderebileceğimizi inceledik. Böylece geldik bir makalemizin daha sonuna. Bir sonraki makalemizde görüşmek dileğiyle hepinize mutlu günler dilerim.

Yazar Hakkında

Hakkında: editor
Kimlik kartı

Bir Cevap Yaz

kendi isteğimle kurallara uygun yazıyorum. (Lütfen yandaki kutuyu işaretleyin.)

Otomatik robotlara karşı soru.