C# İLE BİT TABANLI İŞLEMLER

Oğuzhan KARAGÜZEL
25 min readAug 13, 2023

--

Makeledeki kodlara ulaşmak için

Merhaba.

Uzun, yorucu bir yazılım geliştirme eğitimi sürecinin ardından artık ortaya ürün çıkartabilir hale geldim ve bir yıldız yazılımcı oldum. Ancak kendime tam anlamıyla yazılımcı diyebilmek için yaptığımız işlerin temeline inmeyi görev bildim. Bu yazımda ise bildiklerimi pekiştirmek ve benim gibi düşünenlere yol göstermek adına bit tabanlı işlemleri anlatmak ve basit bir alıştırma yapmak istiyorum. Şimdiden uyarmalıyım! Bütün detaylara değinmeye çalışacağım. bundan dolayı oldukça uzun bir yazı olacak.

Benim gibi yazılım eğitimini yeni tamamlayanlar ya da eğitim sürecinde olanlar, bu konuyu ya bilmiyor ya da sadece adını duydu. Bundan dolayı ilk olarak “bit nedir ve nasıl tanımlarız” konusuyla başlamak istiyorum.

BİT (binary digit). ikili basamak, ikili sayı sistemi veya ikili rakam. Yani ikilik taban. Aslında nasıl tanımlarsanız doğru olacaktır. Prof. Dr. Emrah Sefa Gürkan’ın anlattığına göre “digit” aslında fransızca kökenlidir ve “parmak” demektir.

Bu terim bilgisayar bilimine girdiğinden ve “basamak” olarak kullanıldığından artık anlamı değişmiştir. Bundan dolayı türkçeleştiriken “ikilik taban” bence en doğrusu olup yazı boyunca bu şekilde kullanacağım.

Günlük hayatımızda kullandığımız sayma sayıları ve rakamlar bile “onluk” tabana göre oluşturulmuştur. Tanımlarsak; 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. (on adet olduğuna dikkat!) “Neden onluk taban?” sorusunun cevabı ise on parmağımızın olmasıdır. Bu cevap için referans göstermeye gerek yoktur. Artık genel geçer bir bilgidir. Şimdi bu sayma sayılarına göre sayarsak dokuza ulaştığımızda, artık elimizde sayma sayısı ve rakam kalmadığından sol tarafına bir yazarak basamak atlarız. Bu oldukça tutarlı ve mantıklı bir sistemdir. On tane sayma sayımız var. Tıpkı duvara çentik atar gibi sayılar bitince sol tarafına geçiyoruz. Aynı şeyi ikilik tabanda yapmaya kalkalım. Bu sefer elimizde sadece iki tane sayma sayısı var. Tanımlarsak; 0, 1.

Elimizde hâlâ daha diğer rakamlar mevcut! (2, 3, 4, 5, 6, 7, 8, 9). Ancak sadece iki adet sayımız var. bu durumda ikilik ve onluk olarak sayarsak şunu elde ederiz.

  • ikilik 0 = onluk 0
  • ikilik 1 = onluk 1

Artık ikilik tabanda sayma sayıları bitti. Onluk tabandan alıştığımız işlemleri yaparak devam edersek;

  • ikilik 10 = onluk 2
  • ikilik 11 = onluk 3
  • ikilik 100 = onluk 4
  • ikilik 101 = onluk 5
  • ikilik 110 = onluk 6
  • ikilik 111 = onluk 7
  • ikilik 1000 = onluk 8
  • ikilik 1001 = onluk 9

Artık onluk tabandaki sayma sayıları ve hatta rakamlarda bitti.

  • ikilik 1010 = onluk 10

İKİLİK TABANDAN ONLUK TABANA DÖNÜŞÜM.

Bu işlem oldukça kolaydır. onluk taban için kısa bir aydınlatmanın ardından ikilik tabana bakalım.

354 sayısını incelersek;

354 = (3*100) + (5*10) + (4*1) = (3*(10²)) + (5*10¹) + (4*10⁰) görüldüğü üzere tabanımız “10” olduğundan basamağın sahip olduğu değeri üst alarak bu işlemi tamamlayabiliriz.

ikilik 1001 = onluk 9 olduğunu dönüşüm ile gösterirsek;

(1*(2³)) + (0*(2²)) + (0*(2¹)) + (1*(2⁰)) = 8 + 0 + 0 + 1 = 9 olduğu görülebilir.

ONLUK TABANDAN İKİLİK TABANA DÖNÜŞÜM.

Bu işlem de oldukça basittir. İstediğiniz sayıyı istediğiniz tabana dönüştürebilirsiniz. Sadece dönüştürmek istediğiniz tabana sayıyı sürekli olarak bölmek gerekir. Bölme işleminin elemanlarını incelersek işimiz kolaylaşır.

Dönüştürmek istediğimiz sayı (Bölünen).

Dönüştürmek istediğimiz taban (Bölen).

n+1. işlem için dönüştürmek istediğimiz sayı (Bölüm, n+1. bölünen).

Dönüştürmek istediğimiz tabanda ki basamak değeri “n” olan sayımız (kalan).

Matematiksel işleme dökersek anlaşılması daha kolay olacaktır. Örnek olarak Yine 9 sayısını ve rakamını (rakam diye vurgulamamın sebebi onaltılık sistemde rakam kalmadığından harfleri kullanıp onlara rakam dememizden kaynaklanmaktadır!) ikilik tabana dönüştürelim.

sırayla başlarsak.

  • (n = 0) 9/2 işlemi için bölüm = 4 ve kalan = 1
  • (n = 1) 4/2 işlemi için bölüm = 2 ve kalan = 0
  • (n = 2) 2/2 işlemi için bölüm = 1 ve kalan = 0
  • (n = 3) 1/2 işlemi için bölüm = 0 ve kalan = 1

bölüm 0' ulaştığında artık işleme devam etmenin manası yoktur. Zaten her sayının solunda sonsuza kadar giden “0” sayısı bulunur. Elde ettiğimiz kalanları, basamak değerine göre sıra ile yazarsak “1001” sayısını elde ederiz. Bu da işlemimizin doğru olduğunu göstermektedir.

Buranın anlaşıldığını varsayarak, C#’ta bir değişkene nasıl bit bazlı değer ataması yapıldığını anlatırsam, asıl konumuz için altyapıyı tamamlamış oluruz.

C#’TA BİT TEMELLİ DEĞİŞKEN ATAMALARI

Bu işlemi doğrudan kodlayarak göstermek en doğrusu olacaktır;

// Bit temelli değişken atama
// Bit-based variable assignment

int bitWise = 0b1001;
Console.WriteLine("int bitWise = 0b1001; => bitwise = " + bitWise);
Console.WriteLine("****************************************************************************");

// C# bit temelli değişken atamalarında, atama işlemini kolaylaştırmak adına, "_" kullanımını münkün kılar ve sayıları 4'lü gruplar halinde yazabiliriz.
// In C# bit-based variable assignments, we can use "_" to facilitate assignment and write numbers in groups of 4.
bitWise = 0b_1001;
Console.WriteLine("int bitWise = 0b_1001; => bitwise = " + bitWise);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir.

int bitWise = 0b1001; => bitwise = 9
****************************************************************************
int bitWise = 0b_1001; => bitwise = 9
****************************************************************************

Görüldüğü üzere 9 sayısını ikilik tabanda yazmanın tek kuralı şudur. bu sayıyı tanımlamadan önce “0b” yazarak bit temelli olduğunu belirtmek zorundayız.

Onaltılık olarak atama yapmak istersek “0x” yazmamız yeterli olacaktır.

// Hexadecimal atama için
// For hexadecimal assignment

int hexaDecimal = 0x_1001;
Console.WriteLine("int hexaDecimal = 0x_1001; => hexaDecimal = " + hexaDecimal);

Bu satırların çıktısı şu şekildedir.

int hexaDecimal = 0x_1001; => hexaDecimal = 4097

Peki normal olarak atanmış onluk bir sayıyı diğer tabanlarda yazdırmak istersek? Cevap şu şekilde;

// Rastgele onluk bir sayıyı diğer tabanlarda yazdırma.
// Printing a random decimal number in other bases.
// "decimal" bir anahtar kelimedir kullanılamaz!!!
// "decimal" is a keyword cannot be used!!!
Random rnd = new Random();
int random = rnd.Next(1, 255);
string binaryBase = Convert.ToString(random, 2);
string octalBase = Convert.ToString(random, 8);
string baseTen = Convert.ToString(random, 10);
string hexaDecimalBase = Convert.ToString(random, 16);
Console.WriteLine(
"number = " + random +
"\nbinaryBase = " + binaryBase +
"\noctalBase = " + octalBase +
"\nbaseTen = " + baseTen +
"\nhexaDecimalBase = " + hexaDecimalBase);

bu satırların çıktısı şu şekildedir.

number = 219
binaryBase = 11011011
octalBase = 333
baseTen = 219
hexaDecimalBase = db

İşte tam bu noktada mükemmel bir yere geldik.

ONALTILIK TABAN

Yukarıdaki örnekte “219” sayısının onaltılık tabanda “db” olduğunu gördük. Peki neden db?

Öncelikli olarak onaltılık taban neden var ve neden kullanıyoruz sorusuna kısa bir açıklama yapalım;

ikilik tabanda tanımlama yaparken kolaylık olması için “_” kullanarak sayıları dörtlü gruplara ayırmıştık. Aslında olay bu dörtlü gruplardan başlıyor. Şu anda kullandığınız bilgisayarın bellek ölçüm birimi çok çok yüksek ihtimalle byte’tır. 1 byte ise 8 adet bit’ten oluşmaktadır. Yani her 8 basamaklı ikilik sistemdeki bir sayıya bilgisayar bilimlerinde byte diyoruz ve promramlama dillerinde “byte” adlı değişken olarakta kullanıyoruz. En küçük sekiz basamaklı ikilik sayı “00000000” yani onluk olarak 0'dır. En büyük sekiz basamaklı ikilik sayı “11111111” olup onluk olarak 255'dir.

Bilgisayar teknolojisinin erken aşamalarında, verilerin kayıt edilmesinde bu tarz gruplandırmalar kullanılmıştır. Konudan sapmamak için kısaca bahsedeyim; 4'lü grup, 5'li grup, 6'lı grup ve 8'li grup (byte). olarak ölçü birimleri kullanılmıştır. Günümüzde ise baskın olarak 8'li grup kullanılır. Zayıf bir teoriye göre “Byte” kelimesi aslında “by eight” kelimesinin kısaltılmışıdır. Türkçe sekiz kez veya sekiz ile. “ASCII” tablosuna bakarsanız temel olarak 0'dan başlayıp 127'ye kadar tüm sayıların bir karşılığı vardır. Bu da aslında byte temelli veri gruplandırmanın bir ürünüdür. Neden 128 adet? Bu bambaşka bir konu! Belki başka bir yazıda. Neden bu gruplandırma yapılıyor sorusunun cevabı ise şu şekildedir; İşlemci hafızadan veri okurken, bu veriyi anlamlandırabilmesi için. Daha doğrusu biz insan oğlunun bu sistemi düzgün bir biçimde inşa edebilmesi için bu şekilde. Yoksa işlemci dediğiniz elektrik anahtarından başka bir şey değildir. Örnek olarak bilgisayarınızın özelliklerine bakın. Çok yüksek ihtimalle x64 ya da 64 bit tabanlı işlemci olduğu yazacaktır. Bu da demek oluyor ki işlemciniz tek bir döngüde (clock cycle) hafızadan okuduğu verinin miktarıdır. 64/8 = 8 byte demektir. Yani işlemci tek seferde hafızadan 8 byte’lık veri okumaktadır ve her bir byte’ı kendi içinde değerlendirmektedir. Bu kadar detay yeterli. Artık konumuza dönelim.

Buradaki 4'lük grup oldukça önemli bir gruptur. Nedenini açıklayıp konudan sapmayacağım. Bu 4'lük grubu incelersek 4 adet bit’ten oluşur ve en küçük değerinin onluk karşılığı “0000”2 = “0”10. En büyük değeri ise “1111”2 = “15”10. Bu dörtlük sistem çok kullanıldığından ve önem arz ettiğinde 0'dan 15'e kadar olan sayılar 16'lık sistem olarak kullanmaya başlandı. Tanımlarsak (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15). Gördüğünüz üzere onaltılık sistemin tüm sayıları ve rakamları. Ancak burada ciddi bir problem mevcut! Bu şekilde onaltılık sistemde sayı yazabiliriz ancak okuyamayız. Okumak için rakamlara ihtiyacımız var. Örnek olarak 256 sayısının onaltılık sistemde olduğunu söylersem onluk sisteme kolaylıkla çevirebilirsiniz ve sıkıntı olmaz. Ancak 111 sayısı ciddi problem olur. Bu şekilde 111 sayısının onluk sistemdeki karşılığı;

  • (1*(16²)) + (1*(16¹)) +(1*(16⁰)) = 273
  • (11*(16¹)) + (1*(16⁰)) = 177
  • (1*(16¹)) + (11*(16⁰)) = 27

değerlerinden herhangi biri olabilir. Bunu bilemeyeceğimizden yeni rakamlar icat edilmesi gerekti. Ancak icat edilmedi ve onaltılık sistemin yeni sayıları ve rakamları şu şekilde tanımlandı; (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, a, b, c, d, e, f).

Bu sayede her dörtlü grubu tek bir rakam ile tanımlayabilir hale geldik. Bu da bize programlamada önemli fayda sağlamaktadır. Örnek olarak bir değişken tanımladığımızda ram’de belirli bir alan ayrılır ve adres bilgisi değişken adı ile temsil edilir. Bu adres bilgisi ise devasa ikilik sayılar yerine daha kısa onaltılık sayılar ile tanımlanır. Bu fayda bizim ilgi alanımıza giren kısmın ufak bir parçasıdır. Bilgisayar bilimlerinde ise yeri çok daha büyüktür. Kısa bir örnekle bunuda görelim;

// Bellek Adresleri
// Memory Addresses

int value = 42;

unsafe
{
int* ptr = &value;

Console.WriteLine($"Değer - value: {value}");
Console.WriteLine($"Bellek Adresi - Memory Address: 0x{(ulong)ptr:x}");

}

Bu kod buloğunu doğrudan çalıştıramazsınız. Çalıştırmak için projenizin ayarlarına girip güvensiz kodları çalıştır seçeneğinin işaretlenmiş olması gerek. Ancak bunu yapmanıza gerek yok. Sonucu zaten vereceğim. Bu satırların çıktısı şu şekildedir.

Değer — value: 42
Bellek Adresi — Memory Address: 0x945558e58c

Bilgisayar biliminin erken aşamalarında “Assembly” programlama dili kullanılarak programlama yapılırken bu adres değerleri el ile giriliyordu. Sizce 945558e58c sayısının ikilik tabanda karşılığı nedir? Cevap;

100101000101010101001110100010001100

Bundan dolayı onaltılık sistem bilgisayar bilimlerinde önem arz etmektedir. Umarım yeterince açıklayıcı olmuştur. Günümüzde ki programlama dilleri sayesinde adres yazmak yerine sadece değişken ismi yazdığımızdan bu problemin farkında bile değiliz. Ancak aklınızda bulunsun. Aslında o bir adres.

Konumuza Bitwise işlemlerle devam edelim.

BİTWİSE OPERATÖRLERİ

İlk olarak operatör bilgilerini verelim;

  1. Unary ~ (bitwise complement) operatörü:

Bu işleç, bir sayının her bitinin tamamlayıcı (ters) değerini üretir. Yani, 0 biti 1 yapılır, 1 biti ise 0 yapılır. Örneğin, ~5 ifadesi, ~00000101 olarak düşünülürse, sonuç 11111010 olacaktır.

2. Binary << (sol kaydırma), >> (sağ kaydırma) ve >>> (işaretsiz sağ kaydırma) operatörleri:

  • Sol Kaydırma (<<): Bir sayının veya bit dizisinin bitlerini belirli bir adım sayısı kadar sola kaydırır. Her kaydırma işlemi, sayıyı 2 ile çarpmak gibi etki eder.
  • Sağ Kaydırma (>>): Bir sayının veya bit dizisinin bitlerini belirli bir adım sayısı kadar sağa kaydırır. Her kaydırma işlemi, sayıyı 2'ye bölmek gibi etki eder.
  • İşaret Belirtilmemiş Sağ Kaydırma (>>>): Sadece C#'ta değil, Java gibi dillerde de bulunan bu operatör, işaretsiz (unsigned) sağa kaydırma işlemidir. İşaretli sayılar yerine işaretsiz sayılar üzerinde kaydırma yapar. C#' 10.0'da doğrudan bu operatör yoktur, sağ kaydırma operatörü olan >> kullanılır. Ancak Microsoft’un web sitesinde söylediğini göre C# 11 ile bu özellik geldi. Ben bu kodları .net 6.0 ile çalıştırdığımdan, bu operatör ile ilgili işlemleri, microsoft’un web sitesinde verdiği bilgiler doğrultusunda yazacağım. Bu bağlantılara Kaynakçada yer vereceğim.

3. Binary & (mantıksal VE), | (mantıksal VEYA) ve ^ (mantıksal özel VE) operatörleri:

  • Mantıksal VE (&): İki sayının veya bit dizisinin her bitini karşılaştırır ve her iki bit de 1 ise sonuç bitini 1 yapar.
  • Mantıksal VEYA (|): İki sayının veya bit dizisinin her bitini karşılaştırır ve en az bir bit 1 ise sonuç bitini 1 yapar.
  • Mantıksal Özel VE (^): İki sayının veya bit dizisinin her bitini karşılaştırır ve bitler birbirinden farklı ise sonuç bitini 1 yapar.

Bu operatörler, bit manipülasyonu, veri işleme ve diğer düşük seviyeli işlemlerde kullanılır. Bit tabanlı operasyonlar yaparken bu operatörlerin davranışını anlamak önemlidir.

BİTWİSE İŞLEMLER

Şimdi bu operatörleri kullanarak işlem gerçekleştirmeye ve bu işlemleri anlamaya çalışalım.

~ OPERATÖRÜ

İlk olarak mantıksal terleyiciyi kullanalım ve 5 sayısını terslemeye çalışalım;

// ~ 

int a = 5;
int b = ~a;
Console.WriteLine(
"sayı - value = " + a +
"\nikilik - binary = " + Convert.ToString(a, toBase: 2) +
"\nTerslenmiş hali - inverted state = " + b +
"\nbit bazında terslenmiş hali - bitwise inverted = " + Convert.ToString(b, toBase: 2));

Bu satırların çıktısı şu şekildedir;

sayı — value = 5
ikilik — binary = 101
Terslenmiş hali — inverted state = -6
bit bazında terslenmiş hali — bitwise inverted = 11111111111111111111111111111010

Şimdi kafa karıştıran bir noktaya geldik. Teker teker açıklayalım;

ilk olarak 101 sayısı tersledikten sonra neden ; 11111111111111111111111111111010 oldu? “int” değişken tipi c# dilinde 32 bitlik bir değeri temsil eder. Biz değeri yazdırdığımızda bu değer “101” bile olsa aslında o değer “00000000000000000000000000000101” ‘dir. tüm bitler terslendiği için 11111111111111111111111111111010 sonucu elde edilir. Bu oldukça tutarlıdır. Çünkü dikkat ederseniz bütün 0’lar 1 ve bütün 1'ler 0 oldu.

ikinci soru ise 5 değerinin tesrlenmiş hali nasıl olurda -6 olur. 00000000000000000000000000000101 sayısının onluk değeri 5 iken 11111111111111111111111111111010 sayısının onluk değerinin 4.294.967.290 olması gerekirken nasıl -6 oldu?

Cevabı şu şekilde; en solda bulunan bit ya da 32. bit işaret bitidir. eğer 32. bit 0 ise sayı pozitif, 1 ise sayı negatiftir. Yani çıkan sayının negatif olmasının sebebi budur. Bu oldukça önemli ve aklınızda tutmanız gereken bir bilgidir. Ayrıca, madem 32. bit işaret bit’i, bu durumda -6 sayısının 10000000000000000000000000000110 olması gerekmez mi? hayır gerekmez! İşlemciler toplama çıkarma gibi matematiksel işlemleri bildiğimiz gibi toplama çıkarma yaparak hesaplamaz. Tamamen mantıksal işlemler ile yapar. Bundan dolayı bir bilgisayar için negatif en büyük değer 1111111111111111111111111111111 = -1 iken negatif en küçük değer 10000000000000000000000000000000 = -2.147.483.648'dir. Gerekli satırları yazıp inceleyelim.

// min - max, negative integers

int min = int.MinValue;
int max = -1;
Console.WriteLine("min - max, negative integers; " +
"\nmin = " + min +
"\nBinary min = " + Convert.ToString(min, 2) +
"\nmax = " + max +
"\nBinary max = " + Convert.ToString(max, 2)
);

bu satırların çıktısı şu şekildedir;

min — max, negative integers;
min = -2147483648
Binary min = 10000000000000000000000000000000
max = -1
Binary max = 11111111111111111111111111111111

Daha önceden de söylediğim gibi. Bunun sebebi toplama ve çıkarma işlemlerini tamamen mantık işlemleriyle yaptığımızdan, sayılar bu şekilde tanımlanmaktadır. Burada mantık işlemleriyle nasıl toplama çıkarma yapıldığına girmeyeceğim. Hem konumuzdan çok saparız hem de bununla ilgili oldukça güzel kaynaklar mevcut.

Kafa karışıklığını önlemek adına buradan itibaren “uint” değişkeni ile devam edeceğim. Bu değişken türünde 32. bit’te bir sayı olup işaret ile bir alakası yoktur.

Aynı satırları “uint” değişken türü ile yeniden tanımlayıp çalıştırırsak sonuç;

// ~ 

uint a = 5;
uint b = ~a;
Console.WriteLine(
"sayı - value = " + a +
"\nikilik - binary = " + Convert.ToString(a, toBase: 2) +
"\nTerslenmiş hali - inverted state = " + b +
"\nbit bazında terslenmiş hali - bitwise inverted = " + Convert.ToString(b, toBase: 2));
Console.WriteLine("****************************************************************************");

sayı — value = 5
ikilik — binary = 101
Terslenmiş hali — inverted state = 4294967290
bit bazında terslenmiş hali — bitwise inverted = 11111111111111111111111111111010

Görüldüğü üzere bit bazında aynı değerleri elde etsekte bu sefer sonuç -6 değil, tıpkı yukarıda hesapladığımız gibi 4294967290 çıktı.

<< SOLA KAYDIRMA

Bu oldukça basit işlemdir. Hemen kodlayarak gösterirsek;

// Sola kaydırma - Lest shift

uint number1 = 5;
uint leftShifted = number1 << 1;
Console.WriteLine(
"number = " + number1 +
"\nleftShifted = " + leftShifted +
"\nBitwise number1 = " + Convert.ToString(number1, 2) +
"\nBitwise leftShifted = " + Convert.ToString(leftShifted, 2)
);

Burayı kısaca açıklarsak; number değişkeni kaydırılacak değişkenimizdir. 1 ise kaydırma miktarı. kısaca şu şekilde. number << ?. number = kaydırma yapılacak sayı. ? = kaydırma miktarı. Bu bilgiler ışığında satırların çıktılarına bakacak olursak;

number1 = 5
leftShifted = 10
Bitwise number = 101
Bitwise leftShifted = 1010

tam da beklediğimiz gibi. Gördüğünüz üzere kaydırma işleminde adeta sayıyı 2 ile çaprtık. Sanki 5 * 2 = 10 oldu diyebiliriz. Bu oldukça tutarlı. ikilik 101 sayısının 5 olduğunu biliyoruz. her bir basamağı bir sola kaydırırsak. 1010 elde ederiz. Bu sayıda 10'a eşittir. Peki neden 1011 değil de 1010? Sanırım bu soruya cevap vermeme gerek yok! Eğer 1011 olsaydı o zaman bu soruyu sormak mantıklı olurdu!

>> SAĞA KAYDIRMA

Bu seferde 10 sayısını sağa kaydıralım. Bakalım 5 sayısını elde edebilecek miyiz?

// Sağa kaydırma - Right shift

uint number2 = 10;
uint rightShifted = number2 >> 1;
Console.WriteLine(
"number2 = " + number2 +
"\nrightShifted = " + rightShifted +
"\nBitwise number2 = " + Convert.ToString(number2, 2) +
"\nBitwise rightShifted = " + Convert.ToString(rightShifted, 2)
);

Bu satırların çıktısına bakacak olursak;

number2 = 10
rightShifted = 5
Bitwise number2 = 1010
Bitwise rightShifted = 101

Tam da beklediğimiz gibi. Peki ya bu sefer 1 basamak değilde 2 basamak kaydırırsak ne olur. 5'i 2'ye böldüğümüzde 2,5 elde ederiz. Peki bilgisayar bize hangi sonucu verecek. Tam sayılar ile çalıştığımızdan ya 2 vermesi lazım ya da 3. 101 ikilik sayısını 1 kademe daha kaydırırsak 10 ikilik sayısını elde ederiz. Bu da bize 2 sonucunu verecektir. Peki öyle mi? Hadi yukarıdaki kodu düzenleyerek deneyelim;

// iki kere Sağa kaydırma - shifted right twice

uint number3 = 10;
uint shiftedRightTwice = number3 >> 2;
Console.WriteLine(
"number3 = " + number3 +
"\nshiftedRightTwice = " + shiftedRightTwice +
"\nBitwise number3 = " + Convert.ToString(number3, 2) +
"\nBitwise shiftedRightTwice = " + Convert.ToString(shiftedRightTwice, 2)
);

Bu satırların çıktısı şu şekildedir;

number3 = 10
shiftedRightTwice = 2
Bitwise number3 = 1010
Bitwise shiftedRightTwice = 10

Tam beklediğimiz gibi. Eğer daha önce sormuşsanız sorunu cevabı çok açık. Bilgisayar her zaman aşağıya yuvarlar. Nedenini kendi gözlerinizle gördünüz.

Madem sağa kaydırmak 2'ye bölmek demek. hadi birde bunu negatif tam sayılarda deneyelim ve biraz kafa karıştıralım. Örnek olarak;

// İşaretki sağa kaydırma - Signed right shift

int number4 = -10;
int signedRightShifted = number4 >> 1;
Console.WriteLine(
"number4 = " + number4 +
"\nsignedRightShifted = " + signedRightShifted +
"\nBitwise number4 = " + Convert.ToString(number4, 2) +
"\nBitwise signedRightShifted = " + Convert.ToString(signedRightShifted, 2)
);

Bu satırların çıktısı şu şekildedir;

number4 = -10
signedRightShifted = -5
Bitwise number4 = 11111111111111111111111111110110
Bitwise signedRightShifted = 11111111111111111111111111111011

Gördüğünüz üzere işaretli de olsa yine 2'ye bölme işlemi gerçekleşti. Yine bu oldukça tutarlı. Aklınızda canlanan sorunun farkındayım. Ancak “her zaman aşağıya yuvarlar” önermesi doğru mu onu kontrol edelim? Bu durumda aşağıya kavramını karıştırmayın. İki kere kaydırırsak ve aşağıya yuvarlarsa -3 elde etmemiz gerekmektedir. Bakalım öyle m? Örnek olarak ;

// işaretli iki kere Sağa kaydırma - Signed shifted right twice

int number5 = -10;
int signedRightShiftedTwice = number5 >> 2;
Console.WriteLine(
"number5 = " + number5 +
"\nsignedRightShiftedTwice = " + signedRightShiftedTwice +
"\nBitwise number5 = " + Convert.ToString(number5, 2) +
"\nBitwise signedRightShiftedTwice = " + Convert.ToString(signedRightShiftedTwice, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number5 = -10
signedRightShiftedTwice = -3
Bitwise number5 = 11111111111111111111111111110110
Bitwise signedRightShiftedTwice = 11111111111111111111111111111101

Tam olarak beklediğimiz gibi. Şimdi gelelim sorumuza. Neden negatif tam sayılarda sağa kaydırma yaparken 32. bit’i sürekli olarak 0 yerine 1 atıyor? İşaretsiz ya da pozitif tam sayılarda, sağa kaydırma işleminde sağdan sürekli olarak 0 ekleyip soldan 0. bit’i siliyordu. Ancak işaretli olunca sağa kaydırma işleminde işarete göre atama yapıyor. Bu durumu daha iyi anlayabilmek negatif bir tam sayıyı sola kaydıralım ve sonucu inceleyelim. Sola kaydırmak 2 ile çarpmak demektir. -10 sayısını 2 ile çarparsak -20 elde etmemiz gerekir. Bakalım bu önermemiz hala geçerli mi? Örnek şu şekilde;

// işaretli sola kaydırma - Signed shifted left

int number6 = -10;
int signedLeftShifted = number5 << 1;
Console.WriteLine(
"number6 = " + number6 +
"\nsignedLeftShifted = " + signedLeftShifted +
"\nBitwise number6 = " + Convert.ToString(number6, 2) +
"\nBitwise signedLeftShifted = " + Convert.ToString(signedLeftShifted, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number6 = -10
signedLeftShifted = -20
Bitwise number6 = 11111111111111111111111111110110
Bitwise signedLeftShifted = 11111111111111111111111111101100

Önermemiz hâlâ geçerli. Bu sefer ise sağdan 0 ekleyip soldan sayı siliniyor.

Önemli not: En sağdaki bit’e, yani basamak değeri 0 olan bit’e, en değersiz bit denmektedir. Aynı şekilde en soldaki bit’e, bizim örneğimizde 32. bit ya da basamak değeri 31 olan bit’e en değerli bit denmektedir.

Bu konu ile alakalı daha fazla açıklama yapmanın anlamı yoktur. Bu açıklamaların ardından işaretsiz sağa kaydırma >>> operatörü oldukça kolay bir biçimde anlaşılacaktır.

>>> İŞARETSİZ SAĞA KAYDIRMA OPERATÖRÜ.

Tam olarak bildiğimi söyleyemem ama bu kodları .NET 7.0 sürümünde bir console app uygulamasında çalıştırırsanız. Belki hatasız bir biçimde çalışır. Ya da kaynakçada verdiğim microsoft bağlantısı ile kodları çalıştırmayı belki başarabilirsiniz.

Yukarıda işaretli, işaretsiz tam sayıların veya Pozitif ve negatif tam sayıların bit bazında sağa ve sola kaydırma işlemlerini yaptık ve çeşitli önermeler sunarak bunları kanıtladık. Önermelerimiz ise şu şekildedir;

Pozitif ya da negatif bir tam sayı, bit bazında sağa ya da sola kaydırılsa bile, daimi olarak;

  • Sağa kaydırmak sayıyı 2'ye bölmek ile eşdeğerdir.
  • Sola kaydırmak sayıyı 2 ile çarpmakla eşdeğerdir.
  • Tek sayıları işaret farketmeksizin 2‘ye bölümdükten sonra elde edilen bölüm daima aşağı yuvarlanır.

Peki bu önermeler işaretsiz sağa kaydırma için geçerli mi? Ya da işaretsiz sağa kaydırma var da, işaretsiz sola kaydırma neden yok?

Sağa kaydırma işleminde işaret bitinin değerine göre kaydırma yapıldığını yukarıdaki örneklerde görmüştük. Daha temel anlatımla; kaydırma işlemleri yukarıdaki önermelere daima uyacak şekilde yapılır.

Ancak işaretsiz sağa kaydırma bu önermelere uymaz. Hadi şu örneği inceleyelim;

int x = -8;
Console.WriteLine($"Before: {x,11}, hex: {x,8:x}, binary: {Convert.ToString(x, toBase: 2), 32}");

int y = x >> 2;
Console.WriteLine($"After >>: {y,11}, hex: {y,8:x}, binary: {Convert.ToString(y, toBase: 2), 32}");

int z = x >>> 2;
Console.WriteLine($"After >>>: {z,11}, hex: {z,8:x}, binary: {Convert.ToString(z, toBase: 2).PadLeft(32, '0'), 32}");
// Output:
// Before: -8, hex: fffffff8, binary: 11111111111111111111111111111000
// After >>: -2, hex: fffffffe, binary: 11111111111111111111111111111110
// After >>>: 1073741822, hex: 3ffffffe, binary: 00111111111111111111111111111110

Gördüğünüz üzere -8 sayısı normal bir biçimde 2 basamak sağa kaydırılıyor ve hem beklentilerimize hem de önermelerimize uyacak bir biçimde sonuç -2 çıkıyor. Ancak z değişkeni önermelermize ve beklentilerimize uymuyor. Buradan şu anlaşılıyor ki; işaretsiz sağa kaydırma işlemi, işaretin değerine bakmaksızın, pozitif bir tam sayıyı sağa kaydırır gibi sürekli olarak soldan 0 değerini yazıyor. Yani işaret biti olan 32. bit ya da basamak değeri 31 olan bit bile değişiyor ve sıfır oluyor. Bundan dolayı negatif bir tam sayı pozitif oluyor.

Yukarıda yaptığımız bol örnekler ve açıklamalar sayesinde bu durumun kolay bir biçimde anlaşıldığını düşünüyorum. Bundan dolayı bu konu üzerinde durmanın anlamı yoktur. Şu küçük notu verdikten sonra bu konuyu bitirmek en iyisidir. Örneğimiz şu şekildedir;

// Bit temelli işaretli ve işaretsiz atama
// Bitwise signed and unsigned assignment

// Bu şekilde atama yapılamaz - It cannot be assigned in this way.
int number7 = 0b_1000_1111_0000_1111_0000_1111_0000_1100;
// hata - error = Cannot implicitly convert type 'uint' to 'int'. An explicit conversion exists (are you missing a cast?)

// Bu şekilde atama yapılabilir - This way you can assign
int number8 = 0b_0000_1111_0000_1111_0000_1111_0000_1100;

// veya - or

uint number9 = 0b_1000_1111_0000_1111_0000_1111_0000_1100;
uint number10 = 0b_0000_1111_0000_1111_0000_1111_0000_1100;

& MANTIKSAL “VE” OPERATÖRÜ

Bu operatör sayıları bit bazında “ve” mantıksal işlemine sokarak sonuç elde eder. Aslında bilgisayar biliminde tüm değişkenler birer sayı olduğundan sadece sayıları değil, bildiğiniz tüm tipteki değişkenlere bu operatörü uygulayabilirsiniz. Hızlıca bir örnek yapalım. Örneğin daha anlaşılır ve kısa olması için sayıları küçük tutarsak;

// & operatörü - & operators

int number11 = 10;
int number12 = 7;
int number13 = number11 & number12;
Console.WriteLine(
"number11 = " + number11 +
"\nnumber12 = " + number12 +
"\nnumber13 = number11 & number12 = " + number13 +
"\nBitwise number11 = " + Convert.ToString(number11, 2) +
"\nBitwise number12 = " + Convert.ToString(number12, 2) +
"\nBitwise number13 = " + Convert.ToString(number13, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number11 = 10
number12 = 7
number13 = number11 & number12 = 2
Bitwise number11 = 1010
Bitwise number12 = 111
Bitwise number13 = 10

Çıktılar incelenirse ne olduğu apaçık bir şekilde görülebilir. 32 bitlik bir değişkenimiz var ve her bir bit kendi değer eşiti ile karşılaştırılarak sonuç elde edilmiş. Şimdi bunu daha detaylı bir biçimde inceleyelim. “n” sayısı basamak değerimiz olsun. Ayrıca 10 sayısının ikilik karşılığı 1010, 7 sayısının ikilik karşılığı 111. Bu sayıları bit bazında karşılaştırırsak;

  • (n = 0) 10 sayısı için “0” & 7 sayısı için “1” => 0&1 = 0 elde edilir.
  • (n = 1) 10 sayısı için “1” & 7 sayısı için “1” => 1&1 = 1elde edilir.
  • (n = 2) 10 sayısı için “0” & 7 sayısı için “1” => 0&1 = 0 elde edilir.
  • (n = 3) 10 sayısı için “1” & 7 sayısı için “0” => 0&1 = 0 elde edilir.
  • (n = 4) 10 sayısı için “0” & 7 sayısı için “0” => 0&0 = 0 elde edilir.

Burada daha fazla devam etmeye gerek yoktur. 32. bite kadar 0 olduğunu biliyoruz. Buradan elde edilen sayı şu şekildedir;

00000000000000000000000000000010 = 2 ya da ikilik 10 = onluk 2.

Önemli not: “ve” işlemi asla ama asla sayıları çıkartmak değildir! Benzetilemez, hatta hiç bir zaman benzetilmemelidir!

Bu işlemin anlaşıldığını düşünüyorum. Bu konu hakkında daha fazla anlatacak bir şey kalmadığından diğer konuya geçiyorum. Ancak konu ile alakalı örnek çözümü yapacağım. Yazının sonunda bu örnekleri bulabilirsiniz.

| MANTIKSAL “VEYA” OPERATÖRÜ

Yukaridaki örnekte ve işlemini detaylı bir biçimde gördük. Burada anlaşılan “|” operatörü sayıları bit bazında mantıksal “veya” işlemine tabi tutacaktır. Bu durumda biz de kod yazmadan önce kendi çıkarımımızı yapalım. Ardından program ile bunu doğrulamaya çalışalım. Yine 10 sayısı ile 7 sayısını “|” operatörü ile işleme sokalım; işlemin nasıl yapıldığı bir önceki konuda detaylı olarak gösterilmiştir.

  • (n = 0) 10 sayısı için “0” | 7 sayısı için “1” => 0|1 = 1 elde edilir.
  • (n = 1) 10 sayısı için “1” | 7 sayısı için “1” => 1|1 = 1 elde edilir.
  • (n = 2) 10 sayısı için “0” | 7 sayısı için “1” => 0|1 = 1 elde edilir.
  • (n = 3) 10 sayısı için “1” | 7 sayısı için “0” => 0|1 = 1 elde edilir.
  • (n = 4) 10 sayısı için “0” | 7 sayısı için “0” => 0|0 = 0 elde edilir.

n = 3 değerinden sonra devam etmeye gerek yoktur. Elde edilen sayı “1111” sayısıdır. Bu sayının onluk karşılığı “15”, onaltılık karşılığı ise “f” sayısıdır. Şimdi gerekli kodları yazarsak;

// | operatörü - | operators

int number14 = 10;
int number15 = 7;
int number16 = number14 | number15;
Console.WriteLine(
"number14 = " + number14 +
"\nnumber15 = " + number15 +
"\nnumber16 = number14 | number15 = " + number16 +
"\nBitwise number14 = " + Convert.ToString(number14, 2) +
"\nBitwise number15 = " + Convert.ToString(number15, 2) +
"\nBitwise number16 = " + Convert.ToString(number16, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number14 = 10
number15 = 7
number16 = number14 | number15 = 15
Bitwise number14 = 1010
Bitwise number15 = 111
Bitwise number16 = 1111

Görüldüğü üzere tamamiyle beklentilerimiz doğrultusunda sonuç elde ettik.

Önemli not: “veya” işlemi asla ama asla sayıları toplamak değildir! Benzetilemez, hatta hiç bir zaman benzetilmemelidir!

^ MANTIKSAL “İSE” OPERATÖRÜ

Bu operatöre de yine “veya” operatöründe yaptığımız gibi yaklaşalım. Bu operatör karşılaştırılan bitler birbirinin tersi ya da tamamlayıcısı ise 1 sonucunu yoksa 0 sonucunu döndürür. Bu durumda yine 10 ve 7 sayısını karşılaştırırsak;

  • (n = 0) 10 sayısı için “0” ^ 7 sayısı için “1” => 0^1 = 1 elde edilir.
  • (n = 1) 10 sayısı için “1” ^ 7 sayısı için “1” => 1^1 = 0elde edilir.
  • (n = 2) 10 sayısı için “0” ^ 7 sayısı için “1” => 0^1 = 1elde edilir.
  • (n = 3) 10 sayısı için “1” ^ 7 sayısı için “0” => 0^1 = 1 elde edilir.
  • (n = 4) 10 sayısı için “0” ^ 7 sayısı için “0” => 0^0 = 0 elde edilir.

Elde ettiğimiz sayı “1101” dir. Bu sayının onluk karşılığı “13” ‘tür. Gerekli kodları yazarak inceleyelim;

// ^ operatörü - ^ operators

int number17 = 10;
int number18 = 7;
int number19 = number14 ^ number15;
Console.WriteLine(
"number17 = " + number17 +
"\nnumber18 = " + number18 +
"\nnumber19 = number17 ^ number18 = " + number19 +
"\nBitwise number17 = " + Convert.ToString(number17, 2) +
"\nBitwise number18 = " + Convert.ToString(number18, 2) +
"\nBitwise number19 = " + Convert.ToString(number19, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısına bakacak olursak;

number17 = 10
number18 = 7
number19 = number17 ^ number18 = 13
Bitwise number17 = 1010
Bitwise number18 = 111
Bitwise number19 = 1101

İŞARETLİ YA DA NEGATİF TAM SAYILARDA MANTIKSAL OPERATÖRLER

Yukarıda yaptığımız örneklerin tamamını birde negatif sayılarla deneyelim. Hatta bir negatif birde pozitif sayılarla deneyip sonuçları inceleyelim. Burada ki örnekleri kısa tutacağım. Çünkü burada yaklaşık altı adet örnek yapılması gerekmektedir. Ben bir tane yapıp ilk olarak bir sonuç elde edeceğim. Ardından bu sonucu yorumlayarak bir çıkarım yapacağım. Yaptığım çıkarımı bir örnekle test edeceğim. Bu işlemlerin ardından merak ettiğiniz dört durumu kendiniz deneyebilirsiniz. Hatta denemelisiniz. Şimdi 10 ile -7 sayısını program yazarak “|” operatörü ile karşılaştıralım ve sonucu yorumlayalım.

int number20 = 10;
int number21 = -7;
int number22 = number20 | number21;
Console.WriteLine(
"number20 = " + number20 +
"\nnumber21 = " + number21 +
"\nnumber22 = number20 | number21 = " + number22 +
"\nBitwise number20 = " + Convert.ToString(number20, 2) +
"\nBitwise number21 = " + Convert.ToString(number21, 2) +
"\nBitwise number22 = " + Convert.ToString(number22, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number20 = 10
number21 = -7
number22 = number20 | number21 = -5
Bitwise number20 = 1010
Bitwise number21 = 11111111111111111111111111111001
Bitwise number22 = 11111111111111111111111111111011

Görüldüğü üzere işaret biti olan 32. bit bu işleme aynı şekilde maruz tutuluyor. Yani sayılarımız işaretli olsun ya da olmasın her bir bit işleme sokulur ve özel bir kural uygulanmaz.

bu durumda ^ operatörü ne yapar diye soralım. Pozitif tam sayılarda 13 değerini elde etmiştik. İçimden bir ses -13 değerini elde edeceğimizi söylüyor. Hadi ilk önce görelim;

// negatif sayılar ile ^ operatörü - ^ operators with negative numbers

int number23 = 10;
int number24 = -7;
int number25 = number20 ^ number21;
Console.WriteLine(
"number23 = " + number23 +
"\nnumber24 = " + number24 +
"\nnumber25 = number23 ^ number24 = " + number25 +
"\nBitwise number23 = " + Convert.ToString(number23, 2) +
"\nBitwise number24 = " + Convert.ToString(number24, 2) +
"\nBitwise number25 = " + Convert.ToString(number25, 2)
);
Console.WriteLine("****************************************************************************");

Bu satırların çıktısı şu şekildedir;

number23 = 10
number24 = -7
number25 = number23 ^ number24 = -13
Bitwise number23 = 1010
Bitwise number24 = 11111111111111111111111111111001
Bitwise number25 = 11111111111111111111111111110011

Tamamiyle beklentim doğrultusunda çıktı. Burada biraz kafanız karışmış olabilir. yukarıdaki çıktıyı yeniden düzenlersek;

number23 = 10
number24 = -7
number25 = number23 ^ number24 = -13
Bitwise number23 = 00000000000000000000000000001010
Bitwise number24 = 11111111111111111111111111111001
Bitwise number25 = 11111111111111111111111111110011

Yine her bir bit’in kendi içerisinde işleme tabi tutulduğu apaçıktır. Ancak burada -7 sayısının 11111111111111111111111111111001 olması, bundan dolayıda elde edilen sonucun 11111111111111111111111111110011 çıkması kafa karıştırıcı olabilir. Negatif sayıların neden 0 olması gereken bit’leri 1? sorusune dediğim gibi bu yazıda cevap vermeyeceğim. Belki başka bir yazıda. Ancak bunu şimdilik bu şekilde kabul edip işlemleri uygularsanız, diğer dört durumunda sonucunu oldukça rahat bir biçimde tahmin edebilirsiniz.

ÖRNEKLER VE ÇÖZÜMLERİ

Not: Bu çözümlerde ihtiyaç duymasakta bu bilgileri vermekte fayda vardır. Bu operatörlerin bir işlem önceliği sırası var. Bu sıra en öncelikliden başlayarak şu şekildedir;

  • ~
  • << , >> ve >>>
  • &
  • ^
  • |

1. ÖRNEK: TEK Mİ ÇİFT Mİ?

Yazılım eğitimi almış ya da yazılım dünyasına ucundan bakınan kişiler bile bu örneği yapmıştır. Hadi biraz farklı yaklaşalım.

// TEK Mİ ÇİFT Mİ? - ODD OR EVEN?
Console.WriteLine("Lütfen bir sayı giriniz - Please enter a number");
int number26 = Convert.ToInt32(Console.ReadLine());
int number27 = number26 & 1;
if (number27 == 1)
{
Console.WriteLine("tek - odd");
}
else
{
Console.WriteLine("çift - even");
}
Console.WriteLine("****************************************************************************");

Bu programa girdi olarak 10 sayısını verelim. Çıktı şu şekildedir;

Lütfen bir sayı giriniz — Please enter a number
10
çift — even

Bu sefer 7 sayısını girelim. Çıktı şu şekildedir;

Lütfen bir sayı giriniz — Please enter a number
7
tek — odd

Şimdi burada olan işlemi biraz açıklamakta fayda var. İlk olarak bir sayının tek ya da çift olduğunu belirleyebilmek için 2 ile bölüp kalanına bakarak belirliyorduk. Eğer kalan 0 ise çift. Eğer 1 ise tek. Örnek olarak;

// TEK Mİ ÇİFT Mİ? - ODD OR EVEN?
Console.WriteLine("Lütfen bir sayı giriniz - Please enter a number");
int number26 = Convert.ToInt32(Console.ReadLine());
int number27 = number26%2; // ← ← ←
if (number27 == 1)
{
Console.WriteLine("tek - odd");
}
else
{
Console.WriteLine("çift - even");
}
Console.WriteLine("****************************************************************************");

Ancak ikilik sistemde bir sayıyı incelerseniz şunu rahatlıkla görebilirsiniz. Yukarıdaki taban dönüştürme işlemini inceleyelim;

(1*(2³)) + (0*(2²)) + (0*(2¹)) + (1*(2⁰)) = 8 + 0 + 0 + 1 = 9

Bu örneğe dikkat ederseniz tüm basamaklar 2'nin katları ile çarpılıyor ve ardından toplanıyor. Ancak burada dikkatinizi çekmek istediğim bir nokta var. Eğer örneği dikkatli incelerseniz şunu apaçık bir biçimde görebilirsiniz;

ikilik sistemde bir basamak ya 0 ya da 1 olabilir. 0 çift 1 ise tektir. 0 çift değildir tartışmasına girmek istemiyorum. Şimdilik sadece böyle kabul edin. Şimdi dikkatlice bakarsanız ikilik bir sayıyı onluk sisteme çevirirken her bir basamak 2 ve katları ile çarpılıyor. Bu da demek oluyor ki yukarıda parantez içerisindeki tüm işlemlerin sonucunda çift sayılar elde edilecektir. Lâkin tek bir parantez ya da tek bir basamak hariç. O basamak ise basamak değeri 0 olan ya da 1. basamak veya bit ya da en değersiz bit. herhangi bir sayının üssü 0 olursa sonucun 1 olduğunu biliyoruz. Tabi ki 2⁰’da aynı şekilde 1'dir. bu durumda bu basamaktan gelen sonuç ya 0 olacak ya da 1 olacaktır. Geriye kalan tüm basamaklardan çift sayılar gelecektir. Bu toplama işleminin sonucunda çıkan sayının tek mi ya da çift mi olduğunu en değersiz bit belirler.

Bu durumda bizde program yazarken sayının çift mi ya da tek mi olduğunu bulurken sadece en değersiz bit’e bakmak bize doğru sonucu verecektir. Peki ama nasıl;

Yukarıdaki programı adım adım inceleyelim. Varsayalım ki 10 değerini girdik. 10 sayısının ikilik karşılığı 1010'dır. İkinci adımda biz bu sayıyı 1 sayısı ile ve işlemine sokuyoruz. bu işlemin sonucunda şu elde edilir;

  • (n = 0) 10 sayısı için “0” & 1 sayısı için “1” => 0 & 1 = 0 elde edilir.
  • (n = 1) 10 sayısı için “1” & 1 sayısı için “0” => 0 & 0 = 0 elde edilir.
  • (n = 2) 10 sayısı için “0” & 1 sayısı için “0” => 0 & 0 = 0 elde edilir.
  • (n = 3) 10 sayısı için “1” & 1 sayısı için “0” => 1 & 0 = 0 elde edilir.
  • (n = 4) 10 sayısı için “0” & 1 sayısı için “0” => 0 & 0 = 0 elde edilir.

göreceğiniz üzere bu işlemin sonucunda elde edilen sayı 0'dır. Ardından karşılartırma sırasında 0 = 1 işlemi sonucunda if bloğu içine girilmez ve else bloğu içerisinde sayının çift olduğu söylenir.

Lütfen aynı işlemi 7 sayısı için siz yapın. Çıkan sonucun 1 olduğunu göreceksiniz.

Peki bu işlemi % yani mod kullanmak yerine bitwise operatör kullanmanın faydası var mı? Evet var! Ancak benim gibi yıldız yazılımcıların ya da eğitim sürecinde olanların yazacakları herhangi bir programda bu derece optimizasyona ihtiyaç yoktur. Bu işlem mod kullanmaya kıyasla oldukça hızlıdır. Tahmin edebilirsiniz ilk olarak bölme işlemi yapılacak ve bu işlem, bizim yaptığımız işleme göre daha uzun sürecektir. Ancak dediğim gibi bizim programlarımızda bu hız farkı önemsizdir.

Bu hız farkını görebileceğiniz bir program yazmanızı tavsiye edeceğim.

2. ÖRNEK: 30 GÜNLÜK AYLARI BULMA

Değerli hocam Fatih Kaan Açıkgöz eğitimimiz sırasında bize bir problem verdi. Bu problemi çözerken iki tarih arasındaki süreyi hesaplamam gerekiyordu. Ancak bu iki tarih arasında ki süreyi “DateTime” sınıfını kullanarak değilde, kendi yazacağım bir algoritma ile hesaplamak istemiştim. Açıkçası yaptım ve başarılı oldum. Lâkin önemli bir sorun vardı! 1 ya da 2 yıllık gibi kısa süreleri yeteri kadar hızlı hesaplayan program, Tarihler arasındaki fark ciddi boyutlarda olunca, 100 ya da 200 değil 15 yıllık bir süredeki gün farkını hesaplaması yaklaşık 15 dakika alıyordu. Bundan dolayı bende vazgeçip sildim.

Ancak şimdi düşünüyorum da belki siz bu algoritmayı yazabilirsiniz. Benim uğraşmaya hiç niyetim yok! Ancak sizin yazacağınız programa nereden başlamanız gerektiği hakkında önemli bir ip ucu vereceğim. Şimdi programımızı yazmaya başlayalım. İlk olarak bir Enum’a ihtiyacımız var. Burada elemanlara değer verirken dikkat etmeniz gereken sadece bir kural var.

[Flags]
public enum Aylar_Months
{
Hiçbiri_None = 0,
Ocak_January = 1,
Şubat_February = 2,
Mart_March = 4,
Nisan_April = 8,
Mayıs_May = 16,
Haziran_June = 32,
Temmuz_July = 64,
Ağustos_August = 128,
Eylül_September = 256,
Ekim_October = 512,
Kasım_November = 1024,
Aralık_December = 2048
}

Kuralın ne olduğunu anlamışsınızdır. mesela ocak ayı için değer 1 iken şubat ayı için 10. Mart için 100, Nisan için 1000 derken nereye varmak istdiğim konusunda kafa yorun.

Bu arada Flags özniteliğini kendiniz inceleyebilirsiniz. Eğlenceli bir konudur. Detaylarına girmeyeceğim ancak bu enum’a bit alanı olarak davranacağımızı söylemiş oluyoruz o kadar.

Şimdi bize sadece 30 gün çeken ayları listeleyen bir program yazalım;

// 30 GÜNLÜK AYLARI BULMA - FIND 30 DAY MONTHS

Aylar_Months ay_Month = Aylar_Months.Ocak_January | Aylar_Months.Mart_March | Aylar_Months.Mayıs_May | Aylar_Months.Temmuz_July | Aylar_Months.Ağustos_August | Aylar_Months.Ekim_October | Aylar_Months.Aralık_December;

Console.WriteLine("30 gün çeken aylar - Months with 30 days:");
foreach (Aylar_Months bit in Enum.GetValues(typeof(Aylar_Months)))
{
if (bit == Aylar_Months.Hiçbiri_None)
continue;
if ((ay_Month & bit) != 0)
{
Console.WriteLine(bit.ToString());
}
} }
}

Bu satırların çıktısı şu şekildedir;

30 gün çeken aylar — Months with 30 days:
Ocak_January
Mart_March
Mayıs_May
Temmuz_July
Ağustos_August
Ekim_October
Aralık_December

Görüldüğü üzere bu işlemleri bilmeden tahmini olarak yazacağınız programdan epey farklı. Ancak temelde aynı işlemleri barındırıyor. Sadece bit bazında hareket ettiğimizden çok daha hızlı çalışıyor. Bu şekilde devasa bir sınıf yazabiliriz. Yazılan bu sınıf benim yavaş algoritmamın yanında üstün performanslı olacaktır.

Birazcık algoritmayı anlatayım. Ardından lütfen bu programı dikkatlice inceleyin. İlk olarak “ay_Month” değişkeninin sayısal değerine bakalım. yukarıda anlattıklarımı baz alırsak bu yaptığımız işlemin sonucunda “ay_Month” değişkeninin sayısal değeri; 1010101010101 onluk 5461 ve onaltılık 1557'dir

Ocak_January: 0000 0000 0000 0000 0000 0000 0000 0001
Mart_March: 0000 0000 0000 0000 0000 0000 0000 0100
Mayıs_May: 0000 0000 0000 0000 0000 0000 0001 0000
Temmuz_July: 0000 0000 0000 0000 0000 0000 0100 0000
Ağustos_August: 0000 0000 0000 0000 0000 0001 0000 0000
Ekim_October: 0000 0000 0000 0000 0000 0100 0000 0000
Aralık_December: 0000 0000 0000 0000 0001 0000 0000 0000

Şimdi tam liste ile mantıksal operatörün yaptığı işleme bakalım

örnek olarak ocak ayını seçelim. bit değeri şu an ocak ayında ise şu işlem gerçekleştiriliyor.

(1010101010101 & 00000000000001) != 0

(00000000000001) != 0 doğru! Bu durumda ocak ayı 30 gün çekiyor. peki ya şubat ayı. Hadi bu durumada hızlıca bakalım;

(1010101010101 & 00000000000010) != 0

(00000000000000) != 0 yanlış! Gördüğünüz gibi algoritmamız oldukça düzgün bir biçimde işliyor.

Lütfen bu şekilde programı adım adım inceleyin ve anlamaya çalışın.

Benden şimdilik bu kadar. Diğer yazılarda buluşmak üzere. Sıkı çalışın ve sağlıcakla kalın.

KAYNAKÇA

Bitwise and shift operators (C# reference). MICROSOFT 02/08/2023

Bit WIKIPEDIA 04/08/2023

Makaledeki tüm kodlara ulaşmak için github sayfamı inceleyebilirsiniz

--

--