Solidity'de Veri Konum Etiketleri (memory, storage, calldata)

Solidity'de Veri Konum Etiketleri (memory, storage, calldata)

Bu yazımızda Solidity dilinde kullanılan veri konum etiketlerini öğrenip nasıl kullanıldıklarını örnekler üzerinde inceleyeceğiz

·

5 min read

Solidity dilinde değişkenler iki farklı veri konumunda tutulabilirler. Bu veri konumları depo (storage) ve bellektir (memory).

Veri konum etiketleri, referans tipi değişkenlerde (array, struct, mapping) belirtilir. Solidity’de 3 farklı veri konum etiketi bulunur. Bunlar;

  • storage: Veri blokzinciri içinde kalıcıdır.

  • memory: Veri geçicidir, sadece bulunduğu fonksiyon çağırıldığında oluşturulur ve ardından yok olur.

  • calldata: Veri geçicidir. Memory etiketinden farkı değiştirilemez olmasıdır.

Veri konumlarının daha iyi anlaşılması adına Ethereum Sanal Makinesini (EVM), HDD ve RAM’i olan basit bir bilgisayar olarak düşünürsek; Kalıcı veriler için harddiskinde storage etiketi olan değişkenlerin tutulduğunu, geçici veriler için ise RAM’inde memory ve calldata etiketli değişkenlerin tutulduğunu düşünebiliriz.

Farklı veri konumları kullanmamızın bazı sebepleri vardır. Bunlar Gas maliyeti, güvenlik veya performans olabilir.

Storage:

Storage tipinde tutulan veriler, akıllı sözleşme ile beraber blokzincirine yazılır ve kalıcıdır demiştik. Bu veriler istenildiğinde okuma ve değiştirmeye müsait olmakla beraber kulanım maliyeti yüksektir. Ek bilgi olarak Durum (state) değişkenleri varsayılan olarak depo (storage) konumundadır.

Şimdi storage etiketli bir değişkeni örnek üzerinde gösterelim:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BuildChain
{
    // karakterler dizisi oluşturma
    string[] public characters;

    function Characters() public
    {   
        // Karakterler dizisine eleman ekleme
        characters.push("A");
        characters.push("B");

        // Yeni bir storage etiketli dizi oluşturma
        string[] storage myArray = characters;

        // Dizinin ilk elemanına yeni karakter atama
        myArray[0] = "C";
    }
}

Yukarıdaki örnekte sırasıyla yapılanlar:

  • Önce “characters” adlı bir string dizisi oluşturduk.

  • Ardından “Characters” adlı bir fonksiyon oluşturup burada ilk diziye “A” ve “B” karakterlerini ekledik.

  • Yine fonksiyon içinde storage tipinde oluşturduğumuz “myArray” dizisine, ilk oluşturduğumuz diziyi referans aldırdık.

  • myArray” dizisinin ilk elemanını “C” olarak değiştirdik.

Bu işlemlerin ardından ilk oluşturduğumuz “characters” adlı dizinin çıktısı [C,B] olacaktır.

Storage etiketinin doğru ve yanlış kullanımlarına aşağıdaki örnek üzerinden bakalım:

bytes data = "test";
function referances() public 
{
    bytes memory memData = new bytes(5);
    bytes calldata callData = msg.data;
    bytes storage a = data; 
    bytes storage b;

    bytes storage c = a;        // Doğru. storage -> storage       
    bytes storage d = b;         // Yanlış. “b” değişkeni boş/başlatılmamış
    bytes storage e = memData;     // Yanlış. storage -> memory
    bytes storage f = callData;    // Yanlış. storage -> calldata     
}

Uyarı: Bir storage etiketli değişken, memory ve calldata etiketli bir değişkeni veya henüz değer atanmamış/başlatılmamış bir storage etiketli değişkeni referans alamaz!

Memory

Memory etiketli değişkenlerde veriler kalıcı değildir demiştik. Örneğin memory etiketli bir değişken, kullanıldığı fonksiyon çağrıldığı anda oluşturulur ve ardından görevini tamamlandığından yok olur. Yine storage etiketli değişkenlerden bir diğer farkı ise, örneğin memory etiketi ile oluşturduğumuz bir dizi ile bir başka storage etiketli diziyi referans aldığımızda, memory etiketli dizide yaptığımız herhangi bir değişiklik, orijinal diziyi etkilemeyecektir. Bunun nedeni ise memory etiketli değişkenler ile storage etiketli bir değişkeni referans aldığımızda arka planda alınan değişkenin bir kopyası oluşturulup ardından kopya referans alındığındandır.

Yukarıdaki örneğimize memory etiketli bir değişken ekleyelim:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BuildChain
{
    // karakterler dizisi oluşturma
    string[] public characters;

    // Karakterler dizisine eleman ekleme
    function Characters() public
    {
        characters.push("A");
        characters.push("B");

        // Yeni bir storage etiketli dizi oluşturma
        string[] storage myArray = characters;
        // Yeni bir memory etiketli dizi oluşturma
        string[] memory myMemoryArray = characters;

        // Dizilerin ilk elemanına yeni karakter atama
        myArray[0] = "C";
        myMemoryArray[0] = "D";
    }
}

Bu örnekte ilk yaptığımız storage örneğimize ek olarak:

  • "myMemoryArray" adlı memory etiketli bir dizi oluşturduk.

  • Bu diziye “characters” dizisini referans gösterdik.

  • "myMemoryArray" dizisinin ilk elemanını “D” karakteri ile değiştirdik.

Burada fonksiyon çalıştıktan sonra, “characters” dizisinin çıktısı [C,B] olacaktır. Oysaki en sonda dizinin ilk elemanını “D” karakteri ile değiştirmiştik fakat “characters” dizisinin çıktısını etkilemediğini görüyoruz. Bunun sebebi, yukarıda da bahsettiğimiz memory etiketli değişkenlerin referans aldıkları orijinal değişkenleri etkilememesinden kaynaklıdır.

Memory etiketinin doğru ve yanlış kullanımlarına aşağıdaki örnek üzerinden bakalım:

bytes data = "deneme";
function referances() public {
    bytes memory a;
    bytes calldata b;
    bytes calldata c = msg.data;
    bytes storage d = data; 
    bytes storage e;

    bytes memory f = d; // Doğru. memory -> storage       
    bytes memory g = e; // Yanlış. “e” değişkeni hem boş hem memory değil
    bytes memory h = b; // Yanlış. “b” değişkeni hem boş hem memory değil
    bytes memory i = c; // Doğru. memory -> calldata
    bytes memory j = a; // Doğru. memory -> memory, “a” boş ama aynı tip!
}

Uyarı: memory etiketli değişkenler, hem bir değer atanmamış/boş olup hem de farklı konum etiketine sahip değişkenleri referans alamazlar!

Calldata:

Calldata, memory ile benzerlik göstermekle beraber en önemli farkı değiştirilemez olmasıdır. Fonksiyon parametrelerinde sıkça kullanılır. Kod üzerinde basit bir calldata kullanım örneği gösterelim:

function updateText(uint _index, string calldata _text) public {
        Todo storage todo = todos[_index];
        todo.text = _text;
    }

Bu örnekte, fonksiyondaki “_text” parametresi calldata etiketi ile kullanılmıştır. Dikkat edilirse fonksiyon içinde referans alan değil alınandır. Unutmayalım ki calldata etiketli bir değişken oluşturulduktan sonra değiştirilemez!

Özetle;

  • Yerel (local) değişkenler varsayılan olarak bellek (memory) konumludur fakat yerel (local) değişken olan bir array, struct, mapping gibi referans tipli değişkenleri tanımlarken veri konum etiketini yazmak gerekir.

  • Durum (state) değişkenleri varsayılan olarak depo (storage) konumludur.

  • Fonksiyon parametreleri bellekte (memory) saklanır yani geçicidir.

  • Referans alan ve alınan değişkenlerin etiketleri sırasıyla memory ve storage ise bir değişkende yapılan değişiklik diğerini etkilemez çünkü referans bağımsız bir kopya üzerinden oluşturulur.

  • Referans alan ve alınan değişkenlerin etiketleri memory ve memory ise birinde yapılan değişiklik diğerini de etkiler.

  • Referans alan ve alınan değişkenlerin etiketleri sırasıyla storage ve storage ise yapılan değişiklikler diğerini etkiler.

  • Diğer tüm storage değişkene yapılan referanslarda bir kopya oluşturulur.

  • GAS maliyeti açısından calldata, memory’e göre daha uygundur.


Kaynaklar:

  1. https://ethereum.org/en/developers/docs/smart-contracts/anatomy/#data

  2. https://betterprogramming.pub/solidity-tutorial-all-about-data-locations-dabd33212471


İletişim için bana Twitter'dan ulaşabilirsiniz.

Did you find this article valuable?

Support Buildchain by becoming a sponsor. Any amount is appreciated!