Skip to content

We try to learn the Rust programming language together in a simple beginner's way on the weekends.

Notifications You must be signed in to change notification settings

WEB3-PLUS/rust-start

Repository files navigation

1. ایجاد پروژه با Cargo

برای شروع یک پروژه جدید در Rust، می‌توانید از دستور زیر استفاده کنید:

cargo new projectName

این دستور یک دایرکتوری جدید به نام projectName ایجاد می‌کند و در آن یک پروژه جدید با ساختار اولیه و فایل‌های لازم (از جمله Cargo.toml و یک فایل main.rs) ایجاد می‌کند.

2. ساخت پروژه

برای ساخت پروژه، از دستور زیر استفاده می‌شود:

cargo build

این دستور کد شما را کامپایل می‌کند و فایل‌های اجرایی را در دایرکتوری target/debug ذخیره می‌کند. این دایرکتوری حاوی باینری‌هایی است که برای توسعه و تست استفاده می‌شوند.

3. اجرای پروژه

برای اجرای برنامه‌ی Rust، می‌توانید از یکی از دو روش زیر استفاده کنید:

  • اجرای مستقیم باینری:

    ./target/debug/projectName

    این روش به شما امکان می‌دهد تا باینری ساخته شده را به صورت مستقیم اجرا کنید.

  • استفاده از Cargo:

    cargo run

    این دستور به طور خودکار کد شما را کامپایل کرده و سپس آن را اجرا می‌کند. این روش برای توسعه‌دهندگان بسیار راحت‌تر است، زیرا نیازی به یادآوری مسیر باینری نیست.

4. بررسی کد بدون تولید باینری

اگر می‌خواهید کد خود را بررسی کنید و مطمئن شوید که بدون خطا کامپایل می‌شود، می‌توانید از دستور زیر استفاده کنید:

cargo check

این دستور کد شما را سریعاً بررسی می‌کند، اما باینری تولید نمی‌کند. این ویژگی برای شناسایی سریع خطاها و مشکلات در کد بسیار مفید است.

5. ساخت پروژه برای انتشار

زمانی که پروژه شما آماده‌ی انتشار است و می‌خواهید آن را با بهینه‌سازی‌های لازم کامپایل کنید، می‌توانید از دستور زیر استفاده کنید:

cargo build --release

این دستور کد شما را با بهینه‌سازی‌های خاصی کامپایل می‌کند و باینری تولید شده را در دایرکتوری target/release ذخیره می‌کند. باینری‌های تولید شده در این دایرکتوری معمولاً سریع‌تر و بهینه‌تر هستند و برای استفاده در محیط‌های تولید توصیه می‌شوند.

6. بنچمارک‌گیری

اگر قصد دارید زمان اجرای کد خود را بنچمارک کنید، مهم است که از باینری موجود در target/release استفاده کنید. این باینری بهینه‌سازی شده است و نتایج دقیق‌تری از عملکرد کد شما ارائه می‌دهد.


کد کامل بازی حدس عدد

use rand::Rng; // برای تولید اعداد تصادفی
use std::cmp::Ordering; // برای مقایسه مقادیر
use std::io; // برای ورودی و خروجی

fn main() {
    println!("guess the number:");

    // تولید یک عدد تصادفی بین 1 تا 100
    let secret_number = rand::thread_rng().gen_range(1..=100);

    // چاپ عدد مخفی (برای تست)
    println!("the secret number is: {secret_number}");

    loop {
        println!("please input your guess");

        let mut guess = String::new(); // ایجاد یک رشته خالی برای ورودی کاربر
        io::stdin()
            .read_line(&mut guess) // خواندن ورودی کاربر
            .expect("failed to read line"); // مدیریت خطا

        println!("you guessed: {guess}"); // چاپ حدس کاربر

        // تبدیل ورودی کاربر به عدد
        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num, // اگر تبدیل موفق بود، عدد را ذخیره کن
            Err(_) => continue, // اگر خطا بود، حلقه را ادامه بده
        };

        // مقایسه حدس کاربر با عدد مخفی
        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"), // اگر حدس کمتر بود
            Ordering::Greater => println!("Too big!"), // اگر حدس بیشتر بود
            Ordering::Equal => {
                println!("You win!"); // اگر حدس برابر بود
                return; // پایان بازی
            }
        }
    }
}

1. وارد کردن کتابخانه‌ها

use rand::Rng; // برای تولید اعداد تصادفی
use std::cmp::Ordering; // برای مقایسه مقادیر
use std::io; // برای ورودی و خروجی
  • use rand::Rng;: این خط ماژول Rng از کتابخانه rand را وارد می‌کند که برای تولید اعداد تصادفی استفاده می‌شود. این ماژول توابعی را برای تولید اعداد تصادفی فراهم می‌کند.
  • use std::cmp::Ordering;: ماژول Ordering شامل سه حالت Less, Greater, و Equal است که برای مقایسه مقادیر استفاده می‌شود. این مقادیر به ما اجازه می‌دهند تا بفهمیم یک عدد کمتر، بیشتر یا برابر با عدد دیگر است.
  • use std::io;: ماژول io برای کار با ورودی و خروجی در Rust استفاده می‌شود. ما از آن برای خواندن ورودی کاربر استفاده خواهیم کرد.

2. تابع اصلی

fn main() {
    println!("guess the number:");
  • fn main(): این تابع نقطه شروع هر برنامه Rust است. هر برنامه Rust باید یک تابع main داشته باشد.
  • println!: این ماکرو برای چاپ متن به کنسول استفاده می‌شود. در اینجا، به کاربر اعلام می‌شود که باید عددی را حدس بزند.

3. تولید عدد تصادفی

let secret_number = rand::thread_rng().gen_range(1..=100);
  • rand::thread_rng(): این تابع یک ژنراتور تصادفی برای ترد فعلی ایجاد می‌کند. این ژنراتور برای تولید اعداد تصادفی استفاده می‌شود.
  • gen_range(1..=100): این تابع یک عدد تصادفی در بازه 1 تا 100 (شامل هر دو) تولید می‌کند. علامت ..= نشان‌دهنده این است که انتهای بازه نیز شامل می‌شود.

4. چاپ عدد مخفی (برای تست)

println!("the secret number is: {secret_number}");
  • این خط عدد مخفی را چاپ می‌کند. این کار برای تست مفید است، اما در یک بازی واقعی باید این خط را حذف کرد تا کاربر نتواند عدد مخفی را ببیند.

5. حلقه اصلی بازی

loop {
    println!("please input your guess");
  • loop { ... }: این یک حلقه بی‌نهایت است که به کاربر اجازه می‌دهد تا حدس‌های خود را وارد کند. حلقه تا زمانی که کاربر برنده نشود یا برنامه به صورت دستی متوقف نشود، ادامه خواهد داشت.

6. خواندن ورودی کاربر

let mut guess = String::new(); // ایجاد یک رشته خالی برای ورودی کاربر
io::stdin()
    .read_line(&mut guess) // خواندن ورودی کاربر
    .expect("failed to read line"); // مدیریت خطا
  • let mut guess = String::new();: یک رشته خالی به نام guess ایجاد می‌شود. mut به این معنی است که این متغیر قابل تغییر است.
  • io::stdin().read_line(&mut guess): این خط ورودی کاربر را از کنسول می‌خواند و آن را در متغیر guess ذخیره می‌کند. &mut guess به این معناست که ما به تابع اجازه می‌دهیم تا محتوای این متغیر را تغییر دهد.
  • .expect("failed to read line"): این متد در صورت بروز خطا، یک پیام خطا چاپ می‌کند و برنامه را متوقف می‌کند. این کار برای اطمینان از این است که ورودی به درستی خوانده شده است.

7. چاپ حدس کاربر

println!("you guessed: {guess}"); // چاپ حدس کاربر
  • این خط حدس کاربر را چاپ می‌کند. این کار برای تأیید به کاربر است که ورودی او به درستی خوانده شده است.

8. تبدیل ورودی به عدد

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num, // اگر تبدیل موفق بود، عدد را ذخیره کن
    Err(_) => continue, // اگر خطا بود، حلقه را ادامه بده
};
  • guess.trim(): این متد فضای خالی را از ابتدا و انتهای رشته حذف می‌کند. این کار برای جلوگیری از خطا در تبدیل رشته به عدد ضروری است.
  • .parse(): این متد سعی می‌کند رشته را به نوع عددی تبدیل کند. در اینجا ما از u32 (عدد صحیح غیر منفی) استفاده می‌کنیم.
  • match: این ساختار برای بررسی نتیجه‌ی parse() استفاده می‌شود:
    • Ok(num): اگر تبدیل موفق باشد، عدد در متغیر num ذخیره می‌شود و به متغیر guess نسبت داده می‌شود.
    • Err(_): اگر تبدیل با خطا مواجه شود (به عنوان مثال، اگر کاربر چیزی غیر از عدد وارد نکند)، حلقه دوباره شروع می‌شود و از کاربر خواسته می‌شود که دوباره حدس بزند.

9. مقایسه حدس با عدد مخفی

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"), // اگر حدس کمتر بود
    Ordering::Greater => println!("Too big!"), // اگر حدس بیشتر بود
    Ordering::Equal => {
        println!("You win!"); // اگر حدس برابر بود
        return; // پایان بازی
    }
}
  • guess.cmp(&secret_number): این متد مقایسه‌ای بین guess و secret_number انجام می‌دهد و نتیجه را به یکی از مقادیر Ordering (کمتر، بیشتر، برابر) برمی‌گرداند.
  • match: با توجه به نتیجه مقایسه، یکی از سه حالت زیر اجرا می‌شود:
    • Ordering::Less: اگر حدس کاربر کمتر از عدد مخفی باشد، پیام "Too small!" چاپ می‌شود.
    • Ordering::Greater: اگر حدس کاربر بیشتر از عدد مخفی باشد، پیام "Too big!" چاپ می‌شود.
    • Ordering::Equal: اگر حدس برابر با عدد مخفی باشد، پیام "You win!" چاپ می‌شود و با استفاده از return از تابع main خارج می‌شود، که به معنای پایان بازی است.

10. پایان حلقه و برنامه

  • اگر کاربر حدس صحیح را بزند، برنامه خاتمه می‌یابد. در غیر این صورت، حلقه ادامه می‌یابد و از کاربر خواسته می‌شود که دوباره حدس بزند.

نکات اضافی

  • مدیریت خطا: در این برنامه، از expect برای مدیریت خطاها استفاده شده است. این روش ساده است، اما در برنامه‌های بزرگ‌تر، ممکن است بخواهید از مدیریت خطای پیچیده‌تری استفاده کنید تا به کاربر اطلاعات بیشتری بدهید.
  • توسعه و بهبود: این بازی می‌تواند با افزودن ویژگی‌های جدید مانند شمارش تعداد حدس‌ها، ارائه گزینه‌های دوباره بازی، یا ذخیره‌سازی رکوردها بهبود یابد.
  • استفاده از ماژول‌ها: اگر برنامه بزرگ‌تر شود، می‌توانید از ماژول‌ها برای سازماندهی کد خود استفاده کنید. Rust به شما اجازه می‌دهد تا کد خود را به ماژول‌های مختلف تقسیم کنید که به مدیریت بهتر پروژه کمک می‌کند.

variable and constant in rust

1. متغیرها و تغییرپذیری

let mut x: i32 = 5; // متغیر قابل تغییر
println!("x value is : {x}");
x = 6;
println!("x value is : {x}");
  • متغیرها: در Rust، متغیرها به طور پیش‌فرض غیرقابل تغییر (immutable) هستند. این به این معنی است که پس از تعریف، نمی‌توانید مقدار آن را تغییر دهید. برای تعریف یک متغیر قابل تغییر، از کلمه کلیدی mut استفاده می‌کنیم.

  • تعریف نوع: در اینجا، x به عنوان یک عدد صحیح 32 بیتی (i32) تعریف شده است. Rust از نوع داده‌های استاتیک استفاده می‌کند، به این معنی که نوع هر متغیر در زمان کامپایل مشخص می‌شود. این ویژگی به بهبود کارایی و ایمنی برنامه کمک می‌کند.

  • چاپ مقادیر: از ماکرو println! برای چاپ مقادیر استفاده می‌شود. با استفاده از {x}، مقدار متغیر x در خروجی چاپ می‌شود.

  • تغییر مقدار: مقدار x از 5 به 6 تغییر می‌کند و این تغییر در خروجی نشان داده می‌شود. این قابلیت تغییر مقدار متغیرهای قابل تغییر، یکی از ویژگی‌های مهم Rust است.

2. ثابت‌ها

const ONE_DAY_IN_SEC: u64 = 24 * 60 * 60;
println!("one day in second: {ONE_DAY_IN_SEC}");
  • تعریف ثابت: با استفاده از کلمه کلیدی const، می‌توان یک مقدار ثابت تعریف کرد که نمی‌تواند تغییر کند. ثابت‌ها باید در زمان کامپایل مشخص شوند و در کل برنامه قابل دسترسی هستند.

  • نوع داده: در اینجا، ONE_DAY_IN_SEC به عنوان یک عدد صحیح بدون علامت 64 بیتی (u64) تعریف شده است. این ثابت تعداد ثانیه‌های یک روز را محاسبه می‌کند (24 ساعت × 60 دقیقه × 60 ثانیه).

  • چاپ مقدار ثابت: مقدار ثابت با استفاده از println! چاپ می‌شود. این به شما امکان می‌دهد تا مقادیر ثابت را به راحتی در برنامه‌های خود استفاده کنید.

3. سایه‌زنی (Shadowing)

let y = 5;
let y = y + 1;
{
    let y = y * 2;
    println!("y value is: {y}");
}
println!("y value is : {y}");
  • سایه‌زنی: در Rust، می‌توانید یک متغیر با همان نام دوباره تعریف کنید. این به شما اجازه می‌دهد تا در یک بلوک خاص، متغیر جدیدی با نام مشابه تعریف کنید که بر روی متغیر قبلی سایه می‌زند.

  • مقداردهی اولیه: y ابتدا به 5 مقداردهی می‌شود. سپس، در خط بعدی، y به 6 تغییر می‌کند.

  • بلوک داخلی: در این بلوک، یک y جدید تعریف می‌شود که بر روی y قبلی سایه می‌زند. این y جدید مقدار 12 را دارد (6 × 2). فقط در این بلوک، این y معتبر است و در خط بعدی، y اصلی (6) چاپ می‌شود. این ویژگی به شما این امکان را می‌دهد که متغیرها را در محدوده‌های مختلف مدیریت کنید.

4. نوع داده‌ها

let age = "18";
let age: u8 = age.parse().expect("type is not casting to string");
println!("age is : {age}");
  • تعریف رشته: age به عنوان یک رشته (string) تعریف شده است که مقدار "18" را دارد. در Rust، رشته‌ها به صورت پیش‌فرض از نوع &str هستند که یک رشته قابل تغییر نیست.

  • تبدیل نوع: با استفاده از متد parse، رشته به نوع u8 (عدد صحیح 8 بیتی) تبدیل می‌شود. این متد به صورت عمومی برای تبدیل رشته‌ها به انواع مختلف داده‌ها استفاده می‌شود. اگر تبدیل موفقیت‌آمیز نباشد، از expect برای مدیریت خطا استفاده می‌شود. این به شما این امکان را می‌دهد که در صورت بروز خطا، پیام خطای مناسبی دریافت کنید.

  • چاپ مقدار: مقدار نهایی age که اکنون از نوع u8 است، چاپ می‌شود. این روش به شما کمک می‌کند تا ورودی‌های کاربر را به نوع مناسب تبدیل کنید.

5. انواع مرکب

let _tup: (&str, u8, bool) = (&"mohammadreza", 2, false);
let names = ["mohammadreza", "reza"];
let ages: [u8; 2] = [29, 10];
let role = ["test"; 10];
  • تاپل:

    • tup یک تاپل است که شامل سه نوع مختلف داده است: یک رشته (&str)، یک عدد صحیح 8 بیتی (u8)، و یک مقدار بولی (bool).
    • تاپل‌ها می‌توانند مقادیر مختلفی از انواع مختلف را در خود نگه دارند. تاپل‌ها به شما این امکان را می‌دهند که چندین مقدار را به عنوان یک واحد مدیریت کنید.
  • آرایه‌ها:

    • names یک آرایه از رشته‌ها است که شامل دو نام است. آرایه‌ها در Rust باید دارای اندازه ثابت باشند.
    • ages یک آرایه از نوع u8 است که شامل دو سن است.
    • role یک آرایه است که 10 بار مقدار "test" را تکرار می‌کند. این نوع تعریف آرایه به شما این امکان را می‌دهد که یک آرایه با اندازه ثابت و مقادیر تکراری ایجاد کنید.

6. ورودی از کاربر و دسترسی به آرایه

let a = [1, 2, 3, 4, 5];

println!("please enter array index : ");
let mut index = String::new();

io::stdin()
    .read_line(&mut index)
    .expect("failed to read line");
let index: usize = index.trim().parse().expect("index entered is not a number");

let element = a[index];

println!("the index value is {element}");
  • تعریف آرایه: a یک آرایه از اعداد صحیح است که شامل 5 عدد (1 تا 5) است. آرایه‌ها در Rust دارای اندازه ثابت هستند و می‌توانند مقادیر مشابه یا متفاوتی را نگه دارند.

  • ورودی از کاربر:

    • با استفاده از println! از کاربر خواسته می‌شود که ایندکس آرایه را وارد کند.
    • یک رشته خالی به نام index تعریف می‌شود و با استفاده از io::stdin().read_line() ورودی کاربر خوانده می‌شود. این ورودی به index اضافه می‌شود.
    • expect برای مدیریت خطا در صورت عدم موفقیت در خواندن ورودی استفاده می‌شود. این به شما کمک می‌کند تا در صورت بروز خطا، پیام خطای مناسبی دریافت کنید.
  • تبدیل ورودی: ورودی کاربر که به صورت رشته است، با استفاده از trim() و parse() به نوع usize تبدیل می‌شود. این نوع برای ایندکس‌ها مناسب است و به شما این امکان را می‌دهد که به عناصر آرایه دسترسی پیدا کنید.

  • دسترسی به عنصر آرایه: عنصر مربوط به ایندکس وارد شده از آرایه a خوانده می‌شود. این بخش از کد می‌تواند منجر به خطا شود اگر کاربر ایندکس نامعتبری وارد کند (مثلاً ایندکس‌های خارج از محدوده آرایه).

  • چاپ مقدار: مقدار عنصر آرایه با استفاده از ایندکس چاپ می‌شود. این به شما امکان می‌دهد تا ببینید که کدام عنصر از آرایه با ایندکس وارد شده مطابقت دارد.

نکات اضافی

  • مدیریت خطا: استفاده از expect به شما کمک می‌کند تا در صورت بروز خطا، پیام خطای مناسبی دریافت کنید. این روش به شما امکان می‌دهد تا در زمان توسعه و دیباگ کردن، مشکلات را سریع‌تر شناسایی کنید.

  • نوع داده‌های استاتیک: Rust به خاطر نوع داده‌های استاتیک و ایمنی حافظه مشهور است. این ویژگی‌ها باعث می‌شوند که بسیاری از خطاهای رایج در زمان کامپایل شناسایی شوند، که این امر به کاهش خطاها در زمان اجرا کمک می‌کند.

  • قابلیت‌های اضافی Rust: Rust دارای ویژگی‌های دیگری نیز هست که می‌تواند به شما کمک کند، مانند:

    • مدیریت حافظه بدون جمع‌آوری زباله (Garbage Collection): Rust از یک سیستم مالکیت (Ownership) استفاده می‌کند که به شما این امکان را می‌دهد که حافظه را به طور ایمن مدیریت کنید بدون اینکه نیاز به جمع‌آوری زباله داشته باشید.
    • مدیریت همزمانی: Rust به شما این امکان را می‌دهد که برنامه‌های همزمان را به راحتی بنویسید و از خطاهای رایج در برنامه‌های همزمان جلوگیری کنید.
    • ماکروها: Rust از ماکروها برای تولید کد به صورت خودکار استفاده می‌کند که می‌تواند به کاهش تکرار و ساده‌سازی کد کمک کند.

functions in rust

1. وارد کردن ماژول io

use std::io;
  • وارد کردن ماژول: این خط ماژول io را از کتابخانه استاندارد Rust وارد می‌کند. ماژول io شامل توابعی برای کار با ورودی و خروجی (I/O) است، مانند خواندن ورودی از کاربر.

2. تعریف تابع sum

fn sum(a: i32, b: i32) -> bool {
    let sum = a + b;
    println!("sum: {sum}");
    return true;
}
  • تعریف تابع: تابع sum با دو پارامتر a و b از نوع i32 (عدد صحیح 32 بیتی) تعریف شده است.

  • محاسبه مجموع: درون تابع، مقدار مجموع a و b محاسبه شده و در متغیر sum ذخیره می‌شود.

  • چاپ مجموع: با استفاده از ماکرو println!، مقدار مجموع چاپ می‌شود. {sum} به مقدار متغیر sum اشاره دارد.

  • نوع بازگشتی: تابع sum از نوع bool (بولی) است، اما در اینجا همیشه مقدار true را باز می‌گرداند. در واقع، در این کد، مقدار بازگشتی چندان کاربردی ندارد.

3. تابع main

fn main() {
    println!("Hello, world!");
    let mut a = String::new();
    let mut b = String::new();
    io::stdin().read_line(&mut a).expect("value is not number");
    io::stdin().read_line(&mut b).expect("value is not number");
    let a: i32 = a.trim().parse().expect("error");
    let b: i32 = b.trim().parse().expect("error");
    sum(a, b);
}
  • تعریف تابع main: این تابع نقطه شروع اجرای برنامه است.

  • چاپ پیام خوشامدگویی: با استفاده از println!، پیام "Hello, world!" چاپ می‌شود.

  • تعریف متغیرها: دو متغیر a و b به عنوان رشته‌های خالی (String::new()) تعریف شده‌اند. این متغیرها برای ذخیره ورودی کاربر استفاده می‌شوند.

  • خواندن ورودی از کاربر:

    • با استفاده از io::stdin().read_line(&mut a)، برنامه از کاربر می‌خواهد که یک خط ورودی وارد کند و آن را در متغیر a ذخیره می‌کند.
    • expect("value is not number") به شما این امکان را می‌دهد که در صورت بروز خطا در خواندن ورودی، یک پیام خطا چاپ کنید.
  • تبدیل ورودی به عدد صحیح:

    • ورودی کاربر که به صورت رشته است، با استفاده از trim() (برای حذف فضاهای اضافی) و parse() به نوع i32 تبدیل می‌شود.
    • اگر تبدیل موفقیت‌آمیز نباشد، expect("error") پیام خطا چاپ می‌کند.
  • فراخوانی تابع sum: در نهایت، تابع sum با مقادیر a و b به عنوان آرگومان فراخوانی می‌شود.

نکات اضافی

  1. مدیریت خطا: استفاده از expect در این کد به شما کمک می‌کند تا در صورت بروز خطا، پیام خطای مناسبی دریافت کنید. این روش به شما امکان می‌دهد تا در زمان توسعه و دیباگ کردن، مشکلات را سریع‌تر شناسایی کنید.

  2. نوع بازگشتی تابع: در این کد، تابع sum از نوع bool است، اما هیچ دلیلی برای این نوع بازگشتی وجود ندارد، زیرا مقدار بازگشتی هیچ کاربردی ندارد. می‌توانید نوع بازگشتی را به () (واحد) تغییر دهید که نشان‌دهنده عدم وجود مقدار بازگشتی است.

  3. بهبود کد: اگر فقط می‌خواهید مجموع دو عدد را محاسبه کنید، می‌توانید از نوع بازگشتی () استفاده کنید و مقدار true را از تابع حذف کنید. این کار باعث ساده‌تر شدن کد می‌شود.


loops and conditions in rust

1. بررسی نمره (Grade)

fn main() {
    let grade = 1;
    if grade >= 12 && grade <= 20 {
        println!("pass")
    } else if grade >= 7 {
        println!("failed")
    } else {
        println!("ridi");
    }
  • تعریف متغیر:

    • let grade = 1; یک متغیر به نام grade تعریف می‌کند و به آن مقدار 1 اختصاص می‌دهد. این متغیر نشان‌دهنده نمره یک دانش‌آموز است.
  • شرط‌ها:

    • شرط اول: if grade >= 12 && grade <= 20:

      • این شرط بررسی می‌کند که آیا نمره در بازه 12 تا 20 (شامل) قرار دارد. اگر این شرط برقرار باشد، پیام "pass" چاپ می‌شود.
      • عملگر && به معنای "و" است و به این معنی است که هر دو شرط باید درست باشند. در اینجا، نمره باید هم بزرگتر یا مساوی 12 و هم کمتر یا مساوی 20 باشد.
    • شرط دوم: else if grade >= 7:

      • اگر نمره در بازه 7 تا 11 باشد (و نه در بازه قبلی)، پیام "failed" چاپ می‌شود. این به این معناست که نمره در حد قبولی نیست اما به اندازه‌ای خوب است که به طور کامل مردود نشود.
    • شرط سوم: else:

      • اگر هیچ‌یک از شرایط قبلی برقرار نباشد، یعنی نمره کمتر از 7 باشد، پیام "ridi" چاپ می‌شود. این پیام به نظر می‌رسد یک اصطلاح غیررسمی یا طنزآمیز باشد و ممکن است به معنای "خنده‌دار" یا "مسخره" باشد.

2. حلقه loop

    //loop
    let mut count = 0;
    'counter: loop {
        if count == 100 {
            println!("number: {count}");
            break 'counter;
        }
        if count % 2 == 0 {
            println!("number: {count}")
        }
        count += 1;
    }
  • تعریف متغیر شمارنده:

    • let mut count = 0; یک متغیر به نام count تعریف می‌کند و آن را به 0 مقداردهی می‌کند. این متغیر به عنوان شمارنده در حلقه استفاده می‌شود.
  • حلقه بی‌پایان:

    • 'counter: loop { ... } یک حلقه بی‌پایان ایجاد می‌کند. نام 'counter به این حلقه اختصاص داده شده است تا بتوانیم از آن در دستور break استفاده کنیم. این نام‌گذاری به ما این امکان را می‌دهد که از چندین حلقه در یک بلوک کد استفاده کنیم و به راحتی از یک حلقه خاص خارج شویم.
  • شرط خروج از حلقه:

    • if count == 100:
      • اگر مقدار count برابر با 100 شود، پیام "number: {count}" چاپ می‌شود و با استفاده از break 'counter از حلقه خارج می‌شویم. در اینجا، {count} مقدار شمارنده را در خروجی نمایش می‌دهد.
  • چاپ اعداد زوج:

    • if count % 2 == 0:
      • اگر count عددی زوج باشد (یعنی باقی‌مانده تقسیم بر 2 برابر با 0 باشد)، مقدار آن چاپ می‌شود. این به ما امکان می‌دهد تا تمام اعداد زوج بین 0 و 100 را مشاهده کنیم.
  • افزایش شمارنده:

    • count += 1;:
      • در نهایت، مقدار count به اندازه 1 افزایش می‌یابد. این خط اطمینان می‌دهد که حلقه به سمت پایان پیش می‌رود.

3. حلقه while

در کد شما به حلقه while اشاره شده، اما در واقع کدی برای while وجود ندارد. اما می‌توانیم به صورت کلی بگوییم که در Rust می‌توانیم از حلقه while به شکل زیر استفاده کنیم:

let mut count = 0;
while count < 100 {
    // انجام کارها
    count += 1;
}

حلقه while به شما این امکان را می‌دهد که تا زمانی که یک شرط خاص برقرار باشد، کد را تکرار کنید. در اینجا، حلقه تا زمانی که count کمتر از 100 باشد، ادامه می‌یابد.

4. حلقه for

    for element in (1..4).rev() {
        println!("element: {element}");
    }
  • حلقه for:

    • این حلقه بر روی یک رنج (range) از اعداد تکرار می‌کند. (1..4) یک رنج از 1 تا 3 (شامل 1 و غیرشامل 4) را تولید می‌کند. توجه داشته باشید که در Rust، رنج‌ها شامل عدد ابتدایی و غیرشامل عدد انتهایی هستند.
  • معکوس کردن رنج:

    • rev() متدی است که رنج را معکوس می‌کند، بنابراین حلقه از 3 به 1 تکرار می‌شود. در این حالت، element به ترتیب 3، 2 و 1 خواهد بود.
  • چاپ مقادیر:

    • در هر تکرار، مقدار element چاپ می‌شود. این به شما امکان می‌دهد تا ببینید که چه مقادیری در هر تکرار از حلقه تولید می‌شوند.

5. جمع‌بندی

کد شما شامل چهار بخش اصلی است:

  1. بررسی نمره: با استفاده از شرط‌ها، نمره یک دانش‌آموز را بررسی می‌کند و پیام مناسب را چاپ می‌کند.
  2. حلقه loop: یک حلقه بی‌پایان که شمارنده را از 0 تا 100 افزایش می‌دهد و اعداد زوج را چاپ می‌کند.
  3. حلقه while: اگرچه در این کد وجود ندارد، اما می‌توان از آن برای تکرار بر اساس یک شرط خاص استفاده کرد.
  4. حلقه for: یک حلقه که بر روی یک رنج معکوس از 3 تا 1 تکرار می‌کند و مقادیر را چاپ می‌کند.

6. نکات اضافی

  • مدیریت خطا: در این کد، هیچ مدیریت خطایی وجود ندارد. در شرایط واقعی، بهتر است برای ورودی‌ها و تبدیل‌ها از مدیریت خطا استفاده کنید تا از بروز مشکلات جلوگیری شود.

  • عملگرهای منطقی: استفاده از عملگر && برای ترکیب شرایط در شرط‌ها به شما این امکان را می‌دهد که چندین شرط را به طور همزمان بررسی کنید.

  • حلقه‌های تودرتو: اگر بخواهید، می‌توانید از حلقه‌های تودرتو (nested loops) نیز استفاده کنید. این کار به شما این امکان را می‌دهد که ساختارهای پیچیده‌تری از تکرار را پیاده‌سازی کنید.

  • بهینه‌سازی کد: می‌توانید برخی از بخش‌های کد را بهینه کنید، مانند استفاده از توابع برای جدا کردن منطق و بهبود خوانایی کد.


Ownership in rust

Rust به طور خاص به مدیریت ارجحیت بین استفاده از استک و هپ توجه دارد و این کار را از طریق سیستم مالکیت (ownership) و اعتبارسنجی زمان کامپایل (compile-time checks) انجام می‌دهد. در ادامه، به چگونگی مدیریت این ارجحیت‌ها در Rust می‌پردازیم:
  1. سیستم مالکیت (Ownership) Rust دارای یک سیستم مالکیت قوی است که به شما کمک می‌کند تا به طور مؤثری از حافظه استفاده کنید. این سیستم شامل سه اصل اصلی است:

هر متغیر دارای یک مالک است: هر داده در Rust یک مالک (owner) دارد که مسئول مدیریت آن داده است. وقتی مالک داده از بین می‌رود (مثلاً وقتی از محدوده‌ای خارج می‌شود)، داده به طور خودکار آزاد می‌شود.

تنها یک مالک می‌تواند داده را در یک زمان داشته باشد: این اصل به جلوگیری از شرایط رقابتی (race conditions) و مشکلات مربوط به دسترسی همزمان به داده‌ها کمک می‌کند.

انتقال مالکیت (Ownership Transfer): شما می‌توانید مالکیت داده‌ها را به متغیرهای دیگر منتقل کنید. این کار به شما اجازه می‌دهد تا از هپ به طور مؤثر استفاده کنید بدون اینکه نیاز به مدیریت دستی حافظه داشته باشید.

  1. ارجحیت در استفاده از استک و هپ Rust به شما این امکان را می‌دهد که به طور خودکار تصمیم بگیرید که داده‌ها باید در کجا ذخیره شوند:

استفاده از استک: اگر داده‌ها اندازه ثابتی دارند و نیازی به زندگی طولانی‌مدت ندارند، Rust به طور خودکار آن‌ها را در استک قرار می‌دهد. این کار به دلیل سرعت و کارایی بالای استک انجام می‌شود.

استفاده از هپ: اگر داده‌ها اندازه نامشخصی دارند یا نیاز به زندگی طولانی‌مدت دارند (مثلاً آرایه‌های دینامیک، ساختارهای پیچیده)، Rust به شما این امکان را می‌دهد که آن‌ها را در هپ قرار دهید. برای این کار، معمولاً از نوع‌های اشاره‌گر مانند Box, Rc, یا Arc استفاده می‌شود که به شما اجازه می‌دهند تا به داده‌های موجود در هپ دسترسی پیدا کنید و مالکیت را مدیریت کنید.

  1. اعتبارسنجی زمان کامپایل Rust با استفاده از اعتبارسنجی زمان کامپایل، از بروز مشکلات مربوط به مدیریت حافظه جلوگیری می‌کند. این اعتبارسنجی به شما این امکان را می‌دهد که قبل از اجرای برنامه، خطاهای مربوط به مالکیت و دسترسی به حافظه را شناسایی کنید. این کار باعث می‌شود که برنامه‌های Rust معمولاً ایمن‌تر و پایدارتر از برنامه‌های نوشته شده در زبان‌های دیگر باشند که مدیریت حافظه را به عهده برنامه‌نویس می‌گذارند.

stack and heap

حافظه استک (Stack)

  1. ساختار و مدیریت:

    • استک به صورت یک ساختار داده‌ای LIFO (آخرین وارد، اولین خارج) عمل می‌کند. این به این معناست که آخرین مقداری که به استک اضافه می‌شود، اولین مقداری است که حذف می‌شود.
    • مدیریت حافظه در استک به صورت خودکار انجام می‌شود. وقتی یک تابع فراخوانی می‌شود، فضای لازم برای متغیرهای محلی و پارامترها به طور خودکار در بالای استک قرار می‌گیرد و وقتی تابع به پایان می‌رسد، این فضا به طور خودکار آزاد می‌شود.
  2. سرعت:

    • افزودن و حذف داده‌ها از استک بسیار سریع است، زیرا مکان داده‌ها همیشه در بالای استک قرار دارد و نیازی به جستجو ندارد.
    • به همین دلیل، استفاده از استک برای داده‌هایی که اندازه آن‌ها در زمان کامپایل مشخص است (مانند اعداد صحیح، آرایه‌های با اندازه ثابت و ...) بسیار مناسب است.
  3. محدودیت‌ها:

    • تمامی داده‌هایی که در استک قرار می‌گیرند باید اندازه ثابتی داشته باشند. این بدان معناست که نمی‌توانید داده‌هایی با اندازه نامشخص یا متغیر را در استک ذخیره کنید.
    • استک دارای محدودیت اندازه است و اگر بیش از حد از آن استفاده کنید، ممکن است با خطای "Stack Overflow" مواجه شوید.

حافظه هپ (Heap)

  1. ساختار و مدیریت:

    • هپ به صورت یک فضای بزرگ از حافظه عمل می‌کند که می‌توانید با درخواست‌های خاص، فضاهای مورد نیاز را از آن بگیرید.
    • مدیریت حافظه در هپ نیاز به تخصیص و آزادسازی دستی دارد. شما باید خودتان مشخص کنید که چه زمانی فضا را اختصاص داده و چه زمانی آن را آزاد کنید. این کار معمولاً با استفاده از اشاره‌گرها انجام می‌شود.
  2. سرعت:

    • تخصیص فضا در هپ معمولاً کندتر از استک است، زیرا سیستم باید یک مکان خالی مناسب پیدا کند و مدیریت کند.
    • دسترسی به داده‌ها در هپ نیز معمولاً کندتر است، زیرا باید به اشاره‌گر مراجعه کنید.
  3. مزایا:

    • هپ به شما این امکان را می‌دهد که داده‌هایی با اندازه نامشخص یا متغیر را ذخیره کنید، مانند آرایه‌های دینامیک یا ساختارهای پیچیده.
    • همچنین می‌توانید از هپ برای ذخیره‌سازی داده‌هایی که باید در طول عمر بیشتری از برنامه باقی بمانند استفاده کنید.

ارجحیت بین استک و هپ

  • استفاده از استک: به طور کلی، اگر داده‌ای دارید که اندازه آن در زمان کامپایل مشخص است و نیازی به زندگی طولانی‌مدت ندارد، بهتر است از استک استفاده کنید. این کار به دلیل سرعت و مدیریت خودکار حافظه، بهینه‌تر است.
  • استفاده از هپ: اگر داده‌ای دارید که اندازه آن نامشخص است یا نیاز به زندگی طولانی‌مدت دارد (مانند داده‌هایی که باید پس از خروج از یک تابع همچنان در دسترس باشند)، باید از هپ استفاده کنید.

مفهوم مالکیت (Ownership)

مالکیت یکی از اصول کلیدی زبان Rust است که به مدیریت حافظه کمک می‌کند. این مفهوم به شما اجازه می‌دهد تا بدون نیاز به جمع‌آوری زباله (Garbage Collection) یا مدیریت دستی حافظه، به طور ایمن و کارآمد از حافظه استفاده کنید. در ادامه، به تفصیل به قوانین مالکیت، محدوده متغیرها، نوع داده‌ها، و تعاملات آن‌ها می‌پردازیم.

1. قوانین مالکیت

مالکیت در Rust شامل سه قانون اصلی است:

  • هر مقدار در Rust یک مالک دارد: هر داده‌ای که در Rust ایجاد می‌شود، یک مالک (owner) دارد که مسئول مدیریت آن داده است. این مالک معمولاً یک متغیر است.

  • در هر زمان فقط یک مالک وجود دارد: این بدان معناست که نمی‌توانید دو متغیر به طور همزمان مالک یک داده باشند. این ویژگی به جلوگیری از شرایط رقابتی (race conditions) و مشکلات مربوط به دسترسی همزمان به داده‌ها کمک می‌کند.

  • وقتی مالک از محدوده خارج می‌شود، مقدار رها می‌شود: هنگامی که متغیر مالک از محدوده (scope) خارج می‌شود، حافظه مربوط به آن به طور خودکار آزاد می‌شود. این کار توسط تابع خاصی به نام drop انجام می‌شود.

2. محدوده متغیرها (Variable Scope)

محدوده به محدوده‌ای در برنامه اشاره دارد که یک متغیر معتبر است. متغیرها از زمانی که تعریف می‌شوند تا زمانی که از محدوده خارج شوند، معتبر هستند. به عنوان مثال:

{
    let s = "hello"; // s از این نقطه معتبر است
    // do something with s
} // s دیگر معتبر نیست

3. نوع داده String

برای درک بهتر قوانین مالکیت، نوع داده‌ای به نام String معرفی می‌شود. این نوع داده به شما این امکان را می‌دهد که متونی با اندازه متغیر را مدیریت کنید، زیرا داده‌ها در هپ ذخیره می‌شوند.

ایجاد یک String

شما می‌توانید یک String از یک رشته ثابت (string literal) با استفاده از تابع String::from ایجاد کنید:

let s = String::from("hello");

تغییر یک String

String به شما این امکان را می‌دهد که محتوای آن را تغییر دهید:

let mut s = String::from("hello");
s.push_str(", world!"); // محتوای s به "hello, world!" تغییر می‌کند
println!("{s}"); // خروجی: hello, world!

4. حافظه و تخصیص

  • رشته‌های ثابت (string literals): مقادیر آن‌ها در زمان کامپایل مشخص است و به طور مستقیم در باینری نهایی قرار می‌گیرد. این رشته‌ها غیرقابل تغییر (immutable) هستند و در استک ذخیره می‌شوند.

  • نوع داده String: برای ذخیره‌سازی متون متغیر، حافظه‌ای در هپ تخصیص داده می‌شود. این حافظه در زمان اجرا درخواست می‌شود و وقتی متغیر مالک از محدوده خارج می‌شود، حافظه به طور خودکار آزاد می‌شود.

5. تعامل متغیرها و داده‌ها با مفهوم Move

در Rust، وقتی یک متغیر به متغیر دیگری اختصاص داده می‌شود، مالکیت داده منتقل می‌شود. به عنوان مثال:

let s1 = String::from("hello");
let s2 = s1; // مالکیت s1 به s2 منتقل می‌شود

در اینجا، s1 دیگر معتبر نیست و اگر بخواهید از آن استفاده کنید، خطای کامپایل دریافت خواهید کرد. این به دلیل جلوگیری از مشکلات مربوط به آزادسازی دو بار حافظه (double free) است.

6. استفاده از Clone

اگر بخواهید یک کپی عمیق (deep copy) از داده‌ها داشته باشید، می‌توانید از متد clone استفاده کنید:

let s1 = String::from("hello");
let s2 = s1.clone(); // یک کپی عمیق از s1 ایجاد می‌کند

7. داده‌های فقط استک و Copy Trait

برخی از نوع‌ها، مانند اعداد صحیح، به دلیل اینکه اندازه آن‌ها در زمان کامپایل مشخص است، به طور کامل در استک ذخیره می‌شوند و می‌توانند به راحتی کپی شوند. این نوع‌ها Copy trait را پیاده‌سازی می‌کنند و بنابراین می‌توانند بدون انتقال مالکیت کپی شوند.

مثال از Copy Trait

let x = 5;
let y = x; // x به y کپی می‌شود و x همچنان معتبر است
println!("x = {x}, y = {y}"); // خروجی: x = 5, y = 5

8. مالکیت و توابع

وقتی یک متغیر به یک تابع منتقل می‌شود، مالکیت آن متغیر منتقل می‌شود. این به این معناست که اگر بخواهید از متغیری که به تابع منتقل شده استفاده کنید، دیگر نمی‌توانید.

مثال از توابع

fn main() {
    let s = String::from("hello");  // s به محدوده می‌آید
    takes_ownership(s);              // s به تابع منتقل می‌شود و دیگر معتبر نیست
    // println!("{s}"); // این خط خطا می‌دهد

    let x = 5;                       // x به محدوده می‌آید
    makes_copy(x);                   // x به تابع منتقل می‌شود، اما چون i32 نوعی Copy است، x هنوز معتبر است
    println!("{x}");                 // خروجی: 5
}

fn takes_ownership(some_string: String) {
    println!("{some_string}");
} // some_string از محدوده خارج می‌شود و حافظه آزاد می‌شود

fn makes_copy(some_integer: i32) {
    println!("{some_integer}");
} // some_integer از محدوده خارج می‌شود، اما هیچ کاری انجام نمی‌دهد

9. بازگشت مقادیر و مالکیت

وقتی یک تابع مقداری را برمی‌گرداند، مالکیت آن مقدار نیز منتقل می‌شود. این روند ممکن است کمی خسته‌کننده باشد، زیرا باید اطمینان حاصل کنید که داده‌ها به درستی مدیریت می‌شوند.

مثال از بازگشت مقادیر

fn main() {
    let s1 = gives_ownership(); // gives_ownership مقدار خود را به s1 منتقل می‌کند
    let s2 = String::from("hello"); // s2 به محدوده می‌آید
    let s3 = takes_and_gives_back(s2); // s2 به تابع منتقل می‌شود و مقدار بازگشتی به s3 منتقل می‌شود
}

fn gives_ownership() -> String {
    let some_string = String::from("yours");
    some_string // some_string به تابع فراخوانی منتقل می‌شود
}

fn takes_and_gives_back(a_string: String) -> String {
    a_string // a_string به تابع فراخوانی منتقل می‌شود
}

10. مراجع (References)

Rust به شما این امکان را می‌دهد که بدون انتقال مالکیت از یک مقدار استفاده کنید. این کار با استفاده از مراجع (references) انجام می‌شود. مراجع به شما این امکان را می‌دهند که به داده‌ها دسترسی پیدا کنید بدون اینکه مالکیت آن‌ها را تغییر دهید.

مثال از مراجع

fn main() {
    let s1 = String::from("hello");
    let len = calculate_length(&s1); // با استفاده از مرجع به s1 دسترسی پیدا می‌کنیم
    println!("The length of '{}' is {}.", s1, len); // s1 هنوز معتبر است
}

fn calculate_length(s: &String) -> usize {
    s.len() // طول رشته را برمی‌گرداند
}

11. مراجع قابل تغییر (Mutable References)

اگر بخواهید به داده‌ای که در یک مرجع ذخیره شده است، تغییراتی اعمال کنید، باید از مراجع قابل تغییر (mutable references) استفاده کنید. در Rust، تنها می‌توانید یک مرجع قابل تغییر در هر زمان داشته باشید تا از بروز شرایط رقابتی جلوگیری شود.

مثال از مراجع قابل تغییر

fn main() {
    let mut s = String::from("hello");
    change(&mut s); // مرجع قابل تغییر به تابع ارسال می‌شود
    println!("{}", s); // خروجی: hi
}

fn change(s: &mut String) {
    s.push_str(", world!"); // محتوای s تغییر می‌کند
}

12. قوانین مراجع

Rust برای مراجع قوانین خاصی دارد:

  1. هر مرجع باید به یک مقدار معتبر اشاره کند: اگر مرجع به مقداری اشاره کند که دیگر معتبر نیست، خطای کامپایل ایجاد می‌شود.
  2. تنها می‌توانید یک مرجع قابل تغییر در هر زمان داشته باشید: این کار به جلوگیری از شرایط رقابتی کمک می‌کند.
  3. می‌توانید چندین مرجع غیرقابل تغییر در یک زمان داشته باشید: این به شما این امکان را می‌دهد که به داده‌ها دسترسی داشته باشید بدون اینکه مالکیت آن‌ها را تغییر دهید.

14. مزایای مالکیت در Rust

  • ایمنی حافظه: با استفاده از مالکیت، Rust از بروز مشکلاتی مانند آزادسازی دو بار حافظه (double free) و استفاده از حافظه آزاد شده (dangling pointers) جلوگیری می‌کند.
  • کارایی: سیستم مالکیت به Rust این امکان را می‌دهد که بدون نیاز به جمع‌آوری زباله، حافظه را به طور مؤثری مدیریت کند. این باعث می‌شود که برنامه‌های Rust سریع‌تر و کارآمدتر باشند.
  • شفافیت: قوانین مالکیت و مراجع به وضوح مشخص می‌کنند که چه کسی مالک یک داده است و چه زمانی آن داده آزاد می‌شود، که به کد شما شفافیت بیشتری

کد

use std::string;

fn main() {
    //ownership in rust
    //variable scope
    let s = "hello";
    {
        println!("s: {s}");
        let s = "h";
        println!("s: {s}");
    } //drop new s => "h"
    println!("s: {s}");

    //muteable string
    let mut str = String::from("mohammadreza "); //idont know this
    str.push_str("hooshmand");
    println!("str name: {str}");

    //variables and data intracting with move
    let x = 5;
    let y = x; //copy x value to y
    let s1 = String::from("test"); //{ptr:HEAP,len:4Byte,capacity:4memory}
    let s2 = s1; //move s1 value to s2,1 is not shollow copy is move
                 // for copy value we can use s1.clone()
    take_ownership(s2);
    //we can use return for takes ownership
}

fn take_ownership(s2: String) -> String {
    return s2;
}

توضیحات

1. وارد کردن ماژول

use std::string;
  • این خط ماژول string را از کتابخانه استاندارد Rust وارد می‌کند. با این حال، در این کد خاص، نیازی به وارد کردن std::string نیست، زیرا از نوع String به طور مستقیم استفاده نمی‌شود.

2. تابع اصلی

fn main() {
  • این خط شروع تابع اصلی برنامه است. در Rust، اجرای برنامه از تابع main آغاز می‌شود.

3. مالکیت و محدوده متغیرها

let s = "hello";
{
    println!("s: {s}");
    let s = "h";
    println!("s: {s}");
} //drop new s => "h"
println!("s: {s}");
  • تعریف متغیر s: متغیر s به رشته ثابت "hello" اشاره می‌کند. این رشته در استک ذخیره می‌شود. در Rust، رشته‌های ثابت (string literals) به طور خودکار در حافظه‌ای که به استک تعلق دارد، ذخیره می‌شوند و به دلیل اینکه اندازه آن‌ها در زمان کامپایل مشخص است، سریع و کارآمد هستند.

  • محدوده داخلی: درون {} یک محدوده جدید تعریف شده است. این محدوده به Rust می‌گوید که متغیرها و داده‌های جدیدی که در اینجا تعریف می‌شوند، فقط در این بخش معتبر هستند.

    • println!("s: {s}");: این خط مقدار s را در این محدوده چاپ می‌کند، که "hello" است.

    • let s = "h";: در اینجا، یک متغیر جدید به نام s تعریف می‌شود که در این محدوده معتبر است و مقدار آن "h" است. این متغیر جدید، متغیر قبلی را در این محدوده پنهان می‌کند. به عبارت دیگر، این یک پنهان‌سازی (shadowing) است.

    • println!("s: {s}");: این خط مقدار جدید s را چاپ می‌کند، که "h" است.

  • خروج از محدوده: وقتی از محدوده خارج می‌شویم، متغیر جدید s رها می‌شود و متغیر قبلی ("hello") دوباره معتبر می‌شود. در اینجا، Rust به طور خودکار حافظه مربوط به متغیر جدید را آزاد می‌کند.

  • println!("s: {s}");: این خط مقدار اصلی s را چاپ می‌کند، که "hello" است.

4. رشته قابل تغییر

let mut str = String::from("mohammadreza "); //idont know this
str.push_str("hooshmand");
println!("str name: {str}");
  • تعریف رشته قابل تغییر: متغیر str به یک رشته قابل تغییر (String) با مقدار اولیه "mohammadreza " اختصاص داده می‌شود. از String::from برای ایجاد این نوع داده استفاده می‌شود. نوع String در هپ ذخیره می‌شود و به شما این امکان را می‌دهد که محتوای آن را تغییر دهید.
  • اضافه کردن به رشته: با استفاده از متد push_str، رشته "hooshmand" به انتهای str اضافه می‌شود. حالا مقدار str به "mohammadreza hooshmand" تغییر می‌کند. این کار به دلیل این است که نوع String می‌تواند مقادیر متغیر را مدیریت کند.
  • چاپ مقدار رشته: این خط مقدار جدید str را چاپ می‌کند. خروجی این بخش "str name: mohammadreza hooshmand" خواهد بود.

5. تعامل متغیرها و داده‌ها با مفهوم Move

let x = 5;
let y = x; //copy x value to y
let s1 = String::from("test"); //{ptr:HEAP,len:4Byte,capacity:4memory}
let s2 = s1; //move s1 value to s2,1 is not shollow copy is move
             // for copy value we can use s1.clone()
take_ownership(s2);
  • تعریف عدد صحیح: let x = 5; یک عدد صحیح به نام x تعریف می‌کند. از آنجا که i32 (نوع عدد صحیح) یک نوع Copy است، می‌توانیم مقدار آن را به متغیر دیگری بدون از بین رفتن اعتبار x کپی کنیم.
  • کپی عدد صحیح: let y = x; در اینجا، مقدار x به y کپی می‌شود. از آنجا که i32 یک نوع Copy است، x و y هر دو معتبر هستند و می‌توانند به طور مستقل استفاده شوند.
  • تعریف یک رشته: let s1 = String::from("test"); یک رشته قابل تغییر با مقدار "test" ایجاد می‌کند. این رشته در هپ ذخیره می‌شود و شامل یک اشاره‌گر به داده‌ها، طول و ظرفیت است. این نوع داده به شما این امکان را می‌دهد که محتوای آن را تغییر دهید و اندازه آن را در زمان اجرا تغییر دهید.
  • انتقال مالکیت: let s2 = s1; در اینجا، مالکیت s1 به s2 منتقل می‌شود. از آنجا که String یک نوع Copy نیست، s1 دیگر معتبر نیست و نمی‌توانید از آن استفاده کنید. این عمل به عنوان move شناخته می‌شود. به عبارت دیگر، s1 دیگر به داده‌های هپ اشاره نمی‌کند و s2 اکنون مالک آن داده‌ها است.
  • فراخوانی تابع: take_ownership(s2); تابع take_ownership با s2 به عنوان آرگومان فراخوانی می‌شود. در اینجا، s2 مالکیت داده را به تابع منتقل می‌کند.

6. تابع take_ownership

fn take_ownership(s2: String) -> String {
    return s2;
}
  • تعریف تابع: تابع take_ownership یک آرگومان از نوع String می‌گیرد و آن را برمی‌گرداند. این تابع در واقع مالکیت s2 را می‌گیرد و سپس آن را برمی‌گرداند.
  • بازگشت مقدار: مقدار s2 به تابعی که آن را فراخوانی کرده است، بازگردانده می‌شود. در این کد، این مقدار به هیچ متغیر دیگری اختصاص داده نمی‌شود و در واقع هیچ استفاده‌ای از آن نمی‌شود. با این حال، این نشان می‌دهد که چگونه می‌توان مالکیت را به تابعی دیگر منتقل کرد و سپس آن را برگرداند.

نکات کلیدی

  1. مالکیت و انتقال: در Rust، انتقال مالکیت یک مفهوم کلیدی است. وقتی یک متغیر به متغیر دیگری اختصاص داده می‌شود، مالکیت داده به متغیر جدید منتقل می‌شود. این به Rust کمک می‌کند تا از بروز مشکلاتی مانند آزادسازی دو بار حافظه جلوگیری کند.

  2. محدوده متغیرها: محدوده متغیرها در Rust بسیار مهم است. متغیرها فقط در محدوده‌ای که تعریف شده‌اند معتبر هستند و وقتی از آن محدوده خارج می‌شوند، حافظه مربوط به آن‌ها به طور خودکار آزاد می‌شود.

  3. رشته‌های ثابت و قابل تغییر: رشته‌های ثابت (string literals) در استک ذخیره می‌شوند و غیرقابل تغییر هستند، در حالی که رشته‌های قابل تغییر (String) در هپ ذخیره می‌شوند و می‌توانند تغییر کنند.

  4. کپی و Move: انواعی که اندازه آن‌ها در زمان کامپایل مشخص است (مانند اعداد صحیح) به طور خودکار کپی می‌شوند، در حالی که انواعی مانند String که اندازه آن‌ها در زمان کامپایل مشخص نیست، با انتقال مالکیت (move) مدیریت می‌شوند.

  5. استفاده از clone: اگر بخواهید یک کپی عمیق از داده‌ها داشته باشید، می‌توانید از متد clone استفاده کنید. این کار داده‌ها را در هپ کپی می‌کند و به شما این امکان را می‌دهد که از هر دو متغیر به طور مستقل استفاده کنید.