Akıllı Sözleşmeler
Solidity’deki akıllı sözleşmeler nesne yönelimli programlama dillerine benzerdir. State değişkenlerinde kalıcı data içerirler ve fonksiyonlar bu değişkenlerin değerini değiştirebilir. Başka bir akıllı sözleşmedeki fonksiyonu çağırmak bir EVM fonksiyon çağrısı gerçekleştirir ve burada çağıran akıllı sözleşmenin state değişkenlerine erişilemez. Akıllı sözleşmede herhangi bir şeyin yaşanmasını istiyorsanız o akıllı sözleşmenin herhangi bir fonksiyonunu çağırmanız gerekir. Çünkü Ethereum’da “cron” konsepti yoktur, yani akıllı sözleşmeler kendi başlarına bir şeyler yapamaz. Dışarıdan tetiklenmeleri gerekir.
Akıllı Sözleşme Oluşturma
Akıllı sözleşmeler iki şekilde oluşturulabilir; “dışarıdan” bir Ethereum transactionı ile veya Solidity kullanarak direkt başka bir akıllı sözleşme içerisinde.
Remix gibi IDE’ler oluşturma aşamasını kullanıcı arayüzü kullanarak kolayca gerçekleştirmenize yardımcı olur.
Programlama ile akıllı sözleşme oluşturmanın bir yolu ise web3.js gibi bir JavaScript API kullanımıdır. Akıllı sözleşme oluşturmaya yarayan web3.eth.Contract isimli bir methodu vardır.
Bir akıllı sözleşme oluşturulduğu zaman constructor isimli bir fonksiyon sadece bir kere olmak üzere çalıştırılır. Bundan sonra bu fonksiyona erişim mümkün değildir.
Constructor kullanmak zorunlu değildir. Bir akıllı sözleşmede sadece bir adet constructor olabilir ve overloading yapılması mümkün değildir.
Constructor çalıştırıldıktan sonra akıllı sözleşme kodunun son hali blok zincirinde saklanır. Bu kod bütün public ve external fonksiyonları içerir. Deploy edilen kod constructor fonksiyonu ve sadece constructor içerisinde çağrılan internal fonksiyonları içermez.
Özünde constructor parametreleri ABI encoded olarak akıllı sözleşme kodunun sonuna eklenir
(bytecode halinin), ama eğer web3.js
kullanıyorsanız bunu umursamanıza gerek yok. Çünkü o
sizin için bu işlemleri gerçekleştiriyor.
Eğer bir akıllı sözleşme başka bir akıllı sözleşme oluşturmak istiyorsa, oluşturmak istediği akıllı sözleşmenin kaynak kodunu (ve binary halini) bilmelidir. Bu demektir ki döngüsel olarak akıllı sözleşme oluşturmak mümkün değildir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.22 <0.9.0;
contract OwnedToken {
// `TokenCreator` aşağıda belirtilmiş bir akıllı sözleşme tipidir.
// Yeni bir akıllı sözleşme oluşturmak için kullanılmadığı sürece
// referans etmekte sorun yoktur.
TokenCreator creator;
address owner;
bytes32 name;
// Burası constructor fonksiyonumuz. Burada
// belirtilen isim ve akıllı sözleşmeyi oluşturan adres
// akıllı sözleşmede kaydedilir.
constructor(bytes32 name_) {
// State değişkenlerine isimleri kullanılarak
// erişilir. `this.owner` şeklinde bir kullanım
// ile değil. Fonksiyonlara direkt olarak kendi
// isimlerini kullanarak veya `this.f` şeklinde
// bir kullanım ile erişebiliriz. Ancak ikinci
// şekildeki kullanım external olarak (dışarıdan)
// bir görüş sağlar. Özellikle constructorlarda,
// fonksiyonlara external olarak erişmemelisiniz.
// Çünkü o fonksiyonlar henüz oluşturulmadı, yani
// erişilebilir değil.
// Daha fazlası için bir sonraki bölüme bakabilirsiniz.
owner = msg.sender;
// Burada `address` tipinden `TokenCreator` tipine
// bir explicit (açık) dönüşüm sağlarız ve bu fonksiyonu
// çağıran akıllı sözleşmenin bir `TokenCreator` olduğunu varsayarız.
// Bunu doğrulamanın gerçek bir yöntemi bulunmamakta.
// Bu işlem yeni bir akıllı sözleşme oluşturmaz.
creator = TokenCreator(msg.sender);
name = name_;
}
function changeName(bytes32 newName) public {
// Sadece `creator` `name` değişkenini değiştirebilir.
// Akıllı sözleşmenin adresini explicit bir dönüşüm ile
// elde edebilir ve karşılaştırmamızı yapabiliriz.
if (msg.sender == address(creator))
name = newName;
}
function transfer(address newOwner) public {
// Sadece şu anki `owner` token transferi gerçekleştirebilir.
if (msg.sender != owner) return;
// `creator` adresindeki akıllı sözleşmenin bir fonksiyonunu
// kullanarak, işlemin gerçekleştirilebilirliğini
// kontrol edebilir. Eğer bu işlem hata verirse
// (örneğin, out-of-gas (gazın tükenmesi)),
// işlem burada son bulur.
if (creator.isTokenTransferOK(owner, newOwner))
owner = newOwner;
}
}
contract TokenCreator {
function createToken(bytes32 name)
public
returns (OwnedToken tokenAddress)
{
// Yeni bir `Token` akıllı sözleşmeyi oluşturur ve adresini return eder.
// JavaScript tarafında return tipi `address` tipidir.
return new OwnedToken(name);
}
function changeName(OwnedToken tokenAddress, bytes32 name) public {
// `tokenAddress` isimli parametrenin tipi
// `address` tipindendir.
tokenAddress.changeName(name);
}
// Bir transferin gerçekleşip gerçekleşmeyeceğini belirler
function isTokenTransferOK(address currentOwner, address newOwner)
public
pure
returns (bool ok)
{
// Keyfi bir koşul ile işlemin gerçekleşip gerçekleşmeyeceğini
// belirler ve sonucu return eder.
return keccak256(abi.encodePacked(currentOwner, newOwner))[0] == 0x7f;
}
}
Görünürlük ve Getter Fonksiyonlar
Durum Değişkenlerinde Görünürlük
public
Public durum değişkenleri internallerden sadece bir açıdan farklıdır, o da derleyicinin direkt olarak bir getter fonksiyon oluşturmasıdır. Bu şekilde diğer akıllı sözleşmeler bu değerlere erişebilir. Aynı fonksiyon içerisinde external erişim sağlandığında (örneğin,
this.x
) getter fonksiyonu çağrılırken, internal erişimde (örneğin,x
) değer direkt olarak storage’den alınır. Setter fonksiyonlar derleyici tarafından tanımlanmaz. Bu yüzden siz kendiniz bir setter fonksiyon eklemediyseniz diğer fonksiyonlar bu değişkeni değiştiremez.internal
Internal durum değişkenleri sadece tanımlandıkları akıllı sözleşmeler ve o akıllı sözleşmelerden türetilen (inherited) akıllı sözleşmeler tarafından erişebilir durumdadır. External erişim mümkün değildir. Bütün durum değişkenlerinin default hali internal’dir.
private
Private durum değişkenlerine sadece tanımlandıkları akıllı sözleşmeden erişim mümkündür. Internal’den farklı olarak türetilen akıllı sözleşmelerden de erişilemez.
Uyarı
Bir şeyi private
veya internal
yapmak sadece diğer akıllı sözleşmelerin o bilgiye erişimini veya değiştirilmesini engeller. Ama bu bilgiler blok zinciri dışından erişilebilir durumdadır.
Fonksiyonlarda Görünürlük
Solidity iki tip fonksiyon çağrısı bilir: gerçek bir EVM mesaj çağrısı yapan external’lar ve bu çağrıyı yapmayan internal’lar. Ayrıca internal fonksiyonlar türetilen fonksiyonlardan erişilemez hale de getirilebilir. Bu da ortaya dört çeşit fonksiyon görünürlüğü çıkarır.
external
External fonksiyonlar akıllı sözleşme interface’inin bir parçasıdır, bu da demektir ki diğer akıllı sözleşmeler ve transactionlar tarafından çağrılabilirler. Bir
f
external fonksiyonu internal olarak çağrılamaz (yani,f()
işe yaramaz, amathis.f()
çalışır).public
Public fonksiyonlar akıllı sözleşme interface’inin bir parçasıdır ve internal olarak veya mesaj çağrıları ile kullanılabilirler.
internal
Internal fonksiyonlar sadece tanımlandıkları akıllı sözleşmeden veya o akıllı sözleşmeden türetilen akıllı sözleşmeler tarafından erişilebilir. External olarak erişim mümkün değildir. ABI kullanılarak external olarak erişilemese bile mapping ve storage referanslarını parametre olarak alabilirler.
private
Private fonksiyonlar internaller gibidir ama bunlara türetilen fonksiyonlardan da erişim mümkün değildir.
Uyarı
Bir şeyi private
veya internal
yapmak sadece diğer akıllı sözleşmelerin o bilgiye erişimini veya değiştirilmesini engeller. Ama bu bilgiler blok zinciri dışından erişilebilir durumdadır.
Görünürlük parametreleri durum değişkenleri için değişkenin tipinden sonra yazılırken fonksiyonlarda parametreler ve return tanımının arasına yazılır.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
function f(uint a) private pure returns (uint b) { return a + 1; }
function setData(uint a) internal { data = a; }
uint public data;
}
Aşağıdaki örnekte, D
, c.getData()
çağrısı yapabilir ve data
değerini elde eder,
ama f
fonksiyonunu çağıramaz. E
akıllı sözleşmeleri ise C
akıllı sözleşmesinden türetildiği için
compute
fonksiyonunu çağırabilir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint private data;
function f(uint a) private pure returns(uint b) { return a + 1; }
function setData(uint a) public { data = a; }
function getData() public view returns(uint) { return data; }
function compute(uint a, uint b) internal pure returns (uint) { return a + b; }
}
// Bu akıllı sözleşme derlenemez, hata verir
contract D {
function readData() public {
C c = new C();
uint local = c.f(7); // hata: `f` görünür değil
c.setData(3);
local = c.getData();
local = c.compute(3, 5); // hata: `compute` görünür değil
}
}
contract E is C {
function g() public {
C c = new C();
uint val = compute(3, 5); // internal fonksiyona türetilen fonksiyon sayesinde erişim sağlanabilir
}
}
Getter Fonksiyonlar
Derleyici bütün public durum değişkenleri için getter fonksiyonu oluşturur.
Örneğin aşağıdaki akıllı sözleşme için, derleyici data
adında bir fonksiyon üretir.
Bu fonksiyon hiçbir parametre almaz ve uint
tipinde bir değişken return eder.
Return edilen değer ise data
değişkeninde saklanan değerdir. Durum değişkenleri
tanımlandıkları yerde initialize edilebilir (initialize, bir değişkenin ilk defa tanımlanması olarak çevrilebilir).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract C {
uint public data = 42;
}
contract Caller {
C c = new C();
function f() public view returns (uint) {
return c.data();
}
}
Getter fonksiyonların görünürlüğü external’dir. Eğer internal olarak
erişim sağlandıysa (this.
olmadan), bu bir durum değişkenine erişim
anlamına gelir. Eğer external olarak erişildiyse
(this.
kullanarak), bu getter fonksiyonuna erişim anlamına gelir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract C {
uint public data;
function x() public returns (uint) {
data = 3; // internal erişim
return this.data(); // external erişim
}
}
Eğer bir public
görünürlüğe sahip dizi tipinden bir durum değişkenine sahipseniz, getter
fonksiyonunu kullanarak sadece tek bir elemana erişim sağlayabilirsiniz. Bu mekanizma
tüm diziyi return ederken oluşan yüksek gaz ücretlerinden sıyrılmak için kurulmuştur. Hangi elemanın
return edileceğini belirtmek için parametreleri kullanabilirsiniz (örneğin, myArray(0)
).
Eğer bütün diziyi tek bir fonksiyon ile elde etmeniz gerekiyorsa, bunun için aşağıdaki gibi
bir fonksiyon yazmanız gerekir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract arrayExample {
// public durum değişkeni
uint[] public myArray;
// Derleyici tarafından tanımlanan getter fonksiyonu
/*
function myArray(uint i) public view returns (uint) {
return myArray[i];
}
*/
// Bütün array'i return eden fonksiyon
function getArray() public view returns (uint[] memory) {
return myArray;
}
}
Artık tüm dizine erişmek için her bir aramayı tek bir öğeye döndüren
myArray(i)
yerine getArray()
kullanabilirsiniz.
Sıradaki örnek biraz daha karmaşık.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract Complex {
struct Data {
uint a;
bytes3 b;
mapping (uint => uint) map;
uint[3] c;
uint[] d;
bytes e;
}
mapping (uint => mapping(bool => Data[])) public data;
}
Derleyici bize aşağıdaki gibi bir getter fonksiyonu oluşturur. Struct’daki mapping’ler ve diziler (byte dizileri istisnadır) gözardı edilmiştir. Çünkü getter fonksiyonlarında onların spesifik bir elemanına uygun bir şekilde erişim mümkün değildir.
function data(uint arg1, bool arg2, uint arg3)
public
returns (uint a, bytes3 b, bytes memory e)
{
a = data[arg1][arg2][arg3].a;
b = data[arg1][arg2][arg3].b;
e = data[arg1][arg2][arg3].e;
}
Fonksiyon Modifier’ları
Modifier’lar fonksiyonların tanımlandığı şekillerinden farklı davranmalarını sağlamak için kullanılabilir. Örneğin, bir fonksiyonun çalıştırılmasından hemen önce bir koşulun kontrolünü gerçekleştirebilirsiniz.
Modifier’lar akıllı sözleşmelerin türetilebilen özelliklerindendir. Bu yüzden türetilmiş bir akıllı sözleşme
bir modifier’ı eğer virtual
olarak belirtilmişse onu override edebilir. Daha fazla bilgi için
Modifier Overriding kısmına bakabilirsiniz.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
// Bu akıllı sözleşme sadece bir tane modifier tanımlar ve onu da kullanmıyor.
// Tanımlanan modifier türetilen fonksiyonda kullanılacaktır.
// Fonksiyon içerisindeki kodların kullanılacağı yer
// `_;` şeklinde modifier içerisinde belirtilir.
// Yani `_;` gördüğünüz yerde o modifier'ın kullanıldığı fonksiyonun
// içerisindeki kodlar yazılmış gibi düşünebilirsiniz.
// Bu modifier eklendiği fonksiyonu sadece akıllı sözleşmeyi oluşturan
// kişinin çağırmasını sağlar. Diğer erişimlerde ise işlemi revert eder.
modifier onlyOwner {
require(
msg.sender == owner,
"Only owner can call this function."
);
_;
}
}
contract destructible is owned {
// Bu akıllı sözleşme türetildiği `owned` fonksiyonundaki
// `onlyOwner` modifier'ını `destroy` fonksiyonuna ekler.
// Böylece `destroy` fonksiyonunu sadece `owner` çağırabilir.
function destroy() public onlyOwner {
selfdestruct(owner);
}
}
contract priced {
// Modifier'lar parametre alabilir:
modifier costs(uint price) {
if (msg.value >= price) {
_;
}
}
}
contract Register is priced, destructible {
mapping (address => bool) registeredAddresses;
uint price;
constructor(uint initialPrice) { price = initialPrice; }
// Buradaki `payable` sözcüğü de oldukça önemlidir.
// Eğer bu fonksiyon `payable` olmazsa kendisine gelen bütün
// etherleri reddeder.
function register() public payable costs(price) {
registeredAddresses[msg.sender] = true;
}
function changePrice(uint price_) public onlyOwner {
price = price_;
}
}
contract Mutex {
bool locked;
modifier noReentrancy() {
require(
!locked,
"Reentrant call."
);
locked = true;
_;
locked = false;
}
/// Bu fonksiyon bir mutex ile korunmaktadır.
/// Yani, bu akıllı sözleşme re-entrancy çağrılarına karşı zaafiyetli değildir.
/// `return 7` fonksiyonun bittiğini belirtse de henüz modifier'ımızın işi bitmedi.
/// `locked = false;` satırı return ifademizden sonra çalışır.
function f() public noReentrancy returns (uint) {
(bool success,) = msg.sender.call("");
require(success);
return 7;
}
}
Eğer C
akıllı sözleşmesindeki m
modifier’ına erişmek istiyorsanız, C.m
şeklinde erişebilirsiniz.
Modifier’lar sadece tanımlandıkları akıllı sözleşmede veya türetilen bir akıllı sözleşmede kullanılabilir.
Modifier’lar kütüphanelerde de tanımlanabilir. Ancak kullanımları o kütüphanenin fonksiyonlarıyla kısıtlıdır.
Yani tanımlandıkları kütüphane dışında kullanılamazlar.
Bir fonksiyona birden fazla modifier tanımlanabilir. Bunu gerçekleştirmek için her bir modifier isminden sonra bir boşluk bırakılmalıdır. Modifier’lar tanımlandıkları sıraya göre çalışacaktır.
Modifier’lar eklendikleri fonksiyonların parametrelerine veya return değerlerine kendi başlarına erişemezler. Eğer bir parametreyi bir modifier’da kullanmak istiyorsanız, o modifier’ı eklediğiniz yerde parametreyi de vermelisiniz. Fonksiyon çağırma yapısına benzer bir şekilde kullanılırlar.
Modifier’daki veya fonksiyon’daki return işlemi sadece o yazıldığı modifier’dan veya fonksiyon’dan
çıkmaya yarar. Program akışı _
işaretinin olduğu yerden çalışmaya devam eder.
Uyarı
Daha önceki Solidity versiyonlarında modifier’a sahip fonksiyonlarda return
ifadesi farklı
bir şekilde davranış sergiler.
Açık bir şekilde return;
ifadesinin yer aldığı bir modifier, fonksiyonun return edeceği değerle alakalı değildir.
Modifier’lar fonksiyon içerisindeki kodları hiç çalıştırmamayı da tercih edebilirler.
Bu durumda return değerleri default değerlerine eşitlenebilir. Böylelikle,
fonksiyonun hiç bir kodu yokmuş gibi bir davranış sergilenir.
_
sembolü bir modifier’da birden fazla kez kullanılabilir. Her bir kullanım, fonksiyon
içerisindeki kodla değiştirilecektir. Yani, _
gördüğünüz her yerde, eklenen fonksiyonun kodlarının
bulunduğunu düşünebilirsiniz.
Modifier’lar parametre alabildiği için, bir fonksiyondaki bütün parametreler istenilen modifier’a gönderilebilir. Modifier’da tanımlanan semboller, fonksiyonlarda görülemez (override ile değiştirilebilir).
Constant ve Immutable State Değişkenleri
State değişkenleri constant
veya immutable
olarak tanımlanabilir.
Her iki durumda da akıllı sözleşme kurulduktan sonra (constructor çalıştıktan sonra) bu tür değişkenler değiştirilemez.
constant
compile-time’da (kodun içerisinde) tanımlı olması gerekirken,
immutable
değişkenler constructor içerisinde de tanımlanabilir.
constant
değişkenleri akıllı sözleşmelerin dışarısında (dosya seviyesinde) da tanımlayabiliriz.
Derleyici bu tür değişkenler için storage’de slot ayırmaz. Çünkü bu değişkenlerin kullanıldığı her yer, belirlenmiş değerle değiştirilir.
Normal state değişkenleriyle karşılaştırıldığında, constant ve immutable değişkenler çok daha az gaz harcar. Constant değişkenlerde, kullanıldıkları her yere karşılığında verilen değer kopyalanıp yapıştırılır. Bu, lokal optimizasyon olarak kullanılır. Immutable değişkenlerde ise, akıllı sözleşmenin kurulum anında (construction time) karşılık gelen değeri belirlenir ve kullanıldığı her yere kopyalanıp yapıştırılır. Bu değerler 32 byte’dan daha az yer kaplasa bile 32 byte’lık bir alanda muhafaza edilir. Bu sebepten ötürü, bazı durumlarda constant değerler kullanmak, immutable değerleri kullanmaktan daha ucuz olabilir.
Şu anda constant ve immutable bütün tipler için uygulanamamaktadır. Desteklenen tipler strings (sadece constant) ve değer tipleridir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.4;
uint constant X = 32**22 + 8;
contract C {
string constant TEXT = "abc";
bytes32 constant MY_HASH = keccak256("abc");
uint immutable decimals;
uint immutable maxBalance;
address immutable owner = msg.sender;
constructor(uint decimals_, address ref) {
decimals = decimals_;
// Immutable tanımlamalarında blok zincirinden veri de okunabilir.
maxBalance = ref.balance;
}
function isBalanceTooHigh(address other) public view returns (bool) {
return other.balance > maxBalance;
}
}
Constant
constant
değişkenlerin değerleri derleme anında (compile-time) sabit olmalı ve değişkenin
tanımlandığı konumda belirtilmelidir. Herhangi bir storage’e erişim, blok zinciri verisi
(örneğin, block.timestamp
, address(this).balance
veya
block.number
) veya
çalıştırma verisi (msg.value
veya gasleft()
) veya başka akıllı sözleşmelere yapılan external
çağrılara izin verilmez. Kullanılacak memory’i belirleme konusunda yan etki oluşturacak tanımalamalara
izin verilirken, başka memory objeleri üzerinde yan etki oluşturan tanımlamalara izin verilmez.
Built-in fonksiyonlarından keccak256
, sha256
, ripemd160
, ecrecover
, addmod
ve mulmod
fonksiyonlarının kullanımına izin verilmiştir (keccak256
başka akıllı sözleşmeleri çağırsa da, bir istisnadır).
Memory belirleyicisi üzerinde yan etkiye izin verilmesinin sebebi, karmaşık yapılarında kurulabilinmesi gereksinimidir (örneğin, lookup-table). Bu özellikler henüz tamamen kullanılabilir değildir.
Immutable
immutable
olarak tanımlanan değişkenler constant
olarak tanımlananlara göre
biraz daha az kısıtlanmıştır: Immutable değişkenler akıllı sözleşmenin constructor fonksiyonunda
keyfi bir değere atanabilir. Sadece bir kere tanımlanabilirler ve tanımlandıktan sonra
istenilen anda sahip oldukları değer okunabilir.
Derleyici tarafından oluşturulmuş akıllı sözleşmenin creation code’u, runtime code’u return etmeden önce bütün immutable referanslarını tanımlanan değerle değiştirir. Bu yüzden immutable değişken kullandığınız bir akıllı sözleşme için, compiler’ın oluşturduğu runtime code ile blok zincirinde saklanan runtime code’u karşılaştırdığınızda farklı sonuçlar alırsınız.
Not
Tanımlandıkları satırda direkt olarak değerleri atanan immutable değişkenler akıllı sözleşmenin constructor fonksiyonu çalıştıktan sonra initialize edilmiş olarak düşünülür. Bu demek oluyor ki başka bir immutable değişkenin değerini kullanan bir immutable değişkenin değerini direkt olarak atayamazsınız. Bunu ancak constructor içerisinde yapabilirsiniz.
Bu state değişkenlerini ilk defa tanımlama sırasının farklı bir şekilde yorumlanmasını engellemek amacıyla konulmuş bir önleyicidir, özellikle de türetme (inheritance) konusunda.
Fonksiyonlar
Fonksiyonlar akıllı sözleşmelerin içerisinde veya dışarısında tanımlanabilir.
Akıllı sözleşmelerin dışarısında tanımlanan fonksiyonlara “özgür fonksiyonlar” denir ve her zaman
internal
görünürlüktedirler. Kodları, o fonksiyonları
kullanan her bir akıllı sözleşmeye eklenir, tıpkı internal kütüphane fonksiyonları gibi.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.1 <0.9.0;
function sum(uint[] memory arr) pure returns (uint s) {
for (uint i = 0; i < arr.length; i++)
s += arr[i];
}
contract ArrayExample {
bool found;
function f(uint[] memory arr) public {
// Burada özgür bir fonksiyon internal olarak çağrılıyor.
// Derleyici `sum` fonksiyonunu bu akıllı sözleşmenin kodları arasına ekleyecek.
uint s = sum(arr);
require(s >= 10);
found = true;
}
}
Not
Akıllı sözleşme dışında tanımlanan bir fonksiyon her zaman o akıllı sözleşmenin içeriği ile birlikte
çalıştırılırlar. Hâlâ diğer akıllı sözleşmeleri çağırabilir, onlara Ether gönderebilir ve kendilerini
çağıran akıllı sözleşmeleri yok edebilirler. Akıllı sözleşme içerisinde tanımlanan bir fonksiyon ile özgür bir fonksiyonun arasındaki en temel farklar özgür fonksiyonların this
değişkenine erişimi olmaması, ve de kendi alanlarında (scope) bulunmayan storage değişkenlerine ve fonksiyonlara direkt erişime sahip olmamalarıdır.
Fonksiyon Parametreleri ve Return Parametreleri
Fonksiyonlar tipi belirtilmiş parametreler alabilir ve diğer birçok programlama dilinin aksine keyfi sayıda değişkeni return edebilirler.
Fonksiyon Parametreleri
Fonksiyon parametreleri değişkenlerle aynı şekilde tanımlanırlar. Ayrıca kullanılmayan parametreler gözardı edilebilirler.
Örneğin, eğer akıllı sözleşmenizdeki bir fonksiyonun iki adet integer değişkeni parametre olarak almasını isterseniz, aşağıdaki gibi bir yapı kullanabilirsiniz:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
uint sum;
function taker(uint a, uint b) public {
sum = a + b;
}
}
Fonksiyon parametreleri herhangi bir lokal değişken olarak kullanılaiblir ve ayrıca lokal değişkenlere atanabilirler.
Not
Bir external fonksiyon çok boyutlu bir
diziyi parametre olarak alamazlar. Bu özelliği eğer ABI coder v2’yi
kaynak kodunuzda pragma abicoder v2;
bu şekilde aktifleştirdiyseniz
kullanabilirsiniz.
Bir internal fonksiyon o özelliği aktifleştirmeden de çok boyutlu bir diziyi parametre olarak alabilir.
Return Değişkenleri
Fonksiyon return değişkenleri aynı şekilde returns
sözcüğünden sonra tanımlanır.
Örneğin, iki adet sonucu return etmek istediğinizi düşünün: fonksiyon parametresi olarak verilmiş iki adet integer’ın toplamı ve çarpımı. Şu şekilde bir kod işinizi görecektir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
sum = a + b;
product = a * b;
}
}
Return değişkenlerinin tipleri gözardı edilebilirler. Return değişkenleri herhangi bir lokal değişken olarak kullanılabilirler. Bu değişkenler direkt olarak default değerine eşitlenir ve değiştirilene kadar bu değere eşit olurlar.
İsterseniz yukarıdaki gibi açık bir şekilde return değişkenlerinin değerlerini
verebilir veya aşağıdaki gibi direkt olarak return
ifadesini kullanabilirsiniz
(ister tek, isterseniz de çoklu return):
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract Simple {
function arithmetic(uint a, uint b)
public
pure
returns (uint sum, uint product)
{
return (a + b, a * b);
}
}
Eğer fonksiyondan çıkmak için erkenden return
kullanmanak istiyorsanız,
bütün return değişkenlerini vermeniz gerekir.
Not
Bazı tipleri internal olmayan fonksiyonlardan return edemezsiniz,
örneğin, çok boyutlu dinamik boyutlu diziler ve structlar. Eğer
ABI coder v2’yi pragma abicoder v2;
şeklinde kodunuza eklerseniz
daha fazla tip kullanılabilir olacaktır, ancak mapping
tipi
hâlâ bir akıllı sözleşme içerisinde sınırlıdır ve onları transfer edemezsiniz.
Çoklu Değer Return Etme
Bir fonksiyonda birden fazla değişkeni return etmek istiyorsanız return (v0, v1, ..., vn)
şeklinde
bir ifade kullanabilirsiniz. Return değişkeni sayısı ve tipleri, bir
implicit dönüşümden sonra belirtilen değerlerle eşleşmelidir.
State Değişkenliği
View Fonksiyonlar
view
ile tanımlanan fonksiyonlar state’te herhangi bir değişikliği yapamaz, sadece
state’deki değerleri okuyabilirler.
Not
Eğer derleyicinin EVM target kısmı Byzantium veya daha yenisi (default) ise view
fonksiyonlar çağrıldığında STATICCALL
opcode’u kullanılır ve bu opcode state’i
değişmemeye zorlar. Kütüphanelerdeki view
fonksiyonlarında ise DELEGATECALL
kullanılır. Çünkü DELEGATECALL
ve STATICCALL
opcode’larından kombine edilmiş
bir opcode bulunmamaktadır. Bu demek oluyor ki view
fonksiyonlar state değişikliğini
önlemek için run-time kontrollerine sahip değildirler. Bunun kötü bir güvenlik etkisi
olmamalıdır. Çünkü kütüphane kodu genellikle derlenirken bilinir ve statik kontrol edici
(static checker) compile-time kontrollerini gerçekleştirir.
Aşağıdaki ifadeler state değişikliğini temsil eder:
State değişkenlerine yazmak.
selfdestruct
kullanmak.Ether göndermek.
view
veyapure
olarak belirtilmeyen bir fonksiyon çağırmak.Low-level çağrılar kullanmak.
Belirli opcode’ları kullanan inline assembly kullanmak.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public view returns (uint) {
return a * (b + 42) + block.timestamp;
}
}
Not
Versiyon 0.5.0 öncesinde fonksiyonlarda constant
sözcüğü şu anki view
için kullanılırdı, ancak artık kullanılmıyor.
Not
Getter fonksiyonlar otomatik olarak view
görünürlüğüne sahip olur.
Not
Versiyon 0.5.0 öncesinde derleyici view
için STATICCALL
opcode’unu
kullanmazdı. Bu, view
fonksiyonlarda yanlış explicit tip dönüşümlerini
kullanarak state değişikliği yapılmasına izin verdi. STATICCALL
opcode’unu
view
fonksiyonlar için kullanarak EVM seviyesinde state değişikliklerinin
yapılmasının önüne geçildi.
Pure Fonksiyonlar
Fonksiyonlar pure
olarak tanımlanabilir ve bu şekilde tanımlanan fonksiyonlar state’i okuyamaz ve
değişiklik yapamaz. Pure fonksiyonlar içerisinde immutable
değişkenler okuyabilir durumdadır.
Not
Eğer derleyicinin EVM target kısmı Byzantium veya daha yeni (default) ise, STATICCALL
opcode’u kullanılır. Bu opcode state’in okunmadığına dair garanti vermez ama en azından
değiştirilmediğine dair bir garanti verir.
Yukarıda state’i değiştiren ifadeleri açıklamışken, state’i okuduğu düşünülen ifadeleri de aşağıda bulabilirsiniz:
State değişkenlerini okumak.
address(this).balance
veya<address>.balance
değişkenlerine erişmek.block
,tx
veyamsg
değişkenlerinin herhangi bir üyesine erişmek (msg.sig
vemsg.data
istisnadır).pure
olmayan herhangi bir fonksiyonu çağırmak.Belirli opcode’ları kullanan inline assembly kullanmak.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.0 <0.9.0;
contract C {
function f(uint a, uint b) public pure returns (uint) {
return a * (b + 42);
}
}
Pure fonksiyonlar revert()
ve require()
ifadelerini kullanarak hata oluşması
durumunda potansiyel state değişikliğini engelleyebilirler.
State değişikliğini revert etmek bir “state değişikliği” olarak düşünülmez.
Bir state değişikliğini revert etmek bir “state değişikliği” olarak kabul edilmez, çünkü yalnızca
daha önce kodda view
veya pure
kısıtlamaya sahip olmayan state’de yapılan değişiklikler
revert edilir ve bu kodun revert
’i yakalama ve aktarmama seçeneği vardır.
Bu davranış STATICCALL
için de geçerlidir.
Uyarı
EVM seviyesinde fonksiyonların state’den okuma yapmasını engellemek mümkün değildir,
sadece yazma engellenebilir (yani, EVM seviyesinde sadece view
zorunlu kılınabilir, pure
kılınamaz).
Not
Versiyon 0.5.0 öncesinde derleyici pure
için STATICCALL
opcode’unu
kullanmazdı. Bu, pure
fonksiyonlarda yanlış explicit tip dönüşümlerini
kullanarak state değişikliği yapılmasına izin verdi. STATICCALL
opcode’unu
pure
fonksiyonlar için kullanarak EVM seviyesinde state değişikliklerinin
yapılmasının önüne geçildi.
Not
Versiyon 0.4.17 öncesinde derleyici pure
fonksiyonların state’i okuması durumunda
hata vermezdi. Bu, sözleşme türleri arasında geçersiz açık dönüşümler yaparak atlatılabilen ve bir
tür denetim olan derleme zamanı yüzünden kaynaklanmaktaydı. Çünkü derleyici, sözleşme
türünün durum değiştirme işlemleri yapmadığını doğrulayabilir, fakat çalışma zamanında
çağrılacak olan sözleşmenin gerçekten bu türden olup olmadığını kontrol edemez.
Özel Fonksiyonlar
Receive Ether Fonksiyonu
Bİr akıllı sözleşme sadece bir adet receive
fonksiyonuna sahip olabilir. Bu fonksiyon
şu şekilde tanımlanır: receive() external payable { ... }
(function sözcüğü olmadan).
Bu fonksiyon parametre alamaz, hiçbir şey return edemez, görünürlüğü external
olmalı ve ayrıca payable
olarak tanımlanmalıdır. Bir receive fonksiyonu virtual olabilir, override edilebilir
ve modifier’lara sahip olabilir.
Receive fonksiyonu akıllı sözleşmemize gelen boş bir calldata’sı bulunan çağrılarda çalıştırılır.
Bu fonksiyon, akıllı sözleşmemize direkt Ether transferi gerçekleştirildiğinde (.send()
veya .transfer()
kullanılarak) çalıştırılır. Eğer bu fonksiyon tanımlı değil ama payable bir fallback fonksiyon
tanımlı ise, direkt Ether transferlerinde bu fallback fonksiyonu çalıştırılır. Eğer akıllı sözleşme ne bir receive
fonksiyonu, ne de bir payable fallback fonksiyonu tanımlamamışsa, akıllı sözleşmemiz direkt Ether transflerlerini
kabul edemez, kendisine ether gönderildiğinde bir hata verir.
En kötü durumda receive
fonksiyonu 2300 adet gazın mevcut olduğunu varsayabilir
(örneğin send
veya transfer
kullanımında), geriye ise sadece log işlemleri gibi basit işlemler için gaz kalır.
Aşağıdaki işlemler 2300 gazdan daha fazlasını harcar:
Storage’e yazmak
Akıllı sözleşme oluşturmak
Yüksek miktarda gaz harcayan bir external fonksiyonun çağrılması
Ether gönderimi
Uyarı
Bir akıllı sözleşmede direkt olarak Ether gönderirken (bir fonksiyon çağrısı olmadan, yani gönderenin
send
veya transfer
kullandığı durumda) eğer akıllı sözleşme bir receive fonksiyonu veya
bir payable fallback fonksiyonu tanımlamamışsa, bir hata oluşur ve Etherler gönderene iade edilir
(bu durum Solidity 0.4.0 öncesinde farklıydı). Eğer akıllı sözleşmenizin direkt Ether transferlerini kabul
etmesini istiyorsanız, bir receive fonksiyonu tanımlayın (Ether kabulu için payable fallback fonksiyonunun
kullanımını tavsiye etmiyoruz, çünkü fallback fonksiyonu interface karışıklığı yaşandığında kullanıcıya
hata vermeyecektir).
Uyarı
Bir akıllı sözleşme receive fonksiyonu olmadan da Ether kabul edebilir;
coinbase transaction (diğer adıyla miner block reward)
veya selfdestruct
kullanılırken hedef adres olarak verilmesi halinde
akıllı sözleşme Etherleri kabul etmek zorundadır.
Bir akıllı sözleşme bu gibi durumlardaki Ether transferlerine herhangi bir tepki veremez ve dolayısıyla bunları reddedemez. Bu EVM’in tasarım tercihlerinden biridir ve Solidity bunu es geçemez.
Bu ayrıca demek oluyor ki address(this).balance
değişkenindeki değer
sizin kendi hesaplamanızla (örneğin, receive fonksiyonunda her gelen miktarı
hesaplamanız halinde) farklı olabilir.
Aşağıdaki Sink akıllı sözleşmesi receive
kullanımına bir örnektir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// Bu akıllı sözleşmeye gönderilen Etherleri geri almanın hiçbir
// yolu yoktur.
contract Sink {
event Received(address, uint);
receive() external payable {
emit Received(msg.sender, msg.value);
}
}
Fallback Fonksiyonu
Bir akıllı sözleşme sadece bir adet fallback
fonksiyonuna sahip olabilir. Bu fonksiyon
şu iki şekilde tanımlanabilir: fallback () external [payable]
veya
fallback (bytes calldata input) external [payable] returns (bytes memory output)
(ikisi de function
sözcüğü olmadan kullanılıyor). Bu fonksiyon external
görünürlüğe sahip olmalıdır. Bir fallback fonksiyonu virtual olabilir, override edilebilir
ve modifier’lara sahip olabilir.
Fallback fonksiyonu bir çağrıda gönderilen fonksiyon imzasının (function signature) akıllı sözleşmedeki
herhangi bir fonksiyon ile eşleşmediği durumda çalıştırılır, yani, eğer kullanıcının çalıştırmak
istediği fonksiyon akıllı sözleşmede yoksa, fallback fonksiyonu çalıştırılır. Bir diğer kullanım alanı ise
direkt Ether gönderimlerinde eğer akıllı sözleşmede receive Ether fonksiyonu
yoksa ve fallback fonksiyonumuz payable
ise, fallback fonksiyonu çalıştırılır.
Eğer yukarıda gösterdiğimiz iki kullanım şeklinden input
kullanılanı kullanmak isterseniz,
input
akıllı sözleşmeye gönderilen tüm data, msg.data
, olacaktır. Ayrıca output
ile de
data return edebilir. Return edilen data ABI-encoded olmayacaktır, onun yerine herhangi bir
düzenleme olmadan (hatta padding bile olmadan) return edilecektir.
En kötü durumda, eğer bir payable fallback fonksiyonu receive fonksiyonun da yerine kullanıldıysa, sadece 2300 adet gaz ile işlemini tamamlayabilir (receive Ether fonksiyonu).
Diğer herhangi bir fonksiyon gibi fallback fonksiyonu da yeterli gaza sahip olduğu sürece çok karmaşık işlemleri yürütebilir.
Uyarı
Bir payable
fallback fonksiyonu ayrıca direkt Ether transferlerinde
de, eğer receive Ether fonksiyonu kullanılmadıysa,
çalıştırılabilir. Eğer payable fallback fonksiyonuna spesifik bir kullanım için
ihtiyacınız yoksa, receive fonksiyonunu kullanmanızı tavsiye ederiz.
Not
Eğer input verisini decode etmek istiyorsanız, ilk dört byte’ı fonksiyon
imzası için kullanabilir ve kalan kısmı abi.decode
kullanarak ABI-encoded
veriyi decode edebilirsiniz: (c, d) = abi.decode(input[4:], (uint256, uint256));
Şunu unutmayın ki, bu bir son çaredir. Eğer yapabiliyorsanız daha uygun bir fonksiyon
kullanmaya çalışın.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
contract Test {
uint x;
// Bu akıllı sözleşmeye gelen bütün mesaj çağrılarını
// bu fonksiyon karşılar (akıllı sözleşmede başka bir
// fonksiyon bulunmadığı için).
// Fonksiyon payable olarak belirtilmediği için
// Ether gönderimlerinde hata alınacaktır.
fallback() external { x = 1; }
}
contract TestPayable {
uint x;
uint y;
// Bu akıllı sözleşmeye gelen direkt Ether gönderimleri dışındaki bütün mesajları
// bu fonksiyon karşılayacaktır (receive dışında başka bir fonksiyon
// bulunmamakta). Calldatası boş olmayan bütün çağrıları bu fonksiyon
// karşılar (çağrı ile birlikte Ether gönderilse bile).
fallback() external payable { x = 1; y = msg.value; }
// Bu fonksiyon sadece direkt Ether gönderimleri için kullanılır, yani,
// boş bir calldata ve Ether gönderilen çağrıları bu fonksiyon karşılar.
receive() external payable { x = 2; y = msg.value; }
}
contract Caller {
function callTest(Test test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x'in == 1 olmasına neden olur.
// address(test) direkt olarak ``send`` kullanımına izin vermez.
// ``send`` fonksiyonunu çağırabilmek için bile ``address payable``
// tipine dönüştürme gerekmektedir.
address payable testPayable = payable(address(test));
// Eğer biri burada da olduğu gibi payable fallback fonksiyonu olmayan bir
// akıllı sözleşmeye ether göndermeye çalışırsa, hata alacaktır.
// Dolayısıyla burada ``false`` return edilir.
return testPayable.send(2 ether);
}
function callTestPayable(TestPayable test) public returns (bool) {
(bool success,) = address(test).call(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x == 1 olur ve test.y 0 olur.
(success,) = address(test).call{value: 1}(abi.encodeWithSignature("nonExistingFunction()"));
require(success);
// test.x == 1 olur ve test.y 1 olur.
// Eğer biri aşağıdaki gibi TestPayable akıllı sözleşmesine Ether gönderirse, receive fonksiyonu çalışır.
// Yukarıda tanımladığımız receive fonksiyonu storage'e yazdığı için 2300'den daha fazla
// gaz harcanmasına sebep olur. O yüzden ``send`` ve ``transfer`` kullanılamaz.
// Onların yerine low-level call kullanmalıyız.
(success,) = address(test).call{value: 2 ether}("");
require(success);
// test.x'in == 2 ve test.y'nin 2 Ether olmasıyla sonuçlanır.
return true;
}
}
Fonksiyon Overloading
Bir akıllı sözleşme aynı isimde fakat farklı parametre tiplerine sahip fonksiyonlara sahip olabilir.
Bu işlem “overloading” olarak adlandırılır ve ayrıca türetilen fonksiyonlar için de geçerlidir.
Aşağıdaki örnek A
akıllı sözleşmesindeki f
fonksiyonları ile overloading’i gösterir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint value) public pure returns (uint out) {
out = value;
}
function f(uint value, bool really) public pure returns (uint out) {
if (really)
out = value;
}
}
Overload edilmiş fonksiyonlar external interface’de de göründüğü için iki fonksiyonun aldığı parametreler external tiplerine göre karşılaştırılır. Yani, örneğin aşağıdaki fonksiyonlardan biri parametre olarak akıllı sözleşme aldığını belirtmiş. Ancak external interface’de bu, bir akıllı sözleşme değil, adres olarak görünür. O yüzden bu akıllı sözleşme compile edilemez.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
// Compile edilemez
contract A {
function f(B value) public pure returns (B out) {
out = value;
}
function f(address value) public pure returns (address out) {
out = value;
}
}
contract B {
}
Yukarıdaki iki f
fonksiyonu da ABI’leri aracılığı ile address tipinden bir parametre
kabul ediyor, her ne kadar Solidity içerisinde farklı tipler kabul etseler de.
Overload Ayrıştırma ve Parametre Eşleştirme
Overload edilmiş fonksiyonlar, geçerli kapsamdaki fonksiyon tanımlamalarını fonksiyon çağrısında sağlanan parametrelerle eşleştirerek seçilir. Tüm parametreler implicit olarak beklenen türlere dönüştürülebiliyorsa, fonksiyon overload adayı olarak seçilir. Tam olarak bir aday yoksa, çözümleme başarısız olur.
Not
Overload ayrıştırma için return parametreleri dikkate alınmaz.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.16 <0.9.0;
contract A {
function f(uint8 val) public pure returns (uint8 out) {
out = val;
}
function f(uint256 val) public pure returns (uint256 out) {
out = val;
}
}
f(50)
çağrısını yaptığımızda bir hata alırız. Bunun sebebi 50
sayısının hem uint8
hem de uint256
tipinde de kullanılabilmesidir. Ama eğer f(256)
çağrısını gerçekleştirirsek
256
sayısı direkt olarak f(uint256)
bu şekilde tanımlanan fonksiyona gönderilir. Çünkü
256
uint8
olarak gösterilemez.
Eventler
Solidity eventleri EVM’nin loglama işlevinin üzerine bir soyutlama verir. Uygulamalar Ethereum clientlarının RPC arayüzüne abone olarak bu eventleri dinleyebilirler.
Eventler akıllı sözleşmelerin türetilebilen üyeleridir. Çağrıldıklarında işlemlerin log kısmında - blok zincirindeki özel bir veri yapısı - depolanırlar. Bu eventler çağrıldıkları akıllı sözleşmenin adresi ile özdeşleştirilir ve işlemin bulunduğu blok erişilebilir olduğu sürece bu eventlere de erişilebilir (şu anda bu süre sonsuza kadardır ancak Serenity ile bu değişebilir). Log ve event verisi akıllı sözleşme tarafından erişilebilir değildir (eventi oluşturan akıllı sözleşme için bile bu geçerlidir).
Loglar için bir Merkle proof talep etmek mümkündür, bu nedenle external bir varlık böyle bir kanıtla bir akıllı sözleşme sağlarsa, logun blok zinciri içinde gerçekten var olup olmadığını kontrol edebilir. Sözleşme yalnızca son 256 blok hashini görebildiği için blok başlıkları sağlamanız gerekir.
Logun veri kısmı yerine “topics” olarak bilinen özel bir veri yapısına ekleyen
en fazla üç parametreye indexed
özniteliği ekleyebilirsiniz. Bir topic yalnızca tek
bir kelimeyi (32 byte) tutabilir, bu nedenle indekslenmiş bir argüman için bir referans
tipi kullanırsanız, bunun yerine değerin Keccak-256 hashi topic olarak saklanır.
indexed
olmadan kullanılan bütün parametreler logun veri kısmına ABI-encoded olarak
saklanır.
Topicler eventleri aramanıza izin verir, örneğin belirli eventler için bir blok dizisini filtrelerken. Ayrıca eventleri yayınlandıkları akıllı sözleşmede göre de filtreleyebilirsiniz.
Örneğin aşağıdaki kod web3.js’in subscribe("logs")
methodunu kullanarak
logları belirli bir adrese göre filtreleme işlemi yapmıştır:
var options = {
fromBlock: 0,
address: web3.eth.defaultAccount,
topics: ["0x0000000000000000000000000000000000000000000000000000000000000000", null, null]
};
web3.eth.subscribe('logs', options, function (error, result) {
if (!error)
console.log(result);
})
.on("data", function (log) {
console.log(log);
})
.on("changed", function (log) {
});
Eventin imzasının hashi, etkinliği anonim belirteçle bildirmeniz dışında, topiclerden biridir. Bu, belirli anonim eventleri ada göre filtrelemenin mümkün olmadığı, yalnızca akıllı sözleşme adresine göre filtreleyebileceğiniz anlamına gelir. Anonim eventlerin avantajı, deploy etmenin ve çağırmanın daha ucuz olmasıdır. Ayrıca, üç yerine dört indexed değişken bildirmenize olanak tanır.
Not
İşlem logları değişken türünü değil, yalnızca olay verilerini sakladığından, verileri doğru bir şekilde yorumlamak için hangi parametrenin dizine eklendiği ve eventin anonim olup olmadığı dahil olmak üzere olayın türünü bilmeniz gerekir. Özellikle, anonim bir event kullanarak başka bir eventin imzasını “sahte” yapmak mümkündür.
Eventlerin Üyeleri
event.selector
: Anonim olmayan eventlerdebytes32
tipindeki bir değerdir ve eventin imzasının hashini içerirkeccak256
.
Örnek
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.21 <0.9.0;
contract ClientReceipt {
event Deposit(
address indexed from,
bytes32 indexed id,
uint value
);
function deposit(bytes32 id) public payable {
// Eventler `emit` sözcüğü ve sonrasında
// eventin ismi ve parametreleri (varsa) parantez
// içerisine konularak yayınlanır.
// Bu şekildeki herhangi bir çağırma işlemi
// (iç içe olsa bile) `Deposit` ile filtreleme
// yaparak JavaScript API tarafından yakalanabilir.
emit Deposit(msg.sender, id, msg.value);
}
}
JavaScript API kullanımı ise şu şekildedir:
var abi = /* derleyici tarafından üretilen ABI */;
var ClientReceipt = web3.eth.contract(abi);
var clientReceipt = ClientReceipt.at("0x1234...ab67" /* adres */);
var depositEvent = clientReceipt.Deposit();
// değişiklikleri izle
depositEvent.watch(function(error, result){
// sonuç, `Deposit` çağrısına verilen indekslenmemiş
// argümanları ve topicleri içerir.
if (!error)
console.log(result);
});
// veya bir callback fonksiyonu ile direkt olarak dinlemeye başlayabilirsiniz
var depositEvent = clientReceipt.Deposit(function(error, result) {
if (!error)
console.log(result);
});
Yukarıdaki kod şu şekilde bir çıktı verir (trim edilmiş hali ile):
{
"returnValues": {
"from": "0x1111…FFFFCCCC",
"id": "0x50…sd5adb20",
"value": "0x420042"
},
"raw": {
"data": "0x7f…91385",
"topics": ["0xfd4…b4ead7", "0x7f…1a91385"]
}
}
Eventleri Anlamak İçin Ekstra Kaynaklar
Hata ve Geri Alma Durumları
Solidity’de hatalar gaz-verimli ve kullanışlı bir şekilde kullanıcılara bir işlemin neden başarısız olduğunu söylemeyi sağlar. Akıllı sözleşmenin içerisinde veya dışarısında tanımlanabilirler (interface ve kütüphaneler de dahil).
Revert ifadesi ile kullanılmalıdır. Bu ifade anlık çağrıda yapılan bütün değişiklikleri geri alır ve işlemi çağıran kişiye bir hata gönderir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.4;
/// Transfer için yetersiz bakiye. `required` kadar bakiye
/// olmalıyken, `available` kadar bakiye mevcuttur.
/// @param available, kullanılabilir bakiye.
/// @param required, transfer edilmek istenen miktar.
error InsufficientBalance(uint256 available, uint256 required);
contract TestToken {
mapping(address => uint) balance;
function transfer(address to, uint256 amount) public {
if (amount > balance[msg.sender])
revert InsufficientBalance({
available: balance[msg.sender],
required: amount
});
balance[msg.sender] -= amount;
balance[to] += amount;
}
// ...
}
Hatalar overload veya override edilemez ama türetilebilirler.
Alanları farklı olduğu sürece aynı hata birden fazla kere tanımlanabilir.
Hata örnekleri sadece revert
ifadesi kullanılarak üretilebilir.
Hata, daha sonra zincir dışı bileşene geri dönmek veya onu try/catch ifadesiyle. yakalamak için geri alma işlemiyle işlemi çağırana veri iletir. Bir hatanın yalnızca harici bir aramadan geldiğinde yakalanabileceğini, dahili aramalarda veya aynı işlevin içinde gerçekleşen geri dönüşlerin yakalanamayacağını unutmayın.
Herhangi bir parametre sağlamazsanız, hata yalnızca dört bayt veriye ihtiyaç duyar ve zincirde depolanmayan hatanın ardındaki nedenleri daha fazla açıklamak için NatSpec’i yukarıdaki gibi kullanabilirsiniz. Bu, bunu aynı zamanda çok ucuz ve kullanışlı bir hata raporlama özelliği yapar.
Daha spesifik olarak, bir hata örneği, aynı ad ve türdeki bir işleve yapılan bir işlev çağrısıyla aynı şekilde ABI ile kodlanır ve daha sonra geri alma işlem kodunda dönüş verileri olarak kullanılır. Bu, verilerin 4 baytlık bir fonksiyon selector’ünün ve ardından ABI-encoded verilerden oluştuğu anlamına gelir. Selector, hata türünün imzasının keccak256 hash’inin ilk dört baytından oluşur.
Not
Bir sözleşmenin aynı adı taşıyan farklı hatalarla veya hatta işlemi çağıran tarafından ayırt edilemeyen farklı yerlerde tanımlanan hatalarla geri dönmesi mümkündür. Dışarıdan, yani ABI için, tanımlandığı sözleşme veya dosya değil, yalnızca hatanın adı önemlidir.
require(condition, "description");
ifadesi ile
if (!condition) revert Error("description")
ifadesi eğer hata
error Error(string)
bu şekilde tanımlanmışsa, aynı işi yapar.
Error
tipinin bir built-in tipi olduğunu ve kullanıcı tarafından tanımlanamayacağını unutmayın.
Benzer olarak bir assert
ile tespit edilen bir başarısızlık, yine bir built-in
tipi olan Panic(uint256)
ile geri alınacaktır.
Not
Hata verileri sadece bir başarısızlığı işaret etmek için kullanılmalıdır, kontrol akışı için kullanılmamalıdır. Bunun nedeni, dahili çağrıların geri alınan verilerinin, varsayılan olarak harici çağrılar zinciri boyunca geri yayılmasıdır. Bu, bir iç çağrının, kendisini çağıran sözleşmeden gelmiş gibi görünen verileri “sahte” hale getirebileceği anlamına gelir.
Hataların Üyeleri
error.selector
: Hatanın selector’ünü içerenbytes4
dört baytlık bir değer.
Kalıtım
Solidity polimorfizm dahil birçok kalıtım yöntemini destekler.
Polimorfizm, bir fonksiyon çağrısının (dahili ve harici),
kalıtım hiyerarşisinde aynı fonksiyona sahip birden fazla
akıllı sözleşmenin olması durumunda, ilk türetilen akıllı sözleşmenin fonksiyonunun
çalıştırılmasına verilen isimdir.
Bu, virtual
ve override
anahtar sözcükleri kullanılarak hiyerarşideki
her işlevde açıkça etkinleştirilmelidir. Daha fazla ayrıntı için
Function Overriding’e bakın.
Kalıtım hiyerarşisinden bir fonksiyonu çağırmak isterseniz;
ContractName.functionName()
bu şekilde çağırabilirsiniz. Veya
kalıtım hiyerarşisinde bir üst akıllı sözleşmede bulunan bir fonksiyonu
çağırmak isterseniz de; super.functionName()
kullanabilirsiniz.
Bir akıllı sözleşme başka bir akıllı sözleşmeyi türettiğinde, blockchainde sadece bir adet
akıllı sözleşme oluşturulur ve tüm ana akıllı sözleşmelerden gelen kodlar oluşturulan
akıllı sözleşmeye eklenir. Bu demek oluyorki ana akıllı sözleşmelerin fonksiyonlarına
yapılan bütün internal çağrılar sadece internal fonksiyon çağrılarını
kullanırlar (super.f(..)
sadece JUMP opcode’unu kullanacaktır, mesaj çağrısı yapmayacaktır).
Durum değişkeni gölgeleme bir hata olarak kabul edilir. Bir türetilen akıllı sözleşme
sadece ve sadece eğer türettiği akıllı sözleşmelerden hiçbiri x
isminde bir değişkeni kullanmıyorsa
bu isimde bir değişken tanımlayabilir.
Genel kalıtım sistemi Python’a oldukça benzer, özellikle de çoklu kalıtım konusunda, fakat ayrıca bazı farklılıklar da bulunmaktadır
Aşağıdaki örnekte detaylar açıklanmıştır.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
// `is` kullanarak başka bir akıllı sözleşmeyi türetebiliriz.
// Türetilen akıllı sözleşmeler private olmayan bütün üyelere
// erişebilir, internal fonksiyonlar ve durum değişkenleri
// dahil. Bunlara external olarak `this` kullanılarak da erişilemez.
contract Destructible is Owned {
// `virtual` sözcüğü bu fonksiyonun, türetilen
// akıllı sözleşmelerde değiştirilebileceğini belirtir ("overriding").
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
// Abstract akıllı sözleşmeler sadece derleyiciye interface'i
// bildirmek için kullanılır. Fonksiyonun kodlarının olmadığına
// dikkat edin. Eğer bir akıllı sözleşme bütün fonksiyonlarının içeriğini
// bulundurmazsa, sadece interface olarak da kullanılabilir.
abstract contract Config {
function lookup(uint id) public virtual returns (address adr);
}
abstract contract NameReg {
function register(bytes32 name) public virtual;
function unregister() public virtual;
}
// Çoklu türetim de mümkündür. `Owned` akıllı sözleşmesinin
// ayrıca `Destructible` akıllı sözleşmesinin ana akıllı sözleşmelerinden
// biri olduğunu unutmayın. Ancak `Owned` akıllı sözleşmesinin
// sadece bir adet örneği vardır (C++'daki sanal kalıtım gibi).
contract Named is Owned, Destructible {
constructor(bytes32 name) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).register(name);
}
// Fonksiyonlar başka bir fonksiyon tarafından aynı isim ve aynı
// sayıda/tipte girdi ile override edilebilir. Eğer override eden
// fonksiyon farklı sayıda çıktı veriyorsa, bu ortaya bir hata çıkarır.
// Hem yerel hem de mesaj-tabanlı fonksiyon çağrıları bu override işlemlerini
// hesaba katar. Eğer bir fonksiyonu override etmek istiyorsanız
// `override` sözcüğünü kullanmak zorundasınız. Ayrıca fonksiyonunuzun
// tekrardan override edilebilir olmasını istiyorsanız, tekrardan
// `virtual` olarak belirlemelisiniz.
function destroy() public virtual override {
if (msg.sender == owner) {
Config config = Config(0xD5f9D8D94886E70b06E474c3fB14Fd43E2f23970);
NameReg(config.lookup(1)).unregister();
// Override edilmiş bir fonksiyonu spesifik olarak
// çağırmak mümkündür.
Destructible.destroy();
}
}
}
// Eğer bir constructor parametre alıyorsa, bu
// başlıkta veya değiştirici-çağırma-stili ile
// türetilen akıllı sözleşmesinin constructor'ında
// verilmelidir (aşağıya bakın).
contract PriceFeed is Owned, Destructible, Named("GoldFeed") {
function updateInfo(uint newInfo) public {
if (msg.sender == owner) info = newInfo;
}
// Burada sadece `override` yazıyoruz, `virtual` yazmıyoruz.
// Bu, `PriceFeed` akıllı sözleşmesinden türetilen akıllı sözleşmelerin
// artık `destroy` fonksiyonunu override edemeyecekleri anlamına geliyor.
function destroy() public override(Destructible, Named) { Named.destroy(); }
function get() public view returns(uint r) { return info; }
uint info;
}
Yukarıdaki Destructible.destroy()
fonksiyon çağrımızın bazı problemlere
yol açtığını aşağıdaki örnekte görebilirsiniz.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
contract Destructible is owned {
function destroy() public virtual {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* do cleanup 1 */ Destructible.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* do cleanup 2 */ Destructible.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { Base2.destroy(); }
}
Final.destroy()
çağrısı Base2.destroy
fonksiyonunu çağıracak.
Çünkü yaptığımız son override’da böyle belirtti. Ancak bu fonksiyon
Base1.destroy
fonksiyonunu bypass eder.
A call to Final.destroy()
will call Base2.destroy
because we specify it
explicitly in the final override, but this function will bypass
Base1.destroy
. Bunu aşmanın yolu super
kelimesini kullanmaktır:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract owned {
constructor() { owner = payable(msg.sender); }
address payable owner;
}
contract Destructible is owned {
function destroy() virtual public {
if (msg.sender == owner) selfdestruct(owner);
}
}
contract Base1 is Destructible {
function destroy() public virtual override { /* do cleanup 1 */ super.destroy(); }
}
contract Base2 is Destructible {
function destroy() public virtual override { /* do cleanup 2 */ super.destroy(); }
}
contract Final is Base1, Base2 {
function destroy() public override(Base1, Base2) { super.destroy(); }
}
Base2
, super
işlevini çağırırsa, bu işlevi temel sözleşmelerinden
birinde çağırmaz. Bunun yerine, son kalıtım grafiğindeki bir sonraki temel
sözleşmede bu işlevi çağırır, bu nedenle Base1.destroy()
u çağırır
(son kalıtım dizisinin – en türetilmiş sözleşmeyle başlayarak şöyle
olduğuna dikkat edin: Final, Base2, Base1, Destructible, owned).
super kullanılırken çağrılan asıl işlev, türü bilinmesine rağmen kullanıldığı
sınıf bağlamında bilinmemektedir. Bu, sıradan sanal yöntem araması için benzerdir.
Fonksiyon Override Etme
Temel fonksiyonlar virtual
olarak işaretlenmişse, davranışlarını
değiştirmek için override edilebilirler. Override eden fonksiyon
override
olarak belirlenmelidir. Override edilen fonksiyonun
görünürlüğü external
’dan public
’e dönüştürülebilir.
Değişebilirlik ise daha fazla kısıtlandırılmış bir yapıya dönüştürülebilir:
nonpayable
, view
ve pure
tarafından override edilebilir.
view
ise pure
tarafından override edilebilir. payable
bir istisna
olarak diğer değişebilirlik türlerine dönüştürülemez.
Aşağıdaki örnek değişebilirliği ve görünürlüğü değiştirmeyi açıklıyor:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base
{
function foo() virtual external view {}
}
contract Middle is Base {}
contract Inherited is Middle
{
function foo() override public pure {}
}
Çoklu kalıtım için, aynı işlevi tanımlayan en çok türetilmiş temel sözleşmeler,
override
anahtar sözcüğünden sonra açıkça belirtilmelidir. Başka bir deyişle,
aynı işlevi tanımlayan ve henüz başka bir temel sözleşme tarafından geçersiz
kılınmamış tüm temel sözleşmeleri belirtmeniz gerekir (miras grafiği boyunca bir yolda).
Ek olarak, bir sözleşme aynı işlevi birden çok (ilgisiz) temelden devralırsa,
bunu açıkça geçersiz kılması gerekir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
function foo() virtual public {}
}
contract Base2
{
function foo() virtual public {}
}
contract Inherited is Base1, Base2
{
// foo() fonksiyonuna sahip birden fazla temel akıllı sözleşmesi türetir.
// Bu yüzden override etmek için açıkça belirtmeliyiz.
function foo() public override(Base1, Base2) {}
}
Fonksiyon, ortak bir temel sözleşmede tanımlanmışsa veya ortak bir temel sözleşmede diğer tüm işlevleri zaten override eden benzersiz bir işlev varsa, açık bir override belirteci gerekli değildir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A { function f() public pure{} }
contract B is A {}
contract C is A {}
// Açıkça override gerekmemektedir.
contract D is B, C {}
Daha resmi olarak, imza için tüm override etme yollarının parçası olan bir temel sözleşme varsa, birden çok tabandan devralınan bir fonksiyonu (doğrudan veya dolaylı olarak) override etme gerekli değildir ve (1) bu taban fonksiyonu uygular ve mevcut akıllı sözleşmeden tabana giden hiçbir yol bu imzaya sahip bir fonksiyondan bahsetmez veya (2) bu taban fonksiyonu yerine getirmiyor ve mevcut akıllı sözleşmeden o tabana kadar olan tüm yollarda fonksiyondan en fazla bir kez söz ediliyor.
Bu anlamda, bir imza için override etme yolu, söz konusu akıllı sözleşmede başlayan ve override etmeyen bu imzaya sahip bir işlevden bahseden bir akıllı sözleşmede sona eren miras grafiği boyunca bir yoldur.
Override eden bir fonksiyonu virtual
olarak işaretlemezseniz,
türetilmiş sözleşmeler artık bu fonksiyonun davranışını değiştiremez.
Not
private
görünürlüğe sahip fonksiyonlar virtual
olamaz.
Not
Interface dışında olup da kodu olmayan fonksiyonlar virtual
olarak işaretlenmelidir. Interface içerisindeki bütün fonksiyonlar
otomatikmen virtual
olarak düşünülür.
Not
Solidity 0.8.8 itibari ile bir interface fonksiyonunu override
ederken override
sözcüğünü kullanmanıza gerek kalmıyor,
birden fazla temel akıllı sözleşmede tanımlanan fonksiyonlar dışında.
Public durum değişkenleri parametre ve dönüş tipleri uyuştuğu zaman bir external fonksiyonu override edebilir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract A
{
function f() external view virtual returns(uint) { return 5; }
}
contract B is A
{
uint public override f;
}
Not
Public durum değişkenleri external fonksiyonları override edebilirken, kendileri override edilemez.
Modifier Override Etme
Fonksiyon modifier’ları birbirlerini override edebilirler. Bu aynı
fonksiyon override etmedeki gibidir
(modifierlarda overload etme olmamakla istisnası ile). virtual
sözcüğü
override edilecek modifier’da kullanılmalı ve override eden modifier’da ise
override
sözcüğü kullanılmalıdır.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base
{
modifier foo() virtual {_;}
}
contract Inherited is Base
{
modifier foo() override {_;}
}
Çoklu kalıtım durumumnda bütün temel akıllı sözleşmeler açıkça override edilme durumunu belirtmelidir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
contract Base1
{
modifier foo() virtual {_;}
}
contract Base2
{
modifier foo() virtual {_;}
}
contract Inherited is Base1, Base2
{
modifier foo() override(Base1, Base2) {_;}
}
Constructor’lar
Constructor isteğe bağlı olarak tanımlanan özel fonksiyonlardan biridir ve
constructor
sözcüğü ile tanımlanır. Bu fonksiyon akıllı sözleşme oluşumu sırasında
çalıştırılır ve akıllı sözleşme başlatma kodunuz burada bulunmaktadır.
Constructor kodu çalıştırılmadan önce durum değişkenleri eğer aynı satırda tanımladıysanız gerekli değer atamalarını veya tanımlamadıysanız default değerlerini alırlar.
Constructor çalıştırıldıktan sonra kodun son hali blockchain’e yüklenir. Bu işlemin ücreti ise lineer bir şekilde olup kodun uzunluğuna bağımlıdır. Bu kod dışarıdan erişilebilecek ve bir fonksiyon tarafından erişilen bütün fonksiyonları içerir. Constructor kodunu veya sadece constructor tarafından erişilen internal fonksiyonları içermez.
Eğer constructor yoksa, default constructor çalıştırılır constructor() {}
.
Örneğin:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
abstract contract A {
uint public a;
constructor(uint a_) {
a = a_;
}
}
contract B is A(1) {
constructor() {}
}
Constructor’larda internal parametreleri kullanabilirsiniz (örneğin, storage pointer’ları). Bu durumda akıllı sözleşme abstract olarak işaretlenmelidir. Çünkü bu parametrelere dışarıdan geçerli değerler atanamaz, ancak yalnızca türetilmiş sözleşmelerin constructor’ları aracılığıyla atanır.
Uyarı
Versiyon 0.4.22 öncesinde constructor’lar akıllı sözleşme ile aynı isme sahip fonksiyonlar olarak kullanılırdı. Ancak bu yazılış biçiminin Versiyon 0.5.0 sonrasında kullanımına izin verilmemektedir.
Uyarı
Versiyon 0.7.0 öncesinde constructor’ların görünürlüğünü internal
veya public
olarak belirtmek zorundaydınız.
Temel Constructor’lar için Argümanlar
Tüm temel akıllı sözleşmelerin constructor’ları, aşağıda açıklanan doğrusallaştırma kurallarına göre çağrılacaktır. Temel akıllı sözleşmelerin argümanları varsa, türetilmiş akıllı sözleşmelerin hepsini belirtmesi gerekir. Bu iki şekilde yapılabilir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base {
uint x;
constructor(uint x_) { x = x_; }
}
// Direkt kalıtım listesinde belirtme...
contract Derived1 is Base(7) {
constructor() {}
}
// veya "modifier" stilinde belirtme...
contract Derived2 is Base {
constructor(uint y) Base(y * y) {}
}
// veya abstract olarak belirtin...
abstract contract Derived3 is Base {
}
// ve bir sonraki contractın onu başlatmasını sağlayın.
contract DerivedFromDerived is Derived3 {
constructor() Base(10 + 10) {}
}
Bir yol doğrudan kalıtım listesindedir (is Base(7)
). Diğeri, türetilmiş constructor’ın
bir parçası olarak bir modifier’ın çağrılma biçimindedir (Base(y * y)
).
Bunu yapmanın ilk yolu, constructor argümanının sabit olması ve akıllı sözleşmenin davranışını
tanımlaması veya tanımlaması durumunda daha uygundur. Temel constructor argümanları
türetilmiş akıllı sözleşmenin argümanlarına bağlıysa, ikinci yol kullanılmalıdır.
Argümanlar ya kalıtım listesinde ya da türetilmiş constructor’da değiştirici-tarzda verilmelidir.
Argümanları her iki yerde de belirtmek bir hatadır.
Türetilmiş bir akıllı sözleşme, temel akıllı sözleşmelerin tüm constructorları için argümanları belirtmiyorsa,
özet olarak bildirilmelidir. Bu durumda, ondan başka bir akıllı sözleşme türetildiğinde, diğer
akıllı sözleşmenin miras listesi veya constructor’ı, parametreleri belirtilmemiş tüm temel sınıflar
için gerekli parametreleri sağlamalıdır (aksi takdirde, diğer akıllı sözleşme da soyut olarak bildirilmelidir).
Örneğin, yukarıdaki kod parçacığında, bkz. Derived3
ve DerivedFromDerived
.
Çoklu Kalıtım ve Doğrusallaştırma
Çoklu kalıtıma izin veren diller birkaç problemle uğraşmak zorundadır.
Bunlardan bir tanesi Elmas Problemi’dir.
Solidity Python’a benzer olarak “C3 Linearization”
kullanarak directed acyclic graph’da (DAG) spesifik bir sırayı zorlar. Bu, istenen monotonluk özelliği
ile sonuçlanır, ancak bazı kalıtım grafiklerine izin vermez. Özellikle is
yönergesinde temel
sınıfların veriliş sırası önemlidir: Doğrudan temel sözleşmeleri “en temele benzeyen”den
“en çok türetilene” doğru sıralamalısınız. Bu sıralamanın Python’da kullanılanın tersi olduğuna dikkat edin.
Bunu açıklamanın bir başka basitleştirici yolu, farklı akıllı sözleşmelerde birden çok kez tanımlanan bir fonksiyon çağrıldığında, verilen tabanların sağdan sola (Python’da soldan sağa) derinlemesine ilk olarak aranması ve ilk eşleşmede durdurulmasıdır. . Bir temel akıllı sözleşme zaten aranmışsa, atlanır.
Aşağıdaki kodda Solidity “Linearization of inheritance graph impossible” hatası verecektir.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.4.0 <0.9.0;
contract X {}
contract A is X {}
// Bu derlenemez
contract C is A, X {}
Bunun sebebi C
akıllı sözleşmesinin X
akıllı sözleşmesinin A
akıllı sözleşmesini
override etmesini istemesidir (A, X
sırası ile bunu belirtiyor),
ancak A
akıllı sözleşmesinin kendisi X
akıllı sözleşmesini override etmeyi talep
eder ki bu çözülemeyecek bir çelişkidir.
Benzersiz bir override olmadan birden çok tabandan devralınan bir fonksiyonu açıkça override etmek gerektiğinden, pratikte C3 doğrusallaştırması çok önemli değildir.
Kalıtım doğrusallaştırmasının özellikle önemli olduğu ve belki de o kadar net olmadığı bir alan, miras hiyerarşisinde birden çok constructor olduğu zamandır. Constructor’lar, argümanlarının devralınan akıllı sözleşmenin constructor’ında sağlandığı sıraya bakılmaksızın her zaman doğrusallaştırılmış sırada yürütülür. Örneğin:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.7.0 <0.9.0;
contract Base1 {
constructor() {}
}
contract Base2 {
constructor() {}
}
// Constructor'lar aşağıdaki sıra ile çalışır:
// 1 - Base1
// 2 - Base2
// 3 - Derived1
contract Derived1 is Base1, Base2 {
constructor() Base1() Base2() {}
}
// Constructor'lar aşağıdaki sıra ile çalışır:
// 1 - Base2
// 2 - Base1
// 3 - Derived2
contract Derived2 is Base2, Base1 {
constructor() Base2() Base1() {}
}
// Constructors are still executed in the following order:
// 1 - Base2
// 2 - Base1
// 3 - Derived3
contract Derived3 is Base2, Base1 {
constructor() Base1() Base2() {}
}
Farklı Türden Aynı İsme Sahip Üyeleri Türetme
- Bir akıllı sözleşmede aşağıdaki çiftlerden herhangi birinin miras nedeniyle aynı ada sahip olması bir hatadır:
bir fonksiyon ve bir modifier
bir fonksiyon ve bir event
bir event ve bir modifier
İstisna olarak, bir durum değişkeninin getirici fonksiyonu bir external fonksiyonu override edebilir.
Abstract Akıllı Sözleşmeler
Sözleşmeler, işlevlerinden en az biri uygulanmadığında veya bütün temel sözleme yapıcılar için argüman sağlamadığında abstract olarak işaretlenmelidir. Bu durumlardan herhangi biri geçerli değilse bile bir akıllı sözleşme abstract olarak işaretlenebilir. Örneğin bir akıllı sözleşmenin direkt olarak oluşturulmasını istemediğiniz durumlarda bunu gerçekleştirebilirsiniz. Abstract akıllı sözleşmeler Interface’ler oldukça benzerdir ancak interface’ler çok daha kısıtlı bir yapıdadır.
Aşağıdaki örnekte belirtildiği gibi, Abstract akıllı sözleşmeler abstract
olarak işaretlenerek belirtilir.
Aşağıdaki akıllı sözleşmenin abstract olarak tanımlanması gerektiğine dikkat edin. Çünkü utterance()
fonksiyonu tanımlanıp kodları yazılmamıştır ({ }
arasında kod bulunmamakta).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public virtual returns (bytes32);
}
Bu tip abstract akıllı sözleşmeler direkt olarak örneklendirilemez. Bu, abstract sözleşmenin kendisi tanımlanmış tüm işlevleri yerine getiriyorsa da geçerlidir. Abstract bir akıllı sözleşmenin temel sınıf olarak kullanımı aşağıda gösterilmiştir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
abstract contract Feline {
function utterance() public pure virtual returns (bytes32);
}
contract Cat is Feline {
function utterance() public pure override returns (bytes32) { return "miaow"; }
}
Bir akıllı sözleşme bir abstract akıllı sözleşmeden türetiliyorsa ve abstract akıllı sözleşmedeki bütün kodu yazılmamış fonksiyonların kodunu yazmıyorsa, o akıllı sözleşme da abstract olarak belirtilmelidir.
Kodu olmayan bir fonksiyonun Fonksiyon Tipinden farklı olduğuna dikkat edin, her ne kadar yazılışları oldukça benzer olsa da.
Kodu olmayan bir fonksiyona örnek olarak (fonksiyon tanımlaması):
function foo(address) external returns (address);
Türü bir fonksiyon türü olan bir değişken bildirimi örneği:
function(address) external returns (address) foo;
Abstract akıllı sözleşmeler, daha iyi genişletilebilirlik ve kendi kendine belgeleme sağlayarak ve Template yöntemi gibi kalıpları kolaylaştırarak ve kod tekrarını ortadan kaldırarak bir akıllı sözleşmenin tanımını uygulamasından ayırır. Abstract akıllı sözleşmeler, bir arabirimdeki yöntemleri tanımlamanın yararlı olduğu şekilde yararlıdır. Abstract akıllı sözleşmenin tasarımcısının “her çocuğum bu yöntemi uygulamalı” demesinin bir yoludur.
Not
Abstract akıllı sözleşmeler kodu yazılmış bir virtual fonksiyonu kodu yazılmamış bir fonksiyon ile override edemezler.
Interface’ler
Interface’ler abstract akıllı sözleşmelere benzerler ama onlardan farklı olarak hiçbir fonksiyonunun kodu yazılamaz. Daha fazla kısıtlama vardır:
Diğer akıllı sözleşmelerden miras alamazken, diğer interface’lerden alabilirler.
Interface’deki bütün fonksiyonlar external olmalıdır, akıllı sözleşmede public olsalar dahi.
Constructor tanımlayamazlar.
Durum değişkeni tanımlayamazlar.
Modifier tanımlayamazlar.
Bu kısıtlamalardan bazıları ilerleyen zamanlarda kaldırılabilir.
Interface’ler kabaca akıllı sözleşme ABI’sinin temsil edebileciği ile kısıtlıdır. Bu yüzden ABI ve interface arasındaki dönüşümler bilgi kaybı yaşanmadan gerçekleştirilebilmelidir.
Interface’ler kendi anahtar sözcükleri ile tanımlanırlar:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface Token {
enum TokenType { Fungible, NonFungible }
struct Coin { string obverse; string reverse; }
function transfer(address recipient, uint amount) external;
}
Akıllı sözleşmeler diğer akıllı sözleşmelerden miras alabildikleri gibi diğer interface’lerden de alabilirler.
Interface’lerdeki bütün fonksiyonlar gizlici virtual
olarak işaretlenmiş haldedir ve
onları override ederken override
kelimesine gerek yoktur. Bu, otomatik olarak override eden bir
fonksiyonun yeniden override edilebileceği anlamına gelmez - bu yalnızca override
eden fonksiyon virtual
olarak işaretlenmişse mümkündür.
Interface’ler diğer interfacelerden miras alabilirler, normal kalıtım kuralında olduğu gibi.
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.2 <0.9.0;
interface ParentA {
function test() external returns (uint256);
}
interface ParentB {
function test() external returns (uint256);
}
interface SubInterface is ParentA, ParentB {
// Ebeveny anlamlarının uyumlu olduğunu iddia
// etmek için test yeniden tanımlanmalıdır.
function test() external override(ParentA, ParentB) returns (uint256);
}
Interface’lerde tanımlanan tiplere ve diğer akıllı sözleşme benzeri yapılara diğer akıllı sözleşmelerden
erişilebilir: Token.TokenType
veya Token.Coin
.
Kütüphaneler
Kütüphaneler akıllı sözleşmelere benzerler, ama onların amacı sadece bir kere deploy edilip
daha sonrasında ihtiyaç duyulması halinde DELEGATECALL
ile çağrılmalarıdır
(Homestead’a kadar CALLCODE
kullanılırdı). Bu demek oluyor ki kütüphane fonksiyonları
çağrıldığında, onların kodu çağıran akıllı sözleşmenin içeriği ile çalıştırılıyor, mesela this
sözcüğü çağıran akıllı sözleşmeyi işaret eder ve özellikle storage olarak çağıran akıllı sözleşmenin
storage kısmı kullanılır. Bir kütüphane izole edilmiş bir kaynak kodu parçası olduğundan,
yalnızca açıkça sağlanmışlarsa çağrı sözleşmesinin durum değişkenlerine erişebilir
(aksi takdirde bunları adlandırmanın hiçbir yolu yoktur). Kütüphane fonksiyonları yalnızca
durumu değiştirmedikleri takdirde (yani view
veya pure
fonksiyonlarsa) doğrudan
(yani DELEGATECALL
kullanılmadan) çağrılabilir, çünkü kütüphanelerin durumsuz
olduğu varsayılır. Özellikle, bir kütüphaneyi yok etmek mümkün değildir.
Not
0.4.20 sürümüne kadar, Solidity’nin tip sistemini atlayarak kütüphaneleri yok etmek mümkündü.
Bu sürümden başlayarak, kütüphaneler, durumu değiştiren fonksiyonların doğrudan çağrılmasına
izin vermeyen bir mekanizma içerir (yani DELEGATECALL
olmadan).
Kütüphaneler, onları kullanan akıllı sözleşmelerin zımni temel akıllı sözleşmeleri olarak görülebilir.
Miras hiyerarşisinde açıkça görünmezler, ancak kütüphane fonksiyonlarına yapılan çağrılar,
açık temel akıllı sözleşmelerin fonksiyonlarına yapılan çağrılara benzer
(L.f() gibi nitelikli erişim kullanarak).
Tabii ki, dahili fonksiyonlara yapılan çağrılar dahili çağrı kuralını kullanır;
bu, tüm dahili türlerin iletilebileceği ve bellekte depolanan türlerin kopyalanmadan
referans olarak iletileceği anlamına gelir. Bunu EVM’de gerçekleştirmek için, bir akıllı sözleşmeden
çağrılan dahili kütüphane fonksiyonlarının ve buradan çağrılan tüm fonksiyonların kodu
derleme zamanında çağrı akıllı sözleşmesine dahil edilecek ve bir DELEGATECALL
yerine normal
bir JUMP çağrısı kullanılacaktır.
Not
Public fonksiyonlar söz konusu olduğunda miras analojisi bozulur.
L.f() ile bir genel kütüphane fonksiyonunun çağrılması,
harici bir çağrıyla sonuçlanır (kesin olarak DELEGATECALL
).
Buna karşılık, A mevcut akıllı sözleşmesinin temel akıllı sözleşmesi olduğunda, A.f() dahili bir çağrıdır.
Aşağıdaki örnek, kütüphanelerin nasıl kullanılacağını gösterir (ancak manuel bir yöntem kullanarak, bir kümeyi uygulamak için daha gelişmiş bir örnek için kullanmayı kontrol ettiğinizden emin olun).
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.6.0 <0.9.0;
// Çağrı akıllı sözleşmesinde verilerini tutmak
// için kullanılacak yeni bir struct veri türü tanımlıyoruz.
struct Data {
mapping(uint => bool) flags;
}
library Set {
// İlk parametrenin "depolama referansı" türünde
// olduğunu ve bu nedenle çağrının bir parçası
// olarak içeriğinin değil, yalnızca depolama
// adresinin iletildiğini unutmayın.
// Bu, kütüphane fonksiyonlarının özel bir özelliğidir.
// Fonksiyon, o nesnenin bir yöntemi olarak görülebiliyorsa,
// ilk parametreyi 'self' olarak adlandırmak deyimseldir.
function insert(Data storage self, uint value)
public
returns (bool)
{
if (self.flags[value])
return false; // zaten orada
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
public
returns (bool)
{
if (!self.flags[value])
return false; // orada değil
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
public
view
returns (bool)
{
return self.flags[value];
}
}
contract C {
Data knownValues;
function register(uint value) public {
// "Instance" geçerli akıllı sözleşme olacağından,
// kütüphane fonksiyonları kütüphanenin belirli
// bir örneği olmadan çağrılabilir.
require(Set.insert(knownValues, value));
}
// Bu sözleşmede ayrıca direkt olarak knownValues.flags değişkenine de erişebiliriz.
}
Elbette kütüphaneleri kullanmak için bu yolu izlemeniz gerekmez: struct veri türleri tanımlamadan da kullanılabilirler. Fonksiyonlar ayrıca herhangi bir depolama referans parametresi olmadan da çalışırlar ve herhangi bir pozisyonda birden fazla depolama referans parametresine sahip olabilirler.
Set.contains
, Set.insert
ve Set.remove
çağrılarının hepsi
harici çağrı olarak derlenir (DELEGATECALL
). Eğer kütüphaneleri kullanacaksanız
gerçekten bir harici fonksiyon çağrısı yaptığınızı unutmayın.
msg.sender
, msg.value
ve this
çağrı boyunca kendi değerlerini koruyacaktır
(Homestead öncesi CALLCODE
yüzünden msg.sender
ve msg.value
değişiyordu).
Aşağıdaki örnek, harici fonksiyon çağrılarının ek yükü olmadan özel türleri uygulamak için bellekte depolanan türlerin ve kütüphanelerdeki dahili fonksiyonların nasıl kullanılacağını gösterir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;
struct bigint {
uint[] limbs;
}
library BigInt {
function fromUint(uint x) internal pure returns (bigint memory r) {
r.limbs = new uint[](1);
r.limbs[0] = x;
}
function add(bigint memory a, bigint memory b) internal pure returns (bigint memory r) {
r.limbs = new uint[](max(a.limbs.length, b.limbs.length));
uint carry = 0;
for (uint i = 0; i < r.limbs.length; ++i) {
uint limbA = limb(a, i);
uint limbB = limb(b, i);
unchecked {
r.limbs[i] = limbA + limbB + carry;
if (limbA + limbB < limbA || (limbA + limbB == type(uint).max && carry > 0))
carry = 1;
else
carry = 0;
}
}
if (carry > 0) {
// çok kötü, bir limb eklemeliyiz
uint[] memory newLimbs = new uint[](r.limbs.length + 1);
uint i;
for (i = 0; i < r.limbs.length; ++i)
newLimbs[i] = r.limbs[i];
newLimbs[i] = carry;
r.limbs = newLimbs;
}
}
function limb(bigint memory a, uint index) internal pure returns (uint) {
return index < a.limbs.length ? a.limbs[index] : 0;
}
function max(uint a, uint b) private pure returns (uint) {
return a > b ? a : b;
}
}
contract C {
using BigInt for bigint;
function f() public pure {
bigint memory x = BigInt.fromUint(7);
bigint memory y = BigInt.fromUint(type(uint).max);
bigint memory z = x.add(y);
assert(z.limb(1) > 0);
}
}
Bir kütüphanenin adresini, kütüphane tipini address
tipine çevirerek,
yani address(LibraryName)
kullanarak elde etmek mümkündür.
Derleyici kütüphanenin konuşlandırılacağı adresi bilmediğinden,
derlenmiş onaltılık kod __$30bbc0abd4d6364515865950d3e0d10953$__
biçiminde yer tutucular
içerecektir. Yer tutucu, tam nitelikli kütüphane adının keccak256 hashinin hex kodlamasının
34 karakterlik bir önekidir; bu, örneğin kütüphane bigint.sol
isimli bir dosyada
ve libraries/
isimli bir dizinde bulunuyorsa şu şekilde gösterilir libraries/bigint.sol:BigInt
.
Bu tür bayt kodu eksiktir ve dağıtılmamalıdır. Yer tutucuların gerçek adreslerle değiştirilmesi gerekir.
Bunu, kütüphane derlenirken bunları derleyiciye ileterek veya önceden derlenmiş bir ikili dosyayı
güncellemek için bağlayıcıyı kullanarak yapabilirsiniz. Bağlama için komut satırı derleyicisinin
nasıl kullanılacağı hakkında bilgi için Kütüphane Bağlantıları (Library Linking) konusuna bakın.
Akıllı sözleşmelerle kıyaslandığında, kütüphaneler aşağıdaki şekillerde kısıtlanmışlardır:
durum değişkenleri olamaz
miras veremezler veya alamazlar
Ether kabul edemezler
yok edilemezler
(Bunlar ilerleyen zamanlarda kaldırılabilirler.)
Function Signatures and Selectors in Libraries
Public veya external kütüphane fonksiyonlarına harici çağrılar mümkün olsa da, bu tür çağrılar için çağrı kuralının Solidity’nin içinde olduğu ve normal contract ABI için belirtilenle aynı olmadığı kabul edilir. External kütüphane fonksiyonları, örneğin özyinelemeli yapılar ve depolama işaretçileri gibi external kütüphane fonksiyonlarından daha fazla bağımsız değişken türünü destekler. Bu nedenle, 4 baytlık seçiciyi hesaplamak için kullanılan fonksiyon imzaları, bir internal adlandırma şemasının ardından hesaplanır ve ABI akıllı sözleşmesinde desteklenmeyen türdeki bağımsız değişkenler bir dahili kodlama kullanır.
İmzalardaki türler için aşağıdaki tanımlayıcılar kullanılır:
Değer tipleri, storage olmayan
string
ve storage olmayanbytes
tipleri akıllı sözleşme ABI’sinde aynı tanımlayıcıları kullanır.Storage olmayan array tipleri de akıllı sözleşme ABI’sindeki genel görüşü kabul eder, yani dinamik arrayler için
<type>[]
ve fixed-size arrayler için<type>[M]
kullanılır.Storage olmayan structlar tam isimleri ile referans edilir, yani
contract C { struct S { ... } }
içinC.S
.Storage pointer mappingleri de
mapping(<keyType> => <valueType>) storage
kullanır. Burada<keyType>
ve<valueType>
sırasıyla mappingdeki anahtar ve değer tipleridir.Diğer storage pointer tipleri de kendi storage olmayan tiplerinin tanımlayıcılarını kullanırlar, ama bir boşluk ile
storage
eklenmiş halleri ile.
Argüman encode’lama da sıradan akıllı sözleşme ABI’si gibidir, storage pointerları hariç,
işaret ettikleri storage slotuna atıfta bulunan bir uint256
değeri olarak kodlanmıştır.
Akıllı sözleşme ABI’sine benzer bir şekilde, selector, imzanın Keccak256-hashinin ilk dört baytından oluşur.
Değeri, .selector
üyesi kullanılarak Solidity’den şu şekilde elde edilebilir:
// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.5.14 <0.9.0;
library L {
function f(uint256) external {}
}
contract C {
function g() public pure returns (bytes4) {
return L.f.selector;
}
}
Kütüphaneler İçin Çağrı Koruması
Girişte belirtildiği gibi, bir kütüphanenin kodu DELEGATECALL
veya CALLCODE
yerine bir CALL
kullanılarak yürütülürse, bir view
veya pure
fonksiyon
çağrılmadığı sürece geri dönecektir.
EVM, bir akıllı sözleşmenin CALL
kullanılarak çağrılıp çağrılmadığını tespit etmek
için doğrudan bir yol sağlamaz, ancak bir sözleşme, “nerede” çalıştığını bulmak
için ADDRESS
işlem kodunu kullanabilir. Oluşturulan kod, arama modunu
belirlemek için bu adresi yapım sırasında kullanılan adresle karşılaştırır.
Daha spesifik olarak, bir kütüphanenin çalışma zamanı kodu her zaman derleme zamanında 20 bayt sıfır olan bir push komutuyla başlar. Dağıtım kodu çalıştığında, bu sabit bellekte geçerli adresle değiştirilir ve bu değiştirilmiş kod sözleşmede saklanır. Çalışma zamanında, bu, dağıtım zamanı adresinin yığına gönderilecek ilk sabit olmasına neden olur ve dağıtıcı kodu, herhangi bir görünüm olmayan ve saf olmayan işlev için geçerli adresi bu sabitle karşılaştırır.
Bu, bir kitaplık için zincirde depolanan gerçek kodun
derleyici tarafından bildirilen koddan farklıdır.
deployedBytecode
.
Using For
using A for B;
yönergesi, (A
) fonksiyonlarını herhangi bir türe
(B
) üye fonksiyonlar olarak eklemek için kullanılabilir. Bu fonksiyonlar,
çağrıldıkları nesneyi ilk parametreleri olarak alırlar (Python’daki self
değişkeni gibi).
Dosya seviyesinde veya bir akıllı sözleşme içerisinde, akıllı sözleşme seviyesinde, geçerlidir.
İlk kısım, A
, aşağıdakilerden biri olabilir:
dosya seviyesindeki fonksiyonların bir listesi veya kütüphane fonksiyonları (
using {f, g, h, L.t} for uint;
) - sadece o fonksiyonlar eklenecektir.kütüphanenin adı (
using L for uint;
) - bütün fonksiyonlar (public ve internallerin hepsi) tipe eklenir.
Dosya seviyesinde, ikinci kısım, B
, açık bir tip olmalıdır (veri konumu belirtici olmadan).
Akıllı sözleşmenin içerisinde, ayrıca şu ifadeyi de kullanabilirsiniz using L for *;
, böylece L
kütüphanesinin bütün fonksiyonları bütün tiplere eklenmiş olur.
Bir kütüphane belirtirseniz, kütüphanedeki tüm fonksiyonlar, ilk parametrenin türü nesnenin türüyle eşleşmese bile eklenir. Fonksiyonun çağrıldığı noktada tip kontrol edilir ve fonksiyon aşırı yük çözünürlüğü gerçekleştirilir.
Eğer bir fonksiyon listesi kullanırsanız (using {f, g, h, L.t} for uint;
),
ardından gelen tip (uint
) o kütüphanedeki bütün fonksiyonların ilk parametrelerine
gizlice dönüştürülebilir olmalıdır. Bu kontrol, fonksiyonların hiçbiri çağrılmasa bile
gerçekleştirilir.
using A for B;
direktifi, tüm fonksiyonları dahil olmak üzere yalnızca mevcut kapsamda
(sözleşme veya mevcut modül/kaynak birim) etkindir ve kullanıldığı sözleşme veya modül
dışında hiçbir etkisi yoktur.
Yönerge dosya düzeyinde kullanıldığında ve aynı dosyada dosya düzeyinde tanımlanmış
kullanıcı tanımlı bir türe uygulandığında, sonuna global
sözcüğü eklenebilir.
Bu, yalnızca using ifadesinin kapsamında değil, türün kullanılabilir olduğu her yerde
(diğer dosyalar dahil) işlevlerin türe eklenmesi etkisine sahip olacaktır.
Kütüphaneler bölümünde yazdığımız bir örneği dosya seviyesindeki fonksiyonlarla yeniden yazalım:
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
struct Data { mapping(uint => bool) flags; }
// Şimdi örneğe fonksiyonları ekliyoruz.
// Eklenen fonksiyonlar modül boyuna kullanılabilir.
// Eğer modülü başka bir dosyadan eklerseniz
// using yönergesini orada yeniden kullanmalısınız:
// import "flags.sol" as Flags;
// using {Flags.insert, Flags.remove, Flags.contains}
// for Flags.Data;
using {insert, remove, contains} for Data;
function insert(Data storage self, uint value)
returns (bool)
{
if (self.flags[value])
return false; // already there
self.flags[value] = true;
return true;
}
function remove(Data storage self, uint value)
returns (bool)
{
if (!self.flags[value])
return false; // not there
self.flags[value] = false;
return true;
}
function contains(Data storage self, uint value)
view
returns (bool)
{
return self.flags[value];
}
contract C {
Data knownValues;
function register(uint value) public {
// Burada, Data türündeki tüm değişkenlerin karşılık
// gelen üye işlevleri vardır. Aşağıdaki işlev çağrısı,
// `Set.insert(knownValues, value)` ile aynıdır.
require(knownValues.insert(value));
}
}
Yerleşik türleri bu şekilde genişletmek de mümkündür. Bu örnekte bir kütüphane kullanacağız.
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.13;
library Search {
function indexOf(uint[] storage self, uint value)
public
view
returns (uint)
{
for (uint i = 0; i < self.length; i++)
if (self[i] == value) return i;
return type(uint).max;
}
}
using Search for uint[];
contract C {
uint[] data;
function append(uint value) public {
data.push(value);
}
function replace(uint from, uint to) public {
// Bu, kütüphane işlev çağrısını gerçekleştirir
uint index = data.indexOf(from);
if (index == type(uint).max)
data.push(to);
else
data[index] = to;
}
}
Tüm harici kütüphane çağrılarının gerçek EVM fonksiyon çağrıları olduğunu unutmayın.
Bu, bellek veya değer türlerini geçerseniz, self
değişken durumunda bile bir kopyanın
gerçekleştirileceği anlamına gelir. Kopyalama yapılmayacak tek durum, depolama referans
değişkenlerinin kullanıldığı veya dahili kütüphane fonksiyonlarının çağrıldığı durumlardır.