Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

মেথড সিনট্যাক্স (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 বৈশিষ্ট্যটির দিকে নজর দিই এবং আপনার টুলবক্সে আরেকটি টুল যোগ করি।