English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

معالجة الأخطاء في Rust

يتمتع Rust بنظام فريد للتعامل مع حالات الاستثناء، ولا يختلف عن ميكانيكية try في لغات البرمجة الأخرى.

أولاً، عادةً ما تظهر في البرنامج نوعين من الأخطاء: الأخطاء القابلة للإصلاح والأخطاء غير القابلة للإصلاح.

أحد الأمثلة الشائعة للأخطاء القابلة للإصلاح هو خطأ الوصول إلى ملف، إذا فشل الوصول إلى ملف، قد يكون السبب أنه يتم استخدامه حاليًا، وهو أمر طبيعي، يمكننا حل المشكلة عبر الانتظار.

لكن هناك أيضًا أخطاء تسببها أخطاء منطقية غير قابلة للحل في البرمجة، مثل الوصول إلى موقع خارج نطاق نهاية الحلقة.

معظم لغات البرمجة لا تفصل بين هذين النوعين من الأخطاء ويتم تمثيل الأخطاء عبر كلاس Exception (الاستثناءات). في Rust لا يوجد Exception.

للتعامل مع الأخطاء القابلة للإصلاح يتم استخدام كلاس Result<T, E>، وللتعامل مع الأخطاء غير القابلة للإصلاح يتم استخدام macro panic!.

الخطأ غير القابل للإصلاح

لم يتم تقديم تعريف دقيق للغة Rust macros في هذا الفصل، ولكن تم استخدام macro println!، لأن استخدام هذه الماكروات بسيط، لذا لا يتطلب ذلك فهمًا كاملاً، يمكننا تعلم كيفية استخدام macro panic! باستخدام نفس الطريقة.

fn main() {
    panic!("error occurred");
    println!("Hello, Rust");
}

نتيجة التنفيذ:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

من الواضح أن البرنامج لم يتمكن من تشغيل println!("Hello, Rust") كما هو متوقع، بل توقف التشغيل عند استدعاء macro panic!.

الخطأ غير القابل للإصلاح يؤدي دائمًا إلى إصابة البرنامج بضربة قاتلة ويؤدي إلى توقف تشغيله.

دعونا ننتبه إلى السطرين التاليين من مخرجات الخطأ:

  • السطر الأول قد قام بطباعة موقع مكالمة macro panic! ورسالة الخطأ التي تم إطلاقها.

  • السطر الثاني هو رسالة توجيه، وترجمتها إلى الصينية هي "تشغيل عبر متغير البيئة `RUST_BACKTRACE=1` لعرض التتبع". سنقوم بتقديم تتبع (backtrace) فيما بعد.

بعد المثال السابق، قم بإنشاء واجهة تشغيل جديدة في VSCode:

في واجهة التشغيل الجديدة، قم بتعيين متغيرات البيئة (تختلف طرق التشغيل في كل واجهة، هنا سيتم تقديم طريقتين رئيسيتين):

إذا كنت تستخدم إصدار Windows 7 أو أحدث، فإنه في العادة يستخدم Powershell كخط الأوامر، يرجى استخدام الأمر التالي:

$env:RUST_BACKTRACE=1 ; cargo run

إذا كنت تستخدم نظام UNIX مثل Linux أو macOS، فإنه في العادة يستخدم bash كخط الأوامر، يرجى استخدام الأمر التالي:

RUST_BACKTRACE=1 cargo run

ثم، ستشاهد النص التالي:

thread 'main' panicked at 'error occurred', src\main.rs:3:5
stack backtrace:
  ...
  11: greeting::main
             at .\src\main.rs:3
  ...

الاستدلال هو طريقة أخرى لمعالجة الأخطاء غير القابلة للإصلاح، حيث يتم فتح جدول الاستدلال وتسجيل جميع المعلومات، ثم يترك البرنامج يعمل بشكل طبيعي. تم حذف الكثير من المعلومات التي تم تسجيلها في الاستدلال، يمكننا العثور على الأخطاء التي تُطلق عليها macro panic!.

الخطأ القابل للإصلاح

هذا المفهوم يشبه كثيرًا استثناءات لغة البرمجة Java. في الواقع، في لغة C، نحن نستخدم دائمًا إعدادًا من الأرقام لتمثيل الأخطاء التي يواجهها الدوال، في Rust، نستخدم تجميع Result<T, E> كقيمة للإشارة إلى استثناءات.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

في مكتبة Rust القياسية، جميع قيم الدوال التي قد تسبب استثناءً هي من نوع Result. على سبيل المثال: عند محاولة فتح ملف:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    match f {
        Ok(file) => {
            println!("تم فتح الملف بنجاح.");
        },
        Err(err) => {
            println!("فشل في فتح الملف.");
        }
    }
}

إذا لم يكن ملف hello.txt موجودًا، سيتم طباعة "Failed to open the file.".

بالطبع، يمكننا تبسيط بناء الجملة match باستخدام بناء الجملة if let الذي نتحدث عنه في فصل التجميعات:

use std::fs::File;
fn main() {
    let f = File::open("hello.txt");
    if let Ok(file) = f {
        println!("تم فتح الملف بنجاح.");
    } else {
        println!("فشل في فتح الملف.");
    }
}

إذا كنت تريد معالجة خطأ القابل للإصلاح كخطأ غير قابل للإصلاح، يقدم كلا من unwrap() و expect(message: &str) في فئة Result:

use std::fs::File;
fn main() {
    let f1 = File::open("hello.txt").unwrap();
    let f2 = File::open("hello.txt").expect("Failed to open.");
}

هذا البرنامج يعادل استدعاء ماكرو panic! عند Result يكون Err. الفرق بينه وبين expect هو أن expect يمكنه إرسال معلومات خطأ معينة إلى ماكرو panic!.

نقل الأخطاء القابلة للإصلاح

ما تم مناقشته سابقاً هو كيفية التعامل مع الأخطاء التي يتم استلامها، ولكن ماذا إذا كنا نكتب وظيفة نريد نقل الأخطاء فيها عند حدوثها؟

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn main() {
    let r = f(10000);
    if let Ok(v) = r {
        println!("Ok: f(-1) = {}", v);
    } else {
        println!("Err");
    }
}

نتيجة التنفيذ:

Ok: f(-1) = 10000

هذا البرنامج يحتوي على وظيفة f كنقطة بداية الخطأ، الآن سنكتب وظيفة أخرى لنقل الأخطاء:

fn g(i: i32) -> Result<i32, bool> {
    let t = f(i);
    return match t {
        Ok(i) => Ok(i),
        Err(b) => Err(b)
    };
}

يتم نقل الأخطاء التي قد تحدث في وظيفة f إلى وظيفة g (هذا g مجرد مثال بسيط، في الواقع، وظائف نقل الأخطاء غالباً تحتوي على العديد من العمليات الأخرى).

الكتابة بهذا الشكل طويلة بعض الشيء، يمكن في Rust إضافة علامة الفاصلة العكسية '?' بعد Result لتحويل الاستثنائية من نفس النوع مباشرة خارجاً:

fn f(i: i32) -> Result<i32, bool> {
    if i >= 0 { Ok(i) }
    else { Err(false) }
}
fn g(i: i32) -> Result<i32, bool> {
    let t = f(i)?;
    Ok(t) // لأن t ليس Err، t هنا أصبح نوع i32
}
fn main() {
    let r = g(10000);
    if let Ok(v) = r {
        println!("Ok: g(10000) = {}", v);
    } else {
        println!("Err");
    }
}

نتيجة التنفيذ:

Ok: g(10000) = 10000

? علامة الفاصلة العكسية تؤدي إلى إخراج القيمة غير الاستثنائية من فئة Result مباشرة، وإذا كان هناك استثنائية، يتم إرجاع Result يحتوي على الاستثنائية. لذلك، علامة الفاصلة العكسية تستخدم فقط في الوظائف التي تكون نوعها Result<T, E>، حيث يجب أن تكون نوع الاستثنائية في Result متطابقة مع نوع الاستثنائية التي يتم معالجتها من قبل علامة الفاصلة العكسية.

طريقة kind

إلى هنا، يبدو أن Rust لا يحتوي على بنية try بلوك يمكن أن تحصل على استثناءات مشابهة في أي مكان، ولكن هذا لا يعني أن Rust لا يمكن تنفيذ ذلك: يمكننا بسهولة تنفيذ try بلوك في دالة مستقلة، ومرور جميع الاستثناءات إلى الخارج لحلها. في الواقع، هذا هو أسلوب برمجة جيد للبرامج المعدة بشكل جيد: يجب التركيز على كامل ميزات الوظيفة.

لكن يجب التحقق من نوع Result (Err) للحصول على قيمة الحالة، الحصول على الحالة باستخدام وظيفة kind().

use std::io;
use std::io::Read;
use std::fs::File;
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}
fn main() {
    let str_file = read_text_from_file("hello.txt");
    match str_file {
        Ok(s) => println!("{}", s),
        Err(e) => {
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("No such file");
                },
                _ => {
                    println!("Cannot read the file");
                }
            }
        }
    }
}

نتيجة التنفيذ:

لا يوجد ملف