বিভিন্ন টাইপের ভ্যালুর জন্য ট্রেইট অবজেক্ট ব্যবহার করা (Using Trait Objects That Allow for Values of Different Types)
আমরা Chapter 8-এ উল্লেখ করেছি যে ভেক্টরের একটি সীমাবদ্ধতা হল যে তারা শুধুমাত্র একটি টাইপের এলিমেন্ট সংরক্ষণ করতে পারে। আমরা Listing 8-9-এ একটি ওয়ার্কঅ্যারাউন্ড তৈরি করেছি যেখানে আমরা একটি SpreadsheetCell
এনাম সংজ্ঞায়িত করেছি যাতে ইন্টিজার, ফ্লোট এবং টেক্সট ধারণ করার জন্য ভেরিয়েন্ট ছিল। এর মানে হল যে আমরা প্রতিটি সেলে বিভিন্ন ধরনের ডেটা সংরক্ষণ করতে পারি এবং তবুও একটি ভেক্টর রাখতে পারি যা সারির সেলগুলিকে উপস্থাপন করে। এটি একটি উপযুক্ত সমাধান যখন আমাদের বিনিময়যোগ্য আইটেমগুলি টাইপের একটি নির্দিষ্ট সেট হয় যা আমাদের কোড কম্পাইল করার সময় আমরা জানি।
যাইহোক, কখনও কখনও আমরা চাই যে আমাদের লাইব্রেরি ব্যবহারকারী কোনও নির্দিষ্ট পরিস্থিতিতে বৈধ টাইপের সেট প্রসারিত করতে সক্ষম হোক। এটি কীভাবে অর্জন করা যেতে পারে তা দেখানোর জন্য, আমরা একটি গ্রাফিক্যাল ইউজার ইন্টারফেস (GUI) টুলের উদাহরণ তৈরি করব যা আইটেমগুলির একটি তালিকার মধ্যে পুনরাবৃত্তি করে, প্রতিটি আইটেমকে স্ক্রিনে আঁকার জন্য একটি draw
মেথড কল করে—GUI টুলগুলির জন্য এটি একটি সাধারণ কৌশল। আমরা gui
নামে একটি লাইব্রেরি ক্রেট তৈরি করব যাতে একটি GUI লাইব্রেরির গঠন থাকবে। এই ক্রেটে ব্যবহারকারীদের ব্যবহারের জন্য কিছু টাইপ থাকতে পারে, যেমন Button
বা TextField
। এছাড়াও, gui
ব্যবহারকারীরা তাদের নিজস্ব টাইপ তৈরি করতে চাইবে যা আঁকা যায়: উদাহরণস্বরূপ, একজন প্রোগ্রামার একটি Image
যোগ করতে পারে এবং অন্যজন একটি SelectBox
যোগ করতে পারে।
আমরা এই উদাহরণের জন্য সম্পূর্ণরূপে বিকশিত GUI লাইব্রেরি বাস্তবায়ন করব না কিন্তু দেখাব কিভাবে অংশগুলি একসাথে ফিট হবে। লাইব্রেরি লেখার সময়, আমরা জানতে এবং সংজ্ঞায়িত করতে পারি না যে অন্য প্রোগ্রামাররা কী কী টাইপ তৈরি করতে চাইতে পারে। কিন্তু আমরা জানি যে gui
-কে বিভিন্ন টাইপের অনেকগুলি মানের ট্র্যাক রাখতে হবে এবং এই ভিন্ন টাইপের মানগুলির প্রত্যেকটিতে একটি draw
মেথড কল করতে হবে। আমরা যখন draw
মেথড কল করি তখন ঠিক কী ঘটবে তা জানার দরকার নেই, শুধুমাত্র এইটুকু জানলেই হবে যে মানটিতে সেই মেথডটি আমাদের কল করার জন্য উপলব্ধ থাকবে।
ইনহেরিটেন্স সহ একটি ভাষায় এটি করার জন্য, আমরা Component
নামে একটি ক্লাস সংজ্ঞায়িত করতে পারি যার উপর draw
নামে একটি মেথড রয়েছে। অন্যান্য ক্লাস, যেমন Button
, Image
, এবং SelectBox
, Component
থেকে ইনহেরিট করবে এবং এইভাবে draw
মেথড ইনহেরিট করবে। তারা প্রত্যেকে তাদের নিজস্ব আচরণ সংজ্ঞায়িত করতে draw
মেথডটিকে ওভাররাইড করতে পারে, কিন্তু ফ্রেমওয়ার্কটি সমস্ত টাইপকে Component
ইনস্ট্যান্সের মতো আচরণ করতে পারে এবং তাদের উপর draw
কল করতে পারে। কিন্তু যেহেতু Rust-এ ইনহেরিটেন্স নেই, তাই আমাদের gui
লাইব্রেরিটিকে গঠন করার জন্য অন্য একটি উপায় প্রয়োজন যাতে ব্যবহারকারীরা এটিকে নতুন টাইপ দিয়ে প্রসারিত করতে পারে।
সাধারণ আচরণের জন্য একটি ট্রেইট সংজ্ঞায়িত করা (Defining a Trait for Common Behavior)
আমরা gui
-এর যে আচরণ চাই তা বাস্তবায়ন করতে, আমরা Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করব যাতে draw
নামে একটি মেথড থাকবে। তারপর আমরা একটি ভেক্টর সংজ্ঞায়িত করতে পারি যা একটি ট্রেইট অবজেক্ট নেয়। একটি ট্রেইট অবজেক্ট আমাদের নির্দিষ্ট করা trait ইমপ্লিমেন্ট করা টাইপের একটি ইনস্ট্যান্স এবং রানটাইমে সেই টাইপের ট্রেইট মেথডগুলি সন্ধান করার জন্য ব্যবহৃত একটি টেবিল উভয়ের দিকে নির্দেশ করে। আমরা কিছু ধরনের পয়েন্টার, যেমন একটি &
রেফারেন্স বা একটি Box<T>
স্মার্ট পয়েন্টার, তারপর dyn
কীওয়ার্ড এবং তারপর প্রাসঙ্গিক trait উল্লেখ করে একটি ট্রেইট অবজেক্ট তৈরি করি। (আমরা Chapter 20-এর “Dynamically Sized Types and the Sized
Trait”-এ ট্রেইট অবজেক্টগুলিকে কেন একটি পয়েন্টার ব্যবহার করতে হবে সে সম্পর্কে কথা বলব।) আমরা জেনেরিক বা কংক্রিট টাইপের পরিবর্তে ট্রেইট অবজেক্ট ব্যবহার করতে পারি। আমরা যেখানেই একটি ট্রেইট অবজেক্ট ব্যবহার করি, Rust-এর টাইপ সিস্টেম কম্পাইল করার সময় নিশ্চিত করবে যে সেই প্রসঙ্গে ব্যবহৃত যেকোনো মান ট্রেইট অবজেক্টের trait ইমপ্লিমেন্ট করবে। ফলস্বরূপ, আমাদের কম্পাইল করার সময় সমস্ত সম্ভাব্য টাইপ জানার প্রয়োজন নেই।
আমরা উল্লেখ করেছি যে, Rust-এ, আমরা স্ট্রাক্ট এবং এনামগুলিকে “অবজেক্ট” বলা থেকে বিরত থাকি যাতে সেগুলিকে অন্যান্য ভাষার অবজেক্ট থেকে আলাদা করা যায়। একটি স্ট্রাক্ট বা এনামে, স্ট্রাক্ট ফিল্ডের ডেটা এবং impl
ব্লকের আচরণ আলাদা করা হয়, যেখানে অন্য ভাষাগুলিতে, ডেটা এবং আচরণকে একত্রিত করে একটি ধারণাকে প্রায়শই অবজেক্ট লেবেল করা হয়। যাইহোক, ট্রেইট অবজেক্টগুলি অন্যান্য ভাষার অবজেক্টের মতো, এই অর্থে যে তারা ডেটা এবং আচরণকে একত্রিত করে। কিন্তু ট্রেইট অবজেক্টগুলি ঐতিহ্যগত অবজেক্ট থেকে আলাদা যে আমরা একটি ট্রেইট অবজেক্টে ডেটা যোগ করতে পারি না। ট্রেইট অবজেক্টগুলি অন্যান্য ভাষার অবজেক্টের মতো সাধারণভাবে দরকারী নয়: তাদের নির্দিষ্ট উদ্দেশ্য হল সাধারণ আচরণ জুড়ে অ্যাবস্ট্রাকশনকে অনুমতি দেওয়া।
Listing 18-3 দেখায় কিভাবে draw
নামে একটি মেথড সহ Draw
নামে একটি ট্রেইট সংজ্ঞায়িত করতে হয়:
pub trait Draw {
fn draw(&self);
}
এই সিনট্যাক্সটি Chapter 10-এ ট্রেইট সংজ্ঞায়িত করার বিষয়ে আমাদের আলোচনার মতোই পরিচিত। এরপর কিছু নতুন সিনট্যাক্স আসে: Listing 18-4 Screen
নামক একটি স্ট্রাক্ট সংজ্ঞায়িত করে যাতে components
নামক একটি ভেক্টর থাকে। এই ভেক্টরটি Box<dyn Draw>
টাইপের, যা একটি ট্রেইট অবজেক্ট; এটি একটি Box
-এর ভিতরের যেকোনো টাইপের জন্য একটি স্ট্যান্ড-ইন যা Draw
trait ইমপ্লিমেন্ট করে।
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen
স্ট্রাক্টে, আমরা run
নামে একটি মেথড সংজ্ঞায়িত করব যা তার প্রতিটি components
-এ draw
মেথড কল করবে, যেমনটি Listing 18-5-এ দেখানো হয়েছে:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি ট্রেইট বাউন্ড সহ একটি জেনেরিক টাইপ প্যারামিটার ব্যবহার করে এমন একটি স্ট্রাক্ট সংজ্ঞায়িত করার চেয়ে আলাদাভাবে কাজ করে। একটি জেনেরিক টাইপ প্যারামিটার একবারে শুধুমাত্র একটি কংক্রিট টাইপ দিয়ে প্রতিস্থাপিত করা যেতে পারে, যেখানে ট্রেইট অবজেক্টগুলি রানটাইমে ট্রেইট অবজেক্টের জন্য একাধিক কংক্রিট টাইপ পূরণ করার অনুমতি দেয়। উদাহরণস্বরূপ, আমরা Listing 18-6-এর মতো একটি জেনেরিক টাইপ এবং একটি ট্রেইট বাউন্ড ব্যবহার করে Screen
স্ট্রাক্ট সংজ্ঞায়িত করতে পারতাম:
pub trait Draw {
fn draw(&self);
}
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
এটি আমাদের একটি Screen
ইনস্ট্যান্সে সীমাবদ্ধ করে যেখানে সমস্ত কম্পোনেন্ট হয় Button
টাইপের অথবা সমস্ত TextField
টাইপের। আপনি যদি শুধুমাত্র হোমোজিনিয়াস কালেকশন রাখতে চান, তাহলে জেনেরিক এবং ট্রেইট বাউন্ড ব্যবহার করা বাঞ্ছনীয় কারণ সংজ্ঞাগুলি কংক্রিট টাইপ ব্যবহার করার জন্য কম্পাইল করার সময় মনোমরফাইজ করা হবে।
অন্যদিকে, ট্রেইট অবজেক্ট ব্যবহার করে মেথডের সাহায্যে, একটি Screen
ইনস্ট্যান্স একটি Vec<T>
ধারণ করতে পারে যাতে একটি Box<Button>
এবং সেইসাথে একটি Box<TextField>
থাকে। আসুন দেখি এটি কীভাবে কাজ করে এবং তারপরে আমরা রানটাইম পারফরম্যান্সের প্রভাব সম্পর্কে কথা বলব।
ট্রেইট ইমপ্লিমেন্ট করা (Implementing the Trait)
এখন আমরা কিছু টাইপ যোগ করব যা Draw
trait ইমপ্লিমেন্ট করে। আমরা Button
টাইপ সরবরাহ করব। আবারও, একটি GUI লাইব্রেরি বাস্তবায়ন করা এই বইয়ের সুযোগের বাইরে, তাই draw
মেথডের বডিতে কোনও দরকারী ইমপ্লিমেন্টেশন থাকবে না। ইমপ্লিমেন্টেশনটি দেখতে কেমন হতে পারে তা কল্পনা করার জন্য, একটি Button
স্ট্রাক্টে width
, height
এবং label
-এর জন্য ফিল্ড থাকতে পারে, যেমনটি Listing 18-7-এ দেখানো হয়েছে:
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
Button
-এর width
, height
এবং label
ফিল্ডগুলি অন্যান্য কম্পোনেন্টের ফিল্ড থেকে আলাদা হবে; উদাহরণস্বরূপ, একটি TextField
টাইপের সেই একই ফিল্ড এবং একটি placeholder
ফিল্ড থাকতে পারে। আমরা স্ক্রিনে যে টাইপগুলি আঁকতে চাই তার প্রত্যেকটি Draw
trait ইমপ্লিমেন্ট করবে কিন্তু সেই নির্দিষ্ট টাইপটি কীভাবে আঁকতে হয় তা সংজ্ঞায়িত করতে draw
মেথডে ভিন্ন কোড ব্যবহার করবে, যেমনটি Button
এখানে করেছে (উল্লেখিত প্রকৃত GUI কোড ছাড়া)। উদাহরণস্বরূপ, Button
টাইপের একটি অতিরিক্ত impl
ব্লক থাকতে পারে যাতে একজন ব্যবহারকারী বাটনে ক্লিক করলে কী ঘটে তার সাথে সম্পর্কিত মেথড থাকতে পারে। এই ধরনের মেথড TextField
-এর মতো টাইপের ক্ষেত্রে প্রযোজ্য হবে না।
যদি আমাদের লাইব্রেরি ব্যবহার করে এমন কেউ SelectBox
স্ট্রাক্ট ইমপ্লিমেন্ট করার সিদ্ধান্ত নেয় যাতে width
, height
এবং options
ফিল্ড থাকে, তাহলে তারা SelectBox
টাইপেও Draw
trait ইমপ্লিমেন্ট করে, যেমনটি Listing 18-8-এ দেখানো হয়েছে:
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
fn main() {}
আমাদের লাইব্রেরির ব্যবহারকারী এখন একটি Screen
ইনস্ট্যান্স তৈরি করতে তাদের main
ফাংশন লিখতে পারে। Screen
ইনস্ট্যান্সে, তারা প্রতিটি Box<T>
-তে রেখে একটি SelectBox
এবং একটি Button
যোগ করতে পারে যাতে সেগুলি ট্রেইট অবজেক্টে পরিণত হয়। তারপর তারা Screen
ইনস্ট্যান্সে run
মেথড কল করতে পারে, যা প্রতিটি কম্পোনেন্টে draw
কল করবে। Listing 18-9 এই ইমপ্লিমেন্টেশনটি দেখায়:
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// code to actually draw a select box
}
}
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
যখন আমরা লাইব্রেরিটি লিখেছিলাম, তখন আমরা জানতাম না যে কেউ SelectBox
টাইপ যোগ করতে পারে, কিন্তু আমাদের Screen
ইমপ্লিমেন্টেশন নতুন টাইপের উপর কাজ করতে এবং এটিকে আঁকতে সক্ষম হয়েছিল কারণ SelectBox
Draw
trait ইমপ্লিমেন্ট করে, যার মানে এটি draw
মেথড ইমপ্লিমেন্ট করে।
এই ধারণা—একটি মানের কংক্রিট টাইপের পরিবর্তে শুধুমাত্র সেই মানটি যে মেসেজগুলির প্রতিক্রিয়া জানায় সেগুলির প্রতি যত্নশীল হওয়া—ডায়নামিকালি টাইপ করা ভাষাগুলিতে ডাক টাইপিং-এর ধারণার অনুরূপ: যদি এটি হাঁসের মতো হাঁটে এবং হাঁসের মতো ডাকে, তাহলে এটি অবশ্যই একটি হাঁস! Listing 18-5-এ Screen
-এ run
-এর ইমপ্লিমেন্টেশনে, run
-এর প্রতিটি কম্পোনেন্টের কংক্রিট টাইপ কী তা জানার প্রয়োজন নেই। এটি পরীক্ষা করে না যে কোনও কম্পোনেন্ট একটি Button
বা একটি SelectBox
-এর ইনস্ট্যান্স কিনা, এটি কেবল কম্পোনেন্টের draw
মেথড কল করে। components
ভেক্টরের মানগুলির টাইপ হিসাবে Box<dyn Draw>
উল্লেখ করে, আমরা Screen
-কে এমন মানগুলির প্রয়োজন হিসাবে সংজ্ঞায়িত করেছি যেগুলিতে আমরা draw
মেথড কল করতে পারি।
ট্রেইট অবজেক্ট এবং Rust-এর টাইপ সিস্টেম ব্যবহার করে ডাক টাইপিং ব্যবহার করে কোডের মতো কোড লেখার সুবিধা হল যে আমাদের কখনই রানটাইমে কোনও মান কোনও নির্দিষ্ট মেথড ইমপ্লিমেন্ট করে কিনা তা পরীক্ষা করতে হবে না বা কোনও মান একটি মেথড ইমপ্লিমেন্ট না করলে এবং আমরা এটি কল করলেও error পাওয়ার বিষয়ে চিন্তা করতে হবে না। যদি মানগুলি ট্রেইট অবজেক্টের প্রয়োজনীয় ট্রেইটগুলি ইমপ্লিমেন্ট না করে তবে Rust আমাদের কোড কম্পাইল করবে না।
উদাহরণস্বরূপ, Listing 18-10 দেখায় যে আমরা যদি একটি কম্পোনেন্ট হিসাবে একটি String
সহ একটি Screen
তৈরি করার চেষ্টা করি তাহলে কী ঘটে:
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
আমরা এই error টি পাব কারণ String
Draw
trait ইমপ্লিমেন্ট করে না:
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` (bin "gui") due to 1 previous error
এই error টি আমাদের জানায় যে হয় আমরা Screen
-এ এমন কিছু পাস করছি যা পাস করার কথা ছিল না এবং তাই অন্য একটি টাইপ পাস করা উচিত অথবা আমাদের String
-এ Draw
ইমপ্লিমেন্ট করা উচিত যাতে Screen
এটিতে draw
কল করতে পারে।
ট্রেইট অবজেক্ট ডায়নামিক ডিসপ্যাচ সম্পাদন করে (Trait Objects Perform Dynamic Dispatch)
Chapter 10-এর “Performance of Code Using Generics”-এ জেনেরিক্সে সম্পাদিত মনোমরফাইজেশন প্রক্রিয়া সম্পর্কে আমাদের আলোচনা স্মরণ করুন: কম্পাইলার প্রতিটি কংক্রিট টাইপের জন্য ফাংশন এবং মেথডগুলির ননজেনরিক ইমপ্লিমেন্টেশন তৈরি করে যা আমরা জেনেরিক টাইপ প্যারামিটারের পরিবর্তে ব্যবহার করি। মনোমরফাইজেশন থেকে প্রাপ্ত কোডটি স্ট্যাটিক ডিসপ্যাচ করে, যখন কম্পাইলার কম্পাইল করার সময় জানে আপনি কোন মেথড কল করছেন। এটি ডায়নামিক ডিসপ্যাচ-এর বিপরীত, যখন কম্পাইলার কম্পাইল করার সময় বলতে পারে না আপনি কোন মেথড কল করছেন। ডায়নামিক ডিসপ্যাচের ক্ষেত্রে, কম্পাইলার এমন কোড নির্গত করে যা রানটাইমে কোন মেথড কল করতে হবে তা নির্ধারণ করবে।
যখন আমরা ট্রেইট অবজেক্ট ব্যবহার করি, তখন Rust-কে অবশ্যই ডায়নামিক ডিসপ্যাচ ব্যবহার করতে হবে। কম্পাইলার সমস্ত টাইপ জানে না যা ট্রেইট অবজেক্ট ব্যবহার করা কোডের সাথে ব্যবহার করা যেতে পারে, তাই এটি জানে না কোন টাইপে ইমপ্লিমেন্ট করা কোন মেথড কল করতে হবে। পরিবর্তে, রানটাইমে, Rust কোন মেথড কল করতে হবে তা জানতে ট্রেইট অবজেক্টের ভিতরের পয়েন্টারগুলি ব্যবহার করে। এই লুকআপে একটি রানটাইম খরচ হয় যা স্ট্যাটিক ডিসপ্যাচের সাথে ঘটে না। ডায়নামিক ডিসপ্যাচ কম্পাইলারকে একটি মেথডের কোড ইনলাইন করা থেকে বিরত রাখে, যা ফলস্বরূপ কিছু অপটিমাইজেশন প্রতিরোধ করে এবং Rust-এর কিছু নিয়ম রয়েছে যে আপনি কোথায় এবং কোথায় ডায়নামিক ডিসপ্যাচ ব্যবহার করতে পারবেন এবং পারবেন না, যাকে ডাইন কম্প্যাটিবিলিটি বলা হয়। যাইহোক, আমরা Listing 18-5-এ যে কোডটি লিখেছিলাম এবং Listing 18-9-এ সমর্থন করতে সক্ষম হয়েছিলাম তাতে অতিরিক্ত নমনীয়তা পেয়েছি, তাই এটি বিবেচনা করার মতো একটি ট্রেড-অফ।