آموزش

چرا Rust به‌جای segfault، خطای کامپایل می‌دهد

در C یا ++C یک اشاره‌گر نامعتبر معمولاً ماه‌ها بعد، در پروداکشن، خودش را نشان می‌دهد. در Rust همان اشتباه، همان لحظه‌ی کامپایل متوقفت می‌کند.

تحریریه‌ی دینا کد
نویسنده
۵ دقیقه مطالعه۱ بازدید
اشتراک گذاری۱

روی یک برنامه‌ی C یا ++C، دسترسی به حافظه‌ای که دیگر معتبر نیست معمولاً چیزی است که در تست هم رد می‌شود و ماه‌ها بعد، در پروداکشن، به یک segfault یا رفتار نامشخص تبدیل می‌شود. در Rust همان اشتباه، همان لحظه که کامپایل می‌کنی، با یک پیام خطای مشخص متوقفت می‌کند — نه در ران‌تایم، نه در پروداکشن.

مثال اول: مقداری که «جابه‌جا» شده

rustfn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}, world!", s1);
}
error[E0382]: borrow of moved value: `s1`
 --> main.rs:4:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

s1 یک String است — داده‌ای که روی heap زندگی می‌کند. خط let s2 = s1؛ این مالکیت را به s2 منتقل می‌کند؛ s1 از آن لحظه دیگر معتبر نیست. در ++C همین کد کامپایل می‌شد و s1 یا داده‌ی خراب نشان می‌داد یا کرش می‌کرد — رفتاری که فقط با تست دقیق پیدا می‌شود، نه با خواندن کد. کامپایلر Rust همین را همان‌جا رد می‌کند.

مثال دوم: قرضی که هنوز باز است

rustfn main() {
    let mut v = vec![1, 2, 3];
    let first = &v[0];
    v.push(4);
    println!("first is: {}", first);
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> main.rs:4:5
  |
3 |     let first = &v[0];
  |                  - immutable borrow occurs here
4 |     v.push(4);
  |     ^^^^^^^^^ mutable borrow occurs here

این یکی ظریف‌تر است: v.push(4) می‌تواند بافر پشتِ vec را کامل جابه‌جا کند تا جا برای عضو جدید باز شود — و اگر این اتفاق بیفتد، first به آدرسی اشاره می‌کند که دیگر وجود ندارد. در بیشتر زبان‌ها این یک باگ نادر است که فقط گاهی، وقتی بافر واقعاً جابه‌جا شود، خودش را نشان می‌دهد. Rust صبر نمی‌کند ببیند جابه‌جایی واقعاً اتفاق می‌افتد یا نه؛ قانون ساده است: تا وقتی یک قرض غیرقابل‌تغییر (&v[0]) باز است، نمی‌شود از خودِ v به‌صورت قابل‌تغییر استفاده کرد.

راه‌حل، معمولاً یکی از این دو تاست

rustfn main() {
    let mut v = vec![1, 2, 3];
    let first = v[0];       // کپی مقدار، نه قرض گرفتن آدرس
    v.push(4);
    println!("first is: {}", first);
    println!("v is now: {:?}", v);
}
first is: 1
v is now: [1, 2, 3, 4]

چون i32 تریت Copy دارد، v[0] یک عدد کپی می‌کند نه یک قرض — تعارضی باقی نمی‌ماند. برای مثال اول هم دو راه هست: s1.clone() بزنی، یا از اول به‌جای انتقال مالکیت، رفرنس بدهی:

rustfn greet(name: &String) {
    println!("hello, {}!", name);
}
fn main() {
    let s1 = String::from("hello");
    greet(&s1);
    println!("still have s1: {}", s1);
}
hello, hello!
still have s1: hello

با &s1، تابع greet فقط قرض می‌گیرد، مالکیت را نمی‌برد — s1 بعد از فراخوانی هم در دسترس می‌ماند.

این‌ها را کامپایلر چک می‌کند، نه شما

نکته‌ی اصلی همین است: در C یا ++C، درست بودن مدیریت حافظه به انضباط برنامه‌نویس و کیفیت تست‌ها بستگی دارد. در Rust، این‌ها قوانین زبان‌اند و کامپایلر (borrow checker) قبل از این‌که کد اصلاً اجرا شود، رعایتشان را تضمین می‌کند. هزینه‌اش یک منحنی یادگیری واقعی در چند هفته‌ی اول است؛ نتیجه‌اش نبودِ یک دسته کامل از باگ‌های حافظه که در زبان‌های دیگر فقط در پروداکشن پیدا می‌شوند.

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

برچسب‌ها:#Rust#حافظه#مبتدی
تحریریه‌ی دینا کد
نویسنده

نظرها

اولین نفری باشید که نظر می‌دهد
برای ثبت نظر وارد شوید

سؤالتان را بپرسید یا تجربه‌تان را بنویسید — نویسنده‌ی مطلب پاسخ می‌دهد.

مطالب مرتبط

از همین دسته
چرا Rust به‌جای segfault خطای کامپایل می‌دهد — دینا کد