মেথড সিনট্যাক্স (Method Syntax)
মেথড (Methods) অনেকটা ফাংশনের মতোই: আমরা fn
কীওয়ার্ড এবং একটি নাম দিয়ে এগুলো ডিক্লেয়ার করি, এগুলোর প্যারামিটার এবং একটি রিটার্ন ভ্যালু থাকতে পারে, এবং এগুলোর মধ্যে কিছু কোড থাকে যা অন্য কোথাও থেকে মেথডটি কল করা হলে রান হয়। ফাংশনের সাথে এর পার্থক্য হলো, মেথডগুলো একটি struct
(অথবা একটি enum
বা একটি trait
অবজেক্ট, যা আমরা যথাক্রমে অধ্যায় ৬ এবং অধ্যায় ১৮-এ আলোচনা করব) এর কনটেক্সটে ডিফাইন করা হয়, এবং তাদের প্রথম প্যারামিটার সবসময় self
হয়, যা struct
-এর সেই ইনস্ট্যান্সটিকে প্রতিনিধিত্ব করে যার উপর মেথডটি কল করা হচ্ছে।
মেথড ডিফাইন করা (Defining Methods)
আসুন area
ফাংশনটিকে পরিবর্তন করি, যেটি একটি Rectangle
ইনস্ট্যান্সকে প্যারামিটার হিসেবে নেয়, এবং এর পরিবর্তে Rectangle
struct
-এর উপর ডিফাইন করা একটি area
মেথড তৈরি করি, যেমনটি তালিকা ৫-১৩-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() ); }
Rectangle
-এর কনটেক্সটে ফাংশনটি ডিফাইন করার জন্য, আমরা Rectangle
-এর জন্য একটি impl
(implementation) ব্লক শুরু করি। এই impl
ব্লকের মধ্যে থাকা সবকিছু Rectangle
টাইপের সাথে যুক্ত থাকবে। তারপর আমরা area
ফাংশনটিকে impl
কার্লি ব্র্যাকেটের মধ্যে নিয়ে যাই এবং সিগনেচারে এবং বডির সর্বত্র প্রথম (এবং এই ক্ষেত্রে, একমাত্র) প্যারামিটারটিকে self
এ পরিবর্তন করি। main
ফাংশনে, যেখানে আমরা area
ফাংশন কল করেছিলাম এবং rect1
কে আর্গুমেন্ট হিসাবে পাস করেছিলাম, তার পরিবর্তে আমরা আমাদের Rectangle
ইনস্ট্যান্সের উপর area
মেথড কল করার জন্য মেথড সিনট্যাক্স (method syntax) ব্যবহার করতে পারি। মেথড সিনট্যাক্স একটি ইনস্ট্যান্সের পরে বসে: আমরা একটি ডট এবং তারপরে মেথডের নাম, প্যারেন্থেসিস এবং যেকোনো আর্গুমেন্ট যোগ করি।
area
-এর সিগনেচারে, আমরা rectangle: &Rectangle
এর পরিবর্তে &self
ব্যবহার করি। &self
আসলে self: &Self
-এর সংক্ষিপ্ত রূপ। একটি impl
ব্লকের মধ্যে, Self
টাইপটি সেই টাইপের একটি অ্যালিয়াস (alias) যার জন্য impl
ব্লকটি তৈরি করা হয়েছে। মেথডগুলোর প্রথম প্যারামিটার হিসাবে self
নামের একটি Self
টাইপের প্যারামিটার থাকতে হয়, তাই রাস্ট আপনাকে প্রথম প্যারামিটারের স্থানে শুধুমাত্র self
নামটি দিয়ে এটিকে সংক্ষিপ্ত করার অনুমতি দেয়। মনে রাখবেন যে আমাদের এখনও self
শর্টহ্যান্ডের আগে &
ব্যবহার করতে হবে এটি বোঝাতে যে এই মেথডটি Self
ইনস্ট্যান্সটিকে ধার (borrow) করে, ঠিক যেমনটি আমরা rectangle: &Rectangle
-এ করেছিলাম। মেথডগুলো self
-এর মালিকানা নিতে পারে, self
-কে অপরিবর্তনীয়ভাবে ধার করতে পারে, যেমনটি আমরা এখানে করেছি, অথবা self
-কে পরিবর্তনীয়ভাবে ধার করতে পারে, ঠিক যেমনটি তারা অন্য যেকোনো প্যারামিটারের ক্ষেত্রে পারে।
আমরা এখানে &self
বেছে নিয়েছি একই কারণে যে কারণে আমরা ফাংশন সংস্করণে &Rectangle
ব্যবহার করেছিলাম: আমরা মালিকানা নিতে চাই না, এবং আমরা কেবল struct
-এর ডেটা পড়তে চাই, এতে লিখতে চাই না। যদি আমরা যে ইনস্ট্যান্সের উপর মেথডটি কল করেছি সেটিকে মেথডের কাজের অংশ হিসাবে পরিবর্তন করতে চাইতাম, তাহলে আমরা প্রথম প্যারামিটার হিসাবে &mut self
ব্যবহার করতাম। self
-কে প্রথম প্যারামিটার হিসাবে ব্যবহার করে ইনস্ট্যান্সের মালিকানা নেওয়া একটি মেথড বিরল; এই কৌশলটি সাধারণত ব্যবহৃত হয় যখন মেথডটি self
-কে অন্য কিছুতে রূপান্তরিত করে এবং আপনি চান যে রূপান্তরের পরে কলার মূল ইনস্ট্যান্সটি ব্যবহার করা থেকে বিরত থাকুক।
মেথড সিনট্যাক্স সরবরাহ করা এবং প্রতিটি মেথডের সিগনেচারে self
-এর টাইপ পুনরাবৃত্তি না করার পাশাপাশি, ফাংশনের পরিবর্তে মেথড ব্যবহারের মূল কারণ হলো অর্গানাইজেশন বা সংগঠন। আমরা একটি টাইপের ইনস্ট্যান্সের সাথে যা যা করা যায় তার সবকিছু একটি impl
ব্লকের মধ্যে রেখেছি, যাতে আমাদের কোডের ভবিষ্যতের ব্যবহারকারীদেরকে আমাদের সরবরাহ করা লাইব্রেরির বিভিন্ন জায়গায় Rectangle
-এর ক্ষমতা খুঁজতে না হয়।
মনে রাখবেন যে আমরা একটি মেথডকে struct
-এর একটি ফিল্ডের সমান নাম দিতে পারি। উদাহরণস্বরূপ, আমরা Rectangle
-এর উপর width
নামে একটি মেথড ডিফাইন করতে পারি:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
এখানে, আমরা width
মেথডটিকে true
রিটার্ন করার জন্য তৈরি করছি যদি ইনস্ট্যান্সের width
ফিল্ডের মান 0
-এর চেয়ে বড় হয় এবং false
যদি মান 0
হয়: আমরা একই নামের একটি মেথডের মধ্যে একটি ফিল্ড যেকোনো উদ্দেশ্যে ব্যবহার করতে পারি। main
-এ, যখন আমরা rect1.width
-এর পরে প্যারেন্থেসিস ব্যবহার করি, রাস্ট জানে যে আমরা width
মেথডটির কথা বলছি। যখন আমরা প্যারেন্থেসিস ব্যবহার করি না, রাস্ট জানে যে আমরা width
ফিল্ডটির কথা বলছি।
প্রায়শই, কিন্তু সবসময় নয়, যখন আমরা একটি মেথডকে একটি ফিল্ডের সমান নাম দিই তখন আমরা চাই এটি শুধুমাত্র ফিল্ডের মান রিটার্ন করুক এবং অন্য কিছু না করুক। এই ধরনের মেথডকে গেটার (getters) বলা হয়, এবং রাস্ট অন্য কিছু ভাষার মতো struct
ফিল্ডের জন্য এগুলো স্বয়ংক্রিয়ভাবে ইমপ্লিমেন্ট করে না। গেটারগুলো দরকারী কারণ আপনি ফিল্ডটিকে প্রাইভেট কিন্তু মেথডটিকে পাবলিক করতে পারেন, এবং এইভাবে টাইপের পাবলিক API-এর অংশ হিসাবে সেই ফিল্ডে শুধুমাত্র-পড়ার (read-only) অ্যাক্সেস সক্ষম করতে পারেন। আমরা পাবলিক এবং প্রাইভেট কী এবং কীভাবে একটি ফিল্ড বা মেথডকে পাবলিক বা প্রাইভেট হিসাবে চিহ্নিত করতে হয় তা অধ্যায় ৭-এ আলোচনা করব।
->
অপারেটরটি কোথায়?C এবং C++ এ, মেথড কল করার জন্য দুটি ভিন্ন অপারেটর ব্যবহৃত হয়: আপনি
.
ব্যবহার করেন যদি আপনি সরাসরি অবজেক্টের উপর একটি মেথড কল করেন এবং->
ব্যবহার করেন যদি আপনি অবজেক্টের একটি পয়েন্টারের উপর মেথড কল করেন এবং প্রথমে পয়েন্টারটিকে ডি-রেফারেন্স করতে হয়। অন্য কথায়, যদিobject
একটি পয়েন্টার হয়,object->something()
অনেকটা(*object).something()
-এর মতো।রাস্টের
->
অপারেটরের সমতুল্য কিছু নেই; এর পরিবর্তে, রাস্টের একটি বৈশিষ্ট্য রয়েছে যার নাম স্বয়ংক্রিয় রেফারেন্সিং এবং ডি-রেফারেন্সিং (automatic referencing and dereferencing)। মেথড কল করা রাস্টের কয়েকটি জায়গার মধ্যে একটি যেখানে এই আচরণ রয়েছে।এটি যেভাবে কাজ করে: যখন আপনি
object.something()
দিয়ে একটি মেথড কল করেন, রাস্ট স্বয়ংক্রিয়ভাবে&
,&mut
, বা*
যোগ করে দেয় যাতেobject
মেথডের সিগনেচারের সাথে মেলে। অন্য কথায়, নিম্নলিখিতগুলো একই:#![allow(unused)] fn main() { #[derive(Debug,Copy,Clone)] struct Point { x: f64, y: f64, } impl Point { fn distance(&self, other: &Point) -> f64 { let x_squared = f64::powi(other.x - self.x, 2); let y_squared = f64::powi(other.y - self.y, 2); f64::sqrt(x_squared + y_squared) } } let p1 = Point { x: 0.0, y: 0.0 }; let p2 = Point { x: 5.0, y: 6.5 }; p1.distance(&p2); (&p1).distance(&p2); }
প্রথমটি দেখতে অনেক পরিষ্কার। এই স্বয়ংক্রিয় রেফারেন্সিং আচরণটি কাজ করে কারণ মেথডগুলোর একটি স্পষ্ট রিসিভার (receiver) আছে—
self
-এর টাইপ। রিসিভার এবং একটি মেথডের নাম দেওয়া হলে, রাস্ট নির্দিষ্টভাবে বের করতে পারে যে মেথডটি পড়ছে (&self
), পরিবর্তন করছে (&mut self
), বা ব্যবহার করে ফেলছে (self
)। রাস্ট যে মেথড রিসিভারের জন্য ধার করাকে উহ্য (implicit) করে তোলে তা বাস্তবে মালিকানাকে অর্গোনমিক (ergonomic) করার একটি বড় অংশ।
একাধিক প্যারামিটার সহ মেথড (Methods with More Parameters)
আসুন Rectangle
struct
-এর উপর একটি দ্বিতীয় মেথড ইমপ্লিমেন্ট করে মেথড ব্যবহার করার অনুশীলন করি। এবার আমরা চাই Rectangle
-এর একটি ইনস্ট্যান্স Rectangle
-এর আরেকটি ইনস্ট্যান্স নিক এবং true
রিটার্ন করুক যদি দ্বিতীয় Rectangle
-টি self
-এর (প্রথম Rectangle
) মধ্যে সম্পূর্ণরূপে ফিট করতে পারে; অন্যথায়, এটি false
রিটার্ন করবে। অর্থাৎ, একবার আমরা can_hold
মেথডটি ডিফাইন করে ফেলার পর, আমরা তালিকা ৫-১৪-এ দেখানো প্রোগ্রামটি লিখতে চাই।
fn main() {
let rect1 = Rectangle {
width: 30,
height: 50,
};
let rect2 = Rectangle {
width: 10,
height: 40,
};
let rect3 = Rectangle {
width: 60,
height: 45,
};
println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}
প্রত্যাশিত আউটপুট নিম্নলিখিতটির মতো হবে কারণ rect2
-এর উভয় ডাইমেনশন rect1
-এর ডাইমেনশনের চেয়ে ছোট, কিন্তু rect3
rect1
-এর চেয়ে চওড়া:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
আমরা জানি আমরা একটি মেথড ডিফাইন করতে চাই, তাই এটি impl Rectangle
ব্লকের মধ্যে থাকবে। মেথডের নাম হবে can_hold
, এবং এটি অন্য একটি Rectangle
-এর একটি অপরিবর্তনীয় ধার (immutable borrow) প্যারামিটার হিসাবে নেবে। আমরা প্যারামিটারের টাইপ কী হবে তা মেথড কল করা কোডটি দেখে বলতে পারি: rect1.can_hold(&rect2)
&rect2
পাস করে, যা rect2
, একটি Rectangle
ইনস্ট্যান্সের একটি অপরিবর্তনীয় ধার। এটি যৌক্তিক কারণ আমাদের কেবল rect2
পড়তে হবে (লেখার পরিবর্তে, যার জন্য আমাদের একটি পরিবর্তনযোগ্য ধার প্রয়োজন হতো), এবং আমরা চাই main
rect2
-এর মালিকানা ধরে রাখুক যাতে আমরা can_hold
মেথড কল করার পরেও এটি আবার ব্যবহার করতে পারি। can_hold
-এর রিটার্ন ভ্যালু একটি বুলিয়ান হবে, এবং ইমপ্লিমেন্টেশনটি পরীক্ষা করবে যে self
-এর প্রস্থ এবং উচ্চতা অন্য Rectangle
-এর প্রস্থ এবং উচ্চতার চেয়ে বড় কিনা। আসুন নতুন can_hold
মেথডটি তালিকা ৫-১৩ থেকে impl
ব্লকে যোগ করি, যা তালিকা ৫-১৫-এ দেখানো হয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
যখন আমরা এই কোডটি তালিকা ৫-১৪-এর main
ফাংশন দিয়ে চালাই, আমরা আমাদের কাঙ্ক্ষিত আউটপুট পাব। মেথডগুলো self
প্যারামিটারের পরে একাধিক প্যারামিটার নিতে পারে যা আমরা সিগনেচারে যোগ করি, এবং সেই প্যারামিটারগুলো ফাংশনের প্যারামিটারের মতোই কাজ করে।
অ্যাসোসিয়েটেড ফাংশন (Associated Functions)
একটি impl
ব্লকের মধ্যে ডিফাইন করা সমস্ত ফাংশনকে অ্যাসোসিয়েটেড ফাংশন (associated functions) বলা হয় কারণ সেগুলো impl
-এর পরে নাম দেওয়া টাইপের সাথে যুক্ত। আমরা এমন অ্যাসোসিয়েটেড ফাংশন ডিফাইন করতে পারি যেগুলোর প্রথম প্যারামিটার self
নয় (এবং এইভাবে সেগুলো মেথড নয়) কারণ সেগুলোর সাথে কাজ করার জন্য টাইপের একটি ইনস্ট্যান্সের প্রয়োজন হয় না। আমরা ইতিমধ্যে এই ধরনের একটি ফাংশন ব্যবহার করেছি: String::from
ফাংশন যা String
টাইপের উপর ডিফাইন করা আছে।
যেসব অ্যাসোসিয়েটেড ফাংশন মেথড নয় সেগুলো প্রায়শই কনস্ট্রাক্টর (constructors) হিসাবে ব্যবহৃত হয় যা struct
-এর একটি নতুন ইনস্ট্যান্স রিটার্ন করবে। এগুলোকে প্রায়শই new
বলা হয়, কিন্তু new
কোনো বিশেষ নাম নয় এবং এটি ভাষায় বিল্ট-ইন নয়। উদাহরণস্বরূপ, আমরা square
নামে একটি অ্যাসোসিয়েটেড ফাংশন সরবরাহ করতে পারি যা একটি ডাইমেনশন প্যারামিটার নেবে এবং সেটিকে প্রস্থ এবং উচ্চতা উভয় হিসাবে ব্যবহার করবে, এইভাবে একটি বর্গক্ষেত্র Rectangle
তৈরি করা সহজ করে তুলবে, একই মান দুবার নির্দিষ্ট করার পরিবর্তে:
Filename: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
রিটার্ন টাইপ এবং ফাংশনের বডিতে Self
কীওয়ার্ডগুলো সেই টাইপের অ্যালিয়াস যা impl
কীওয়ার্ডের পরে আসে, যা এই ক্ষেত্রে Rectangle
।
এই অ্যাসোসিয়েটেড ফাংশনটি কল করার জন্য, আমরা struct
-এর নামের সাথে ::
সিনট্যাক্স ব্যবহার করি; let sq = Rectangle::square(3);
একটি উদাহরণ। এই ফাংশনটি struct
দ্বারা নেমস্পেসড (namespaced) হয়: ::
সিনট্যাক্সটি অ্যাসোসিয়েটেড ফাংশন এবং মডিউল দ্বারা তৈরি নেমস্পেস উভয়ের জন্যই ব্যবহৃত হয়। আমরা অধ্যায় ৭-এ মডিউল নিয়ে আলোচনা করব।
একাধিক impl
ব্লক (Multiple impl
Blocks)
প্রতিটি struct
-এর একাধিক impl
ব্লক থাকতে পারে। উদাহরণস্বরূপ, তালিকা ৫-১৫ তালিকা ৫-১৬-এ দেখানো কোডের সমতুল্য, যেখানে প্রতিটি মেথড তার নিজস্ব impl
ব্লকে রয়েছে।
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn area(&self) -> u32 { self.width * self.height } } impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3)); }
এখানে এই মেথডগুলোকে একাধিক impl
ব্লকে বিভক্ত করার কোনো কারণ নেই, তবে এটি একটি বৈধ সিনট্যাক্স। আমরা অধ্যায় ১০-এ একটি কেস দেখব যেখানে একাধিক impl
ব্লক দরকারী, যেখানে আমরা জেনেরিক টাইপ এবং ট্রেইট নিয়ে আলোচনা করব।
সারসংক্ষেপ (Summary)
struct
আপনাকে আপনার ডোমেইনের জন্য অর্থপূর্ণ কাস্টম টাইপ তৈরি করতে দেয়। struct
ব্যবহার করে, আপনি সম্পর্কিত ডেটার অংশগুলোকে একে অপরের সাথে সংযুক্ত রাখতে পারেন এবং আপনার কোডকে স্পষ্ট করার জন্য প্রতিটি অংশের নাম দিতে পারেন। impl
ব্লকে, আপনি আপনার টাইপের সাথে যুক্ত ফাংশন ডিফাইন করতে পারেন, এবং মেথডগুলো এক ধরনের অ্যাসোসিয়েটেড ফাংশন যা আপনাকে আপনার struct
-এর ইনস্ট্যান্সগুলোর আচরণ নির্দিষ্ট করতে দেয়।
কিন্তু struct
আপনার কাস্টম টাইপ তৈরি করার একমাত্র উপায় নয়: আসুন রাস্টের enum
বৈশিষ্ট্যটির দিকে নজর দিই এবং আপনার টুলবক্সে আরেকটি টুল যোগ করি।