Diğer İşlev Olanakları
İşlevleri daha önce aşağıdaki derslerde görmüştük:
Bu derste, önceki derslerde yer almayan başka işlev olanaklarını anlatacağım.
Dönüş türü olanakları
İşlevler auto, ref, inout, ve auto ref olarak bildirilebilirler. Bunlar, işlevlerin dönüş türleriyle ilgilidir.
auto işlevler
auto olarak bildirilen işlevlerin dönüş türlerinin açıkça yazılması gerekmez:
auto topla(int birinci, double ikinci) { double sonuç = birinci + ikinci; return sonuç; }
Derleyici dönüş türünü return satırından otomatik olarak çıkarsar. Yukarıdaki işlevin return ile döndürdüğü sonuç double olduğu için, o işlev sanki dönüş türü double yazılmış gibi derlenir.
ref işlevler
İşlevlerin döndürdükleri değerler, normalde işlevi çağıran tarafa kopyalanırlar. ref belirteci, dönüş değerinin kopyalanmak yerine referans olarak döndürülmesini sağlar.
Örneğin aşağıdaki işlev, kendisine verilen iki parametreden büyük olanını döndürmektedir:
int büyüğü(int birinci, int ikinci) { return (birinci > ikinci) ? birinci : ikinci; }
O işlevin hem parametreleri hem de dönüş değeri normalde kopyalanır:
int a = 1; int b = 2; int sonuç = büyüğü(a, b); sonuç += 10; // ← ne a ne de b etkilenir writefln("a: %s, b: %s, sonuç: %s", a, b, sonuç);
büyüğü işlevinin dönüş değeri sonuç değişkenine kopyalandığı için; o değişkenin arttırılması, yalnızca sonuç isimli kopyayı etkiler. İşleve kendileri de zaten kopyalanarak geçirilmiş olan a ve b değişmezler:
a: 1, b: 2, sonuç: 12
Parametrelerin kopyalanmak yerine referans olarak gönderilmeleri için ref anahtar sözcüğünün kullanıldığını biliyorsunuz. Aynı sözcük dönüş türü için de kullanılabilir ve işlevin dönüş değerinin de referans olarak döndürülmesini sağlar:
ref int büyüğü(ref int birinci, ref int ikinci) { return (birinci > ikinci) ? birinci : ikinci; }
Döndürülen referans, parametrelerden birisinin takma ismi yerine geçecek ve onda yapılan değişiklik artık ya a'yı ya da b'yi değiştirecektir:
int a = 1; int b = 2; büyüğü(a, b) += 10; // ← ya a ya b etkilenir writefln("a: %s, b: %s", a, b);
Dikkat ederseniz, işlevin döndürdüğü referansı sonuç diye bir değişken kullanmadan doğrudan arttırıyoruz. O işlem, a ve b'den büyük olanını etkiler:
a: 1, b: 12
Yerel referans için gösterge gerekir: Burada bir noktaya dikkatinizi çekmek istiyorum. ref kullanılmış olmasına rağmen, o dönüş değerinin yerel bir değişkene atanması; a veya b'yi yine de değiştirmez:
int sonuç = büyüğü(a, b); sonuç += 10; // ← yine yalnızca 'sonuç' değişir
büyüğü işlevi a'ya veya b'ye referans döndürüyor olsa da, o referans sonuç ismindeki yerel değişkene kopyalandığı için a veya b değişmez:
a: 1, b: 2, sonuç: 12
sonuç'un a'nın veya b'nin referansı olması istenseydi, bir gösterge olarak tanımlanması gerekirdi:
int * sonuç = &büyüğü(a, b); *sonuç += 10; writefln("a: %s, b: %s, sonuç: %s", a, b, *sonuç);
sonuç artık ya a'ya ya da b'ye erişim sağladığı için; onun aracılığıyla yapılan değişiklik, o ikisinin büyük olanını etkilerdi:
a: 1, b: 12, sonuç: 12
Yerel değişkene referans döndürülemez: Yukarıdaki ref dönüş değeri, daha işlev çağrılmadan önce yaşamaya başlayan iki değişkenden birisinin takma ismi gibi çalışmaktadır. Bir başka deyişle, birinci de döndürülse, ikinci de döndürülse; o dönüş değeri hep işlevin çağrıldığı noktada zaten yaşamakta olan a'nın veya b'nin referansıdır.
Yaşam süresi işlevden çıkılırken sona erecek olan bir değişkene referans döndürülemez:
ref string parantezİçinde(string söz) { string sonuç = '(' ~ söz ~ ')'; return sonuç; } // ← sonuç'un yaşamı burada sona erer
İşlev kapsamında tanımlanmış olan sonuç'un yaşamı, o işlevden çıkıldığında sona erer. O yüzden, o değişkenin takma ismi gibi kullanılacak bir dönüş değeri olamaz.
Derleme, yerel değişkene referans döndürüldüğünü bildiren bir hata ile sonlanır:
Error: escaping reference to local variable sonuç
auto ref işlevler
Yukarıdaki parantezİçinde işlevinin yaşam süresi sona eren yerel değişken nedeniyle derlenemediğini gördük. auto ref, öyle durumlarda yararlıdır.
auto ref olarak bildirilmiş olan bir işlevin dönüş türü, auto işlevlerde olduğu gibi otomatik olarak çıkarsanır. Ek olarak, referans olamayacak bir değer döndürüldüğünde; o değer referans olarak değil, kopyalanarak döndürülür.
Aynı işlevi auto ref olarak yazdığımızda program derlenir:
auto ref parantezİçinde(string söz) { string sonuç = '(' ~ söz ~ ')'; return sonuç; // derlenir }
Döndürülen değerin referans olabildiği durumlarda ise o değişkene referans döndürülür.
inout işlevler
Bu belirteç işlev parametrelerinde ve dönüş türünde kullanılır, ve o işlevin parametrelerine bağlı olarak değişebilen, const, veya immutable anlamına gelir.
Yukarıdaki işlevi string alacak ve string döndürecek şekilde tekrar yazalım:
string parantezİçinde(string söz)
{
return '(' ~ söz ~ ')';
}
O işleve string türünde parametre verilmesi gerektiğini, ve sonucunun da string olduğunu biliyoruz:
writeln(parantezİçinde("merhaba"));
"merhaba" hazır değerinin türü string, yani immutable(char)[] olduğu için, o kod derlenir ve çalışır:
(merhaba)
Burada kullanışsız bir durum vardır. O işlev string türüne bağlı olarak yazıldığı için, immutable olmayan bir dizgi ile çağrılamaz:
char[] dizgi; // elemanları değişebilir dizgi ~= "selam"; writeln(parantezİçinde(dizgi));
Derleme hatası, değişebilen karakterlerden oluşan char[] türünün string'e dönüştürülemeyeceğini bildirir:
Error: cannot implicitly convert expression (dizgi) of type char[] to string
Aynı sorun, const(char)[] dizgilerinde de vardır.
Bu sorunu çözmek için bir kaç yöntem düşünülebilir. Bir yöntem, işlevi değişebilen ve const karakter dizileri için de yüklemektir:
char[] parantezİçinde(char[] söz) { return '(' ~ söz ~ ')'; } const(char)[] parantezİçinde(const(char)[] söz) { return '(' ~ söz ~ ')'; }
Bunun programcılıkta kaçınılması gereken kod tekrarı anlamına geldiğini görüyoruz. Bu işlevlerin ileride gelişebileceklerini veya olası hatalarının giderilebileceklerini düşünürsek, o değişikliklerin üçünde de yapılmasının unutulmaması gerekecektir. O yüzden bu riskli bir tasarımdır.
Başka bir yöntem, işlevi şablon olarak tanımlamaktır:
T parantezİçinde(T)(T söz)
{
return '(' ~ söz ~ ')';
}
O çözüm şablon içindeki kullanıma uyan her tür ile kullanılabilir. Bunun bazen fazla esnek olabileceğini ve şablon kısıtlamaları kullanılmasının gerekebileceğini de önceki derste görmüştük.
inout yöntemi şablon çözümüne çok benzer; ama bütün türü değil, yalnızca türün değişebilen, const, veya immutable özelliğini esnek bırakır. O özelliği, işlevin parametrelerinden otomatik olarak çıkarsar:
inout(char[]) parantezİçinde(inout(char[]) söz) { return '(' ~ söz ~ ')'; }
Not: dmd'nin bu dersi yazdığım sırada kullandığım 2.047 sürümünde inout ile ilgili hatalar var. Örneğin parametrelerin daha doğru olarak inout(char)[] olarak yazılmaları derleme hatasına neden oluyor.
inout, parametreden otomatik olarak çıkarsanan özelliği dönüş türüne de aktarır.
O işlev char[] türüyle çağrıldığında; hiç inout yazılmamış gibi derlenir. immutable(char)[] veya const(char)[] türleriyle çağrıldığında ise; inout, sırasıyla immutable veya const yerine geçer. Bunu, işlevin dönüş türünü yazdırarak görebiliriz:
char[] değişebilen; writeln(typeid(parantezİçinde(değişebilen))); const(char)[] sabit; writeln(typeid(parantezİçinde(sabit))); immutable(char)[] değişmez; writeln(typeid(parantezİçinde(değişmez)));
Üç çağrının farklı dönüş türleri:
char[] const(const(char)[]) immutable(immutable(char)[])
Özetle; inout, parametre türünün değişebilme, const, veya immutable özelliğini dönüş türüne aktarır.
Davranış olanakları
pure ve nothrow, işlevlerin genel davranışlarıyla ilgilidir.
pure işlevler
İşlevlerin değer üretebildiklerini veya yan etki oluşturabildiklerini biliyoruz. Bunları İşlevler dersinde görmüştük. Programcılıkta değer üretmenin yan etki oluşturmaktan bir çok açıdan daha iyi olduğu kabul edilir. İşlevlerin sonuçlarının programın belirli bir andaki genel durumundan bağımsız olması ve programın genel durumunu değiştirmemesi yeğlenir.
Bir işlevin sonucunun programın genel durumundan bağımsız ve bütünüyle giriş parametrelerine bağlı olması, o işlevin tutarlı ve güvenli olarak çalışmasına yardım eder. Öyle bir işlev, belirli parametre değerlerine karşılık hep aynı dönüş değerini üretecektir. Yan etki üretmeyen böyle işlevler daha kolay yazılırlar ve test edilirler.
Bu çok önemli bir konu olarak görüldüğü için, bazı fonksiyonel programlama dillerinde işlevlerin yan etki üretmeleri olanaksızdır. Böyle programlama dillerinin "saf" anlamına gelen "pure" olduğu söylenir.
İşlevlerin saflığı konusunda derleyiciden yararlanmanın yolu, onları pure olarak bildirmektir. Derleyici, pure işlevlerin programda evrensel değişiklikler yapmalarına izin vermez:
pure int topla(int birinci, int ikinci) { writeln("topluyorum"); // ← derleme HATASI return birinci + ikinci; }
writeln yan etkisi olan bir işlevdir çünkü programın standart çıkışında değişikliğe neden olur. Çağrılabilse, pure olarak bildirilmiş olan topla'nın da yan etkili olmasına neden olacaktır. O yüzden yukarıdaki işlev derlenemez. Derleme hatası, pure olmayan writeln'in çağrılamayacağını bildirir:
Error: pure function 'topla' cannot call impure function 'writeln'
Öte yandan, pure işlevlerin bu kadar kısıtlı olmaları bazen istenmez. Örneğin yukarıdaki gibi bir writeln ifadesi programdaki bir hatanın giderilmesi sırasında geçici olarak eklenmiş olabilir. Bu gibi durumlarda yararlı olmak amacıyla, debug olarak işaretlenmiş olan ifadeler ve bloklar saf olmasalar bile pure işlevlerde kullanılabilirler:
pure int topla(int birinci, int ikinci) { debug writeln("topluyorum"); // ← şimdi derlenir return birinci + ikinci; }
Not: Bu sefer programın -debug seçeneği ile derlenmesi gerekir.
pure olarak bildirilmiş olan bütün işlevlerin uyması gereken koşul şudur:
- Programın evrensel durumu ile ilgili hiçbir bilgi edinemez, veya o durumu değiştiremez.
İşlevin programın genel durumuna bağlı olması veya o durumu değiştirmesi bu sayede önlenir. Ek olarak, aşağıdaki koşulun sağlanıp sağlanmamasına göre işlev tam saf (strongly pure) veya yarı saf (weakly pure) olarak anılır:
- Tam saf işlevlerin bütün parametreleri ya
immutable'dır ya da otomatik olarakimmutable'a dönüşebilir.
Tam saf işlevlerin, parametrelerinde değişiklik yaparak yan etki oluşturmaları da böylece önlenmiş olur. Yarı saf işlevler ise parametrelerinde değişiklik yapabilirler:
// yarı saf (weakly pure) pure void eksilt(ref int değer) { --değer; }
Yukarıdaki garantilerin sağlanabilmesi için derleyici pure işlevlerden pure olmayan işlevlerin çağrılmalarına izin vermez.
nothrow işlevler
D'nin hata düzeneğini Hata Atma ve Yakalama dersinde görmüştük. Her işlevin hangi durumlarda hangi hataları atabileceği o işlevin belgesinde belirtilmelidir. Genel bir kural olarak, bütün hataların dolaylı da olsa Exception'dan türemiş oldukları varsayılabilir ve tek catch bloğu ile bütün hatalar yakalanabilir. (Not: Error sıradüzeninden türemiş olan hataların ve en genel hata türü olan Throwable türünün yakalanmasının önerilmediğini yine o derste görmüştük.)
Bazı durumlarda ise çağırdığımız işlevlerin ne tür hatalar attıklarını değil, kesinlikle hata atmadıklarını bilmek isteriz. Belirli adımlarının kesintisiz olarak devam etmesi gereken bazı algoritmalar, o adımlar sırasında hata atılmadığından emin olmak zorundadırlar.
nothrow, işlevin hata atmadığını garanti eder:
nothrow int topla(int birinci, int ikinci) { // ... }
O işlev ne kendisi hata atabilir ne de hata atabilen bir işlevi çağırabilir:
nothrow int topla(int birinci, int ikinci) { writeln("topluyorum"); // ← derleme HATASI return birinci + ikinci; }
Derleme hatası, topla'nın hata atabileceğini bildirir:
Error: function deneme.topla 'topla' is nothrow yet may throw
Bunun nedeni, writeln'in nothrow olarak bildirilmiş bir işlev olmamasıdır.
Derleyici, işlevlerin kesinlikle hata atmayacaklarını da anlayabilir. topla'nın aşağıdaki tanımında her tür hata yakalandığı için, nothrow'un getirdiği garantiler geçerliliğini sürdürür, ve işlev nothrow olmayan işlevleri bile çağırabilir:
nothrow int topla(int birinci, int ikinci) { int sonuç; try { writeln("topluyorum"); // derlenir sonuç = birinci + ikinci; } catch (Exception hata) { // bütün hataları yakalar // ... } return sonuç; }
Güvenlik olanakları
@safe, @trusted, ve @system belirteçleri işlevlerin garanti ettikleri güvenliklerle ilgilidir.
@safe işlevler
Programcı hatalarının önemli bir bölümü, farkında olmadan belleğin yanlış yerlerine yazılması ve o yerlerdeki bilgilerin bu yüzden bozulmaları ile ilgilidir. Bu hatalar genellikle göstergelerin yanlış kullanılmaları ve güvensiz tür dönüşümleri sonucunda oluşur.
"Güvenli" anlamına gelen @safe işlevler, belleği bozmayacağını garanti eden işlevlerdir.
Bazılarına bu derslerde hiç değinmemiş olsam da, derleyici @safe işlevlerde şu işlemlere ve olanaklara izin vermez:
- göstergeler
void*dışındaki gösterge türlerine dönüştürülemezler - gösterge olmayan bir tür, gösterge türüne dönüştürülemez
- göstergelerin değerleri değiştirilemez
- gösterge veya referans üyeleri bulunan birlikler kullanılamaz
@systemolarak bildirilmiş olan işlevler çağrılamazExceptionsınıfından türemiş olmayan bir hata yakalanamaz- inline assembler kullanılamaz
- değişebilen değişkenler
immutable'a dönüştürülemezler immutabledeğişkenler değişebilen türlere dönüştürülemezler- işletim dizilerinin yerel değişkenleri
shared'e dönüştürülemezler shareddeğişkenler iş parçacığının yerel değişkeni olacak şekilde dönüştürülemezler- işlevlerin yerel değişkenlerinin veya parametrelerinin adresleri alınamaz
__gshareddeğişkenlere erişilemez
@trusted işlevler
"Güvenilir" anlamına gelen @trusted olarak bildirilmiş olan işlevler, @safe olarak bildirilemeyecek oldukları halde tanımsız davranışa neden olmayan işlevlerdir.
Böyle işlevler, @safe işlevlerin yasakladığı işlemleri yapıyor oldukları halde hatalı olmadıkları programcı tarafından garantilenen işlevlerdir. Programcının derleyiciye "bu işleve güvenebilirsin" demesi gibidir.
Derleyici programcının sözüne güvenir, ve @trusted işlevlerin @safe işlevlerden çağrılmalarına izin verir.
@system işlevler
@safe veya @trusted olarak bildirilmiş olmayan bütün işlevlerin @system oldukları varsayılır. Derleyici böyle işlevlerin doğru veya güvenilir olduklarını düşünemez.
Derleme zamanında işlev işletme
Derleme zamanında yapılabilen hesaplar çoğu programlama dilinde oldukça kısıtlıdır. Bu hesaplar genellikle sabit uzunluklu dizilerin uzunluklarını hesaplamak, veya hazır değerler kullanan aritmetik işlemler yapmak kadar basittir:
writeln(1 + 2);
Yukarıdaki 1 + 2 işlemi derleme zamanında işletilir ve program doğrudan 3 yazılmış gibi derlenir; o hesap için çalışma zamanında hiç zaman harcanmaz.
D'nin "compile time function execution (CTFE)" denen özelliği ise, normal olarak çalışma zamanında işletildiklerini düşüneceğimiz işlevlerin bile derleme zamanında işletilmelerine olanak sağlar.
Örnek olarak, çıktıya bir menü yazdıran bir işleve bakalım:
import std.stdio; string menü(string başlık, string[] seçenekler, int genişlik) { string sonuç = ortalanmışSatır("-= " ~ başlık ~ " =-", genişlik); foreach (i, seçenek; seçenekler) { sonuç ~= ortalanmışSatır(". " ~ seçenek ~ " .", genişlik); } return sonuç; }
O işlevin oldukça karmaşık olduğunu düşünebiliriz. Hem başlık satırını hem de seçenekleri belirli ölçüde işledikten sonra onları ortalanmışSatır isimli başka bir işleve göndermektedir. Benzer karmaşıklığı ortalanmışSatır işlevinde de görüyoruz:
string ortalanmışSatır(string yazı, int genişlik) { string girinti; if (yazı.length < genişlik) { foreach (i; 0 .. (genişlik - yazı.length) / 2) { girinti ~= ' '; } } return girinti ~ yazı ~ '\n'; }
Bütün o karmaşıklığa rağmen, kullanımına bağlı olarak, menü işlevi bütünüyle derleme zamanında işletilebilir. Örneğin static bir değişkenin ilklenmesi sırasında:
void main() { static string tatlıMenüsü = menü("Tatlılar", [ "Baklava", "Kadayıf", "Muhallebi" ], 30); writeln(tatlıMenüsü); }
Yukarıdaki programdaki menü işlevi, ortalanmışSatır işlevinden ve kendi işlemlerinden yararlanarak şu menüyü oluşturur:
-= Tatlılar =-
. Baklava .
. Kadayıf .
. Muhallebi .
Burada önemli olan, yukarıdaki sonucun bütünüyle derleme zamanında oluşturulmuş olmasıdır.
Bu, tatlıMenüsü değişkeni static olarak bildirildiği içindir. static değişkenler programda yalnızca bir kere ilklenirler. static değişkenlerin ilk değerlerinin derleme zamanında bilinmeleri gerekir.
Yukarıdaki programdaki işlevlerin derleme zamanında işletilmelerinin sonucunda, tatlıMenüsü değişkeni sanki o hesapların sonunda oluşan dizgi programa açıkça yazılmış gibi derlenir:
// Yukarıdaki kodun eşdeğeri: static string tatlıMenüsü = " -= Tatlılar =-\n" " . Baklava .\n" " . Kadayıf .\n" " . Muhallebi .\n";
menü gibi bir işlev çağrıldığı halde ve onun sonucunda işlemler yapıldığı halde, o dizginin oluşturulması için çalışma zamanında hiç zaman harcanmaz.
Bu olanak, derleyicinin davranışı ile ilgili olduğundan, işlevlerin gerçekten derleme zamanında işletilip işletilmediklerini görmek o kadar kolay olmayabilir. Dizginin gerçekten derleme zamanında oluşturulduğunu görmenin bir yolu, programı bir hex editör'de açmaktır. Derleme zamanında oluşturulmuşsa, bütün menü dizgisi, derlenmiş olan programın içinde gömülü olarak görülür.
O dizginin tamamını görmenin başka bir yolu, Unix ortamlarında bulunan strings ve grep programlarından yararlanmaktır. strings, bir dosya içindeki bütün okunaklı dizgileri bulur ve standart çıkışına gönderir. grep ise, girişine gelen satırlar arasından belirli bir düzene uyanlarını seçer ve standart çıkışına gönderir.
Programın isminin deneme olduğunu varsayarsak, aşağıdaki komut, içinde "Tatl" geçen satırı, ve sonrasındaki 4 satırı gösterir:
$ strings deneme | grep -A 4 Tatl
-= Tatl
lar =-
. Baklava .
. Kaday
. Muhallebi .
Yukarıdaki çıktıdaki ı harflerinin benim ortamımda sorun çıkartıyor olmaları önemli değildir. Burada göstermek istediğim, sonuç dizginin programın içinde gömülü olarak bulunduğudur. menü işlevi, derleme zamanında işletilmiştir.
Her işlev derleme zamanında işletilemez. Bir işlevin derleme zamanında işletilebilmesi için öncelikle bunun mümkün olduğu bir ifadede kullanılmış olması gerekir:
staticbir değişkenin ilklenmesienumbir değişkenin ilklenmesi; örneğin, yukarıdaki programdakistaticyerine, buradaki kullanımında kesinlikle değişmeyen değer anlamına gelenenumda yazılabilir
enum string tatlıMenüsü = /* ... */
Ek olarak, aşağıdaki koşulların sağlanmaları da gerekir:
- işlevin parametrelerinin uyması gereken koşullar:
- tamsayı hazır değerleri
- kesirli sayı hazır değerleri
- karakter hazır değerleri
- dizgi hazır değerleri
- bu listedeki elemanlardan oluşan dizi hazır değerleri
- bu listedeki elemanlardan oluşan eşleme tablosu hazır değerleri
- bu listedeki elemanlardan oluşan yapı hazır değerleri
- bu listedeki elemanlarla ilklenmiş olan sabit değişkenler
- kapamalar
- işlev göstergeleri
- isimsiz kapamalar
- isimsiz işlevler
- belirsiz sayıda parametre alan işlevlerin C dilindeki gibi tanımlanmış olmaları (Parametre Serbestliği dersini anlatırken bu olanağı bilerek atlamıştım)
- işlev, eş zamanlı çalışacak şekilde tanımlanmış olmamalıdır
- işlevdeki ifadeler
- hata atamazlar
- gösterge veya sınıf kullanamazlar
- evrensel değişkenlere erişemezler
- yerel
staticdeğişkenlere erişemezler delete'i çağıramazlar- derleme zamanında işletilemeyen işlevler çağıramazlar
- şu deyimler kullanılamaz:
- eş zamanlı çalışma deyimleri
throwwithscopetry-catch-finally- etiketli
breakveyacontinue
- bir istisna olarak, şu nitelikler derleme zamanında işletilebilirler:
.dup.length.keys.values
D.ershane
Forum
Wiki
Projeler
Tanıtım
İletişim
Hakları