পাঠ ৪.২

Reference এবং Borrowing

References and Borrowing

আগের পাঠে দেখলাম — function-কে value pass করলে ownership move হয়, আবার ফেরত পেতে return করতে হয়। এটা irritating। এর সমাধান — reference। Reference হলো address — তুমি data দেখতে পারো কিন্তু owner হবে না।

&String — reference নেওয়া

আগের calculate_length function-কে রিরাইট করি — ownership-এর বদলে reference নেবে:

src/main.rsrust
fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

    println!("The length of '{s1}' is {len}.");
}

fn calculate_length(s: &String) -> usize {
    s.len()
}

লক্ষ্য করো — tuple-return-এর ceremony চলে গেছে। &s1 argument হিসেবে যাচ্ছে; function-এর parameter s: &String& চিহ্নটা reference বোঝায়।

Reference একটা pointer-এর মতো — address থাকে, address follow করলে data পাওয়া যায়। কিন্তু pointer থেকে আলাদা: Rust-এর reference সবসময় একটা valid value-এর দিকে point করে — compiler গ্যারান্টি দেয়।

&s1 reference তৈরি করে, যেটা s1-এর value-র দিকে নির্দেশ করে কিন্তু ownership নেয় না। তাই function শেষ হলে value drop হয় না — কারণ reference-এর কাছে ownership-ই ছিল না।

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

Borrowing

Reference তৈরি করার action-কে আমরা borrowing বলি। জীবনের মতোই — কারো জিনিস নিতে পারো, কিন্তু কাজ শেষে ফেরত দিতে হয়। মালিক তো হও না।

Reference default-এ immutable

Reference-এর মাধ্যমে data modify করতে চাইলে — কাজ করবে না:

src/main.rsrust
fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}
compile errortext
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

Variable-এর মতো reference-ও default-এ immutable। Borrow করা data modify করার অধিকার আমাদের নেই।

Mutable reference — &mut

Mutate করতে — তিনটে ছোট পরিবর্তন:

src/main.rsrust
fn main() {
    let mut s = String::from("hello");

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}
  1. s-কে mut করো।
  2. Reference-এ &mut s পাঠাও।
  3. Function signature-এ &mut String নাও।

এখন function intent স্পষ্ট — এটা borrowed value mutate করবে।

Mutable reference-এর restriction

একই value-র mutable reference একসাথে একটাই থাকতে পারে। অন্য কোনো reference (mutable হোক বা immutable) ঐ সময় থাকতে পারবে না।

fn main() {
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{r1}, {r2}");
}
compile errortext
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{r1}, {r2}");
  |                -- first borrow later used here

এই restriction-এ benefit হলো — Rust compile time-এ data race prevent করতে পারে।

Data race-এর তিন শর্ত — যা একসাথে হলে সমস্যা:

  • একই data-তে দু'টা বা তার বেশি pointer access করছে।
  • অন্তত একটা write করছে।
  • Synchronization-এর কোনো mechanism নেই।

Data race undefined behavior তৈরি করে, runtime-এ debug করা ভয়ংকর। Rust এই code compile-ই করতে দেয় না।

Curly bracket দিয়ে scope বানিয়ে আলাদা করা

একসাথে না হলে — মানে একটা reference শেষ হয়ে আরেকটা — সমস্যা নেই। Scope তৈরি করে আলাদা করা যায়:

fn main() {
    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;
}

Mutable + immutable mix-ও না

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{r1}, {r2}, and {r3}");
}
compile errortext
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here

কারণ — যারা immutable reference use করছে, তাদের expectation-এ value stable। হঠাৎ পাল্টে গেলে subtle bug। তবে একাধিক immutable reference একসাথে থাকতে পারে — কেউ write করছে না, কারো interfere নেই।

Reference-এর scope = শেষ use পর্যন্ত

Reference-এর scope যেখানে introduce করা হয়েছে সেখান থেকে শুরু হয়ে শেষ use পর্যন্ত যায়। এই code compile হয়:

fn main() {
    let mut s = String::from("hello");

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{r1} and {r2}");
    // Variables r1 and r2 will not be used after this point.

    let r3 = &mut s; // no problem
    println!("{r3}");
}

r1, r2 println!-এর পর আর use হয় না — তাদের scope সেখানেই শেষ। তারপর r3 মুক্তভাবে borrow করতে পারে। একে বলে Non-Lexical Lifetimes (NLL) — compiler পরের use-পর্যন্তই reference live রাখে, scope-এর শেষ পর্যন্ত না।

Borrowing error frustrating মনে হলেও, মনে রেখো — Rust compiler potential bug-কে compile time-এ ধরিয়ে দিচ্ছে, যাতে runtime-এ debug করতে না হয়।

Dangling reference

C-তে dangling pointer easily বানানো যায় — memory free হয়ে গেছে কিন্তু তার pointer-টা রয়ে গেছে। Rust-এ এটা compile-time-এ blocked।

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}
compile errortext
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from

Error-এ lifetime-এর কথা — সেটা পরের পাঠে আসছে। মূল কথা: s function-এর ভিতরে তৈরি, function শেষ হলেই drop। সেই drop-হওয়া s-এর reference return করলে — invalid pointer। Rust এটা হতে দেয় না।

সমাধান — reference না দিয়ে value-ই return:

fn main() {
    let string = no_dangle();
}

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

Ownership move হয়ে যায়, কিছুই drop হয় না, dangling-ও নেই।

Reference-এর rule (সংক্ষেপে)

  1. যেকোনো সময়ে — হয় একটা mutable reference, না হয় যেকোনো সংখ্যক immutable reference (একসাথে দু'টা mode না)।
  2. Reference সবসময় valid থাকতে হবে।

এই পাঠ থেকে যা শিখলে

  • &value দিয়ে immutable reference, &mut value দিয়ে mutable। Reference owner হয় না — borrowing।
  • একসাথে একটা mutable reference অথবা যেকোনো সংখ্যক immutable — data race prevention।
  • Reference-এর scope শেষ use পর্যন্ত (NLL) — কড়া না, compiler smart।
  • Dangling reference Rust-এ অসম্ভব — compile-time-এ block।