Result
দিয়ে পুনরুদ্ধারযোগ্য এরর (Recoverable Errors)
বেশিরভাগ এরর এতটাই গুরুতর নয় যে প্রোগ্রামটি পুরোপুরি বন্ধ করে দেওয়ার প্রয়োজন হয়। কখনও কখনও যখন একটি ফাংশন ব্যর্থ হয়, তখন তার কারণটি আপনি সহজেই বুঝতে পারেন এবং সেই অনুযায়ী ব্যবস্থা নিতে পারেন। উদাহরণস্বরূপ, যদি আপনি একটি ফাইল খোলার চেষ্টা করেন এবং ফাইলটি না থাকার কারণে সেই অপারেশনটি ব্যর্থ হয়, তাহলে আপনি প্রসেসটি বন্ধ করে দেওয়ার পরিবর্তে ফাইলটি তৈরি করতে চাইতে পারেন।
অধ্যায় ২-এর “Handling Potential Failure with Result
” থেকে মনে করুন যে Result
enum-কে দুটি ভ্যারিয়েন্ট Ok
এবং Err
সহ সংজ্ঞায়িত করা হয়েছে, যা নিম্নরূপ:
#![allow(unused)] fn main() { enum Result<T, E> { Ok(T), Err(E), } }
T
এবং E
হলো জেনেরিক টাইপ প্যারামিটার (generic type parameters): আমরা অধ্যায় ১০-এ জেনেরিক সম্পর্কে আরও বিস্তারিত আলোচনা করব। এখন আপনার যা জানা দরকার তা হলো, T
সফল ক্ষেত্রে Ok
ভ্যারিয়েন্টের মধ্যে ফেরত আসা ভ্যালুর টাইপকে প্রতিনিধিত্ব করে, এবং E
ব্যর্থতার ক্ষেত্রে Err
ভ্যারিয়েন্টের মধ্যে ফেরত আসা এররের টাইপকে প্রতিনিধিত্ব করে। যেহেতু Result
-এর এই জেনেরিক টাইপ প্যারামিটারগুলো রয়েছে, তাই আমরা Result
টাইপ এবং এর উপর সংজ্ঞায়িত ফাংশনগুলো বিভিন্ন পরিস্থিতিতে ব্যবহার করতে পারি যেখানে আমরা যে সফল ভ্যালু এবং এরর ভ্যালু ফেরত দিতে চাই তা ভিন্ন হতে পারে।
আসুন এমন একটি ফাংশন কল করি যা একটি Result
ভ্যালু রিটার্ন করে কারণ ফাংশনটি ব্যর্থ হতে পারে। লিস্টিং ৯-৩-এ আমরা একটি ফাইল খোলার চেষ্টা করছি।
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); }
File::open
-এর রিটার্ন টাইপ হলো একটি Result<T, E>
। জেনেরিক প্যারামিটার T
File::open
-এর ইমপ্লিমেন্টেশনে সফল ভ্যালুর টাইপ std::fs::File
দ্বারা পূর্ণ হয়েছে, যা একটি ফাইল হ্যান্ডেল (file handle)। এরর ভ্যালুতে ব্যবহৃত E
-এর টাইপ হলো std::io::Error
। এই রিটার্ন টাইপের মানে হলো File::open
কলটি সফল হতে পারে এবং একটি ফাইল হ্যান্ডেল রিটার্ন করতে পারে যা থেকে আমরা পড়তে বা লিখতে পারি। ফাংশন কলটি ব্যর্থও হতে পারে: উদাহরণস্বরূপ, ফাইলটি নাও থাকতে পারে, অথবা আমাদের ফাইল অ্যাক্সেস করার অনুমতি নাও থাকতে পারে। File::open
ফাংশনটির আমাদের জানানোর একটি উপায় থাকা দরকার যে এটি সফল হয়েছে নাকি ব্যর্থ হয়েছে এবং একই সাথে আমাদের ফাইল হ্যান্ডেল বা এররের তথ্য দেওয়া দরকার। Result
enum ঠিক এই তথ্যই বহন করে।
যে ক্ষেত্রে File::open
সফল হয়, greeting_file_result
ভ্যারিয়েবলের ভ্যালুটি হবে Ok
-এর একটি ইনস্ট্যান্স যা একটি ফাইল হ্যান্ডেল ধারণ করে। যে ক্ষেত্রে এটি ব্যর্থ হয়, greeting_file_result
-এর ভ্যালুটি হবে Err
-এর একটি ইনস্ট্যান্স যা কী ধরনের এরর ঘটেছে সে সম্পর্কে আরও তথ্য ধারণ করে।
File::open
যে ভ্যালু রিটার্ন করে তার উপর নির্ভর করে বিভিন্ন পদক্ষেপ নেওয়ার জন্য আমাদের লিস্টিং ৯-৩-এর কোডে আরও কিছু যোগ করতে হবে। লিস্টিং ৯-৪ Result
হ্যান্ডেল করার একটি উপায় দেখায়, যেখানে একটি বেসিক টুল, match
এক্সপ্রেশন ব্যবহার করা হয়েছে যা আমরা অধ্যায় ৬-এ আলোচনা করেছি।
use std::fs::File; fn main() { let greeting_file_result = File::open("hello.txt"); let greeting_file = match greeting_file_result { Ok(file) => file, Err(error) => panic!("Problem opening the file: {error:?}"), }; }
লক্ষ্য করুন যে, Option
enum-এর মতো, Result
enum এবং এর ভ্যারিয়েন্টগুলো prelude দ্বারা স্কোপে আনা হয়েছে, তাই আমাদের match
arm-গুলোতে Ok
এবং Err
ভ্যারিয়েন্টের আগে Result::
নির্দিষ্ট করার প্রয়োজন নেই।
যখন ফলাফল Ok
হয়, এই কোডটি Ok
ভ্যারিয়েন্ট থেকে ভেতরের file
ভ্যালুটি রিটার্ন করবে, এবং আমরা তারপর সেই ফাইল হ্যান্ডেল ভ্যালুটি greeting_file
ভ্যারিয়েবলে অ্যাসাইন করি। match
-এর পরে, আমরা ফাইল হ্যান্ডেলটি পড়া বা লেখার জন্য ব্যবহার করতে পারি।
match
-এর অন্য arm-টি সেই কেসটি হ্যান্ডেল করে যেখানে আমরা File::open
থেকে একটি Err
ভ্যালু পাই। এই উদাহরণে, আমরা panic!
ম্যাক্রো কল করতে বেছে নিয়েছি। যদি আমাদের বর্তমান ডিরেক্টরিতে hello.txt নামে কোনো ফাইল না থাকে এবং আমরা এই কোডটি চালাই, আমরা panic!
ম্যাক্রো থেকে নিম্নলিখিত আউটপুট দেখতে পাব:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/error-handling`
thread 'main' panicked at src/main.rs:8:23:
Problem opening the file: Os { code: 2, kind: NotFound, message: "No such file or directory" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
বরাবরের মতো, এই আউটপুটটি আমাদের ঠিক কী ভুল হয়েছে তা বলে দেয়।
বিভিন্ন এররের উপর ম্যাচিং (Matching on Different Errors)
লিস্টিং ৯-৪-এর কোডটি File::open
কেন ব্যর্থ হয়েছে তা নির্বিশেষে panic!
করবে। তবে, আমরা বিভিন্ন ব্যর্থতার কারণের জন্য বিভিন্ন পদক্ষেপ নিতে চাই। যদি ফাইলটি না থাকার কারণে File::open
ব্যর্থ হয়, আমরা ফাইলটি তৈরি করতে এবং নতুন ফাইলের হ্যান্ডেল রিটার্ন করতে চাই। যদি File::open
অন্য কোনো কারণে ব্যর্থ হয়—উদাহরণস্বরূপ, কারণ আমাদের ফাইল খোলার অনুমতি ছিল না—আমরা এখনও চাই কোডটি লিস্টিং ৯-৪-এর মতোই panic!
করুক। এর জন্য, আমরা একটি অভ্যন্তরীণ match
এক্সপ্রেশন যোগ করি, যা লিস্টিং ৯-৫-এ দেখানো হয়েছে।
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Problem creating the file: {e:?}"),
},
_ => {
panic!("Problem opening the file: {error:?}");
}
},
};
}
File::open
Err
ভ্যারিয়েন্টের ভিতরে যে ভ্যালুটি রিটার্ন করে তার টাইপ হলো io::Error
, যা standard library দ্বারা সরবরাহ করা একটি struct। এই struct-টির একটি মেথড kind
আছে যা আমরা একটি io::ErrorKind
ভ্যালু পেতে কল করতে পারি। io::ErrorKind
enum-টি standard library দ্বারা সরবরাহ করা হয় এবং এতে এমন ভ্যারিয়েন্ট রয়েছে যা একটি io
অপারেশনের ফলে হতে পারে এমন বিভিন্ন ধরনের এররকে প্রতিনিধিত্ব করে। আমরা যে ভ্যারিয়েন্টটি ব্যবহার করতে চাই তা হলো ErrorKind::NotFound
, যা নির্দেশ করে যে আমরা যে ফাইলটি খোলার চেষ্টা করছি তা এখনও বিদ্যমান নেই। তাই আমরা greeting_file_result
-এর উপর ম্যাচ করি, কিন্তু আমাদের error.kind()
-এর উপর একটি অভ্যন্তরীণ ম্যাচও রয়েছে।
অভ্যন্তরীণ ম্যাচে আমরা যে শর্তটি পরীক্ষা করতে চাই তা হলো error.kind()
দ্বারা রিটার্ন করা ভ্যালুটি ErrorKind
enum-এর NotFound
ভ্যারিয়েন্ট কিনা। যদি তাই হয়, আমরা File::create
দিয়ে ফাইলটি তৈরি করার চেষ্টা করি। তবে, যেহেতু File::create
-ও ব্যর্থ হতে পারে, তাই আমাদের অভ্যন্তরীণ match
এক্সপ্রেশনে একটি দ্বিতীয় arm দরকার। যখন ফাইলটি তৈরি করা যায় না, তখন একটি ভিন্ন এরর বার্তা প্রিন্ট করা হয়। বাইরের match
-এর দ্বিতীয় arm-টি একই থাকে, তাই প্রোগ্রামটি ফাইল না পাওয়ার এরর ছাড়া অন্য যেকোনো এররের জন্য প্যানিক করে।
Result<T, E>
-এর সাথেmatch
ব্যবহারের বিকল্পএখানে অনেক
match
ব্যবহার হয়েছে!match
এক্সপ্রেশনটি খুব দরকারী কিন্তু এটি একটি বেশ আদিম (primitive) টুল। অধ্যায় ১৩-তে, আপনি closures সম্পর্কে শিখবেন, যাResult<T, E>
-তে সংজ্ঞায়িত অনেক মেথডের সাথে ব্যবহৃত হয়। আপনার কোডেResult<T, E>
ভ্যালু হ্যান্ডেল করার সময় এই মেথডগুলোmatch
ব্যবহারের চেয়ে বেশি সংক্ষিপ্ত হতে পারে।উদাহরণস্বরূপ, লিস্টিং ৯-৫-এর মতো একই লজিক লেখার আরেকটি উপায় এখানে দেওয়া হলো, এবার closures এবং
unwrap_or_else
মেথড ব্যবহার করে:use std::fs::File; use std::io::ErrorKind; fn main() { let greeting_file = File::open("hello.txt").unwrap_or_else(|error| { if error.kind() == ErrorKind::NotFound { File::create("hello.txt").unwrap_or_else(|error| { panic!("Problem creating the file: {error:?}"); }) } else { panic!("Problem opening the file: {error:?}"); } }); }
যদিও এই কোডটির আচরণ লিস্টিং ৯-৫-এর মতোই, এতে কোনো
match
এক্সপ্রেশন নেই এবং এটি পড়তে আরও পরিষ্কার। অধ্যায় ১৩ পড়ার পরে এই উদাহরণে ফিরে আসুন, এবং standard library ডকুমেন্টেশনেunwrap_or_else
মেথডটি দেখুন। এরর নিয়ে কাজ করার সময় এরকম আরও অনেক মেথড আছে যা বিশাল নেস্টেডmatch
এক্সপ্রেশনকে পরিষ্কার করতে পারে।
এররের উপর প্যানিকের জন্য শর্টকাট: unwrap
এবং expect
match
ব্যবহার করা যথেষ্ট ভালো কাজ করে, তবে এটি কিছুটা দীর্ঘ হতে পারে এবং সবসময় উদ্দেশ্য ভালোভাবে বোঝাতে পারে না। Result<T, E>
টাইপের উপর বিভিন্ন, আরও নির্দিষ্ট কাজ করার জন্য অনেক হেল্পার মেথড সংজ্ঞায়িত করা আছে। unwrap
মেথডটি একটি শর্টকাট মেথড যা আমরা লিস্টিং ৯-৪-এ লেখা match
এক্সপ্রেশনের মতোই প্রয়োগ করা হয়েছে। যদি Result
ভ্যালুটি Ok
ভ্যারিয়েন্ট হয়, unwrap
Ok
-এর ভিতরের ভ্যালুটি রিটার্ন করবে। যদি Result
Err
ভ্যারিয়েন্ট হয়, unwrap
আমাদের জন্য panic!
ম্যাক্রো কল করবে। এখানে unwrap
-এর একটি উদাহরণ দেওয়া হলো:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt").unwrap(); }
যদি আমরা hello.txt ফাইল ছাড়া এই কোডটি চালাই, আমরা unwrap
মেথডের করা panic!
কল থেকে একটি এরর বার্তা দেখতে পাব:
thread 'main' panicked at src/main.rs:4:49:
called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }
একইভাবে, expect
মেথডটি আমাদের panic!
এরর বার্তাও বেছে নিতে দেয়। unwrap
-এর পরিবর্তে expect
ব্যবহার করা এবং ভালো এরর বার্তা সরবরাহ করা আপনার উদ্দেশ্য বোঝাতে পারে এবং প্যানিকের উৎস খুঁজে বের করা সহজ করে তুলতে পারে। expect
-এর সিনট্যাক্সটি এইরকম:
use std::fs::File; fn main() { let greeting_file = File::open("hello.txt") .expect("hello.txt should be included in this project"); }
আমরা expect
unwrap
-এর মতোই ব্যবহার করি: ফাইল হ্যান্ডেল রিটার্ন করতে বা panic!
ম্যাক্রো কল করতে। expect
-এর panic!
কলে ব্যবহৃত এরর বার্তাটি হবে expect
-এ পাস করা প্যারামিটার, unwrap
-এর ব্যবহৃত ডিফল্ট panic!
বার্তার পরিবর্তে। এটি দেখতে এইরকম:
thread 'main' panicked at src/main.rs:5:10:
hello.txt should be included in this project: Os { code: 2, kind: NotFound, message: "No such file or directory" }
প্রোডাকশন-মানের কোডে, বেশিরভাগ রাস্টেসিয়ান (Rustaceans) unwrap
-এর পরিবর্তে expect
বেছে নেয় এবং অপারেশনটি কেন সবসময় সফল হবে বলে আশা করা হচ্ছে সে সম্পর্কে আরও প্রসঙ্গ দেয়। এভাবে, যদি আপনার অনুমান কখনও ভুল প্রমাণিত হয়, আপনার ডিবাগিংয়ে ব্যবহার করার জন্য আরও তথ্য থাকবে।
এরর প্রচার করা (Propagating Errors)
যখন একটি ফাংশনের ইমপ্লিমেন্টেশন এমন কিছু কল করে যা ব্যর্থ হতে পারে, তখন ফাংশনের মধ্যেই এররটি হ্যান্ডেল করার পরিবর্তে, আপনি এররটি কলিং কোডে ফেরত দিতে পারেন যাতে এটি কী করতে হবে তা সিদ্ধান্ত নিতে পারে। এটিকে এরর প্রচার করা (propagating) বলা হয় এবং এটি কলিং কোডকে আরও নিয়ন্ত্রণ দেয়, যেখানে আপনার কোডের প্রেক্ষাপটে আপনার কাছে যা উপলব্ধ তার চেয়ে বেশি তথ্য বা লজিক থাকতে পারে যা নির্দেশ করে যে এররটি কীভাবে হ্যান্ডেল করা উচিত।
উদাহরণস্বরূপ, লিস্টিং ৯-৬ একটি ফাংশন দেখায় যা একটি ফাইল থেকে একটি ব্যবহারকারীর নাম পড়ে। যদি ফাইলটি বিদ্যমান না থাকে বা পড়া না যায়, এই ফাংশনটি সেই এররগুলো ফাংশনটিকে কল করা কোডে ফেরত দেবে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let username_file_result = File::open("hello.txt"); let mut username_file = match username_file_result { Ok(file) => file, Err(e) => return Err(e), }; let mut username = String::new(); match username_file.read_to_string(&mut username) { Ok(_) => Ok(username), Err(e) => Err(e), } } }
এই ফাংশনটি অনেক ছোট করে লেখা যায়, কিন্তু আমরা এরর হ্যান্ডলিং অন্বেষণ করার জন্য প্রথমে এটি ম্যানুয়ালি অনেক কিছু করব; শেষে, আমরা ছোট উপায়টি দেখাব। আসুন প্রথমে ফাংশনের রিটার্ন টাইপটি দেখি: Result<String, io::Error>
। এর মানে হলো ফাংশনটি Result<T, E>
টাইপের একটি ভ্যালু রিটার্ন করছে, যেখানে জেনেরিক প্যারামিটার T
কংক্রিট টাইপ String
দিয়ে এবং জেনেরিক টাইপ E
কংক্রিট টাইপ io::Error
দিয়ে পূর্ণ করা হয়েছে।
যদি এই ফাংশনটি কোনো সমস্যা ছাড়াই সফল হয়, তবে এই ফাংশনটি কল করা কোডটি একটি Ok
ভ্যালু পাবে যা একটি String
ধারণ করে—এই ফাংশনটি ফাইল থেকে যে username
পড়েছে। যদি এই ফাংশনটি কোনো সমস্যার সম্মুখীন হয়, তবে কলিং কোডটি একটি Err
ভ্যালু পাবে যা io::Error
-এর একটি ইনস্ট্যান্স ধারণ করে যা সমস্যাগুলো কী ছিল সে সম্পর্কে আরও তথ্য ধারণ করে। আমরা এই ফাংশনের রিটার্ন টাইপ হিসাবে io::Error
বেছে নিয়েছি কারণ এই ফাংশনের বডিতে আমরা যে দুটি অপারেশন কল করছি যা ব্যর্থ হতে পারে—File::open
ফাংশন এবং read_to_string
মেথড—উভয় থেকেই রিটার্ন করা এরর ভ্যালুর টাইপ এটি।
ফাংশনের বডি File::open
ফাংশন কল করে শুরু হয়। তারপর আমরা লিস্টিং ৯-৪-এর match
-এর মতো একটি match
দিয়ে Result
ভ্যালুটি হ্যান্ডেল করি। যদি File::open
সফল হয়, প্যাটার্ন ভ্যারিয়েবল file
-এর ফাইল হ্যান্ডেলটি মিউটেবল ভ্যারিয়েবল username_file
-এর ভ্যালু হয়ে যায় এবং ফাংশনটি চলতে থাকে। Err
ক্ষেত্রে, panic!
কল করার পরিবর্তে, আমরা return
কীওয়ার্ড ব্যবহার করে ফাংশন থেকে পুরোপুরি আগেভাগে রিটার্ন করি এবং File::open
থেকে এরর ভ্যালুটি, এখন প্যাটার্ন ভ্যারিয়েবল e
-তে, এই ফাংশনের এরর ভ্যালু হিসাবে কলিং কোডে ফেরত পাঠাই।
সুতরাং, যদি আমাদের username_file
-এ একটি ফাইল হ্যান্ডেল থাকে, ফাংশনটি তখন username
ভ্যারিয়েবলে একটি নতুন String
তৈরি করে এবং username_file
-এর ফাইল হ্যান্ডেলের উপর read_to_string
মেথড কল করে ফাইলের বিষয়বস্তু username
-এ পড়ে। read_to_string
মেথডটিও একটি Result
রিটার্ন করে কারণ এটিও ব্যর্থ হতে পারে, যদিও File::open
সফল হয়েছিল। তাই আমাদের সেই Result
হ্যান্ডেল করার জন্য আরেকটি match
দরকার: যদি read_to_string
সফল হয়, তাহলে আমাদের ফাংশন সফল হয়েছে, এবং আমরা ফাইল থেকে পড়া ইউজারনেমটি, যা এখন username
-এ আছে, একটি Ok
-তে র্যাপ করে রিটার্ন করি। যদি read_to_string
ব্যর্থ হয়, আমরা এরর ভ্যালুটি একইভাবে রিটার্ন করি যেভাবে আমরা File::open
-এর রিটার্ন ভ্যালু হ্যান্ডেল করা match
-এ এরর ভ্যালু রিটার্ন করেছিলাম। তবে, আমাদের স্পষ্টভাবে return
বলার প্রয়োজন নেই, কারণ এটি ফাংশনের শেষ এক্সপ্রেশন।
এই কোডটি কল করা কোডটি তখন একটি Ok
ভ্যালু যা একটি ইউজারনেম ধারণ করে বা একটি Err
ভ্যালু যা একটি io::Error
ধারণ করে তা হ্যান্ডেল করবে। সেই ভ্যালুগুলো দিয়ে কী করতে হবে তা সিদ্ধান্ত নেওয়া কলিং কোডের উপর নির্ভর করে। যদি কলিং কোড একটি Err
ভ্যালু পায়, তবে এটি panic!
কল করে প্রোগ্রাম ক্র্যাশ করতে পারে, একটি ডিফল্ট ইউজারনেম ব্যবহার করতে পারে, অথবা ফাইল ছাড়া অন্য কোথাও থেকে ইউজারনেম খুঁজতে পারে, উদাহরণস্বরূপ। কলিং কোড আসলে কী করার চেষ্টা করছে সে সম্পর্কে আমাদের কাছে পর্যাপ্ত তথ্য নেই, তাই আমরা সমস্ত সফলতা বা এররের তথ্য উপরে প্রচার করি যাতে এটি যথাযথভাবে হ্যান্ডেল করতে পারে।
এরর প্রচারের এই প্যাটার্নটি Rust-এ এতটাই সাধারণ যে Rust এটিকে সহজ করার জন্য প্রশ্নবোধক চিহ্ন অপারেটর ?
সরবরাহ করে।
এরর প্রচারের জন্য একটি শর্টকাট: ?
অপারেটর
লিস্টিং ৯-৭ read_username_from_file
-এর একটি ইমপ্লিমেন্টেশন দেখায় যা লিস্টিং ৯-৬-এর মতোই কার্যকারিতা সম্পন্ন, কিন্তু এই ইমপ্লিমেন্টেশনটি ?
অপারেটর ব্যবহার করে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username_file = File::open("hello.txt")?; let mut username = String::new(); username_file.read_to_string(&mut username)?; Ok(username) } }
একটি Result
ভ্যালুর পরে রাখা ?
অপারেটরটি প্রায় একইভাবে কাজ করার জন্য সংজ্ঞায়িত করা হয়েছে যেভাবে আমরা লিস্টিং ৯-৬-এ Result
ভ্যালুগুলো হ্যান্ডেল করার জন্য match
এক্সপ্রেশন সংজ্ঞায়িত করেছি। যদি Result
-এর ভ্যালুটি একটি Ok
হয়, তবে Ok
-এর ভিতরের ভ্যালুটি এই এক্সপ্রেশন থেকে ফেরত আসবে, এবং প্রোগ্রামটি চলতে থাকবে। যদি ভ্যালুটি একটি Err
হয়, তবে Err
পুরো ফাংশন থেকে ফেরত আসবে যেন আমরা return
কীওয়ার্ড ব্যবহার করেছি যাতে এরর ভ্যালুটি কলিং কোডে প্রচারিত হয়।
লিস্টিং ৯-৬-এর match
এক্সপ্রেশন যা করে এবং ?
অপারেটর যা করে তার মধ্যে একটি পার্থক্য রয়েছে: যে এরর ভ্যালুগুলোর উপর ?
অপারেটর কল করা হয় সেগুলি standard library-এর From
trait-এ সংজ্ঞায়িত from
ফাংশনের মধ্য দিয়ে যায়, যা এক টাইপের ভ্যালুকে অন্য টাইপে রূপান্তর করতে ব্যবহৃত হয়। যখন ?
অপারেটর from
ফাংশনটি কল করে, তখন প্রাপ্ত এরর টাইপটি বর্তমান ফাংশনের রিটার্ন টাইপে সংজ্ঞায়িত এরর টাইপে রূপান্তরিত হয়। এটি দরকারী যখন একটি ফাংশন একটি এরর টাইপ রিটার্ন করে যা ফাংশনটি ব্যর্থ হওয়ার সমস্ত উপায়কে প্রতিনিধিত্ব করে, এমনকি যদি অংশগুলি বিভিন্ন কারণে ব্যর্থ হতে পারে।
উদাহরণস্বরূপ, আমরা লিস্টিং ৯-৭-এর read_username_from_file
ফাংশনটি পরিবর্তন করে OurError
নামের একটি কাস্টম এরর টাইপ রিটার্ন করতে পারি যা আমরা সংজ্ঞায়িত করি। যদি আমরা একটি io::Error
থেকে OurError
-এর একটি ইনস্ট্যান্স তৈরি করার জন্য impl From<io::Error> for OurError
-ও সংজ্ঞায়িত করি, তবে read_username_from_file
-এর বডিতে ?
অপারেটর কলগুলো from
কল করবে এবং ফাংশনে কোনো অতিরিক্ত কোড যোগ না করেই এরর টাইপগুলো রূপান্তর করবে।
লিস্টিং ৯-৭-এর প্রেক্ষাপটে, File::open
কলের শেষে ?
একটি Ok
-এর ভিতরের ভ্যালুটি username_file
ভ্যারিয়েবলে রিটার্ন করবে। যদি একটি এরর ঘটে, ?
অপারেটরটি পুরো ফাংশন থেকে আগেভাগে রিটার্ন করবে এবং কলিং কোডকে যেকোনো Err
ভ্যালু দেবে। একই জিনিস read_to_string
কলের শেষে ?
-এর ক্ষেত্রেও প্রযোজ্য।
?
অপারেটরটি অনেক বয়লারপ্লেট (boilerplate) দূর করে এবং এই ফাংশনের ইমপ্লিমেন্টেশনকে সহজ করে তোলে। আমরা ?
-এর ঠিক পরে মেথড কল চেইন করে এই কোডটিকে আরও ছোট করতে পারি, যেমনটি লিস্টিং ৯-৮-এ দেখানো হয়েছে।
#![allow(unused)] fn main() { use std::fs::File; use std::io::{self, Read}; fn read_username_from_file() -> Result<String, io::Error> { let mut username = String::new(); File::open("hello.txt")?.read_to_string(&mut username)?; Ok(username) } }
আমরা username
-এ নতুন String
তৈরি করাটা ফাংশনের শুরুতে নিয়ে এসেছি; সেই অংশটি পরিবর্তিত হয়নি। username_file
ভ্যারিয়েবল তৈরি করার পরিবর্তে, আমরা read_to_string
কলটি সরাসরি File::open("hello.txt")?
-এর ফলাফলের সাথে চেইন করেছি। read_to_string
কলের শেষে আমাদের এখনও একটি ?
রয়েছে, এবং File::open
এবং read_to_string
উভয়ই সফল হলে আমরা এখনও এরর রিটার্ন করার পরিবর্তে username
ধারণকারী একটি Ok
ভ্যালু রিটার্ন করি। কার্যকারিতা আবার লিস্টিং ৯-৬ এবং লিস্টিং ৯-৭-এর মতোই; এটি লেখার একটি ভিন্ন, আরও সুবিধাজনক উপায়।
লিস্টিং ৯-৯ fs::read_to_string
ব্যবহার করে এটিকে আরও ছোট করার একটি উপায় দেখায়।
#![allow(unused)] fn main() { use std::fs; use std::io; fn read_username_from_file() -> Result<String, io::Error> { fs::read_to_string("hello.txt") } }
একটি ফাইলকে একটি স্ট্রিং-এ পড়া একটি মোটামুটি সাধারণ অপারেশন, তাই standard library সুবিধাজনক fs::read_to_string
ফাংশন সরবরাহ করে যা ফাইলটি খোলে, একটি নতুন String
তৈরি করে, ফাইলের বিষয়বস্তু পড়ে, সেই String
-এ বিষয়বস্তু রাখে এবং এটি রিটার্ন করে। অবশ্যই, fs::read_to_string
ব্যবহার করা আমাদের সমস্ত এরর হ্যান্ডলিং ব্যাখ্যা করার সুযোগ দেয় না, তাই আমরা প্রথমে দীর্ঘ উপায়টি করেছি।
কোথায় ?
অপারেটর ব্যবহার করা যেতে পারে
?
অপারেটরটি শুধুমাত্র সেই ফাংশনগুলিতে ব্যবহার করা যেতে পারে যাদের রিটার্ন টাইপ ?
যে ভ্যালুর উপর ব্যবহৃত হয় তার সাথে সামঞ্জস্যপূর্ণ। এটি কারণ ?
অপারেটরটি একটি ফাংশন থেকে একটি ভ্যালুর আগেভাগে রিটার্ন করার জন্য সংজ্ঞায়িত করা হয়েছে, ঠিক যেমনটি আমরা লিস্টিং ৯-৬-এ সংজ্ঞায়িত match
এক্সপ্রেশনের মতো। লিস্টিং ৯-৬-এ, match
একটি Result
ভ্যালু ব্যবহার করছিল, এবং আগেভাগে রিটার্ন করা arm-টি একটি Err(e)
ভ্যালু রিটার্ন করেছিল। ফাংশনের রিটার্ন টাইপটি একটি Result
হতে হবে যাতে এটি এই return
-এর সাথে সামঞ্জস্যপূর্ণ হয়।
লিস্টিং ৯-১০-এ, আসুন দেখি আমরা যদি একটি main
ফাংশনে ?
অপারেটর ব্যবহার করি যার রিটার্ন টাইপ আমরা যে ভ্যালুর উপর ?
ব্যবহার করি তার টাইপের সাথে অসামঞ্জস্যপূর্ণ হয় তবে আমরা কী এরর পাব।
use std::fs::File;
fn main() {
let greeting_file = File::open("hello.txt")?;
}
এই কোডটি একটি ফাইল খোলে, যা ব্যর্থ হতে পারে। ?
অপারেটরটি File::open
দ্বারা রিটার্ন করা Result
ভ্যালুটিকে অনুসরণ করে, কিন্তু এই main
ফাংশনটির রিটার্ন টাইপ ()
, Result
নয়। যখন আমরা এই কোডটি কম্পাইল করি, তখন আমরা নিম্নলিখিত এরর বার্তাটি পাই:
$ cargo run
Compiling error-handling v0.1.0 (file:///projects/error-handling)
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:4:48
|
3 | fn main() {
| --------- this function should return `Result` or `Option` to accept `?`
4 | let greeting_file = File::open("hello.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
help: consider adding return type
|
3 ~ fn main() -> Result<(), Box<dyn std::error::Error>> {
4 | let greeting_file = File::open("hello.txt")?;
5 + Ok(())
|
For more information about this error, try `rustc --explain E0277`.
error: could not compile `error-handling` (bin "error-handling") due to 1 previous error
এই এররটি নির্দেশ করে যে আমরা শুধুমাত্র সেই ফাংশনে ?
অপারেটর ব্যবহার করতে পারি যা Result
, Option
, বা FromResidual
ইমপ্লিমেন্ট করে এমন অন্য কোনো টাইপ রিটার্ন করে।
এররটি ঠিক করার জন্য, আপনার দুটি বিকল্প রয়েছে। একটি বিকল্প হলো আপনার ফাংশনের রিটার্ন টাইপ পরিবর্তন করে আপনি যে ভ্যালুর উপর ?
অপারেটর ব্যবহার করছেন তার সাথে সামঞ্জস্যপূর্ণ করা, যতক্ষণ না আপনার কোনো সীমাবদ্ধতা থাকে যা এটি প্রতিরোধ করে। অন্য বিকল্পটি হলো Result<T, E>
-কে যেভাবে উপযুক্ত সেভাবে হ্যান্ডেল করার জন্য একটি match
বা Result<T, E>
-এর কোনো মেথড ব্যবহার করা।
এরর বার্তাটিতে আরও উল্লেখ করা হয়েছে যে ?
Option<T>
ভ্যালুগুলোর সাথেও ব্যবহার করা যেতে পারে। Result
-এর উপর ?
ব্যবহারের মতোই, আপনি শুধুমাত্র সেই ফাংশনে Option
-এর উপর ?
ব্যবহার করতে পারেন যা একটি Option
রিটার্ন করে। Option<T>
-এর উপর কল করা হলে ?
অপারেটরের আচরণ Result<T, E>
-এর উপর কল করা হলে তার আচরণের মতোই: যদি ভ্যালুটি None
হয়, তবে সেই সময়ে ফাংশন থেকে None
আগেভাগে রিটার্ন করা হবে। যদি ভ্যালুটি Some
হয়, তবে Some
-এর ভিতরের ভ্যালুটি এক্সপ্রেশনের ফলস্বরূপ ভ্যালু হয়, এবং ফাংশনটি চলতে থাকে। লিস্টিং ৯-১১-এ একটি ফাংশনের উদাহরণ রয়েছে যা প্রদত্ত টেক্সটের প্রথম লাইনের শেষ অক্ষরটি খুঁজে বের করে।
fn last_char_of_first_line(text: &str) -> Option<char> { text.lines().next()?.chars().last() } fn main() { assert_eq!( last_char_of_first_line("Hello, world\nHow are you today?"), Some('d') ); assert_eq!(last_char_of_first_line(""), None); assert_eq!(last_char_of_first_line("\nhi"), None); }``` </Listing> এই ফাংশনটি `Option<char>` রিটার্ন করে কারণ এটি সম্ভব যে সেখানে একটি অক্ষর আছে, কিন্তু এটিও সম্ভব যে সেখানে নেই। এই কোডটি `text` স্ট্রিং স্লাইস আর্গুমেন্টটি নেয় এবং এর উপর `lines` মেথড কল করে, যা স্ট্রিং-এর লাইনগুলোর উপর একটি ইটারেটর রিটার্ন করে। যেহেতু এই ফাংশনটি প্রথম লাইনটি পরীক্ষা করতে চায়, এটি ইটারেটরের প্রথম ভ্যালুটি পেতে ইটারেটরের উপর `next` কল করে। যদি `text` একটি খালি স্ট্রিং হয়, তবে এই `next` কলটি `None` রিটার্ন করবে, সেক্ষেত্রে আমরা `?` ব্যবহার করে `last_char_of_first_line` থেকে `None` রিটার্ন করে থেমে যাই। যদি `text` একটি খালি স্ট্রিং না হয়, `next` একটি `Some` ভ্যালু রিটার্ন করবে যা `text`-এর প্রথম লাইনের একটি স্ট্রিং স্লাইস ধারণ করে। `?` স্ট্রিং স্লাইসটি এক্সট্র্যাক্ট করে, এবং আমরা সেই স্ট্রিং স্লাইসের উপর `chars` কল করে এর অক্ষরগুলোর একটি ইটারেটর পেতে পারি। আমরা এই প্রথম লাইনের শেষ অক্ষরে আগ্রহী, তাই আমরা ইটারেটরের শেষ আইটেমটি রিটার্ন করার জন্য `last` কল করি। এটি একটি `Option` কারণ এটি সম্ভব যে প্রথম লাইনটি একটি খালি স্ট্রিং; উদাহরণস্বরূপ, যদি `text` একটি ফাঁকা লাইন দিয়ে শুরু হয় কিন্তু অন্য লাইনে অক্ষর থাকে, যেমন `"\nhi"`। তবে, যদি প্রথম লাইনে একটি শেষ অক্ষর থাকে, তবে এটি `Some` ভ্যারিয়েন্টে রিটার্ন করা হবে। মাঝখানে `?` অপারেটরটি আমাদের এই লজিকটি সংক্ষিপ্তভাবে প্রকাশ করার একটি উপায় দেয়, যা আমাদের ফাংশনটি এক লাইনে ইমপ্লিমেন্ট করতে দেয়। যদি আমরা `Option`-এর উপর `?` অপারেটর ব্যবহার করতে না পারতাম, তবে আমাদের এই লজিকটি আরও মেথড কল বা একটি `match` এক্সপ্রেশন ব্যবহার করে ইমপ্লিমেন্ট করতে হতো। মনে রাখবেন যে আপনি একটি ফাংশনে `Result`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Result` রিটার্ন করে, এবং আপনি একটি ফাংশনে `Option`-এর উপর `?` অপারেটর ব্যবহার করতে পারেন যা `Option` রিটার্ন করে, কিন্তু আপনি মিশ্রণ করতে পারবেন না। `?` অপারেটরটি স্বয়ংক্রিয়ভাবে একটি `Result`-কে একটি `Option`-এ বা তার বিপরীতে রূপান্তর করবে না; সেই ক্ষেত্রে, আপনি স্পষ্টভাবে রূপান্তর করার জন্য `Result`-এর উপর `ok` মেথড বা `Option`-এর উপর `ok_or` মেথডের মতো মেথড ব্যবহার করতে পারেন। এখন পর্যন্ত, আমরা যে সমস্ত `main` ফাংশন ব্যবহার করেছি সেগুলি `()` রিটার্ন করে। `main` ফাংশনটি বিশেষ কারণ এটি একটি এক্সিকিউটেবল প্রোগ্রামের প্রবেশ এবং প্রস্থান বিন্দু, এবং প্রোগ্রামটি প্রত্যাশিতভাবে আচরণ করার জন্য এর রিটার্ন টাইপের উপর বিধিনিষেধ রয়েছে। সৌভাগ্যবশত, `main` একটি `Result<(), E>`-ও রিটার্ন করতে পারে। লিস্টিং ৯-১২-এ লিস্টিং ৯-১০-এর কোড রয়েছে, কিন্তু আমরা `main`-এর রিটার্ন টাইপ পরিবর্তন করে `Result<(), Box<dyn Error>>` করেছি এবং শেষে একটি রিটার্ন ভ্যালু `Ok(())` যোগ করেছি। এই কোডটি এখন কম্পাইল হবে। <Listing number="9-12" file-name="src/main.rs" caption="`main`-কে `Result<(), E>` রিটার্ন করার জন্য পরিবর্তন করা `Result` ভ্যালুগুলোর উপর `?` অপারেটর ব্যবহারের অনুমতি দেয়।"> ```rust,ignore use std::error::Error; use std::fs::File; fn main() -> Result<(), Box<dyn Error>> { let greeting_file = File::open("hello.txt")?; Ok(()) }
Box<dyn Error>
টাইপটি একটি trait object, যা আমরা অধ্যায় ১৮-এর “Using Trait Objects That Allow for Values of Different Types” বিভাগে আলোচনা করব। আপাতত, আপনি Box<dyn Error>
-কে “যেকোনো ধরনের এরর” হিসাবে পড়তে পারেন। main
ফাংশনে Box<dyn Error>
এরর টাইপসহ একটি Result
ভ্যালুর উপর ?
ব্যবহার করা অনুমোদিত কারণ এটি যেকোনো Err
ভ্যালুকে আগেভাগে রিটার্ন করার অনুমতি দেয়। যদিও এই main
ফাংশনের বডি শুধুমাত্র std::io::Error
টাইপের এরর রিটার্ন করবে, Box<dyn Error>
নির্দিষ্ট করার মাধ্যমে, এই সিগনেচারটি সঠিক থাকবে এমনকি যদি main
-এর বডিতে অন্য এরর রিটার্ন করে এমন আরও কোড যোগ করা হয়।
যখন একটি main
ফাংশন একটি Result<(), E>
রিটার্ন করে, তখন এক্সিকিউটেবলটি 0
ভ্যালু দিয়ে প্রস্থান করবে যদি main
Ok(())
রিটার্ন করে এবং একটি নন-জিরো ভ্যালু দিয়ে প্রস্থান করবে যদি main
একটি Err
ভ্যালু রিটার্ন করে। C-তে লেখা এক্সিকিউটেবলগুলো প্রস্থান করার সময় ইন্টিজার রিটার্ন করে: যে প্রোগ্রামগুলো সফলভাবে প্রস্থান করে সেগুলি 0
ইন্টিজার রিটার্ন করে, এবং যে প্রোগ্রামগুলো এরর করে সেগুলি 0
ছাড়া অন্য কোনো ইন্টিজার রিটার্ন করে। Rust এই কনভেনশনের সাথে সামঞ্জস্যপূর্ণ হওয়ার জন্য এক্সিকিউটেবল থেকে ইন্টিজার রিটার্ন করে।
main
ফাংশনটি যেকোনো টাইপ রিটার্ন করতে পারে যা std::process::Termination
trait ইমপ্লিমেন্ট করে, যা একটি report
ফাংশন ধারণ করে যা একটি ExitCode
রিটার্ন করে। আপনার নিজের টাইপের জন্য Termination
trait ইমপ্লিমেন্ট করার বিষয়ে আরও তথ্যের জন্য standard library ডকুমেন্টেশন দেখুন।
এখন যেহেতু আমরা panic!
কল করা বা Result
রিটার্ন করার বিস্তারিত আলোচনা করেছি, আসুন আমরা কোন ক্ষেত্রে কোনটি ব্যবহার করা উপযুক্ত তা সিদ্ধান্ত নেওয়ার বিষয়ে ফিরে যাই।