İşleç Yükleme
İşleç yükleme olanağını bu bölümde yapılar üzerinde göreceğiz. Burada anlatılanlar daha sonra göreceğimiz sınıflar için de hemen hemen aynen geçerlidir. En belirgin fark, Kurucu ve Diğer Özel İşlevler bölümünde gördüğümüz atama işleci opAssign
'ın sınıflar için tanımlanamıyor olmasıdır.
İşleç yükleme çok sayıda kavram içerir (şablonlar, auto ref
, vs.). Bu kavramların bazılarını kitabın ilerideki bölümlerinde göreceğimizden bu bölüm size bu aşamada diğer bölümlerden daha zor gelebilir.
İşleç yükleme, işleçlerin kendi türlerimizle nasıl çalışacaklarını belirleme olanağıdır.
Yapıların ve üye işlevlerin yararlarını önceki bölümlerde görmüştük. Bunun bir örneği, GününSaati
nesnelerine Süre
nesnelerini ekleyebilmekti. Kodu kısa tutmak için yalnızca bu bölümü ilgilendiren üyelerini gösteriyorum:
struct Süre { int dakika; } struct GününSaati { int saat; int dakika; void ekle(Süre süre) { dakika += süre.dakika; saat += dakika / 60; dakika %= 60; saat %= 24; } } void main() { auto yemekZamanı = GününSaati(12, 0); yemekZamanı.ekle(Süre(10)); }
Üye işlevlerin yararı, yapıyı ilgilendiren işlemlerin yapının içinde tanımlanabilmeleridir. Üye değişkenler ve üye işlevler bir arada tanımlanınca yapının üyelerinin bütün işlevler tarafından doğru olarak kullanıldıkları daha kolay denetlenebilir.
Yapıların bütün bu yararlarına karşın, temel türlerin işleç kullanımı konusunda yapılara karşı üstünlükleri vardır: Temel türler özel tanımlar gerekmeden işleçlerle rahatça kullanılabilirler:
int ağırlık = 50; ağırlık += 10; // işleçle
Şimdiye kadar öğrendiğimiz bilgiler doğrultusunda benzer işlemleri yapılar için ancak üye işlevlerle gerçekleştirebiliyoruz:
auto yemekZamanı = GününSaati(12, 0); yemekZamanı.ekle(Süre(10)); // üye işlevle
İşleç yükleme, yapıları da temel türler gibi işleçlerle kullanabilme olanağı sunar. Örneğin, GününSaati
yapısı için tanımlayabileceğimiz +=
işleci, yukarıdaki işlemin yazımını kolaylaştırır ve daha okunaklı hale getirir:
yemekZamanı += Süre(10); // yapı için de işleçle
Yüklenebilecek bütün işleçlere geçmeden önce bu kullanımın nasıl sağlandığını göstermek istiyorum. Daha önce ismini ekle
olarak yazdığımız işlevi D'nin özel olarak belirlediği opOpAssign(string işleç)
ismiyle tanımlamak ve bu tanımın "+"
karakteri için yapılmakta olduğunu belirtmek gerekir. Biraz aşağıda açıklanacağı gibi, bu aslında +=
işleci içindir.
Aşağıdaki tanımın şimdiye kadar gördüğümüz işlev tanımlarına benzemediğini farkedeceksiniz. Bunun nedeni, opOpAssign
'ın aslında bir işlev şablonu olmasıdır. Şablonları daha ilerideki bölümlerde göreceğiz; şimdilik işleç yükleme konusunda bu söz dizimini bir kalıp olarak uygulamak gerektiğini kabul etmenizi rica ediyorum:
struct GününSaati { // ... ref GününSaati opOpAssign(string işleç)(Süre süre) // (1) if (işleç == "+") { // (2) dakika += süre.dakika; saat += dakika / 60; dakika %= 60; saat %= 24; return this; } }
Yukarıdaki şablon tanımı iki parçadan oluşur:
opOpAssign(string işleç)
: Bunun aynen yazılması gerekir. (opOpAssign
'dan başka işleç işlevlerinin de bulunduğunu aşağıda göreceğiz.)if (işleç ==
:"+"
)opOpAssign
birden fazla işlecin tanımında kullanılabildiği için hangi işleç karakterinin tanımlanmakta olduğu bu söz dizimiyle belirtilir. Aslında bir şablon kısıtlaması olan bu söz diziminin ayrıntılarını da daha sonraki bir bölümde göreceğiz.
ekle
işlevinden farklı olarak, dönüş türünün bu tanımda void
olmadığına dikkat edin. İşleçlerin dönüş türlerini biraz aşağıda açıklayacağım.
Derleyici, GününSaati
nesnelerinin +=
işleciyle kullanıldığı yerlerde perde arkasında opOpAssign!
işlevini çağırır:
"+"
yemekZamanı += Süre(10); // Aşağıdaki satır üsttekinin eşdeğeridir: yemekZamanı.opOpAssign!"+"(Süre(10));
opOpAssign
'dan sonra yazılan !
, "+"
opOpAssign
'ın +
karakteri için tanımı olan işlevin çağrılmakta olduğu anlamına gelir. Şablonlarla ilgili olan bu söz dizimini de daha sonraki bir bölümde göreceğiz.
Kod içindeki +=
işlecine karşılık gelen yukarıdaki üye işlevde "+="
değil, "+"
kullanıldığına dikkat edin. opOpAssign
'ın isminde geçen ve "değer ata" anlamına gelen "assign" zaten atama kavramını içerir. Sonuçta, opOpAssign!
, atamalı toplama işlemi olan "+"
+=
işlecinin tanımıdır.
İşleçlerin davranışlarını bizim belirleyebiliyor olmamız bize çoğu işleç için istediğimiz şeyi yapma serbestisi verir. Örneğin, yukarıdaki işleci süre ekleyecek şekilde değil, tam tersine süre azaltacak şekilde de tanımlayabilirdik. Oysa kodu okuyanlar +=
işlecini gördüklerinde doğal olarak değerin artmasını bekleyeceklerdir.
Genel beklentilere uyulması işleçlerin dönüş türleri için de önemlidir.
İşleçleri doğal davranışları dışında yazdığınızda bunun herkesi yanıltacağını ve programda hatalara neden olacağını aklınızda bulundurun.
Yüklenebilen işleçler
İşleçler kullanım çeşitlerine göre farklı yüklenirler.
Tekli işleçler
Tek nesneyle işleyen işleçlere tekli işleç denir:
++ağırlık;
Yukarıdaki ++
işleci tekli işleçtir çünkü işini yaparken tek değişken kullanmaktadır ve onun değerini bir arttırmaktadır.
Bu işleçler opUnary
üye işlev ismiyle tanımlanırlar. opUnary
parametre almaz çünkü yalnızca işlecin üzerinde uygulandığı nesneyi (yani this
'i) etkiler.
İşlev tanımında kullanılması gereken işleç dizgileri şunlardır:
Örneğin ++
işlecini Süre
için şöyle tanımlayabiliriz:
struct Süre { int dakika; ref Süre opUnary(string işleç)() if (işleç == "++") { ++dakika; return this; } }
İşlecin dönüş türünün ref
olarak işaretlendiğine dikkat edin. İşleçlerin dönüş türlerini aşağıda açıklayacağım.
Süre
nesneleri bu sayede artık ++
işleci ile arttırılabilirler:
auto süre = Süre(20);
++süre;
Önceki değerli (sonek) arttırma ve önceki değerli (sonek) azaltma işleçleri olan nesne++
ve nesne--
kullanımları yüklenemez. O kullanımlardaki önceki değerleri derleyici otomatik olarak üretir. Örneğin, süre++
kullanımının eşdeğeri şudur:
/* Önceki değer derleyici tarafından otomatik olarak * kopyalanır: */ Süre __öncekiDeğer__ = süre; /* Tanımlanmış olan normal ++ işleci çağrılır: */ ++süre; /* Daha sonra bütün ifadede __öncekiDeğer__ kullanılır. */
Bazı programlama dillerinden farklı olarak, önceki değerin programda kullanılmadığı durumlarda yukarıdaki kopyanın D'de bir masrafı yoktur. İfadenin kullanılmadığı durumlarda önceki değerli arttırma işleçleri normal arttırma işleçleriyle değiştirilirler:
/* Aşağıdaki ifadenin değeri programda kullanılmamaktadır.
* İfadenin tek etkisi, 'i'nin değerini arttırmaktır. */
i++;
i
'nin önceki değeri programda kullanılmadığından derleyici o ifadenin yerine aşağıdakini yerleştirir:
/* Derleyicinin kullandığı ifade: */
++i;
Ek olarak, eğer aşağıda göreceğimiz opBinary
yüklemesi süre += 1
kullanımını destekliyorsa, ++süre
ve süre++
kullanımları için opUnary
gerekmez; derleyici onların yerine süre += 1
ifadesinden yararlanır. Benzer biçimde, süre -= 1
yüklemesi de --süre
ve süre--
kullanımlarını karşılar.
İkili işleçler
İki nesne kullanan işleçlere ikili işleç denir:
toplamAğırlık = kutuAğırlığı + çikolataAğırlığı;
Yukarıdaki satırda iki farklı ikili işleç görülüyor: +
işleci solundaki ve sağındaki değerleri toplar, =
işleci de sağındakinin değerini solundakine atar.
İşleçleri gruplandırmak için aşağıdaki tabloda işleçlerin çeşitlerini de belirttim. "=" ile işaretli olanlar sağ tarafın değerini sol tarafa atarlar.
İşleç | Anlamı | İşlev İsmi | Sağ Taraf için İşlev İsmi | Çeşit |
---|---|---|---|---|
+ | topla | opBinary | opBinaryRight | aritmetik |
- | çıkar | opBinary | opBinaryRight | aritmetik |
* | çarp | opBinary | opBinaryRight | aritmetik |
/ | böl | opBinary | opBinaryRight | aritmetik |
% | kalanını hesapla | opBinary | opBinaryRight | aritmetik |
^^ | üssünü al | opBinary | opBinaryRight | aritmetik |
& | bit işlemi ve | opBinary | opBinaryRight | bit |
| | bit işlemi veya | opBinary | opBinaryRight | bit |
^ | bit işlemi ya da | opBinary | opBinaryRight | bit |
<< | sola kaydır | opBinary | opBinaryRight | bit |
>> | sağa kaydır | opBinary | opBinaryRight | bit |
>>> | işaretsiz sağa kaydır | opBinary | opBinaryRight | bit |
~ | birleştir | opBinary | opBinaryRight | |
in | içinde mi? | opBinary | opBinaryRight | |
== | eşittir | opEquals | - | mantıksal |
!= | eşit değildir | opEquals | - | mantıksal |
< | öncedir | opCmp | - | sıralama |
<= | sonra değildir | opCmp | - | sıralama |
> | sonradır | opCmp | - | sıralama |
>= | önce değildir | opCmp | - | sıralama |
= | ata | opAssign | - | = |
+= | arttır | opOpAssign | - | = |
-= | azalt | opOpAssign | - | = |
*= | katını ata | opOpAssign | - | = |
/= | bölümünü ata | opOpAssign | - | = |
%= | kalanını ata | opOpAssign | - | = |
^^= | üssünü ata | opOpAssign | - | = |
&= | & sonucunu ata | opOpAssign | - | = |
|= | | sonucunu ata | opOpAssign | - | = |
^= | ^ sonucunu ata | opOpAssign | - | = |
<<= | << sonucunu ata | opOpAssign | - | = |
>>= | >> sonucunu ata | opOpAssign | - | = |
>>>= | >>> sonucunu ata | opOpAssign | - | = |
~= | sonuna ekle | opOpAssign | - | = |
Tabloda sağ taraf için olarak belirtilen işlev isimleri, nesne işlecin sağ tarafında da kullanılabildiğinde işletilir. Kodda şöyle bir ikili işleç bulunduğunu düşünelim:
x işleç y
Derleyici hangi üye işlevi işleteceğine karar vermek için yukarıdaki ifadeyi şu iki üye işlev çağrısına dönüştürür:
x.opBinary!"işleç"(y); // x'in solda olduğu durumun tanımı y.opBinaryRight!"işleç"(x); // y'nin sağda olduğu durumun tanımı
O işlev çağrılarından daha uygun olanı seçilir ve işletilir.
opBinaryRight
, işleçlerin her iki tarafında kullanılması beklenen (örneğin int
gibi işleyen) türler tanımlarken kullanışlıdır:
auto x = ÖzelTamsayı(42); x + 1; // opBinary!"+" çağrılır 1 + x; // opBinaryRight!"+" çağrılır
opBinaryRight
'ın bir başka yaygın kullanımı in
işlecidir çünkü in
işleci genellikle sağ tarafındaki nesne ile ilişkilidir. Bunun bir örneğini aşağıda göreceğiz.
Aşağıdaki örneklerde üye işlevleri tanımlarken parametre ismini sağdaki
olarak seçtim. Bununla parametrenin işlecin sağındaki nesne olduğunu vurguluyorum:
x işleç y
O ifade kullanıldığında üye işlevin sağdaki
ismindeki parametresi y
'yi temsil edecektir.
Dizi ve dilim işleçleri
Aşağıdaki işleçler bir türün topluluk olarak kullanılabilmesini sağlarlar.
Anlamı | İşlev İsmi | Örnek kullanım |
---|---|---|
eleman erişimi | opIndex | topluluk[i] |
elemana atama | opIndexAssign | topluluk[i] = 7 |
eleman üzerinde tekli işlem | opIndexUnary | ++topluluk[i] |
atamalı eleman işlemi | opIndexOpAssign | topluluk[i] *= 2 |
eleman adedi | opDollar | topluluk[$ - 1] |
bütün elemanlara eriştiren dilim | opSlice | topluluk[] |
bazı elemanlara eriştiren dilim | opSlice(size_t, size_t) | topluluk[i..j] |
Bu işleçleri aşağıda kendi başlıkları altında göreceğiz.
Aşağıdaki tablodaki işleçler D'nin önceki sürümlerinden kalma olduklarından kullanımları önerilmez. Onların yerine yukarıdaki tablodaki işleçler kullanılır.
Anlamı | İşlev İsmi | Örnek kullanım |
---|---|---|
bütün elemanlar üzerinde tekli işlem | opSliceUnary (önerilmez) | ++topluluk[] |
bazı elemanlar üzerinde tekli işlem | opSliceUnary (önerilmez) | ++topluluk[i..j] |
bütün elemanlara atama | opSliceAssign (önerilmez) | topluluk[] = 42 |
bazı elemanlara atama | opSliceAssign (önerilmez) | topluluk[i..j] = 7 |
bütün elemanlar üzerinde atamalı işlem | opSliceOpAssign (önerilmez) | topluluk[] *= 2 |
bazı elemanlar üzerinde atamalı işlem | opSliceOpAssign (önerilmez) | topluluk[i..j] *= 2 |
Diğer işleçler
Yukarıdaki işleçlere ek olarak aşağıdaki işleçler de yüklenebilir:
Anlamı | İşlev İsmi | Örnek kullanım |
---|---|---|
işlev çağrısı | opCall | nesne(42) |
tür dönüşümü işleci | opCast | to!int(nesne) |
var olmayan üye işlev için sevk | opDispatch | nesne.varOlmayanİşlev() |
Bu işleçleri daha aşağıda kendi başlıkları altında açıklayacağım.
Birden fazla işleci aynı zamanda tanımlamak
Örnekleri kısa tutmak için yukarıda yalnızca ++
, +
ve +=
işleçlerini kullandık. En az bir işlecinin yüklenmesi gereken bir türün başka işleçlerinin de yüklenmelerinin gerekeceği beklenebilir. Örneğin, aşağıdaki Süre
türü için --
ve -=
işleçleri de tanımlanmaktadır:
struct Süre { int dakika; ref Süre opUnary(string işleç)() if (işleç == "++") { ++dakika; return this; } ref Süre opUnary(string işleç)() if (işleç == "--") { --dakika; return this; } ref Süre opOpAssign(string işleç)(int miktar) if (işleç == "+") { dakika += miktar; return this; } ref Süre opOpAssign(string işleç)(int miktar) if (işleç == "-") { dakika -= miktar; return this; } } unittest { auto süre = Süre(10); ++süre; assert(süre.dakika == 11); --süre; assert(süre.dakika == 10); süre += 5; assert(süre.dakika == 15); süre -= 3; assert(süre.dakika == 12); } void main() { }
Yukarıdaki işleç yüklemelerinde kod tekrarı bulunmaktadır. Benzer işlevlerin farklı olan karakterlerini işaretlenmiş olarak gösterdim. Bu kod tekrarı D'nin dizgi katmaları (mixin
) olanağı ile giderilebilir. Daha ilerideki bölümlerde daha ayrıntılı olarak öğreneceğimiz mixin
anahtar sözcüğünün işleç yüklemedeki yararını burada kısaca görelim.
mixin
, kendisine verilen dizgiyi bulunduğu yere kaynak kod olarak yerleştirir. Örneğin, işleç
'in "++"
olduğu durumda aşağıdaki iki satır eşdeğerdir:
mixin (işleç ~ "dakika;"); ++dakika; // üsttekinin eşdeğeri
Dolayısıyla, bu olanaktan yararlanan aşağıdaki yapı yukarıdakinin eşdeğeridir:
struct Süre { int dakika; ref Süre opUnary(string işleç)() if ((işleç == "++") || (işleç == "--")) { mixin (işleç ~ "dakika;"); return this; } ref Süre opOpAssign(string işleç)(int miktar) if ((işleç == "+") || (işleç == "-")) { mixin ("dakika " ~ işleç ~ "= miktar;"); return this; } }
Süre
nesnelerinin belirli bir miktar ile çarpılmalarının veya bölünmelerinin de desteklenmesi istendiğinde yapılması gereken, o işleç karakterlerini de şablon kısıtlamalarına eklemektir:
struct Süre { // ... ref Süre opOpAssign(string işleç)(int miktar) if ((işleç == "+") || (işleç == "-") || (işleç == "*") || (işleç == "/")) { mixin ("dakika " ~ işleç ~ "= miktar;"); return this; } } unittest { auto süre = Süre(12); süre *= 4; assert(süre.dakika == 48); süre /= 2; assert(süre.dakika == 24); }
Şablon kısıtlamaları bu durumda isteğe bağlıdır; özellikle gerekmedikçe yazılmayabilirler:
ref Süre opOpAssign(string işleç)(int miktar) /* kısıtlama yok */ { mixin ("dakika " ~ işleç ~ "= miktar;"); return this; }
İşleçlerin dönüş türleri
İşleçleri kendi türleriniz için tanımlarken o işleçlerin hem davranışlarının hem de dönüş türlerinin temel türlerdeki gibi olmalarına dikkat edin. Bu, kodun anlaşılırlığı ve hataların önlenmesi açısından önemlidir.
Temel türlerle kullanılan hiçbir işlecin dönüş türü void
değildir. Bu, bazı işleçler için barizdir. Örneğin, iki int
değerin a + b
biçiminde toplanmalarının sonucunun yine int
türünde bir değer olduğu açıktır:
int a = 1; int b = 2; int c = a + b; // c, işlecin değeri ile ilklenir
Başka bazı işleçlerin dönüş değerleri ve türleri ise bariz olmayabilir. Örneğin, ++i
gibi bir işlecin bile değeri vardır:
int i = 1; writeln(++i); // 2 yazar
++
işleci i
'yi arttırmakla kalmaz, ayrıca i
'nin yeni değerini de döndürür. Üstelik, ++
işlecinin döndürdüğü değer i
'nin yeni değeri değil, aslında i
'nin ta kendisidir. Bunu ++i
işleminin sonucunun adresini yazdırarak gösterebiliriz:
int i = 1; writeln("i'nin adresi : ", &i); writeln("++i ifadesinin sonucunun adresi: ", &(++i));
Çıktısı, iki adresin aynı olduklarını gösterir:
i'nin adresi : 7FFFAFECB2A8 ++i ifadesinin sonucunun adresi: 7FFFAFECB2A8
Tanımladığınız işleçlerin dönüş türlerinin aşağıdaki listedekilere uymalarına özen göstermenizi öneririm:
- Nesneyi değiştiren işleçler
opAssign
istisna olmak üzere, nesnede değişiklik yapan işleçlerin nesnenin kendisini döndürmeleri uygundur. Bunu yukarıdakiGününSaati.opOpAssign!
ve"+"
Süre.opUnary!
işleçlerinde gördük."++"
Nesnenin kendisini döndürmek için şu adımlar uygulanır:
- Dönüş türü olarak türün kendisi yazılır ve başına "referans" anlamına gelen
ref
anahtar sözcüğü eklenir. - İşlevden bu nesneyi döndür anlamında
return this
ile çıkılır.
Nesneyi değiştiren işleçler şunlardır:
opUnary!
,"++"
opUnary!
, ve bütün"--"
opOpAssign
yüklemeleri. - Dönüş türü olarak türün kendisi yazılır ve başına "referans" anlamına gelen
- Mantıksal işleçler
==
ve!=
işleçlerini temsil edenopEquals
bool
döndürmelidir.in
işleci ise normalde içerilen nesneyi döndürse de, istendiğinde o da basitçebool
döndürebilir. - Sıralama işleçleri
Nesnelerin sıralanmalarında yararlanılan ve
<
,<=
,>
, ve>=
işleçlerinin davranışını belirleyenopCmp
int
döndürmelidir. - Yeni nesne üreten işleçler
Bazı işleçlerin yeni nesne oluşturmaları ve o nesneyi döndürmeleri gerekir:
- Tekli işleçler
-
,+
, ve~
; ve ikili~
işleci. - Aritmetik işleçler
+
,-
,*
,/
,%
, ve^^
. - Bit işleçleri
&
,|
,^
,<<
,>>
, ve>>>
. opAssign
, bir önceki bölümde de gösterildiği gibi,return this
ile bu nesnenin bir kopyasını döndürür.Not: Bir eniyileştirme olarak bu işleç büyük yapılarda
const ref
de döndürebilir. Ben bu kitapta bu eniyileştirmeyi uygulamayacağım.
Yeni nesne üreten işleç örneği olarak
Süre
nesnelerini birbirleriyle toplamayı sağlayanopBinary!
yüklemesine bakalım:"+"
struct Süre { int dakika; Süre opBinary(string işleç)(Süre sağdaki) const if (işleç == "+") { return Süre(dakika + sağdaki.dakika); // yeni nesne } }
O tanımdan sonra programlarımızda artık
Süre
nesnelerini+
işleciyle toplayabiliriz:auto gitmeSüresi = Süre(10); auto dönmeSüresi = Süre(11); Süre toplamSüre; // ... toplamSüre = gitmeSüresi + dönmeSüresi;
Derleyici o ifadeyi dönüştürür ve perde arkasında
gitmeSüresi
nesnesi üzerinde bir üye işlev olarak çağırır:// üsttekinin eşdeğeridir: toplamSüre = gitmeSüresi.opBinary!"+"(dönmeSüresi);
- Tekli işleçler
opDollar
Eleman adedi bilgisini döndürdüğünden en uygun tür
size_t
'dir. Buna rağmen, özellikle gerektiğindeint
gibi başka tamsayı türlerini de döndürebilir.- Serbest işleçler
Bazı işleçlerin dönüş türleri bütünüyle o yapının tasarımına bağlıdır: Tekli
*
işleci,opCall
,opCast
,opDispatch
,opSlice
, ve bütünopIndex
işleçleri.
Eşitlik karşılaştırmaları için opEquals
==
ve !=
işleçlerinin davranışını belirler.
opEquals
işlecinin dönüş türü bool
'dur.
Yapılarda opEquals
işlevinin parametresi in
olarak işaretlenebilir. Ancak, çok büyük yapılarda hız kaybını önlemek için opEquals
, parametresi auto ref const
olan bir şablon olarak da tanımlanabilir (boş parantezler bu tanımın bir şablon olmasını sağlarlar):
bool opEquals()(auto ref const GününSaati sağdaki) const { // ... }
Sol Değerler ve Sağ Değerler bölümünde gördüğümüz gibi, auto ref
sol değerlerin referans olarak, sağ değerlerin ise kopyalanarak geçirilmelerini sağlar. Ancak, D'de sağ değerler kopyalanmak yerine taşındıklarından yukarıdaki işlev bildirimi hem sol değerler hem de sağ değerler için hızlı işler.
Karışıklıklara önlemek için opEquals
ve opCmp
birbirleriyle tutarlı olmalıdır. opEquals
'ın true
döndürdüğü iki nesne için opCmp
sıfır döndürmelidir.
opEquals
üye işlevi ==
ve !=
işleçlerinin ikisini de karşılar. Programcı işlevi ==
işleci için tanımlar; derleyici de !=
işleci için onun tersini kullanır:
x == y; x.opEquals(y); // üsttekinin eşdeğeri x != y; !(x.opEquals(y)); // üsttekinin eşdeğeri
Normalde opEquals
işlevini yapılar için tanımlamaya gerek yoktur; derleyici bütün üyelerin eşitliklerini sırayla otomatik olarak karşılaştırır ve nesnelerin eşit olup olmadıklarına öyle karar verir.
Bazen nesnelerin eşitliklerinin özel olarak belirlenmeleri gerekebilir. Örneğin bazı üyeler eşitlik karşılaştırması için önemsiz olabilirler veya bir türün nesnelerinin eşit kabul edilmeleri özel bir kurala bağlı olabilir, vs.
Bunu göstermek için GününSaati
yapısı için dakika bilgisini gözardı eden bir opEquals
tanımlayalım:
struct GününSaati { int saat; int dakika; bool opEquals(GününSaati sağdaki) const { return saat == sağdaki.saat; } } // ... assert(GününSaati(20, 10) == GününSaati(20, 59));
Eşitlik karşılaştırmasında yalnızca saat bilgisine bakıldığı için 20:10 ve 20:59 zamanları eşit çıkmaktadır. (Not: Bunun karışıklık doğuracağı açıktır; gösterim amacıyla basit bir örnek olarak kabul edelim.)
Sıra karşılaştırmaları için opCmp
Sıralama işleçleri nesnelerin öncelik/sonralık ilişkilerini belirler. Sıralama ile ilgili olan <
, <=
, >
, ve >=
işleçlerinin hepsi birden opCmp
üye işlevi tarafından karşılanır.
Yapılarda opCmp
işlevinin parametresi in
olarak işaretlenebilir. Ancak, opEquals
'da olduğu gibi, çok büyük yapılarda hız kaybını önlemek için opCmp
, parametresi auto ref const
olan bir şablon olarak da tanımlanabilir:
int opCmp()(auto ref const GününSaati sağdaki) const { // ... }
Karışıklıkları önlemek için opEquals
ve opCmp
birbirleriyle tutarlı olmalıdır. opEquals
'ın true
döndürdüğü iki nesne için opCmp
sıfır döndürmelidir.
Bu dört işleçten birisinin şu şekilde kullanıldığını düşünelim:
if (x işleç y) { // ← işleç <, <=, >, veya >= olabilir
Derleyici o ifadeyi aşağıdaki mantıksal ifadeye dönüştürür ve onun sonucunu kullanır:
if (x.opCmp(y) işleç 0) {
Örnek olarak,
if (x <= y) {
ifadesi şuna dönüştürülür:
if (x.opCmp(y) <= 0) {
Kendi yazdığımız bu işlevin bu kurala göre doğru çalışabilmesi için işlevin şu değerleri döndürmesi gerekir:
- Soldaki nesne önce olduğunda eksi bir değer.
- Sağdaki nesne önce olduğunda artı bir değer.
- İki nesne eşit olduklarında sıfır değeri.
Bu sonuçlardan anlaşılacağı gibi, opCmp
'ın dönüş türü bool
değil, int
'tir.
GününSaati
nesnelerinin sıralama ilişkilerini öncelikle saat değerine, saatleri eşit olduğunda da dakika değerlerine bakacak şekilde şöyle belirleyebiliriz:
int opCmp(GününSaati sağdaki) const { /* Not: Buradaki çıkarma işlemleri sonucun alttan * taşabileceği durumlarda hatalıdır. (Metin içindeki * uyarıyı okuyunuz.) */ return (saat == sağdaki.saat ? dakika - sağdaki.dakika : saat - sağdaki.saat); }
Saat değerleri aynı olduğunda dakika değerlerinin farkı, saatler farklı olduğunda da saatlerin farkı döndürülüyor. Bu tanım, zaman sıralamasında soldaki nesne önce olduğunda eksi bir değer, sağdaki nesne önce olduğunda artı bir değer döndürür.
Uyarı: Yasal değerlerinin taşmaya neden olabildiği türlerde opCmp
işlecinin çıkarma işlemi ile gerçekleştirilmesi hatalıdır. Örneğin, aşağıdaki -2
değerine sahip olan nesne int.max
değerine sahip olan nesneden daha büyük çıkmaktadır:
struct S { int i; int opCmp(S rhs) const { return i - rhs.i; // ← HATA } } void main() { assert(S(-2) > S(int.max)); // ← yanlış sonuç }
Öte yandan, çıkarma işleminin GününSaati
yapısında kullanılmasında bir sakınca yoktur çünkü o türün hiçbir üyesinin yasal değerleri çıkarma işleminde taşmaya neden olmaz.
Bütün dizgi türleri ve aralıklar dahil olmak üzere dilimleri karşılaştırırken std.algorithm.cmp
işlevinden yararlanabilirsiniz. cmp()
iki dilimin sıra değerlerine uygun olarak eksi bir değer, sıfır, veya artı bir değer döndürür. Bu değer doğrudan opCmp
işlevinin dönüş değeri olarak kullanılabilir:
import std.algorithm; struct S { string isim; int opCmp(S rhs) const { return cmp(isim, rhs.isim); } }
opCmp
'un tanımlanmış olması bu türün std.algorithm.sort
gibi sıralama algoritmalarıyla kullanılabilmesini de sağlar. sort
sıralamayı belirlemek için nesneleri karşılaştırırken perde arkasında hep opCmp
işletilir. Aşağıdaki program 10 adet rasgele zaman değeri oluşturuyor ve onları sort
ile sıralıyor:
import std.random; import std.stdio; import std.string; import std.algorithm; struct GününSaati { int saat; int dakika; int opCmp(GününSaati sağdaki) const { return (saat == sağdaki.saat ? dakika - sağdaki.dakika : saat - sağdaki.saat); } string toString() const { return format("%02s:%02s", saat, dakika); } } void main() { GününSaati[] zamanlar; foreach (i; 0 .. 10) { zamanlar ~= GününSaati(uniform(0, 24), uniform(0, 60)); } sort(zamanlar); writeln(zamanlar); }
Beklendiği gibi, çıktıdaki saat değerleri zamana göre sıralanmışlardır:
[03:40,04:10,09:06,10:03,10:09,11:04,13:42,16:40,18:03,21:08]
İşlev gibi çağırmak için opCall
İşlev çağırırken kullanılan parantezler de işleçtir. Bu işlecin türün ismi ile kullanımını bir önceki bölümde static opCall
olanağında görmüştük. O kullanım yapı nesnelerinin varsayılan olarak kurulmalarını sağlıyordu.
opCall
türün nesnelerinin de işlev gibi kullanılabilmelerini sağlar:
BirTür nesne;
nesne();
O kodda nesne
bir işlev gibi çağrılmaktadır. Bu kullanım static
olmayan opCall
üye işlevleri tarafından belirlenir.
Bunun bir örneği olarak bir doğrusal denklemin x değerlerine karşılık y değerlerini hesaplayan bir yapı düşünelim:
y = ax + b
O hesaptaki y değerlerini opCall
işlevi içinde şöyle hesaplayabiliriz:
struct DoğrusalDenklem { double a; double b; double opCall(double x) const { return a * x + b; } }
O işlev sayesinde yapının nesneleri işlev gibi kullanılabilir ve verilen x değerlerine karşılık y değerleri hesaplanabilir:
DoğrusalDenklem denklem = { 1.2, 3.4 }; // nesne işlev gibi kullanılıyor: double y = denklem(5.6);
Not: opCall
işlevi tanımlanmış olan yapıları Tür(parametreler)
yazımıyla kuramayız çünkü o yazım da bir opCall
çağrısı olarak kabul edilir. O yüzden, yukarıdaki nesnenin { }
yazımıyla kurulması gerekmiştir. DoğrusalDenklem(1.2, 3.4)
yazımı gerçekten gerektiğinde iki double
parametre alan bir static opCall
işlevi tanımlanmalıdır.
İlk satırda nesne kurulurken denklemin çarpanı olarak 1.2, ekleneni olarak da 3.4 değerinin kullanılacağı belirleniyor. Bunun sonucunda denklem
nesnesi, y = 1.2x + 3.4 denklemini ifade etmeye başlar. Ondan sonra nesneyi artık bir işlev gibi kullanarak x değerlerini parametre olarak gönderiyor ve dönüş değeri olarak y değerlerini elde ediyoruz.
Bunun yararı, çarpan ve eklenen değerlerin baştan bir kere belirlenebilmesidir. Nesne o bilgiyi kendi içinde barındırır ve sonradan işlev gibi kullanıldığında yararlanır.
Başka çarpan ve eklenen değerleri ile kurulan bir nesneyi bu sefer de bir döngü içinde kullanan bir örneğe bakalım:
DoğrusalDenklem denklem = { 0.01, 0.4 }; for (double x = 0.0; x <= 1.0; x += 0.125) { writefln("%f: %f", x, denklem(x)); }
O da y = 0.01x + 0.4 denklemini x'in 0.0 ile 1.0 aralığındaki her 0.125 adımı için hesaplar.
Dizi erişim işleçleri
opIndex
, opIndexAssign
, opIndexUnary
, opIndexOpAssign
, ve opDollar
nesneyi nesne[konum]
biçiminde dizi gibi kullanma olanağı sağlarlar.
Dizilerden farklı olarak, bu işleçler çok boyutlu indeksleri de desteklerler. Çok boyutlu indeksler köşeli parantezler içinde birden fazla konum değeri ile sağlanır. Örneğin, iki boyutlu dizi gibi işleyen bir tür nesne[konum0, konum1]
söz dizimini destekleyebilir. Bu bölümde bu işleçleri yalnızca tek boyutlu olarak kullanacağız ve çok boyutlu örneklerini Ayrıntılı Şablonlar bölümünde göreceğiz.
Aşağıdaki satırlardaki kuyruk
, biraz aşağıda tanıyacağımız ÇiftUçluKuyruk
türünün bir nesnesi, e
ise int
türünde bir değişkendir.
opIndex
eleman erişimi amacıyla kullanılır. Köşeli parantezler içindeki konum değeri işlevin parametresi haline gelir:
e = kuyruk[3]; // 3 numaralı eleman e = kuyruk.opIndex(3); // üsttekinin eşdeğeri
opIndexAssign
atama amacıyla kullanılır. İlk parametresi atanan değer, sonraki parametresi de köşeli parantezler içindeki konum değeridir:
kuyruk[5] = 55; // 5 numaralı elemana 55 ata kuyruk.opIndexAssign(55, 5); // üsttekinin eşdeğeri
opIndexUnary
, opUnary
'nin benzeridir. Farkı, işlemin belirtilen konumdaki eleman üzerinde işleyecek olmasıdır:
++kuyruk[4]; // 4 numaralı elemanı arttır kuyruk.opIndexUnary!"++"(4); // üsttekinin eşdeğeri
opIndexOpAssign
, opOpAssign
'ın benzeridir. Farkı, atamalı işlemin belirtilen konumdaki eleman üzerinde işleyecek olmasıdır:
kuyruk[6] += 66; // 6 numaralı elemana 66 ekle kuyruk.opIndexOpAssign!"+"(66, 6); // üsttekinin eşdeğeri
opDollar
, dilimlerden tanınan $
karakterini tanımlar. İçerilen eleman adedini döndürmek içindir:
e = kuyruk[$ - 1]; // sonuncu eleman e = kuyruk[kuyruk.opDollar() - 1]; // üsttekinin eşdeğeri
Eleman erişimi işleçleri örneği
Çift uçlu kuyruk (double-ended queue, veya kısaca deque) bir dizi gibi işleyen ama başa eleman eklemenin de sona eleman eklemek kadar hızlı olduğu bir veri yapısıdır. (Dizilerde ise başa eleman eklemek bütün elemanların yeni bir diziye taşınmalarını gerektirdiğinden yavaş bir işlemdir.)
Çift uçlu kuyruk veri yapısını gerçekleştirmenin bir yolu, perde arkasında iki adet diziden yararlanmak ama bunlardan birincisini ters sırada kullanmaktır. Başa eklenen eleman aslında birinci dizinin sonuna eklenir ve böylece o işlem de sona eklemek kadar hızlı olur.
Bu veri yapısını gerçekleştiren aşağıdaki yapı bu bölümde gördüğümüz erişim işleçlerinin hepsini tanımlamaktadır:
import std.stdio; import std.string; import std.conv; struct ÇiftUçluKuyruk { private: /* Elemanlar bu iki üyenin hayalî olarak uç uca * gelmesinden oluşurlar. Ancak, 'baş' ters sırada * kullanılır: İlk eleman baş[$-1]'dir, ikinci eleman * baş[$-2]'dir, vs. * * baş[$-1], baş[$-2], ... baş[0], son[0], ... son[$-1] */ int[] baş; // baş taraftaki elemanlar int[] son; // son taraftaki elemanlar /* Belirtilen konumdaki elemanın hangi dilimde olduğunu * bulur ve o elemana bir referans döndürür. */ ref inout(int) eleman(size_t konum) inout { return (konum < baş.length ? baş[$ - 1 - konum] : son[konum - baş.length]); } public: string toString() const { string sonuç; foreach_reverse (eleman; baş) { sonuç ~= format("%s ", to!string(eleman)); } foreach (eleman; son) { sonuç ~= format("%s ", to!string(eleman)); } return sonuç; } /* Not: Sonraki bölümlerde göreceğimiz olanaklardan * yararlanıldığında toString() çok daha etkin olarak * aşağıdaki gibi de yazılabilir: */ version (none) { void toString(void delegate(const(char)[]) hedef) const { import std.format; import std.range; formattedWrite( hedef, "%(%s %)", chain(baş.retro, son)); } } /* Başa eleman ekler. */ void başınaEkle(int değer) { baş ~= değer; } /* Sona eleman ekler. * * Örnek: kuyruk ~= değer */ ref ÇiftUçluKuyruk opOpAssign(string işleç)(int değer) if (işleç == "~") { son ~= değer; return this; } /* Belirtilen elemanı döndürür. * * Örnek: kuyruk[konum] */ inout(int) opIndex(size_t konum) inout { return eleman(konum); } /* Tekli işleci belirtilen elemana uygular. * * Örnek: ++kuyruk[konum] */ int opIndexUnary(string işleç)(size_t konum) { mixin ("return " ~ işleç ~ "eleman(konum);"); } /* Belirtilen elemana belirtilen değeri atar. * * Örnek: kuyruk[konum] = değer */ int opIndexAssign(int değer, size_t konum) { return eleman(konum) = değer; } /* Belirtilen değeri belirtilen işlemde kullanır ve sonucu * belirtilen elemana atar. * * Örnek: kuyruk[konum] += değer */ int opIndexOpAssign(string işleç)(int değer, size_t konum) { mixin ("return eleman(konum) " ~ işleç ~ "= değer;"); } /* Uzunluk anlamına gelen $ karakterini tanımlar. * * Örnek: kuyruk[$ - 1] */ size_t opDollar() const { return baş.length + son.length; } } void main() { auto kuyruk = ÇiftUçluKuyruk(); foreach (i; 0 .. 10) { if (i % 2) { kuyruk.başınaEkle(i); } else { kuyruk ~= i; } } writefln("Üç numaralı eleman: %s", kuyruk[3]); // erişim ++kuyruk[4]; // arttırım kuyruk[5] = 55; // atama kuyruk[6] += 66; // atamalı arttırım (kuyruk ~= 100) ~= 200; writeln(kuyruk); }
opOpAssign
işlevinin dönüş türünün de yukarıdaki ilkeler doğrultusunda ref
olarak işaretlendiğine dikkat edin. ~=
işleci bu sayede zincirleme olarak kullanılabilmektedir:
(kuyruk ~= 100) ~= 200;
O ifadelerin sonucunda 100 ve 200 değerleri aynı kuyruk nesnesine eklenmiş olurlar:
Üç numaralı eleman: 3 9 7 5 3 2 55 68 4 6 8 100 200
Dilim işleçleri
opSlice
nesneyi []
işleciyle kullanma olanağı verir.
Bu işlece ek olarak opSliceUnary
, opSliceAssign
, ve opSliceOpAssign
işleçleri de vardır ama onların kullanımları önerilmez.
D, birden fazla boyutta dilimlemeyi destekler. Çok boyutlu bir dizi dilimleme örneğini ilerideki Ayrıntılı Şablonlar bölümünde göreceğiz. O bölümde anlatılacak olan yöntemler tek boyutta da kullanılabilseler de, hem yukarıdaki indeksleme işleçlerine uymazlar hem de henüz görmediğimiz şablonlar olarak tanımlanırlar. Bu yüzden, bu bölümde opSlice
'ın şablon olmayan ve yalnızca tek boyutta kullanılabilen bir kullanımını göreceğiz. (opSlice
'ın bu kullanımı da önerilmez.)
opSlice
'ın iki farklı kullanımı vardır:
- Bütün elemanlar anlamına gelen
kuyruk[]
biçiminde köşeli parantezlerin içinin boş olduğu kullanım - Belirtilen aralıktaki elemanlar anlamına gelen
kuyruk[baş .. son]
biçiminde köşeli parantezlerin içinde bir sayı aralığı belirtilen kullanım
Hem elemanları bir araya getiren topluluk kavramıyla hem de o elemanlara erişim sağlayan aralık kavramıyla ilgili olduklarından bu işleçler diğerlerinden daha karmaşık gelebilirler. Topluluk ve aralık kavramlarını ilerideki bölümlerde daha ayrıntılı olarak göreceğiz.
Şablon olmayan ve yalnızca tek boyutta işleyen opSlice
'ın buradaki kullanımı, topluluktaki belirli bir aralıktaki elemanları temsil eden bir nesne döndürür. O aralıktaki elemanlara uygulanan işleçleri tanımlamak o nesnenin görevidir. Örneğin, aşağıdaki kullanım perde arkasında önce opSlice
yüklemesinden yararlanarak bir aralık nesnesi üretir, sonra opOpAssign!
işlecini o aralık nesnesi üzerinde işletir:
"*"
kuyruk[] *= 10; // bütün elemanları 10'la çarp // Üsttekinin eşdeğeri: { auto aralık = kuyruk.opSlice(); aralık.opOpAssign!"*"(10); }
Buna uygun olarak, ÇiftUçluKuyruk
türünün opSlice
işlevleri özel bir Aralık
nesnesi döndürür:
import std.exception; struct ÇiftUçluKuyruk { // ... /* Bütün elemanları kapsayan bir aralık döndürür. * ('Aralık' yapısı aşağıda tanımlanıyor.) * * Örnek: kuyruk[] */ inout(Aralık) opSlice() inout { return inout(Aralık)(baş[], son[]); } /* Belirli elemanları kapsayan bir aralık döndürür. * * Örnek: kuyruk[ilkKonum .. sonKonum] */ inout(Aralık) opSlice(size_t ilkKonum, size_t sonKonum) inout { enforce(sonKonum <= opDollar()); enforce(ilkKonum <= sonKonum); /* Belirtilen aralığın 'baş' ve 'son' dilimlerinin * hangi bölgelerine karşılık geldiklerini hesaplamaya * çalışıyoruz. */ if (ilkKonum < baş.length) { if (sonKonum < baş.length) { /* Aralık bütünüyle 'baş' içinde. */ return inout(Aralık)( baş[$ - sonKonum .. $ - ilkKonum], []); } else { /* Aralığın bir bölümü 'baş' içinde, geri * kalanı 'son' içinde. */ return inout(Aralık)( baş[0 .. $ - ilkKonum], son[0 .. sonKonum - baş.length]); } } else { /* Aralık bütünüyle 'son' içinde. */ return inout(Aralık)( [], son[ilkKonum - baş.length .. sonKonum - baş.length]); } } /* Kuyruğun belirli bir aralığını temsil eder. opUnary, * opAssign, ve opOpAssign işleçlerinin tanımları bu yapı * içindedir. */ struct Aralık { int[] başAralık; // 'baş' içindeki elemanlar int[] sonAralık; // 'son' içindeki elemanlar /* Belirtilen tekli işleci elemanlara uygular. */ Aralık opUnary(string işleç)() { mixin (işleç ~ "başAralık[];"); mixin (işleç ~ "sonAralık[];"); return this; } /* Belirtilen değeri elemanlara atar. */ Aralık opAssign(int değer) { başAralık[] = değer; sonAralık[] = değer; return this; } /* Belirtilen değeri her eleman için belirtilen * işlemde kullanır ve sonucu o elemana atar. */ Aralık opOpAssign(string işleç)(int değer) { mixin ("başAralık[] " ~ işleç ~ "= değer;"); mixin ("sonAralık[] " ~ işleç ~ "= değer;"); return this; } } } void main() { auto kuyruk = ÇiftUçluKuyruk(); foreach (i; 0 .. 10) { if (i % 2) { kuyruk.başınaEkle(i); } else { kuyruk ~= i; } } writeln(kuyruk); kuyruk[] *= 10; kuyruk[3 .. 7] = -1; writeln(kuyruk); }
Çıktısı:
9 7 5 3 1 0 2 4 6 8 90 70 50 -1 -1 -1 -1 40 60 80
Tür dönüşümü işleci opCast
opCast
elle açıkça yapılan tür dönüşümünü belirler ve dönüştürülecek her tür için ayrı ayrı yüklenebilir. Daha önceki bölümlerden hatırlayacağınız gibi, açıkça tür dönüşümü hem to
işlevi ile hem de cast
işleciyle sağlanabilir.
Bu işleç de şablon olarak tanımlanır ama kalıbı farklıdır: Hangi dönüşümün tanımlanmakta olduğu (T : dönüştürülecek_tür)
söz dizimiyle belirtilir:
dönüştürülecek_tür opCast(T : dönüştürülecek_tür)() {
// ...
}
Yine şimdilik bir kalıp olarak kabul etmenizi istediğim bu söz dizimini de daha sonraki Şablonlar bölümünde göreceğiz.
Süre
'nin saat ve dakikadan oluşan bir tür olduğunu kabul edelim. Bu türün nesnelerini double
türüne dönüştüren işlev aşağıdaki gibi tanımlanabilir:
import std.stdio; import std.conv; struct Süre { int saat; int dakika; double opCast(T : double)() const { return saat + (to!double(dakika) / 60); } } void main() { auto süre = Süre(2, 30); double kesirli = to!double(süre); // cast(double)süre de olabilirdi writeln(kesirli); }
Yukarıdaki tür dönüşümü satırında derleyici üye işlevi perde arkasında şöyle çağırır:
double kesirli = süre.opCast!double();
double
türüne dönüştüren yukarıdaki işleç iki saat otuz dakikaya karşılık 2.5 değerini üretmektedir:
2.5
opCast
açıkça yapılan tür dönüşümleri için olduğu halde, onun bool
özellemesi mantıksal ifadelerde otomatik olarak işletilir:
struct Süre { // ... bool opCast(T : bool)() const { return (saat != 0) || (dakika != 0); } } // ... if (süre) { // derlenir // ... } while (süre) { // derlenir // ... } auto r = süre ? 1 : 2; // derlenir
Yine de, opCast
'in bool
özellemesi bütün otomatik bool
dönüşümleri için değildir:
void foo(bool b) { // ... } // ... foo(süre); // ← derleme HATASI bool b = süre; // ← derleme HATASI
Error: cannot implicitly convert expression (süre) of type Süre to bool Error: function deneme.foo (bool b) is not callable using argument types (Süre)
Sevk işleci opDispatch
Nesnenin var olmayan bir üyesine erişildiğinde çağrılacak olan üye işlevdir. Var olmayan üyelere yapılan bütün erişimler bu işlece sevk edilir.
Var olmayan üyenin ismi opDispatch
'in bir şablon parametresi olarak belirir.
Bu işleci çok basit olarak gösteren bir örnek:
import std.stdio; struct BirTür { void opDispatch(string isim, T)(T parametre) { writefln("BirTür.opDispatch - isim: %s, değer: %s", isim, parametre); } } void main() { BirTür nesne; nesne.varOlmayanİşlev(42); nesne.varOlmayanBaşkaİşlev(100); }
Var olmayan üyelerine erişildiği halde derleme hatası alınmaz. Bütün o çağrılar opDispatch
işlevinin çağrılmasını sağlarlar. Birinci şablon parametresi işlevin ismidir. Çağrılan noktada kullanılan parametreler de opDispatch
'in parametreleri haline gelirler:
BirTür.opDispatch - isim: varOlmayanİşlev, değer: 42 BirTür.opDispatch - isim: varOlmayanBaşkaİşlev, değer: 100
isim
şablon parametre değeri normalde opDispatch
içinde kullanılabilir ve işlemler onun değerine bağlı olarak seçilebilirler:
switch (isim) { // ... }
İçerme sorgusu için opBinaryRight!"in"
Eşleme tablolarından tanıdığımız in
işlecini nesneler için de tanımlama olanağı sağlar.
Diğer işleçlerden farklı olarak, bu işleç için nesnenin sağda yazıldığı durum daha doğaldır:
if (zaman in öğleTatili) {
O yüzden bu işleç için daha çok opBinaryRight!
yüklenir ve derleyici perde arkasında o üye işlevi çağırır:
"in"
// üsttekinin eşdeğeri if (öğleTatili.opBinaryRight!"in"(zaman)) {
!in
işleci ise bir değerin eşleme tablosunda bulunmadığını belirlemek için kullanılır:
if (a !in b) {
!in
yüklenemez çünkü derleyici perde arkasında in
işlecinin sonucunun tersini kullanır:
if (!(a in b)) { // üsttekinin eşdeğeri
in
işleci örneği
Bu örnek daha önce gördüğümüz Süre
ve GününSaati
yapılarına ek olarak bir de ZamanAralığı
yapısı tanımlıyor. Bu yapı için tanımlanan in
işleci belirli bir zamanın belirli bir aralıkta olup olmadığını bildirmek için kullanılacak.
Bu örnekte de yalnızca gerektiği kadar üye işlev kullandım.
GününSaati
nesnesinin for
döngüsünde nasıl temel türler kadar rahat kullanıldığına özellikle dikkat edin. O döngü işleç yüklemenin yararını gösteriyor.
import std.stdio; import std.string; struct Süre { int dakika; } struct GününSaati { int saat; int dakika; ref GününSaati opOpAssign(string işleç)(Süre süre) if (işleç == "+") { dakika += süre.dakika; saat += dakika / 60; dakika %= 60; saat %= 24; return this; } int opCmp(GününSaati sağdaki) const { return (saat == sağdaki.saat ? dakika - sağdaki.dakika : saat - sağdaki.saat); } string toString() const { return format("%02s:%02s", saat, dakika); } } struct ZamanAralığı { GününSaati baş; GününSaati son; // son aralığın dışında kabul edilir bool opBinaryRight(string işleç)(GününSaati zaman) const if (işleç == "in") { return (zaman >= baş) && (zaman < son); } } void main() { auto öğleTatili = ZamanAralığı(GününSaati(12, 00), GününSaati(13, 00)); for (auto zaman = GününSaati(11, 30); zaman < GününSaati(13, 30); zaman += Süre(15)) { if (zaman in öğleTatili) { writeln(zaman, " öğle tatilinde"); } else { writeln(zaman, " öğle tatili dışında"); } } }
Çıktısı:
11:30 öğle tatili dışında 11:45 öğle tatili dışında 12:00 öğle tatilinde 12:15 öğle tatilinde 12:30 öğle tatilinde 12:45 öğle tatilinde 13:00 öğle tatili dışında 13:15 öğle tatili dışında
Problem
Payını ve paydasını long
türünde iki üye olarak tutan bir kesirli sayı türü tanımlayın. Böyle bir yapının bir yararı, float
, double
, ve real
'deki değer kayıplarının bulunmamasıdır. Örneğin, 1.0/3 gibi bir double
değerin 3 ile çarpılmasının sonucu 1.0 olmadığı halde 1/3'ü temsil eden Kesirli
bir nesnenin 3 ile çarpılmasının sonucu tam olarak 1'dir:
struct Kesir { long pay; long payda; /* Kurucu işlev kolaylık olsun diye paydanın * belirtilmesini gerektirmiyor ve 1 varsayıyor. */ this(long pay, long payda = 1) { enforce(payda != 0, "Payda sıfır olamaz"); this.pay = pay; this.payda = payda; /* Paydanın eksi değer almasını başından önlemek daha * sonraki işlemleri basitleştirecek. */ if (this.payda < 0) { this.pay = -this.pay; this.payda = -this.payda; } } /* ... işleçleri siz tanımlayın ... */ }
Bu yapı için işleçler tanımlayarak olabildiğince temel türler gibi işlemesini sağlayın. Yapının tanımı tamamlandığında aşağıdaki birim testi bloğu hatasız işletilebilsin. O blokta şu işlemler bulunuyor:
- Payda sıfır olduğunda hata atılıyor. (Bu, yukarıdaki kurucudaki
enforce
ile zaten sağlanıyor.) - Değerin eksi işaretlisini üretmek: Örneğin, 1/3 değerinin eksilisi olarak -1/3 değeri elde ediliyor.
++
ve--
ile değer bir arttırılıyor veya azaltılıyor.- Dört işlem destekleniyor: Hem
+=
,-=
,*=
, ve/=
ile tek nesnenin değeri değiştirilebiliyor hem de iki nesne+
,-
,*
, ve/
aritmetik işlemlerinde kullanılabiliyor. (Kurucuda olduğu gibi, sıfıra bölme işlemi de denetlenmeli ve önlenmelidir.)Hatırlatma olarak, a/b ve c/d gibi iki kesirli arasındaki aritmetik işlem formülleri şöyledir:
- Toplama: a/b + c/d = (a*d + c*b)/(b*d)
- Çıkarma: a/b - c/d = (a*d - c*b)/(b*d)
- Çarpma: a/b * c/d = (a*c)/(b*d)
- Bölme: (a/b) / (c/d) = (a*d)/(b*c)
- Nesnenin değeri
double
'a dönüştürülebiliyor. - Sıralama ve eşitlik karşılaştırmaları pay ve paydaların tam değerlerine göre değil, o üyelerin ifade ettikleri değerlere göre uygulanıyorlar. Örneğin 1/3 ve 20/60 kesirli değerleri eşit kabul ediliyorlar.
unittest { /* Payda 0 olduğunda hata atılmalı. */ assertThrown(Kesir(42, 0)); /* 1/3 değeriyle başlayacağız. */ auto a = Kesir(1, 3); /* -1/3 */ assert(-a == Kesir(-1, 3)); /* 1/3 + 1 == 4/3 */ ++a; assert(a == Kesir(4, 3)); /* 4/3 - 1 == 1/3 */ --a; assert(a == Kesir(1, 3)); /* 1/3 + 2/3 == 3/3 */ a += Kesir(2, 3); assert(a == Kesir(1)); /* 3/3 - 2/3 == 1/3 */ a -= Kesir(2, 3); assert(a == Kesir(1, 3)); /* 1/3 * 8 == 8/3 */ a *= Kesir(8); assert(a == Kesir(8, 3)); /* 8/3 / 16/9 == 3/2 */ a /= Kesir(16, 9); assert(a == Kesir(3, 2)); /* double türünde bir değere dönüştürülebilmeli. * * Hatırlarsanız, double türü her değeri tam olarak ifade * edemez. 1.5 değeri tam olarak ifade edilebildiği için * bu testi bu noktada uyguladım. */ assert(to!double(a) == 1.5); /* 1.5 + 2.5 == 4 */ assert(a + Kesir(5, 2) == Kesir(4, 1)); /* 1.5 - 0.75 == 0.75 */ assert(a - Kesir(3, 4) == Kesir(3, 4)); /* 1.5 * 10 == 15 */ assert(a * Kesir(10) == Kesir(15, 1)); /* 1.5 / 4 == 3/8 */ assert(a / Kesir(4) == Kesir(3, 8)); /* Sıfırla bölmek hata atmalı. */ assertThrown(Kesir(42, 1) / Kesir(0)); /* Payı az olan öncedir. */ assert(Kesir(3, 5) < Kesir(4, 5)); /* Paydası büyük olan öncedir. */ assert(Kesir(3, 9) < Kesir(3, 8)); assert(Kesir(1, 1_000) > Kesir(1, 10_000)); /* Değeri küçük olan öncedir. */ assert(Kesir(10, 100) < Kesir(1, 2)); /* Eksi değer öncedir. */ assert(Kesir(-1, 2) < Kesir(0)); assert(Kesir(1, -2) < Kesir(0)); /* Aynı değerler hem <= hem de >= olmalı. */ assert(Kesir(-1, -2) <= Kesir(1, 2)); assert(Kesir(1, 2) <= Kesir(-1, -2)); assert(Kesir(3, 7) <= Kesir(9, 21)); assert(Kesir(3, 7) >= Kesir(9, 21)); /* Değerleri aynı olanlar eşit olmalı. */ assert(Kesir(1, 3) == Kesir(20, 60)); /* Karışık işaretler aynı sonucu üretmeli. */ assert(Kesir(-1, 2) == Kesir(1, -2)); assert(Kesir(1, 2) == Kesir(-1, -2)); }