Solidity 3 — Tipler

Engin UNAL
7 min readSep 24, 2022

Solidity dili öğrenim serimize devam ediyoruz. Önceki yazıların linkleri: Temel Bilgiler, İlk Akıllı Kontrat ve Remix. Bu yazıda Solidity içerisindeki tiplere ve ortak yapılara değinmeye çalışacağım.

Solidity, statik tipli(staticly typed) bir dildir. Yani derleme zamanında(compile time) derleyici(compiler), kullanılan değişkenlerin tiplerini bilmek ister. Aşağıda özetini vereceğim tipler, diğer programlama dillerinin genelindeki veri tipleri ile benzerdir.

Boolean (bool)

Sadece iki değer alabilen veri yapısıdır. true veya false değerlerini alabilir, varsayılan değeri false olarak atanır.

Integer (int, uint)

Tamsayı veritipini tutar. u: unsigned anlamında yani negatif değer almayan tamsayılar için kullanılır.

8-bit’ten başlayarak 256-bit’e kadar 8-bit artırılarak tanımlanabilir. int8, int16, int24, … , int256 veya uint8, uint16, … , uint256 gibi.

Eğer tip tanımında bit değeri verilmezse 256-bit olarak işleme alınır, örneğin: int abc tanımıyla int256 abc aynı şeyi ifade edecektir. Değer aralıklarına gelirsek, int tipi -2²⁵⁵ ile 2²⁵⁵-1 aralığında değerler alabilir, uint tipi ise 0 ile 2²⁵⁶-1 arasındaki değer aralığında çalışır.

Byte ve String (bytes, string, bytes1–bytes32)

bytes ve string benzerdir ikisi de değişken uzunlukta veri tutarlar. bytes içindeki veri hex olarak tutulur. Dikkat edilecek nokta string kullanıldığında indeks ve uzunluk bilgisine erişemezsiniz. Örnekle inceleyelim.

bytes k;
string l;
//Tanımladık. Değer atayalım
k = "deneme";
l = "deneme";
//Değişkenlerin içeriğini görüntülediğimizde
//k:bytes : 0x64656e656d65
//l:string: deneme
//Çıktısını verir.

Bu iki tip değişken uzunlukta veri saklanacağı zaman kullanılmalıdır. Gerekmediği müddetçe dinamik tiplerin kullanılması önerilmez. Bu konu diğer dinamik tipler için de geçerlidir. Eğer uzunluğu belirli bir veri tipi kullanabiliyorsanız öncelikle onu tercih etmelisiniz.

Bunun için bytes1 ile bytes32 arasında sabit uzunlukta byte tipinde tanımlama yapabilirsiniz. Bu şekildeki tanımlamada tanım anında verilen sabit uzunluk kadar alan kullanılabilir.

bytes5 ad;
bytes5 soyad;
//Değerleri atayalım
ad = "ad";
soyad = "soyad";
//Sonuç:
//"ad" : "bytes5: 0x6164000000"
//"soyad": "bytes5: 0x736f796164"

Örnekteki ad ve soyad isimli değişkenler 5 byte uzunlukta veri alır. Statik tip tanımlaması yaptığımız için tanımlı uzunluktan daha uzun bir atama yapmak istersek ise hata verecektir.

String ve byte operasyonları konusuna daha sonraki yazılarda tekrar değineceğim, öncelikle string, bytes, bytes1-bytes32 tanımları nerelerde seçilmelidir? Farkları neler? Gibi sorulara yanıt verebiliyorsak yeterlidir.

Address

Ethereum wallet(cüzdan) adresini veya kontrat adresini tutan değişkendir. Adresler 20-byte uzunlukta sabittir. Adresler balance isimli bir metoda sahiptir ve bununla hesaptaki ETH miktarı öğrenilebilir. Bunu aşağıdaki örnekte daha net görebiliriz, örnek kodlarında adres kaydetme, adresi okuma ve adresin balance bilgisini öğrenebildiğimiz kısa bir kontrat kodu bulunmakta.

Yukarıdaki örnekte setAddr metoduna test hesabı adresi olan 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 adresini vererek çağırdım, adresBilgisi isimli değişken bu adresi aldı. Devamında getBalanceOf metodunu çağırarak bu adresteki miktarı öğrenmiş oldum. Bu işlemlerin çıktıları aşağıda verilmiştir.

//Adres set edildi. Çıktısı:
"address newAddr": "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
//Adres bilgisi okundu.
"0": "address: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4"
//Balance bilgisi alındı. Wei olarak dönmekte.
"0": "uint256: 99999999999995944481"

Ek olarak ödeme işlemlerinde dikkat edilecek konu ise, address tipini tanımlarken payable kullandığımızda (address payable) transfer ve send metodlarını kullanma imkanı olmakta. Bu metodlar ETH gönderimi için kullanılır. payable olarak tanımlanmamış adreslerde kullanılamazlar. Bunun nedeni address tipi wallet adresi veya kontrat adresi olabilir demiştik. Wallet adresi ise sorun olmayacaktır, fakat kontrat adresi ise ve kontrat da ETH kabul edecek şekilde yazılmamış ise gönderilen ETH boşa gitmiş olacaktır. Bunların önüne geçmek için payable adreslere transfer işlemine izin verilmektedir.

Array

Dizi yapıları(array) dinamik veya statik kullanılabilir, tek boyutlu veya çok boyutlu tanımlamalara imkan tanır, veri saklamak için sık kullanılan yapılardır. Statik bir array tanımı array için verilecek tip ve array ismi tanımı ile yapılır. Statik kullanıma örnek:

uint8[3] arr1 = [1,2,3];

arr1 isimli array uint8 tipinde ve üç elemanlı olarak oluşturulmuştur, sonradan boyutu değiştirilemez. İçindeki değişkenlere arr1[0] veya arr[{indeksNo}] kullanımı ile erişilebilir veya değiştirilebilir.

arr1[2] = 1; //olarak değiştirebiliriz
uint8 val = arr1[0]; // şeklinde değeri alabiliriz

Örneklerle devam edelim. Aşağıda statik ve dinamik örnekleri verilmiştir.

int[3] arrStatic;
int[] arrDynamic;
bytes byteArr;

Bu örnekte arrStatic tek boyutlu ve önceden 3 elemanlı olarak tanımlanmış int tipinde array, arrDynamic ise tek boyutlu ve uzunluğu önceden tanımlanmamış int tipinde dinamik bir array, byteArr ise dinamik uzunlukta byte tipinde bir array’dir. Değer ataması ve dinamik oluşturma örneği aşağıdadır.

arrStatic[0] = 1;arrDynamic =  new int[](3);
arrDynamic[2] = 2;
byteArr = "abc";

İki boyutlu array örneklerini inceleyelim.

uint8[2][3] arrStatic2Dim;
uint8[][] arrDynamic2Dim;
bytes[] byteArr2Dim;

Örnekteki arrStatic2Dim iki boyutlu ve ön tanımlı uint8 tipinde bir array, arrDynamic2Dim iki boyutlu uzunluğu dinamik olarak verilecek uint8 tipinde bir array, byteArr2Dim ise yine iki boyutlu bytes array tanımına örnektir. Bunlara değer atma örneği yine aşağıda verilmiştir.

arrStatic2Dim = [[1,2],[3,4],[5,6]];arrDynamic2Dim = new uint8[2][](3);
arrDynamic2Dim[2][1] = 9;
byteArr2Dim = new bytes[](3);
byteArr2Dim[1] = "deneme";

Kullanım örneğinde arrStatic2Dim isimli array içine ikişer elemanlı üç array atanmıştır. arrDynamic2Dim array ikişer elemanlı üç array den oluşacak şekilde oluşturulmuş ve üçüncü array’in ikinci elemanına 9 değeri atanmıştır. byteArr2Dim ise üç bytes tipinde array’den oluşur, ikinci elemana “deneme” değeri atanmıştır.

Dinamik array yapılarına eleman ekleme çıkarma işlemleri push ve pop fonksiyonları ile yapılabilir.

uint8[] public arrEkleCikar;arrEkleCikar.push(10);
arrEkleCikar.push(20);
arrEkleCikar.pop();

Örnekte 10 ve 20 değerleri push fonksiyonu ile eklendikten sonra son eleman pop fonksiyonu ile array’den çıkarılıyor, böylece array içerisinde sadece 10 değerini taşıyan eleman kalıyor.

Enum

Kullanıcı tanımli bir tiptir. Genellikle kodlamanın daha anlaşılabilir olması amacıyla kullanılan ve değerlere isimlendirme getirmeye yarayan yapılardır. Genel tanımı aşağıdaki gibi gösterilebilir. İsim verilir ve değer alanlarına listelenecek enum değerleri eklenerek tanım yapılır. Boş enum tanımı yapılamaz.

enum <enum-name> {
value1,
value2,
...
}

Örnek:

enum MesaiGunleri {Pazartesi, Sali, Carsamba, Persembe, Cuma}

Fonksiyon içi kullanım örneği:

function GunOzelindeIslem(MesaiGunleri gun) public pure {
if (gun == MesaiGunleri.Carsamba) {
//enum tipleriyle int çevrimi yapılabilmekte
uint gunVal = uint(gun);
//çarşamba gününe özel işlem...
}
}

Struct

Kullanıcı tanımlı tip oluşturma ve değişkenleri bu tip içinde gruplama için kullanılır. Struct içerisine mevcut tipler veya diğer kullanıcı tanımlı tipler kullanılabilir. Struct tanımları kontrat içerisinde yapılırsa sadece o kontrat içinden erişilebilir, eğer kontrat dışında yapılırsa genel olarak erişilebilir hale gelirler.

Tanım örneği üzerinden devam edelim.

struct Kisi {
string ad;
string soyad;
uint yas;
}
struct Personel {
Kisi kisi;
string not;
MesaiGunleri[] mesaiGunleri;
}

Personel ve Kisi isminde iki struct tipi oluşturduk. Personel, Kisi tipinde tanımlanmış kisi değişkenini de içermekte. Aynı zamanda enums konusundaki örnekte tanımladığımız MesaiGunleri enum tipinde dinamik bir dizi içermekte. Bu tanımların kullanım örneği:

Personel personel;personel.kisi.ad = "ad";
personel.kisi.yas = 25;
personel.not = "deneme";
personel.mesaiGunleri.push(MesaiGunleri.Carsamba);
personel.mesaiGunleri.push(MesaiGunleri.Cuma);

Personel tipinde personel adıyla bir değişken oluşturduk bunun içerisindeki kisi değişkenine değerler atadık, mesaiGunleri dinamik array’e günler ekledik.

Mapping

Key-Value çiftinde veri tutan yapılardır. Key tipi olarak Solidity ön tanımlı tipler string, int, enum gibi veya kontrat tipleri seçilebilir fakat kullanıcı tanımlı veya kompleks tipler(struct, array tipleri, mapping tipleri) olamaz. Value tipi olarak tüm tipleri kullanmak mümkündür.

Temel olarak hash table yapısındadır. Mapping yapılarında pop, push veya add, remove işlemleri bulunmaz, mapping tüm olası key değerleri önceden girilmiş bir array gibi çalışır. Iteration özelliği yoktur yani mapping içindeki elemanları gezemezsiniz (iteration yapmak için bazı yollar geliştiren kütüphaneler mevcut). Mapping içerisindeki eleman sayısını çekemezsiniz. Key değeri ile value set edilir ve key ile value okunur. Temel işlevi bu şekildedir. Bu çalışma şekli performans olarak bazı durumlarda oldukça avantaj sağlamaktadır.

Örneğin 10.000 kayıtlı bir listeniz olsun bunu array’e atarsanız listeden bir kayda ulaşmak için tek tek gezip aradığınızı bulmanız gerekecektir. Fakat bunu mapping ile yaparsanız tek bir çağrım ile key vererek aradığınız kayda ulaşabilirsiniz. Negatif yönü ise eğer key değerini unutursanız mapping içerisinden bulmanıza imkan yokken array içerisinde iteration yaparak ulaşabilirsiniz. Her iki veri saklama yapısının artıları ve eksileri bulunmaktadır. Array ve mapping yapılarını birlikte kullanan tasarımlar önerilmekte. Örneğin mapping’deki keyleri array’de de saklayan kodlamalar çok yaygın.

Kullanım örneği ile devam edelim. Örnekte amacımız verilen bir ETH adresi ve bakiye bilgisini saklamak daha sonra da bu bakiye bilgisine adresten ulaşmak. Adres için address veri tipini, hesap bakiyesi için uint veri tipini kullanacağız.

// mapping tanımı
mapping(address => uint) balance;

Bu örneğin kontrat içinde uygulanmış hali yukarıdaki gibidir. setB fonksiyonuna geçilen account değeri balance isimli mapping için key olarak kullanılmış, amount ise value olarak kullanılmıştır. getB fonksiyonu ile verilen adres key’indeki value değeri döner.

Eğer mapping içerisinde aranan key değeri yok ise default value dönecektir.

Özel Değişkenler ve Genel Fonksiyonlar

Blockchain işlemlerinde block, transaction veya fonksiyonu çağıran ile ilgili bazı detayları alabildiğimiz yapılar mevcuttur. Bunlar gerektiği durumda kullanılabilirler, detaylarına ve kullanım yerlerine daha ileri aşamalarda girilmek üzere listesini paylaşıyorum.

  • block.basefee (uint): Bulunulan blok’un base fee değeri.
  • block.chainid (uint): Blockchain id değeri.(ileride göreceğiz)
  • block.coinbase (address payable): Blok madencisinin adresi.
  • block.difficulty (uint): Blok zorluğu.
  • block.gaslimit (uint): Blok gas limiti.
  • block.number (uint): Blok numarası.
  • block.timestamp (uint): Blok timestamp değeri.(unix)
  • msg.data (bytes calldata): Mesaj verisi.
  • msg.sender (address): Mesajı gönderen(çağıran) bilgisi.
  • msg.sig (bytes4): Mesaj datasındaki ilk 4-byte’lık veri.
  • msg.value (uint): Mesaj ile iletilen WEI değeri.
  • tx.gasprice (uint): Transaction gas ücreti.
  • tx.origin (address): Transaction’ı gönderenin bilgisi.

Birimler

Kullanım kolaylığı sağlamak için bazı ön tanımlı birimler bulunmaktadır. Bunlar ETH için ve zaman için kullanılan ön tanımlı birimlerdir.

Ether için kullanılabilecek birimler: wei, gwei ve ether birimleridir. Bunları yazıldığı gibi kullanabilirsiniz. Temel birim wei olarak kullanılır. Bu durumda 1 wei == 1, 1 gwei == 10⁹, 1 ether == 10¹⁸ . Örnek:

if(amount == 1 wei)
{
balance[account] = 1;
}
else if(amount == 1 ether)
{
balance[account] = 1e18;
}

Zaman birimleri için ise seconds, minutes, hours, days, weeks kullanılır. Temel birim saniyedir. Dolayısıyla karşılaştırmalarda her birim önce saniyeye çevrilip öyle kontrol edilir, yukarıda bahsettiğim gibi kullanım kolaylığı açısından bu ön tanımlı birim yazımı tercih edilir. Örnek:

if(elapsedTime == 1 minutes){
balance[account] = 1 wei;
}
else if(elapsedTime == 1 weeks)
{
balance[account] = 1 ether;
}

Yukarıdaki örnekte elapsedTime değişkeni bir dakika olduğunda hesap bakiyesi 1 wei olacak şekilde güncellenir, eğer elapsedTime bir hafta olursa(60 * 60 * 24 * 7 saniye- saniye temel birimdi) hesap bakiyesi 1 ether olacak şekilde güncellenir.

Buraya kadar olan konularda Solidity veri tipleri ve bunların çalışma mekaniğine değindim, umarım açıklayıcı olmuştur. Okuduğunuz için teşekkürler.

Serinin diğer yazıları:

Temel Bilgiler

İlk Akıllı Kontrat ve Remix

Engin Ünal

--

--