Referanslar ve Anlık Görüntüler

Önceki Liste 4-3'teki demet kodunun sorunu, calculate_length çağrısından sonra Array'i hala kullanabilmemiz için Array'i çağıran fonksiyona geri döndürmemiz gerektiğidir, çünkü Array calculate_length içine taşınmıştı.

Anlık Görüntüler

Önceki bölümde, Cairo'nun sahiplik sisteminin, bir değişkeni taşıdıktan sonra onu kullanmamızı engelleyerek, aynı bellek hücresine potansiyel olarak iki kez yazmamızı önlediğinden bahsettik. Ancak, bu pek uygun değil. Çağıran fonksiyonda değişkenin sahipliğini anlık görüntüler kullanarak nasıl koruyabileceğimizi görelim.

In Cairo, a snapshot is an immutable view of a value at a certain point in the execution of the program. Recall that memory is immutable, so modifying a variable actually fills a new memory cell. The old memory cell still exists, and snapshots are variables that refer to that "old" value. In this sense, snapshots are a view "into the past".

Here is how you would define and use a calculate_area function that takes a snapshot of a Rectangle struct as a parameter instead of taking ownership of the underlying value. In this example, the calculate_area function returns the area of the Rectangle passed as a snapshot. Since we’re passing it as an immutable view, we can be sure that calculate_area will not mutate the Rectangle, and ownership remains in the main function.

Filename: src/lib.cairo

#[derive(Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let mut rec = Rectangle { height: 3, width: 10 };
    let first_snapshot = @rec; // Take a snapshot of `rec` at this point in time
    rec.height = 5; // Mutate `rec` by changing its height
    let first_area = calculate_area(first_snapshot); // Calculate the area of the snapshot
    let second_area = calculate_area(@rec); // Calculate the current area
    println!("The area of the rectangle when the snapshot was taken is {}", first_area);
    println!("The current area of the rectangle is {}", second_area);
}

fn calculate_area(rec: @Rectangle) -> u64 {
    *rec.height * *rec.width
}

Note: Accessing fields of a snapshot (e.g., rec.height) yields snapshots of those fields, which we desnap with * to get the values. This works here because u64 implements Copy. You’ll learn more about desnapping in the next section.

Bu programın çıktısı:

$ scarb cairo-run 
warn: `scarb cairo-run` will be deprecated soon
help: use `scarb execute` instead
   Compiling no_listing_09_snapshots v0.1.0 (listings/ch04-understanding-ownership/no_listing_09_snapshots/Scarb.toml)
    Finished `dev` profile target(s) in 2 seconds
     Running no_listing_09_snapshots
The area of the rectangle when the snapshot was taken is 30
The current area of the rectangle is 50
Run completed successfully, returning []

First, notice that all the tuple code in the variable declaration and the function return value is gone. Second, note that we pass @rec into calculate_area and, in its definition, we take @Rectangle rather than Rectangle.

Fonksiyon çağrısına burada daha yakından bakalım:

let second_length = calculate_length(@arr1); // Calculate the current length of the array

The @rec syntax lets us create a snapshot of the value in rec. Because a snapshot is an immutable view of a value at a specific point in execution, the usual rules of the linear type system are not enforced. In particular, snapshot variables always implement the Drop trait, never the Destruct trait, even dictionary snapshots.

It’s worth noting that @T is not a pointer—snapshots are passed by value to functions, just like regular variables. This means that the size of @T is the same as the size of T, and when you pass @rec to calculate_area, the entire struct (in this case, a Rectangle with two u64 fields) is copied to the function’s stack. For large data structures, this copying can be avoided by using Box<T>—provided that there's no need to mutate the value, which we’ll explore in Chapter 12, but for now, understand that snapshots rely on this by-value mechanism.

Benzer şekilde, fonksiyonun imzası @ kullanılarak parametre arr'in tipinin bir anlık görüntü olduğunu belirtir. Açıklayıcı notlar ekleyelim:

fn calculate_area(
    rec_snapshot: @Rectangle // rec_snapshot is a snapshot of a Rectangle
) -> u64 {
    *rec_snapshot.height * *rec_snapshot.width
} // Here, rec_snapshot goes out of scope and is dropped.
// However, because it is only a view of what the original `rec` contains, the original `rec` can still be used.

The scope in which the variable rec_snapshot is valid is the same as any function parameter’s scope, but the underlying value of the snapshot is not dropped when rec_snapshot stops being used. When functions have snapshots as parameters instead of the actual values, we won’t need to return the values in order to give back ownership of the original value, because we never had it.

Desnap Operatörü

Bir anlık görüntüyü normal bir değişkene geri dönüştürmek için, @ operatörünün tersi olarak hizmet veren desnap operatörü * kullanılabilir.

Yalnızca Copy tipleri desnap edilebilir. Ancak, genel olarak, değer değiştirilmediğinden, desnap operatörü tarafından oluşturulan yeni değişken eski değeri yeniden kullanır ve bu nedenle desnapping tamamen ücretsiz bir işlemdir, tıpkı Copy gibi.

Aşağıdaki örnekte, bir dikdörtgenin alanını hesaplamak istiyoruz, ancak calculate_area fonksiyonunda dikdörtgenin sahipliğini almak istemiyoruz, çünkü fonksiyon çağrısından sonra dikdörtgeni tekrar kullanmak isteyebiliriz. Fonksiyonumuz dikdörtgen örneğini değiştirmediğinden, fonksiyona dikdörtgenin anlık görüntüsünü geçirebilir ve sonra anlık görüntüleri desnap operatörü * kullanarak değerlere geri dönüştürebiliriz.

#[derive(Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let rec = Rectangle { height: 3, width: 10 };
    let area = calculate_area(@rec);
    println!("Area: {}", area);
}

fn calculate_area(rec: @Rectangle) -> u64 {
    // As rec is a snapshot to a Rectangle, its fields are also snapshots of the fields types.
    // We need to transform the snapshots back into values using the desnap operator `*`.
    // This is only possible if the type is copyable, which is the case for u64.
    // Here, `*` is used for both multiplying the height and width and for desnapping the snapshots.
    *rec.height * *rec.width
}

Bir anlık görüntü olarak geçirdiğimiz bir şeyi değiştirmeyi denediğimizde ne olur? Liste 4-4'teki kodu deneyin. Spoiler uyarısı: işe yaramaz!

Filename: src/lib.cairo

#[derive(Copy, Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let rec = Rectangle { height: 3, width: 10 };
    flip(@rec);
}

fn flip(rec: @Rectangle) {
    let temp = rec.height;
    rec.height = rec.width;
    rec.width = temp;
}

Listing 4-4: Attempting to modify a snapshot value

İşte hata:

$ scarb cairo-run 
   Compiling listing_04_04 v0.1.0 (listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/Scarb.toml)
error: Invalid left-hand side of assignment.
 --> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:15:5
    rec.height = rec.width;
    ^********^

error: Invalid left-hand side of assignment.
 --> listings/ch04-understanding-ownership/listing_04_attempt_modifying_snapshot/src/lib.cairo:16:5
    rec.width = temp;
    ^*******^

error: could not compile `listing_04_04` due to previous error
error: `scarb metadata` exited with error

Derleyici, anlık görüntülere bağlı değerlerin değiştirilmesini engeller.

Değiştirilebilir Referanslar

Liste 4-4'teki istediğimiz davranışı, bir anlık görüntü yerine değiştirilebilir bir referans kullanarak elde edebiliriz. Değiştirilebilir referanslar, aslında bir fonksiyona geçirilen ve fonksiyonun sonunda implisit olarak geri döndürülen değiştirilebilir değerlerdir, sahipliği çağrı yapan bağlama geri döndürür. Bunu yaparak, değeri geçirirken değiştirmenize izin verirler ve yürütmenin sonunda otomatik olarak geri döndürerek onun sahipliğini korumanızı sağlarlar. Cairo'da, bir parametre değiştirilebilir referans olarak ref değiştiricisi kullanılarak geçirilebilir.

Not: Cairo'da, bir parametre yalnızca ref değiştiricisi kullanılarak değiştirilebilir referans olarak geçirilebilir, eğer değişken mut ile değiştirilebilir olarak tanımlanmışsa.

Liste 4-5'te, flip fonksiyonunda Rectangle örneğinin height ve width alanlarının değerini değiştirmek için değiştirilebilir bir referans kullanıyoruz.

#[derive(Drop)]
struct Rectangle {
    height: u64,
    width: u64,
}

fn main() {
    let mut rec = Rectangle { height: 3, width: 10 };
    flip(ref rec);
    println!("height: {}, width: {}", rec.height, rec.width);
}

fn flip(ref rec: Rectangle) {
    let temp = rec.height;
    rec.height = rec.width;
    rec.width = temp;
}

Listing 4-5: Use of a mutable reference to modify a value

Öncelikle, rec'i mut olarak değiştiriyoruz. Sonra rec'in değiştirilebilir bir referansını ref rec ile flip'e geçiriyoruz ve fonksiyon imzasını ref rec: Rectangle ile değiştirilebilir bir referansı kabul edecek şekilde güncelliyoruz. Bu, flip fonksiyonunun parametre olarak geçirilen Rectangle örneğinin değerini değiştireceğini çok açık hale getirir.

Unlike snapshots, mutable references allow mutation, but like snapshots, ref arguments are not pointers—they are also passed by value. When you pass ref rec, the entire Rectangle type is copied to the function’s stack, regardless of whether it implements Copy. This ensures the function operates on its own local version of the data, which is then implicitly returned to the caller. To avoid this copying for large types, Cairo provides the Box<T> type introduced in Chapter 12 as an alternative, but for this example, the ref modifier suits our needs perfectly.

Programın çıktısı:

$ scarb cairo-run 
   Compiling listing_04_05 v0.1.0 (listings/ch04-understanding-ownership/listing_05_mutable_reference/Scarb.toml)
    Finished `dev` profile target(s) in 3 seconds
     Running listing_04_05
height: 10, width: 3
Run completed successfully, returning []

Beklendiği gibi, rec değişkeninin height ve width alanları yer değiştirdi.

Small Recap

Doğrusal tip sistemi, sahiplik, anlık görüntüler ve referanslar hakkında tartıştığımız konuları özetleyelim:

  • Herhangi bir zamanda, bir değişkenin yalnızca bir sahibi olabilir.
  • Bir değişkeni bir fonksiyona değer-olarak, anlık görüntü-olarak veya referans-olarak geçirebilirsiniz.
  • Değer-olarak geçirirseniz, değişkenin sahipliği fonksiyona aktarılır.
  • Değişkenin sahipliğini korumak ve fonksiyonunuzun onu değiştirmeyeceğini bilmek istiyorsanız, @ ile bir anlık görüntü olarak geçirebilirsiniz.
  • Değişkenin sahipliğini korumak ve fonksiyonunuzun onu değiştireceğini bilmek istiyorsanız, ref ile değiştirilebilir bir referans olarak geçirebilirsiniz.