> Delphi > Stream’de Uzmanlaşalım… (Bölüm 1)

Stream’de Uzmanlaşalım… (Bölüm 1)

Diyezon’a yazmayalı baya bir süre olmuştu. Neredeyse diyezon, kendi işlerim yüzünden hayalet site olma yolunda ilerliyordu. Bundan önceki son yazımı yaklaşık bir sene önce yayınlamıştım. Her neyse…

Stream’ler hakkında bir makale yazmak, yapacaklarım arasındaydı. En son gelen bir istek üzerine stream konusuna el atalım istedim. Bu makale serimizde hem VCL’de bulunan TStream, hem de .NET’de bulunan Stream sınıfları ve kullanımlarından bahsetmeye çalışacağım.

Özellikle TStream üzerinde stringlerle uğraşmak gibi bir çok konu programcıların kafasını karıştırabilmektedir. Bu ve bunun gibi sorunların çözümlerine de çareler bulmaya çalışacağız.

Stream Nedir?

Bilgisayar ortamında yazma-okuma işlemlerinden bahsettiğimizde aklımıza bir çok şey gelir. Mesela disk üzerinde bir dosyaya erişip üzerinde yazma ve okuma işlemleri yapabiliriz. Veya bir hafıza bölgesinde yazma-okuma işlemleri yapabileceğimiz gibi bir veritabanındaki bir tablo alanında veya daha başka çeşit çeşit ortamlar üzerinde yazma-okuma işlemleri yapabiliriz. Bu bütün yazma-okuma işlemleri aslında her zaman yöntemi, işleyişi aynı olan bir fonksiyonlar dizisidir. Sadece alt planda yazma-okuma işleminin yapılacağı yüzeyde(disk, hafıza…) farklılıklar oluşmaktadır.

İşte bu işlemleri kolay bir şekilde, ortak bir sınıf üzerinden yapmak istediğimiz zaman ortaya Stream gibi bir yapı çıkıyor. Stream nesnelerini kullanarak, bir dosyayı yazıp okuyabilir, aynı fonksiyonları kullanarak bir hafıza bölgesine ulaşılabilir veya yine aynı fonksiyonlar ile bir veritabanı tablosunun alanına ulaşılabilir.

Stream, burada anlatılan basit özelliklerin yanında, stream verileri üzerinde bir çok işlemi yapabilmemize imkan sağlamaktadır. Bu makalemizde bunları ayrıntısı ile görmeye çalışacağız.

Stream Verileri

Stream nesneleri, stream verilerini kullanmamızı sağlamaktadır. Stream verileri, üzerinde çalışılan yere göre çeşitli veriler içermektedir. Bu bir resim dosyası olabileceği gibi kendimize özel çok farklı bir format da olabilir.

Buradan da anlaşılacağı gibi, stream verileri bir nevi bir byte dizisidir. Yani aynı veriyi "array of byte" şeklinde kullanarak da oluşturabilir ve üzerinde işlem yapabiliriz. Fakat bu, bize ek masraftan başka bir şeye yaramayacaktır. Üstelik yazacağımız fonksiyon ya da sınıf stream nesneleri gib esnek ve hızlı olmayacaktır.

Stream Ne Zaman Kullanılmalı?

Yapacağımız işlemerde stream mi, array mi kullanılmalı noktasında karar vermek istediğimizde bir kaç hususu göz önünde bulundurmamız gerekecektir.

İlk olarak bilmemiz gereken stream verileri array’de olduğu gibi indeksli değildir. Eğer kullanacağımız veriler, sabit veri tipi ise ve bir for döngüsüne girmek üzere oluşturulacak ise kullanacağımız nesne array olacaktır. Stream’de de istediğiniz veriye array’de olduğu gibi ulaşabilirsiniz. Fakat bu ileride de göreceğimiz gibi array indekslerinden farklı olarak çalışmaktadır.

Eğer bu veri dizimiz bir disk veya bir hafıza bölgesi üzerine yazılacaksa ve burada işlem yapılacaksa, bu durumda array yerine stream kullanılması bize bir çok kolaylık sunacaktır.

Aynı şekilde veri dizimiz sabit veri tipleri içermiyorsa yani sadece array of string veya sadece array of Integer değilse bu durumda yine stream kullanmalıyız. Mesela veri dizimizin birinci elemanı 4 karakterlik bir string, ikinci elmanı bir Integer, üçüncü elemanı bir Boolean, vs… gibi. Bu şekilde kendi dosya formatımızı oluşturabileceğimiz gibi, doc, exe, zip gibi bir çok binary dosyasının yapısını bildiğimizde bu dosyaların verilerine de ulaşabiliriz.

VCL’ de Stream Kullanımı

İlk başta VCL ile stream kullanımına değinelim. Ardında .Net üzerinde stream kullanımını göreceğiz.

VCL ile stream işlemleri yapabilmek için TStream sınıfından türeyen bir çok sınıf mevcuttur. TStream sınıfı abstract olarak tanımlanmış bir sınıftır ve direk olarak kullanılamaz. Abstract olarak oluşturulmuş olan bir sınıftan nesne oluşturulamaz. Ancak bu abstract sınıftan türemiş olan abstract olmayan sınıflardan nesne oluşturulabilir. Bu şekilde bir yapı olması, tüm stream sınıflarının aynı olan bir çok metod ve özelliğe sahip olmasını sağlar. Abstract sınıfların avantajları burada anlatılacak kadar kısa değil. Bu yüzden şimdilik bu kadar bilmemiz yeterli.

Help dosyasına göz attığımızda TStream sınıfından türemiş bir çok stream sınıfını görüyoruz.

TFileStream: Disk yani dosya sistemi üzerinde bulunan dosyalarla işlem yapmamızı sağlar.

TMemoryStream: Dosyalar üzerinde yapılan aynı işlemleri bir hafıza bölgesi üzerinde yapmamızı sağlar.

TBlobStream: Veritabanında bulunan bir tablodaki BLOB yani binary veri olarak tanımlanmış alanlar üzerinde okuma-yazma işlemleri yapmamızı sağlar.

TResourceStream: Resource dediğimiz, exe, dll gibi çalıştırılabilir dosyalar içine gömülü olan resim, yazı, dialog kutuları, versiyon bilgisi gibi çok çeşitli verileri okuyup yazmamıza imkan sağlamaktadır.

TStringStream: Aynı işlemleri bir string verisi üzerinde işlem yapabilmemizi sağlar. Böylece, sınırsız uzunlukta bir string üzerinde stream ile yapılabilecek her türlü işlemler yapılabilir.

Bunlar, temelde kullanılan stream çeşitleridir. Bunun gibi özel olarak tasarlanmış bir çok stream sınıfı da olabilir. Fakat hepsi TStream sınıfından türediği için, kullanımları da aynı olacaktır.

Dosyalar Üzerinde Stream İşlemleri

Dosyalar ile uğraşırken TFileStream kullanıldığını söylemiştik. TStream sınıflarının nasıl kullanıldığını, sonucu kolay görme açısından ilk başta dosyalar üzerinde görelim. İleride diğer stream çeşitlerine de değineceğiz.



		

Bu kodları bir button’nun OnClick olayına yerleştirip deneyelim. Kodlar çalıştırıldığında, C: üzerinde eğer bir erişim ihlali yoksa deneme.txt isminde bir dosya göreceğiz.

İlk bakışta bu kodlar ürkütücü gelebilir. Fakat konular ilerledikçe stream kullanmanın ne kadar rahat olduğunu göreceğiz.

AFile‘ın oluşturulduğu satıra bakalım. Bir TFileStream nesnesi oluşturabilmeniz için iki adet parametreye ihtiyacınız var. Birincisi dosyanın yolu, ki bizim örneğimizde "deneme.txt" olarak verilmiş. İkinci parametremiz ise bu dosya üzerinde yapacağımız işlem kipini ve paylaşım kipini belirler. İşlem ve paylaşım kipleri "or" kelimesi ile birleştirilerek kullanılır. Şimdi bu kiplerin ne olduğuna bakalım:

İşlem Kipleri:

fmCreate: Verilen dosya yolunda eğer dosya yoksa oluşturur. Eğer dosya var ise üzerine yazar.

fmOpenRead: Dosyayı sadece okuma amaçlı açar.

fmOpenWrite: Dosyayı sadece yazma amaçlı açar.

fmOpenReadWrite: Dosyayı hem okuma hem de yazma için açar.

Paylaşım Kipleri:

fmShareExclusive: Diğer uygulamaların, bizim üzerinde çalıştığımız dosya üzerinde hem okuma hem de yazma işlemlerini yasaklar.

fmShareDenyWrite: Diğer uygulamalar dosyayı okuyabilir fakat üzerinde yazma işlemi yapamazlar.

fmShareDenyRead: Diğer uygulamaların, dosya üzerinde okuma işlemi yapmasını yasaklar.

fmShareDenyNone: Ne okuma ne de yazma yasağı koyar. Yani diğer uygulamalar, dosya üzerinde yazma ve okuma yapabilirler.

fmShareCompat: Eskiden dos zamanlarında kullanılan FCB(File Control Block) ile uyumluluğu sağlamak için kullanılır. Windows üzerinde fmShareExclusive, Linux üzerinde ise fmShareDenyNone ile aynıdır. Bu yüzden bunu es geçebilirsiniz.

Yukarıda verdiğimiz örnekte olduğu gibi işlem kipleri ve paylaşım kipleri "or" kelimesi ile birleştirilerek kullanılır. Örneğimizde dosya fmCreate ile eğer yoksa oluşturulacak ve eğer varsa üzerine yazılacaktır. Ve dosya fmShareDenyNone kipinde olduğu için üzerinde herhangi bir okuma yazma yasağı bulumamaktadır.

Ardından try..finally buloğunda, stream üzerine yazma işlemleri yapılmaktadır. Şimdilik bu kısmı anlatmayacağız. Çünkü okuma ve yazma işlemleri için ayrı bir konu başlığımız bulunmaktadır.

finally bloğunda gördüğümüz Free metodu dosyalar üzerinde fazladan bir önem arz etmektedir. Çünkü bir kez TFileStream ile bir dosyaya erişildiğinde, bu stream nesnesi Free oluncaya yani yok edilinceye kadar, dosyanın handle’ı işletim sistemi tarafından tutulur. Dosya handle’ı aktif olduğu sürece dosyanın silinmesi taşınması işletim sistemi tarafından engellenir. Ve genelde herkesin karşılaştığı "Bu dosya bir uygulama tarafından kullanıyor." gibi bir mesaj alırız. Bunun için unlocker, process explorer gibi harici programlar ile aktif kalan bu handle’lar kapatılarak dosyalar silinebilir. Bu yüzden okuma yazma işlemlerimiz biter bitmez stream nesnemizi Free yapmalıyız.

Kısaca bir stream nesnesini oluşturmak için aşağıdaki şablonu kullanmalıyız:



		

Stream Üzerinde Okuma-Yazma İşlemleri

Stream’ler üzerinde yazma işlemleri için başlıca Write ve WriteBuffer metodları ve okuma işlemleri için de Read ve ReadBuffer metodları tanımlanmıştır. Şimdi göreceğimiz işlemler Win32‘ye göre anlatılacaktır. İleride ayrı bir başlık halinde TStream sınıfının .NET üzerinde nasıl yazma okuma yapabileceğiniz de göreceğiz. Ayrıca .NET içinde bulunan Stream sınıflarına değineceğiz.

Stream üzerinde yapılan okuma yazma işlemleri bir işaretçi vasıtası ile yapılır. Bu işaretçi hangi pozisyonda ise o pozisyonda okuma ve yazma işlemi yapılır. İlk stream nesnesi oluşturulduğunda işaretçinin pozisyonu sıfırdır. Bu metodlar çağrıldığı vakit, yani her okuma ve yazma işleminin sonunda stream işaretçisi bir sonraki pozisyona kayar. Stream işaretçisi, stream verisinin sonuna gelip gelmediğini test etmek için Eof metodu kullanılır.Bu paragraf streamlerin çalışma mantığının anlaşılması için önemlidir. Bu yüzden dönüp ikinci bir defa okumanızda fayda var.

Sonunda Buffer yazan metodların yazmayanlardan farkından söz etmek istiyorum. Tek fark, "istisna" (Exception) yakalama özelliğidir. Yani mesela ReadBuffer metodu yine alt planda Read metodunu çalıştırır. Fakat Read metodu bir sorunla karşılaşır ve okuyamaz ise, ReadBuffer metodu, EReadError istisnasını yollar. Aynı şekilde WriteBuffer da sorun yaşadığında EWriteError istisnasını yollar. Böylece okuma ve yazma işlemlerinde herhangi bir hata olup olmadığının tespiti için ReadBuffer ve WriteBuffer ile beraber istisnaların yaklanması kafidir.

Okuma ve yazma metodları, işlemlerini bir buffer aracılığı ile gerçekleştirir. Bu buffer bir string olabilceği gibi bir Integer, bir record dahi olabilir. Bu yüzden her veri tipi için okuma ve yazma işlemlerinin nasıl olduğunu ayrı ayrı incelemeye çalışalım.

Sayısal Değelerin Okunması ve Yazılması

Integer, Byte, Double gibi sayısal verileri okuyup yazmak diğerlerine nazaran en basit işlemdir. Ekstra bir çevirim yapmamıza gerek kalmamaktadır.

Bunu bir örnek üzerinde görelim. İki adet button yerleştirelim. Birinin Caption özelliği "Yaz", diğerinin Caption özelliği de "Oku" olsun. "Yaz" butonuna çift tıklayalım ve şunları yazalım:



		

Gördüğünüz gibi bir TFileStream nesnesi oluşturduk ve bu stream nesnesini kullanarak dosyaya bir takım sayısal değerler yazdırdık. Mesela ilk yazdırma örneğimizde buffer, Integer tipinde bir değişkendir. Bu Integer değeri yazdırırken ne kadar yazdırılması gerektiğini WriteBuffer metodunun ikinci parametresi olarak giriyoruz. Yani yazdıracağımız bu buffer kaç byte yer tutacak, onu ikinci parametre olarak giriyoruz. Örneğimizde, bu parametre olarak Integer‘ın boyutunu giriyoruz. Bunun için SizeOf fonksiyonundan faydalanıyoruz. Help dosyasına baktığımızda, Integer‘ın 4 byte yer kapladığını görürüz. Burada yaptığımız işlem dosyada 4 byte’lık bir alana Integer değeri yazdırmaktan ibarettir.

Ardından gelen yazma işlemleri de aynı mantık ile yapılmaktadır. Burada dikkat etmemiz gereken nokta ikinci parametredir. Çünkü bu parametre yanlış girilirse hem yazmada hem de okumada eksik ve yanlış verilerle karşılaşırız. Bu örnekten de anladığımız gibi sayısal veriler ile uğraşırken SizeOf(DeğişkeTipi) şeklinde ikinci parametreyi belirlememiz yeterlidir.

Şimdi bu programı çalıştırıp Yaz butonuna basalım. Diskin C: bölümünde deneme.bin isimli bir dosya oluşmuş olmalı. Herhangi bir hex editor ile (HxD) bu dosyayı açıp inceleyebilirsiniz. Eğer ayrıntısına girmek isterseniz Integer ve diğer sayısal değerlerin nasıl yazıldığını inceleyebilirsiniz. Mesela demiştik ki dosyamızın ilk 4 byte’ında bir Integer bulunmaktadır. Dosyamızı bir hex editör ile açtığımızda ilk 4 byte’ında 98 3A 00 00 yazdığını görürüz. IBM (x86) tabanlı makinelerde bu veriler tersten yazılırlar. Bu yüzden bu ifadeyi 00 00 3A 98 olarak düşüneceğiz. Hesap makinesini açıp 16’lık Hex moda geçelim. Tabi bunu yapabilmek için bilimsel moda geçmeniz gerekmektedir. Hex modu seçili iken 3A98‘i yazalım. Ardından Dec yani ondalık olarak her zaman kullandığınız sayı formatına çevirelim. Ve karşımızda 15000 sayısını göreceğiz ki bu değer örneğimizde Integer olarak dosyanın baş tarafına yazdırılmıştı. Diğer veri tiplerinin nasıl kaydedildiğini bu şekilde inceleyerek bulabilirsiniz.

Şimdi gelelim bu değerleri okumaya… Oku butonumuza çift tıklayıp şu kodları girelim:



		


Şimdi bu programı çalıştıralım ve bu sefer oku butonuna basalım. Ve karşımızda dosyaya yazdırdığımız değerler…

Gördüğünüz gibi okuma işlemi yazma işleminden farklı değil. İhtiyacımız olan tek şey bir buffer ve bu buffer’ın tipinin boyutu. Eğer değişken tipinin boyutunu yanlış girersek veya okuma sırasını yer değiştirirsek, istediğimiz sonucu alamayız. Çünkü stream üzerine yazarken, bu sırada ve bu değişken tipleri ile yazılmıştır. Okurken de aynı sıra ve uzunlukta okunmalıdır. Bu yüzden, bu şekilde kendi dosya formatlarımızı oluştururken, bir plan üzerinde dosya formatında hangi pozisyonda hangi veri bulunacağı ve boyutu belirlenmelidir.

Gördüğünüz gibi sayısal verileri okumak ve yazmak hiç de zor değil. Şimdi dilerseniz karakter dizilerini nasıl okuyup yazacağımızı görelim.

Karakter Dizilerinin Okunup Yazılması

Karakter dizisi dediğimizde aklımıza gelen ilk değişken tipleri string ve PChar‘dır. Eğer Delphi 2009’dan önceki bir sürüm kullanıyorsanız bu değişken tipleri ANSI‘dir. Yani her bir karakter bir byte yer kaplar. D2009 ve sonrası için her bir karakter Unicode olduğu için 2 byte yer kaplarlar.

İlk önce PChar verilerini nasıl yazıyoruz ve okuyoruz onu görelim:



		


Karakter verililerini yazdırırken ve okurken dikkat etmemiz gereken nokta yine verinin kapladığı alandır. Karakter dizisinin kapladığı alanı hesaplayabilmek için bir karakterin ne kadar yer kapladığını bilmemiz gereklidir. Önceden de dediğim gibi, eğer karakterleriniz ANSI ise yani Unicode değilse, her bir karakter 1 byte yer kaplar. Bu yüzden bu örneğimizde yazılacak uzunluk olarak karakter sayısını verdik. Çünkü ANSI karakter katarlarında karakter sayısı, karakter dizisinin boyutuna eşittir. Fakat PChar yerine PWideChar kullansa idik ya da bu kodları D2009 ve üzeri bir sürümde çalıştırsa idik her bir karakteri 2 byte olarak alacaktık. Bu durumda karakter sayısı karakter dizisinin boyutuna eşit değil, boyutun yarısı kadarı olacaktı. Yani Yukarıdaki kodda PChar‘ı PWideChar yaparsak WriteBuffer metodunun ikinci paramteresine Length(Karakterler) * 2 yazmamız gerekiyordu. Bu bahsettiğimizi örneğimizde değiştirip deneyin ve bir hex editör ile her bir karakterin 2 byte yer kapladığına şahid olun. Ama daha sonra tekrar PChar olarak değiştirip dosyayı tekrar yazın. Çünkü birazdan yapacağımız okuma işleminde PChar olarak okutacağız.

Okuma işlemine geçmeden önce WriteBuffer metodunda ilk parametreye bir göz atalım. Burada gördüğünüz gibi Karakterler değişkeninin sonuna ^ operatörünü ekledik. Bu operatör, işaretçilerle (pointer) birlikte kullanılan özel bir operatördür. Bir işaretçinin işaret ettiği esas değere erişmek için bu operatörü kullanıyoruz. Örneğimizde ise PChar tipindeki değişkenimizin işaret ettiği gerçek değeri buffer parametresi olarak girdik. Çünkü PChar da bir çeşit işaretçidir. Ve Karakterler^ yazmak ile bu işaretçinin hafızada işaret ettiği gerçek karakter verisini buffer olarak girdik.

Şimdi yazdırdığımız bu stream verilerini okuyalım:



		


İlk yaptığımız işlem Karakterler değişkeni için hafızadan yer ayırmaktır. Bunun için AllocMem fonksiyonu kullanılır. Tabi bunun için okuyacağımız verinin ne kadar uzunlukta olduğunu bilmemiz gerekiyor. Eğer okuyacağımız karakter verilerinin uzunluğu belli değilse ya da duruma göre boyut değişebiliyorsa bu durumda daha başka bir şeyler yapmamız gerekecek. Bununla ilgili ayrıntılı bilgiyi ilerleyen konularda bulacağız. Ama şimdilik veri uzunluğumuzu sabit kabul ederek işlemlerimizi yapalım.

AllocMem ile hafızadan yer ayırdıktan sonra bu hafıza bölgesine stream verilerini yerleştirebiliriz. Bunun için yine ReadBuffer ile stream’den veri okuyoruz ve buffer’a atıyoruz. Yine buffer olarak işaretçinin gerçek değerini ifade eden ^ operatörünü kullanıyoruz. İkinci parametrede de ne kadar uzunlukta okuyacağımızı belirliyoruz.

PChar ile veri okuma ve yazma bu şekilde olmakta. Peki string bir değişkeni nasıl yazıp okuyacağız? Aslında PChar‘da kullandığımız yöntemden farklı değil. Çünkü string ile PChar arasında çok büyük bir fark bulunmamakta. Zaten çalışma anında bu iki tip arasında dönüşümler rahatlıkla yapılabilmektedir. Yazma için yazdığımız kodları aşağıdaki gibi değiştirelim:



		


Önceki yazma işleminde çok farklı olmadığını burada görüyoruz. Tek yaptığımız işlem derleyiciye, string değişkene bir işaretçi gibi davranmasını söylüyoruz. Bu şekilde derleyici, string olan Karakterler değişkenini bir işaretçi gibi düşünecek ve ^ operatörü ile esas hafıza bölgesindeki veriyi buffer olarak kullanacak. Burada Pointer yerine PChar yazarak da type casting yapabilirsiniz.

Okuma kodları PChar‘a göre fazla ya da eksik değil. Önceki okuma fonksiyonumuzu aşağıdaki gibi değiştirelim.



		


AllocMem yerine string için SetLength fonksiyonunu kullanıyoruz. Böylece string olan değişkenimizin boyutunu belirliyoruz. ReadBuffer‘da yapılanlar önceden anlatılmıştı.

String için yaptığımız bu işlemler biraz pointer yani işaretçi bilgisi gerektiren işlerdi. Halbuki bu pointer işlerine girmeden de string verileri yazıp okuyabiliriz. Ama ilk önce bu şekilde vermemin sebebi, esasında işin arkaplanda çalışma şeklini göstermekti.

Aslında diziler ve işaretçi olan PChar gibi karakter katarları hafızada bulunan esas verilerdeki ilk elemana işaret ederler. Derleyici bu değişken tipleri ile uğraşırken bunların uzunluğundan haberdardır ve ilk pozisyondan itibaren hafızada bu değişkenler üzerinde işlem yapabilir. Esasında PChar ve string‘de ortak olarak yapılan işlem, karakter dizisinin ilk elemanını buffer olarak girmektir. Her iki durumda da ^ operatörü dizinin hafızadaki ilk karakterine işaret eder. Geri kalanında Write ya da Read metodları verilen uzunluk kadar bu ilk karakterden itibaren okumaya ya da yazmaya başlar. Yukarıda string için yazdığımız kodları şu şekilde daha kolay ve sade bir şekilde de yazabiliriz:



		


Gördüğünüz gibi pointer’lardan kurtulup, bize yakın olan kodları kullandık ve aynı sonucu aldık. Ama arka planda derleyici bizim ilk yazdığımız ve işaretçileri kullandığımız kısımdaki gibi çalışır.

Burada tek yaptığımız işlem buffer olarak string değişkeninin ilk karakterini girmek. Tabi okuma işleminde yine SetLength‘i unutmuyoruz. Çünkü okuma işleminden önce hafızada yeterince yer ayrılması şarttır.

Önceden de dediğim gibi uzunluğu sabit uzunlukta olmayan karakter dizileri için ayrı bir konu başlığı ayırdık. O zamana kadar karakter dizimizin boyutunu sabit kabul edeceğiz.

Record’ların Stream Verileri Olarak Kullanılması

Genelde stream ile yapılmak istenen bir gurup veriyi yazmak ve okumaktır. Bu gurup verileri ise genelde record‘lar ile tutarız. Mesela aşağıda bulunan record içinde bir çok değişik değişken tipi bulunmaktadır.



		


Record’ları streamler ile beraber kullanırken şuna dikkat etmemiz gerekiyor. Bir record verisini stream ile okuyup yazabilmek için, record içeriğinin sabit uzunluklu verilerden oluşuyor olmasıdır. Aksi halde bu record, özel işlemler yapılmadığı taktirde, streamlerde kullanılmak için elverişli değildir. Bu yüzden yukarıda string değişkenini 3 karakter ile sınırlandırdık.

Okuma esnasında da aynı record verisini kullanarak rahat bir şekilde okuma işlemini yapabilriiz. Peki ama bu bize ne avantaj sağlayacak?

Önceki örneklerde gördüğümüz gibi, herbir string, Integer, Double gibi değişkenler için ayrı ayrı Read ve Write metodları kullandık. Halbuki bu değişkenler eğer sabit bir formda ise bunları bir paket halinde okuyup yazabilmeliyiz. İşte bu paket halinde okuyup yazma işlemi için recordları kullanıyoruz. Yani yukarıda verdiğimiz TDosyaBasligi isimli recordu yazabilmek için tek tek içinde bulunan 3 değişkeni yazdırmamız gerekmiyor. Sadece TDosyaBasligi isimli recordu yazdırmamız kafidir.

Şimdi yukarıda verdiğmiz recordu interface kısmında, TForm tanımlamasının yukarısında ama type‘ın altında yazalım. Ve Yaz butonunun OnClick olayına şunları yazalım:



		

Gördüğünüz gibi herhangi bir ekstra işlem yapmadım. Sanki basit bir sayısal veriyi yazdırır gibi recordumuzu yazdırdık. Aynı şekilde okumasını da gerçekleştirelim:



		


Bu programı çalıştıralım ve ilk önce Yaz sonra da Oku buttonlarına tıklayalım. Böylece bir recordu yazıp okuyabildik. Fakat disk üzerine yazılan dosyayı bir hex editör yardımı ile incelersek göreceğiz ki ilk kısımda bulunan 3 karakterlik stringten önce 1 byte’lık bir veri bulunmaktadır. Önceki string yazma işlemlerinden farklı olarak burada record içindeki stringlerin sadece verisi yazdırılmaz. String değişkeninin tamamı record ile beraber yazdırılır. String değişkenin tamamından kastımız stringi oluşturan parçalardır. Bir string, PChar gibi karakter dizilerinden farklı olarak ilk 0. baytında stringin veri uzunluğunu tutar. Bu örneğimizde 3 karakter uzunluğunda olduğu için hex editörde ilk göreceğimiz değer 03 değeridir. Tabi bu tek bytelık alanda en fazla 255 değeri yani FF değerini tutlabilir. Bu yüzden sınırlı bir string(literal string) kullanabilmek için en fazla 255 karakter kullanabiliriz. Aksi halde daha başka yollara başvurmalıyız. Başta bulunan 03 ifadesini kaldırıp sadece string verisini yazdırmak istersek array kullanmamız gerekmektedir. Array kullanımını bir sonraki konuda göreceğiz.

Sabit uzunluklu recordları kaydederken gördüğünüz gibi hiç zorlanmıyoruz. Eğer record içeriği burada olduğu gibi sabit uzunluklu değilse yani record içine PChar ya da limitsiz string girersek ne yapacağız. Bu durumda eğer yapabiliyorsak değişken uzunluklu olanları recorddan çıkartıp işlemlerimizi o şekilde yapmalıyız. Değişken uzunluklu verileri recorddan ayrı olarak okuyup yazmamız gerekmektedir. İleriki konularımızda değişken uzunluklu veriler ile nasıl çalışacağımızı da göreceğiz.

Dizileri Stream Verisi Olarak Kullanmak

Aslında dizileri stream üzerine yazmak için, buraya kadar öğrendiğimiz bilgiler yeterli. String’leri nasıl yazdırır ve okutuyorsak, aynı şekilde array’leri de yazdırıp okutabiliyoruz.



		


Gördüğünüz gibi ilk elemanını buffer olarak giriyoruz ve ikinci parametre olarak da dizinin boyutunu parametre olarak giriyoruz.

Eğer dinamik array kullanıyorsak okuma işlemini gerçekleştiren ReadBuffer metodundan önce mutlaka SetLength ile dizinin boyutunu belirlemiş olmalıyız. Ayrıca dizinin kapladığı alanı bulabilmek için dinamik dizinin eleman sayısı ile elaman tipinin boyutunu çarpmalıyız. Yani yukarıdaki örnekte dinamik dizi kullanmış olsa idik Length(BirDizi) * SizeOf(Integer) kazacaktık.

İki ve daha fazla boyuttaki diziler için ise, iç içe bir for döngüsü kurularak bir okuma ve yazma algoritması geliştirilebilir.

Bu kısım diziler için kolay kısımdı. Çünkü bunlar önceki öğrendiklermizden farksızdır. Fakat eğer ActiveX ile uğraşıyorsanız, Word, Excel gibi programların otomosyonu ile uğraşıyorsanız variant array‘ler ile uğraşmışsınızdır. İşte bu durumda variant olan array’leri stream’e kaydetmek ve stream’den okutmak bu kadar kolay değildir. Ama zor da değildir. Eğer ActiveX ile uğraşmıyorsanız diğer başlığa atlayabilirsiniz, ya da nasıl yapıldığı hakkında fikir sahibi olmak için kalıp okuyabilirsiniz.

Bu örneğimizde otomosyon ya da ActiveX nesnelerine girmeyeceğim. Bir şekilde bu nesnelerden variant array verilerini aldığınızı düşünerek haraket edeceğim.



		

Karışık gibi görünse de eğer ActiveX ile uğraşıyorsanız bu kısım size zor gelmeyecektir. İlk başta bir nesneden variant array değerini aldık. Bu değeri siz delphi içinde de oluşturabilirsiniz. Ardından bu array’in boyutunu hesapladık ve VArraySize isimli değişkene attık. Burada dikkat etmeniz gereken nokta, bu örneğimizde diziyi byte, char gibi her elemanı 1 byte olan bir diziye göre hesapladık. Eğer array’deki elemanların boyutu 1 byte’dan fazla ise bu durumda:



		

gibi bir şeyler yazmamız gerekiyordu. Yani eleman sayısı ile bir elemanın boyutunu çarpıyoruz. Daha sonraki gelen işlemlerde, variant array’in ilk elemanının işaretçisini alıyoruz. Bunun için VarArrayLock fonksiyonundan faydalanıyoruz. Bu işaretçiyi daha sonra buffer olarak WriteBuffer‘da kullanıyoruz. Bu işaretçi ile işimiz bitince VarArrayUnlock yapmayı unutmuyoruz.

Variant dizisini stream’den okurken de VarArrayCreate fonksiyonunu kullanıyoruz ve yeni bir dizi oluşturuyoruz. Ardından aynı VarArrayLock ve VarArrayUnlock işlemleri arasında dizimizi stream’den okuyoruz. Bu kısmı size bırakıyorum…

Değişken Uzunluktaki Verileri Okuyup Yazmak

Buraya kadar öğrendiğimiz işlemlerde hep varsayılan bir uzunluktan bahsettik. Fakat her zaman durum bu şekilde olmuyor.

Mesela, kullanıcının bir TMemo bileşenini doldurduğu bir form var. Biz bu TMemo‘da bulunan yazıyı ve başka verileri kendi dosya formatımızda saklamak istiyoruz. Ya da veritabanına kendi formatımızda kaydetmek istiyoruz. Bu durumda string’in ne uzunlukta olduğunu yazarken bilebiliriz fakat okurken bilemeyiz. Çünkü dosya’da ya da herhangi bir stream’de kayıtlı bulunan o string’e ait uzunluk, artık bizim bilgimizin dışına çıkmıştır. Buraya kadar yaptığımız örneklerde hep yazma ve okuma işlemlerini aynı programa koyduk ve aynı çalışma zamanında işlettirdik. Bu yüzden okuduğumuz verilerin boyutunu her zaman biliyorduk. Halbuki okuma fonksiyonu ayrı bir işlemde ise ya da programı açıp kapadığımızda da okuma işleminin çalışmasını istiyorsak bu durumda kaydedeceğimiz veriyi özel bir biçimde kaydetmeliyiz.

Bu ve bunun gibi durumlarda yapacağımız işlem çok basit ve her türlü değişken uzunluklu veri tipi için geçerlidir. Yapacağımız tek şey değişken uzunluklu olan verinin boyutunu sabit bir pozisyona yazmaktır. Ardından okuma esnasında bu sabit pozisyondaki değeri okutarak değişken olan verinin boyutu hakkında bilgi sahibi olabiliriz.

Mesela stream formatımız şu şekilde olsun:

Pozisyon

Boyut

Değişken Tipi

Açıklama

0 3 byte String[3] Dosya İmzası
3 8 byte Double Versiyonu
15 4 byte Integer İçerik Boyutu
19 Değişken String İçerik
Bilinmeyen 4 byte Integer Rastgele

Tabi bu çok basit bir format şekli. Normalde bir çok sabit içerikle beraber bir çok değişken içeriği iç içe kullanmamız gerekebiliyor.

Formatımıza göre ilk 3 byte’da dosya imzası bulunacak. Ardından gelen 8 byte dosyanın versiyonunu tutacak. Ve sonrasında gelen 4 byte’lık Integer değer ise bizim üzerinde uğraştığımız esas konudur. Çünkü bu Integer değer sonrasında gelen string içeriğin boyutunu tutmaktadır. Böylece okuma esnasında string verimizi ne kadar okuyacağımızı belirleyebiliyoruz.

En sonda ise 4 byte’lık bir Integer daha ekledim. Bu değer aslında bu örneğimizi için gerekli değil. Fakat burada bir noktaya dikkatinizi çekmek istediğimden bu değeri de dosya formatına ekledim.

Değişken uzunlukta olan string, array ya da herhangi bir değişkeni kaydederken normalde ilk akla gelen stream’in sonuna kadar okumak olacaktır. Ya da stream’in uzunluğundan diğer verilerin boytunu çıkarmak olacaktır. Halbuki işin içine iki veya daha fazla değişken uzunlukta veri girdiğinde ve buradaki örnekte olduğu gibi değişken uzunluktan sonra da bir başka veri geldiğinde bu mantık bizi idare etmeyecektir. Aynı şekilde dosya formatımızı sürekli olarak güncellenebilen bir format ise bu mantık yine çökecektir.

Bu durumda en mantıklı işlem, değişken verilerin başında bu verinin uzunluğunu saklamak olacaktır. Böylece sonrasında gelen değişken uzunluktaki veri rahatlıkla okunabilir.

Şimdi bu anlattıklarımızı pratiğe dökelim ve yukarıdaki stream formatımızı yine bir dosya üzerinde yazıp okuyalım. Bunun için yeni bir uygulama açın ve form üzerine bir adet button ve bir adet memo yerleştirin.

Unit’in interface kısmında TForm1 tanımlamasının hemen üstünde(type bloğu içinde) şunları yazalım:



		


Buttonnun OnClick olayına aşağıdakileri yazalım.



		


Burada aslında yeni bir şeyler görmedik. Sadece öğrendiklerimizi bir problemin çözümünde kullandık.

Baslik yazdırılmadan önce, değişken uzunluktaki içeriğimiz olan memo’nun Text bilgisinin uzunluğunu, Baslik record verisi içine alıyoruz. Ardından bu recordu stream’e yazdıktan sonra, değişken içeriğin uzunluğu da yazılmış oldu. Ardından normal bir stringi yazdırır gibi bütün karakterlerini stream üzerine yazdırdık. Ardından da rastgele bir sayıyı bu değişken uzunluktaki veriden sonra yazdırdık. Artık programı çalıştırıp Memo içine bir şeyler yazalım ve button’a basıp verileri stream üzerine kaydedilim.

Şimdi gelelim bu stream formatının okunmasına… Bunun için ayrı yeni bir uygulama açalım ve bir button ekleyelim. Ve bu buttonun OnClick olayına geçip şunları yazalım:



		

Burada yaptığımız işlem sadece verilerin okunmasından ibaret. Sadece dikkat etmemiz gereken kısım, ReadBuffer‘ dan önce SetLength ile değişken uzunlukta olan verinin uzunluğunu belirlememiz gerekiyor.

Değişken uzunluktaki stringler bu şekilde okutulabilir.

Bir küçük tüyo olarak, verileri yazdırdığımız kısımdaki şu kodlara bir daha göz atmanızı isteyeceğim:



		


Bu kodlar Baslik recordunun yazılmasından evvel doldurulmaktadır. Bazen öyle durumlar oluyorki, veri uzunluğunu verinin stream’e tamamen yazdırılmasına kadar bilemiyoruz. Mesela değişken uzunlukta bu stringimiz, stream’e yazdırılma esnasında belli bir algoritma ile dinamik olarak oluşturuluyor olabilir. Bu durumda string verisi tamamen stream üzerine yazdırılmadan, uzunluğunu bilemeyiz. Bu durumda TStream‘e ait Seek metodunu kullanacağız. Bu metod stream üzerinde belli bir pozisyona atlamamızı sağlamaktadır. Bu durumda kodumuz şöyle bir şey olacaktı:



		


Aynı şeyi Baslik recordunu en sonda yazdırmakla da yapabilrdik. Veya daha başka yollar ile de bu dediklerimiz yapılabilir. Burada bilmemiz gereken Seek metodunun kullanımıdır. Seek metodunda ilk parametre olarak 15 girdik. Çünkü örneğimizdeki stream format tablomuza baktığımızda 15. byte pozisyonunda içerik boyutu bulunmaktadır. Seek ile stream’in yazma işaretçisini buraya taşıdık ve bu pozisyondan sonra 4 byte’lık bir Integer değer yazdırdık. Seek metodunda kullanılan ikinci parametre için help dosyalarını gezebilirsiniz.

Değişken uzunlukta olan diziler için, yani dinamik diziler için de aynı mantığı kullanmamız gerekmektedir. Değişken olan dizinin boyutunu da stream üzerine yazdırmamız ve okuma anında bu değeri okuyup diziyi SetLength ile tekrar oluşturmamız gerekmektedir. Yapacağımız işlemler aynı olacağı için bu kısmı size bırakıyorum.

Bu bölümde bu kadar bilgi yeterli. Gelecek bölümde stream verilerinin başka streamler üzerine aktarılması ve kopyalanması, streamler ile nesne ve bileşen kullanımı, hafıza, resource, veritabanı gibi ortamlarla stream kullanımı ve son olarak .NET ortamında stream kullanımından bahsedeceğiz. İkinci bölüme geçmeden burada anlatılanları iyi bir şekilde kavrayıp, kendi denemelerinizi yapmanız gerekmektedir.

Eleştiri ve yorumlarınızı bekliyorum…

Fatih Tolga Ata – 2008

» Tags: , , , , ,

13 Comments

  • At 2008.09.13 21:00, Mustafa KUTLU said:

    Ellerine sağlık Fatih. Gene bal dökülmüş parmaklarından. İş güç ev çoluk çocuk hayatı birbirine karışınca en uzun makalem şuralara yorum yazmak oluyor. Bende bir ara birşeyler karalasam iyi olacak sanırım, yoksa iyicene körelecez…

    • At 2008.09.16 14:36, mahir ekici said:

      Ellerine sağıl abi. bu şekilde Streami ayrıntılı anlatan Türkçe makale yok.

      • At 2008.09.17 13:13, someone said:

        Çok güzel, yararlı bi makale olmuş. Ellerinize sağlık…

        • At 2008.09.27 13:22, yakup başer said:

          mükemmel olmuş, ellerine sağlık hocam

          • At 2008.10.09 10:39, özkan danaci said:

            activeX okuma kısmını tam anlayamasak da diğer kısımlar çok güzel olmuş. devamını sabırsızlıkla bekliyoruz. elinize sağlık.
            (merak ettiğim bişey acaba adotable adostoredproc leri de bu şekil okuyabilirmiyiz. mesela clientdataset e stream yoluyla aktarılabilirmi.)

            • At 2008.10.10 11:26, Fatih Tolga Ata said:

              activeX kısmı zaten meraklılarını cezbedecek bir yer. ActiveX ile uğraşmıyorsanız çok da önemli değil. Evet, sonuçta verinin kayıt formatının nasıl olacağını siz belirleyeceksiniz. Bu format ister clientdataset formatı olsun, ister başka bir format…
              Tüm yorumcu arkadaşlara teşekkür ederim.
              Kolay gelsin…

              • At 2008.10.11 23:58, sadettinpolat said:

                tesekkurler fatih. guzel bir yazi olmus. bolum 2 yi sabirsizlikla bekliyoruz.

                • At 2008.10.17 09:39, özkan danaci said:

                  demek istediğim adosp içinden record okuyup client dataset de bir record a atmak değil. mesela böyle bir komut olduğunu düşünelim:
                  clientdataset.data = adosp.data;
                  böyle bir uygulama döngü ile aktarıldığında kayıtlar çok oluca çok zaman alıyor.
                  stream şeklinde yapılabilir mi. ve hızlı olur mu.

                  • At 2009.05.01 09:38, Uğur said:

                    Teşekkür ederim hocam gerçekten harika bi makale olmuş

                    • At 2010.02.20 02:35, exodussian said:

                      öyle güzel açıklamışsınki ekşi sözlükte siteyi reklam yapmak durumunda hissettim kendimi

                      • At 2011.02.10 15:46, BURAK said:

                        Ellerinize sağlık, bu konuyu hiç anlamıyordum, sizin vesilenizle anladım. Çok teşekkür ederim.. Başarılarınızın devamını dilerim..

                        • At 2012.03.14 12:26, Yusuf ŞİMŞEK said:

                          Çok açıklayıcı ve akıcı bir makale. Uğraşlarınızdan dolayı teşekkür ederim.

                          • At 2015.12.23 14:25, Erol Akyüz said:

                            Hocam ellerine, emeğine sağlık hep korktuğum bir türlü anlamadığım bir konuydu. Kendi adıma minnetarım sana, çok çok teşekkür ederim.

                            (Required)
                            (Required, will not be published)