Deref
Trait ব্যবহার করে Smart Pointer-কে সাধারণ Reference-এর মতো ব্যবহার করা
Deref
ট্রেইট ইমপ্লিমেন্ট করার মাধ্যমে আপনি dereference operator *
(মাল্টিপ্লিকেশন বা glob অপারেটরের সাথে বিভ্রান্ত হবেন না) এর আচরণ কাস্টমাইজ করতে পারেন। Deref
এমনভাবে ইমপ্লিমেন্ট করার মাধ্যমে একটি smart pointer-কে সাধারণ reference-এর মতো ব্যবহার করা যায়, যার ফলে আপনি এমন কোড লিখতে পারবেন যা reference-এর উপর কাজ করে এবং সেই কোড smart pointer-এর সাথেও ব্যবহার করতে পারবেন।
চলুন প্রথমে দেখি dereference অপারেটর সাধারণ reference-এর সাথে কীভাবে কাজ করে। তারপর আমরা Box<T>
-এর মতো আচরণ করে এমন একটি কাস্টম টাইপ সংজ্ঞায়িত করার চেষ্টা করব এবং দেখব কেন dereference অপারেটর আমাদের নতুন সংজ্ঞায়িত টাইপের উপর reference-এর মতো কাজ করে না। আমরা দেখব কীভাবে Deref
ট্রেইট ইমপ্লিমেন্ট করা smart pointer-গুলোকে reference-এর মতো কাজ করতে সক্ষম করে। এরপর আমরা রাস্টের deref coercion ফিচারটি দেখব এবং জানব এটি কীভাবে আমাদের reference বা smart pointer উভয়ের সাথেই কাজ করতে দেয়।
Reference অনুসরণ করে ভ্যালু পর্যন্ত পৌঁছানো
একটি সাধারণ reference হলো এক ধরনের pointer, এবং একটি pointer-কে ভাবা যেতে পারে অন্য কোথাও সংরক্ষিত একটি ভ্যালুর দিকে নির্দেশকারী একটি তীর হিসাবে। Listing 15-6-এ, আমরা একটি i32
ভ্যালুর একটি reference তৈরি করেছি এবং তারপর dereference অপারেটর ব্যবহার করে reference অনুসরণ করে সেই ভ্যালু পর্যন্ত গিয়েছি।
fn main() { let x = 5; let y = &x; assert_eq!(5, x); assert_eq!(5, *y); }
x
ভেরিয়েবলটি একটি i32
ভ্যালু 5
ধারণ করে। আমরা y
-কে x
-এর একটি reference-এর সমান সেট করেছি। আমরা assert করতে পারি যে x
এর মান 5
। কিন্তু, যদি আমরা y
-এর ভ্যালু সম্পর্কে একটি assertion করতে চাই, তাহলে আমাদের *y
ব্যবহার করে reference-টিকে অনুসরণ করে তার নির্দেশিত ভ্যালু পর্যন্ত যেতে হবে (এজন্যই dereference) যাতে কম্পাইলার আসল ভ্যালুটি তুলনা করতে পারে। একবার আমরা y
-কে dereference করলে, আমরা y
-এর নির্দেশিত ইন্টিজার ভ্যালুটি অ্যাক্সেস করতে পারি এবং সেটিকে 5
-এর সাথে তুলনা করতে পারি।
যদি আমরা এর পরিবর্তে assert_eq!(5, y);
লেখার চেষ্টা করতাম, আমরা এই কম্পাইলেশন এররটি পেতাম:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0277]: can't compare `{integer}` with `&{integer}`
--> src/main.rs:6:5
|
6 | assert_eq!(5, y);
| ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
|
= help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
= note: this error originates in the macro `assert_eq` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
একটি সংখ্যা এবং একটি সংখ্যার reference-এর তুলনা করার অনুমতি নেই কারণ তারা ভিন্ন টাইপের। আমাদের অবশ্যই dereference অপারেটর ব্যবহার করে reference-টিকে তার নির্দেশিত ভ্যালু পর্যন্ত অনুসরণ করতে হবে।
Box<T>
-কে Reference-এর মতো ব্যবহার করা
আমরা Listing 15-6-এর কোডটি reference-এর পরিবর্তে Box<T>
ব্যবহার করে পুনরায় লিখতে পারি; Listing 15-7-এ Box<T>
-এর উপর ব্যবহৃত dereference অপারেটরটি Listing 15-6-এ reference-এর উপর ব্যবহৃত dereference অপারেটরের মতোই কাজ করে।
fn main() { let x = 5; let y = Box::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
Listing 15-7 এবং Listing 15-6-এর মধ্যে প্রধান পার্থক্য হলো এখানে আমরা y
-কে x
-এর ভ্যালুকে নির্দেশকারী একটি reference-এর পরিবর্তে x
-এর একটি কপি করা ভ্যালুকে নির্দেশকারী একটি box-এর ইনস্ট্যান্স হিসাবে সেট করেছি। শেষ assertion-এ, আমরা box-এর pointer অনুসরণ করতে dereference অপারেটর ব্যবহার করতে পারি, ঠিক সেভাবেই যেভাবে আমরা y
যখন একটি reference ছিল তখন করেছিলাম। এরপর, আমরা দেখব Box<T>
-এর মধ্যে বিশেষ কী আছে যা আমাদের নিজস্ব box টাইপ সংজ্ঞায়িত করে dereference অপারেটর ব্যবহার করতে সক্ষম করে।
আমাদের নিজস্ব Smart Pointer তৈরি করা
চলুন, স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত Box<T>
টাইপের মতো একটি wrapper টাইপ তৈরি করি যাতে ডিফল্টভাবে smart pointer টাইপগুলো reference-এর থেকে কীভাবে ভিন্ন আচরণ করে তা অনুভব করা যায়। তারপর আমরা দেখব কীভাবে dereference অপারেটর ব্যবহার করার ক্ষমতা যোগ করা যায়।
দ্রষ্টব্য: আমরা যে
MyBox<T>
টাইপটি তৈরি করতে যাচ্ছি এবং আসলBox<T>
-এর মধ্যে একটি বড় পার্থক্য আছে: আমাদের সংস্করণটি তার ডেটা হিপ-এ সংরক্ষণ করবে না। আমরা এই উদাহরণেDeref
-এর উপর ফোকাস করছি, তাই ডেটা আসলে কোথায় সংরক্ষিত আছে তা pointer-এর মতো আচরণের চেয়ে কম গুরুত্বপূর্ণ।
Box<T>
টাইপটি মূলত একটি একটি উপাদান সহ একটি tuple struct হিসাবে সংজ্ঞায়িত করা হয়েছে, তাই Listing 15-8 একই ভাবে একটি MyBox<T>
টাইপ সংজ্ঞায়িত করে। আমরা Box<T>
-তে সংজ্ঞায়িত new
ফাংশনের সাথে মেলানোর জন্য একটি new
ফাংশনও সংজ্ঞায়িত করব।
struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() {}
আমরা MyBox
নামে একটি struct সংজ্ঞায়িত করি এবং একটি জেনেরিক প্যারামিটার T
ঘোষণা করি কারণ আমরা চাই আমাদের টাইপ যেকোনো টাইপের ভ্যালু ধারণ করুক। MyBox
টাইপটি T
টাইপের একটি উপাদান সহ একটি tuple struct। MyBox::new
ফাংশনটি T
টাইপের একটি প্যারামিটার নেয় এবং পাস করা ভ্যালুটি ধারণকারী একটি MyBox
ইনস্ট্যান্স রিটার্ন করে।
চলুন Listing 15-7-এর main
ফাংশনটি Listing 15-8-এ যোগ করার চেষ্টা করি এবং এটিকে Box<T>
-এর পরিবর্তে আমাদের সংজ্ঞায়িত MyBox<T>
টাইপ ব্যবহার করার জন্য পরিবর্তন করি। Listing 15-9-এর কোডটি কম্পাইল হবে না কারণ রাস্ট জানে না কীভাবে MyBox
-কে dereference করতে হয়।
struct MyBox<T>(T);
impl<T> MyBox<T> {
fn new(x: T) -> MyBox<T> {
MyBox(x)
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
এর ফলে এই কম্পাইলেশন এররটি আসে:
$ cargo run
Compiling deref-example v0.1.0 (file:///projects/deref-example)
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
--> src/main.rs:14:19
|
14 | assert_eq!(5, *y);
| ^^
For more information about this error, try `rustc --explain E0614`.
error: could not compile `deref-example` (bin "deref-example") due to 1 previous error
আমাদের MyBox<T>
টাইপটি dereference করা যায় না কারণ আমরা আমাদের টাইপের উপর সেই ক্ষমতা ইমপ্লিমেন্ট করিনি। *
অপারেটর দিয়ে dereferencing সক্ষম করতে, আমরা Deref
ট্রেইট ইমপ্লিমেন্ট করি।
Deref
Trait ইমপ্লিমেন্ট করা
Chapter 10-এর ["Implementing a Trait on a Type"][impl-trait] অংশে যেমন আলোচনা করা হয়েছে, একটি ট্রেইট ইমপ্লিমেন্ট করার জন্য আমাদের ট্রেইটের প্রয়োজনীয় মেথডগুলির জন্য ইমপ্লিমেন্টেশন সরবরাহ করতে হবে। স্ট্যান্ডার্ড লাইব্রেরি দ্বারা প্রদত্ত Deref
ট্রেইটের জন্য আমাদের deref
নামে একটি মেথড ইমপ্লিমেন্ট করতে হবে যা self
borrow করে এবং ভেতরের ডেটার একটি reference রিটার্ন করে। Listing 15-10-এ MyBox<T>
-এর সংজ্ঞায় যোগ করার জন্য Deref
-এর একটি ইমপ্লিমেন্টেশন রয়েছে।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn main() { let x = 5; let y = MyBox::new(x); assert_eq!(5, x); assert_eq!(5, *y); }
type Target = T;
সিনট্যাক্সটি Deref
ট্রেইটের ব্যবহারের জন্য একটি associated type সংজ্ঞায়িত করে। Associated type গুলো একটি জেনেরিক প্যারামিটার ঘোষণা করার একটি সামান্য ভিন্ন উপায়, কিন্তু আপনার এখন এগুলি নিয়ে চিন্তা করার দরকার নেই; আমরা Chapter 20-এ এগুলি সম্পর্কে আরও বিস্তারিতভাবে আলোচনা করব।
আমরা deref
মেথডের বডি &self.0
দিয়ে পূরণ করি যাতে deref
সেই ভ্যালুর একটি reference রিটার্ন করে যা আমরা *
অপারেটর দিয়ে অ্যাক্সেস করতে চাই; Chapter 5-এর ["Using Tuple Structs Without Named Fields to Create Different Types"][tuple-structs] থেকে স্মরণ করুন যে .0
একটি tuple struct-এর প্রথম ভ্যালু অ্যাক্সেস করে। Listing 15-9-এর main
ফাংশন যা MyBox<T>
ভ্যালুর উপর *
কল করে, তা এখন কম্পাইল হয় এবং assertion গুলো পাস করে!
Deref
ট্রেইট ছাড়া, কম্পাইলার শুধুমাত্র &
reference-গুলো dereference করতে পারে। deref
মেথড কম্পাইলারকে যেকোনো টাইপের ভ্যালু যা Deref
ইমপ্লিমেন্ট করে তা নিয়ে deref
মেথড কল করে একটি &
reference পাওয়ার ক্ষমতা দেয়, যা সে dereference করতে জানে।
যখন আমরা Listing 15-9-এ *y
লিখেছিলাম, পর্দার আড়ালে রাস্ট আসলে এই কোডটি রান করেছে:
*(y.deref())
রাস্ট *
অপারেটরটিকে deref
মেথডে একটি কল এবং তারপর একটি সাধারণ dereference দিয়ে প্রতিস্থাপন করে যাতে আমাদের deref
মেথড কল করার প্রয়োজন আছে কি না তা নিয়ে ভাবতে না হয়। রাস্টের এই ফিচারটি আমাদের এমন কোড লিখতে দেয় যা একইভাবে কাজ করে, আমাদের কাছে একটি সাধারণ reference থাকুক বা Deref
ইমপ্লিমেন্ট করা একটি টাইপ থাকুক।
deref
মেথড কেন একটি ভ্যালুর reference রিটার্ন করে, এবং *(y.deref())
-এর বন্ধনীর বাইরের সাধারণ dereference কেন এখনও প্রয়োজনীয়, তার কারণ ownership সিস্টেমের সাথে সম্পর্কিত। যদি deref
মেথড ভ্যালুর reference-এর পরিবর্তে সরাসরি ভ্যালুটি রিটার্ন করত, তাহলে ভ্যালুটি self
থেকে মুভ (move) হয়ে যেত। আমরা এই ক্ষেত্রে বা বেশিরভাগ ক্ষেত্রে যেখানে আমরা dereference অপারেটর ব্যবহার করি সেখানে MyBox<T>
-এর ভেতরের ভ্যালুর ownership নিতে চাই না।
মনে রাখবেন যে *
অপারেটরটি deref
মেথডে একটি কল এবং তারপর *
অপারেটরে একটি কল দ্বারা প্রতিস্থাপিত হয়, প্রতিবার যখন আমরা আমাদের কোডে একটি *
ব্যবহার করি। যেহেতু *
অপারেটরের প্রতিস্থাপন অসীমভাবে পুনরাবৃত্তি হয় না, তাই আমরা i32
টাইপের ডেটা পাই, যা Listing 15-9-এর assert_eq!
-তে 5
-এর সাথে মেলে।
ফাংশন এবং মেথডে স্বয়ংক্রিয় Deref Coercion
Deref coercion এমন একটি টাইপের reference-কে যা Deref
ট্রেইট ইমপ্লিমেন্ট করে, অন্য একটি টাইপের reference-এ রূপান্তরিত করে। উদাহরণস্বরূপ, deref coercion &String
-কে &str
-এ রূপান্তরিত করতে পারে কারণ String
Deref
ট্রেইট এমনভাবে ইমপ্লিমেন্ট করে যা &str
রিটার্ন করে। Deref coercion একটি সুবিধা যা রাস্ট ফাংশন এবং মেথডের আর্গুমেন্টের উপর প্রয়োগ করে এবং এটি শুধুমাত্র সেইসব টাইপের উপর কাজ করে যা Deref
ট্রেইট ইমপ্লিমেন্ট করে। এটি স্বয়ংক্রিয়ভাবে ঘটে যখন আমরা একটি নির্দিষ্ট টাইপের ভ্যালুর reference একটি ফাংশন বা মেথডের আর্গুমেন্ট হিসাবে পাস করি যা ফাংশন বা মেথড সংজ্ঞার প্যারামিটার টাইপের সাথে মেলে না। deref
মেথডে একাধিক কলের একটি ক্রম আমাদের দেওয়া টাইপটিকে প্যারামিটারের প্রয়োজনীয় টাইপে রূপান্তরিত করে।
Deref coercion রাস্ট-এ যোগ করা হয়েছিল যাতে ফাংশন এবং মেথড কল লেখার সময় প্রোগ্রামারদের &
এবং *
দিয়ে অনেক বেশি সুস্পষ্ট reference এবং dereference যোগ করার প্রয়োজন না হয়। deref coercion ফিচারটি আমাদের আরও বেশি কোড লিখতে দেয় যা reference বা smart pointer উভয়ের জন্য কাজ করতে পারে।
Deref coercion বাস্তবে দেখতে, আসুন আমরা Listing 15-8-এ সংজ্ঞায়িত MyBox<T>
টাইপ এবং Listing 15-10-এ যোগ করা Deref
-এর ইমপ্লিমেন্টেশন ব্যবহার করি। Listing 15-11 একটি ফাংশনের সংজ্ঞা দেখায় যার একটি স্ট্রিং স্লাইস প্যারামিটার রয়েছে।
fn hello(name: &str) { println!("Hello, {name}!"); } fn main() {}
আমরা hello
ফাংশনটিকে একটি স্ট্রিং স্লাইস আর্গুমেন্ট দিয়ে কল করতে পারি, যেমন hello("Rust");
। Deref coercion hello
-কে MyBox<String>
টাইপের একটি ভ্যালুর reference দিয়ে কল করা সম্ভব করে, যেমনটি Listing 15-12-এ দেখানো হয়েছে।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&m); }
এখানে আমরা hello
ফাংশনটিকে &m
আর্গুমেন্ট দিয়ে কল করছি, যা একটি MyBox<String>
ভ্যালুর একটি reference। যেহেতু আমরা Listing 15-10-এ MyBox<T>
-এর উপর Deref
ট্রেইট ইমপ্লিমেন্ট করেছি, তাই রাস্ট deref
কল করে &MyBox<String>
-কে &String
-এ পরিণত করতে পারে। স্ট্যান্ডার্ড লাইব্রেরি String
-এর উপর Deref
-এর একটি ইমপ্লিমেন্টেশন প্রদান করে যা একটি স্ট্রিং স্লাইস রিটার্ন করে, এবং এটি Deref
-এর API ডকুমেন্টেশনে রয়েছে। রাস্ট &String
-কে &str
-এ পরিণত করতে আবার deref
কল করে, যা hello
ফাংশনের সংজ্ঞার সাথে মেলে।
যদি রাস্ট deref coercion ইমপ্লিমেন্ট না করত, তাহলে hello
-কে &MyBox<String>
টাইপের একটি ভ্যালু দিয়ে কল করার জন্য আমাদের Listing 15-12-এর কোডের পরিবর্তে Listing 15-13-এর কোডটি লিখতে হতো।
use std::ops::Deref; impl<T> Deref for MyBox<T> { type Target = T; fn deref(&self) -> &T { &self.0 } } struct MyBox<T>(T); impl<T> MyBox<T> { fn new(x: T) -> MyBox<T> { MyBox(x) } } fn hello(name: &str) { println!("Hello, {name}!"); } fn main() { let m = MyBox::new(String::from("Rust")); hello(&(*m)[..]); }
(*m)
MyBox<String>
-কে dereference করে একটি String
-এ পরিণত করে। তারপর &
এবং [..]
String
-এর একটি স্ট্রিং স্লাইস নেয় যা hello
-এর সিগনেচারের সাথে মেলানোর জন্য পুরো স্ট্রিংয়ের সমান। এই সমস্ত চিহ্ন জড়িত থাকার কারণে deref coercion ছাড়া এই কোডটি পড়া, লেখা এবং বোঝা কঠিন। Deref coercion রাস্টকে এই রূপান্তরগুলি আমাদের জন্য স্বয়ংক্রিয়ভাবে পরিচালনা করার অনুমতি দেয়।
যখন জড়িত টাইপগুলির জন্য Deref
ট্রেইট সংজ্ঞায়িত করা হয়, রাস্ট টাইপগুলি বিশ্লেষণ করবে এবং প্যারামিটারের টাইপের সাথে মেলানোর জন্য একটি reference পেতে যতবার প্রয়োজন Deref::deref
ব্যবহার করবে। Deref::deref
কতবার সন্নিবেশ করা প্রয়োজন তা কম্পাইল টাইমে সমাধান করা হয়, তাই deref coercion-এর সুবিধা নেওয়ার জন্য কোনো রানটাইম পেনাল্টি নেই!
Deref Coercion এবং Mutability-র সম্পর্ক
আপনি যেভাবে immutable reference-এর উপর *
অপারেটর ওভাররাইড করতে Deref
ট্রেইট ব্যবহার করেন, সেভাবেই আপনি mutable reference-এর উপর *
অপারেটর ওভাররাইড করতে DerefMut
ট্রেইট ব্যবহার করতে পারেন।
রাস্ট তিনটি ক্ষেত্রে deref coercion করে যখন এটি টাইপ এবং ট্রেইট ইমপ্লিমেন্টেশন খুঁজে পায়:
&T
থেকে&U
যখনT: Deref<Target=U>
&mut T
থেকে&mut U
যখনT: DerefMut<Target=U>
&mut T
থেকে&U
যখনT: Deref<Target=U>
প্রথম দুটি ক্ষেত্র একই, শুধুমাত্র দ্বিতীয়টি mutability ইমপ্লিমেন্ট করে। প্রথম ক্ষেত্রটি বলে যে যদি আপনার কাছে একটি &T
থাকে, এবং T
কোনো টাইপ U
-এর জন্য Deref
ইমপ্লিমেন্ট করে, আপনি স্বচ্ছভাবে একটি &U
পেতে পারেন। দ্বিতীয় ক্ষেত্রটি বলে যে mutable reference-এর জন্য একই deref coercion ঘটে।
তৃতীয় ক্ষেত্রটি আরও জটিল: রাস্ট একটি mutable reference-কে একটি immutable reference-এও রূপান্তর করবে। কিন্তু এর বিপরীতটি সম্ভব নয়: immutable reference কখনও mutable reference-এ রূপান্তরিত হবে না। borrowing-এর নিয়ম অনুযায়ী, যদি আপনার কাছে একটি mutable reference থাকে, তবে সেই mutable reference-টি অবশ্যই সেই ডেটার একমাত্র reference হতে হবে (অন্যথায়, প্রোগ্রামটি কম্পাইল হবে না)। একটি mutable reference-কে একটি immutable reference-এ রূপান্তরিত করলে borrowing-এর নিয়ম কখনও ভাঙবে না। একটি immutable reference-কে একটি mutable reference-এ রূপান্তরিত করার জন্য প্রয়োজন হবে যে প্রাথমিক immutable reference-টি সেই ডেটার একমাত্র immutable reference, কিন্তু borrowing-এর নিয়ম তার নিশ্চয়তা দেয় না। অতএব, রাস্ট এই ধারণা করতে পারে না যে একটি immutable reference-কে একটি mutable reference-এ রূপান্তরিত করা সম্ভব।