একটি এনাম সংজ্ঞায়িত করা (Defining an Enum)
স্ট্রাকটগুলো যেমন আপনাকে সম্পর্কিত ফিল্ড এবং ডেটা একত্রিত করার একটি উপায় দেয়, যেমন width
এবং height
সহ একটি Rectangle
, এনামগুলো আপনাকে সম্ভাব্য মানগুলোর একটি সেট থেকে একটি মান বলার উপায় দেয়। উদাহরণস্বরূপ, আমরা বলতে চাইতে পারি যে Rectangle
হল সম্ভাব্য আকারগুলোর একটি সেটের মধ্যে একটি, যার মধ্যে Circle
এবং Triangle
-ও রয়েছে। এটি করার জন্য, Rust আমাদের এই সম্ভাবনাগুলোকে একটি এনাম হিসাবে এনকোড করার অনুমতি দেয়।
আসুন এমন একটি পরিস্থিতি দেখি যা আমরা কোডে প্রকাশ করতে চাইতে পারি এবং দেখি কেন এই ক্ষেত্রে এনামগুলো দরকারী এবং স্ট্রাকটগুলোর চেয়ে বেশি উপযুক্ত। ধরুন আমাদের IP অ্যাড্রেস নিয়ে কাজ করতে হবে। বর্তমানে, IP অ্যাড্রেসের জন্য দুটি প্রধান স্ট্যান্ডার্ড ব্যবহার করা হয়: ভার্সন চার এবং ভার্সন ছয়। যেহেতু এগুলোই আমাদের প্রোগ্রামের সামনে আসতে পারে এমন IP অ্যাড্রেসের একমাত্র সম্ভাবনা, তাই আমরা সমস্ত সম্ভাব্য ভেরিয়েন্টগুলো গণনা করতে পারি, যেখান থেকে এনিউমারেশন (enumeration) নামটি এসেছে।
যেকোনো IP অ্যাড্রেস হয় একটি ভার্সন চার বা একটি ভার্সন ছয় অ্যাড্রেস হতে পারে, কিন্তু একই সময়ে উভয়ই নয়। IP অ্যাড্রেসের এই বৈশিষ্ট্যটি এনাম ডেটা স্ট্রাকচারকে উপযুক্ত করে তোলে কারণ একটি এনাম মান শুধুমাত্র তার ভেরিয়েন্টগুলোর মধ্যে একটি হতে পারে। ভার্সন চার এবং ভার্সন ছয় উভয় অ্যাড্রেসই এখনও মৌলিকভাবে IP অ্যাড্রেস, তাই কোড যখন যেকোনো ধরনের IP অ্যাড্রেসের ক্ষেত্রে প্রযোজ্য পরিস্থিতিগুলো পরিচালনা করে তখন তাদের একই টাইপ হিসাবে বিবেচনা করা উচিত।
আমরা কোডে এই ধারণাটি প্রকাশ করতে পারি একটি IpAddrKind
এনিউমারেশন সংজ্ঞায়িত করে এবং একটি IP অ্যাড্রেস হতে পারে এমন সম্ভাব্য প্রকারগুলো তালিকাভুক্ত করে, V4
এবং V6
। এগুলো হল এনামের ভেরিয়েন্ট:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
IpAddrKind
এখন একটি কাস্টম ডেটা টাইপ যা আমরা আমাদের কোডের অন্য কোথাও ব্যবহার করতে পারি।
এনাম মান (Enum Values)
আমরা IpAddrKind
-এর দুটি ভেরিয়েন্টের প্রত্যেকটির ইন্সট্যান্স তৈরি করতে পারি এইভাবে:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
লক্ষ্য করুন যে এনামের ভেরিয়েন্টগুলো এর আইডেন্টিফায়ারের অধীনে নেমস্পেস করা হয়েছে এবং আমরা দুটিকে আলাদা করতে একটি ডাবল কোলন ব্যবহার করি। এটি দরকারী কারণ এখন IpAddrKind::V4
এবং IpAddrKind::V6
উভয় মান একই টাইপের: IpAddrKind
। তারপর আমরা, উদাহরণস্বরূপ, এমন একটি ফাংশন সংজ্ঞায়িত করতে পারি যা যেকোনো IpAddrKind
নেয়:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
এবং আমরা এই ফাংশনটিকে যেকোনো ভেরিয়েন্ট দিয়ে কল করতে পারি:
enum IpAddrKind { V4, V6, } fn main() { let four = IpAddrKind::V4; let six = IpAddrKind::V6; route(IpAddrKind::V4); route(IpAddrKind::V6); } fn route(ip_kind: IpAddrKind) {}
এনাম ব্যবহারের আরও সুবিধা রয়েছে। আমাদের IP অ্যাড্রেস টাইপ সম্পর্কে আরও চিন্তা করলে, এই মুহূর্তে আমাদের কাছে প্রকৃত IP অ্যাড্রেস ডেটা সংরক্ষণ করার কোনো উপায় নেই; আমরা কেবল জানি এটি কোন ধরনের। যেহেতু আপনি এইমাত্র চ্যাপ্টার ৫-এ স্ট্রাকট সম্পর্কে শিখেছেন, তাই আপনি হয়তো Listing 6-1-এ দেখানো স্ট্রাকটগুলো দিয়ে এই সমস্যার সমাধান করতে চাইতে পারেন।
fn main() { enum IpAddrKind { V4, V6, } struct IpAddr { kind: IpAddrKind, address: String, } let home = IpAddr { kind: IpAddrKind::V4, address: String::from("127.0.0.1"), }; let loopback = IpAddr { kind: IpAddrKind::V6, address: String::from("::1"), }; }
এখানে, আমরা একটি স্ট্রাকট IpAddr
সংজ্ঞায়িত করেছি যাতে দুটি ফিল্ড রয়েছে: একটি kind
ফিল্ড যার টাইপ IpAddrKind
(আমরা পূর্বে সংজ্ঞায়িত করা এনাম) এবং একটি address
ফিল্ড যার টাইপ String
। আমাদের কাছে এই স্ট্রাকটের দুটি ইন্সট্যান্স রয়েছে। প্রথমটি হল home
, এবং এর kind
-এর মান হল IpAddrKind::V4
যার সাথে 127.0.0.1
-এর সংশ্লিষ্ট অ্যাড্রেস ডেটা রয়েছে। দ্বিতীয় ইন্সট্যান্সটি হল loopback
। এটির kind
মান হিসাবে IpAddrKind
-এর অন্য ভেরিয়েন্টটি রয়েছে, V6
, এবং এর সাথে ::1
অ্যাড্রেস যুক্ত রয়েছে। আমরা kind
এবং address
মানগুলোকে একসাথে বান্ডিল করার জন্য একটি স্ট্রাকট ব্যবহার করেছি, তাই এখন ভেরিয়েন্টটি মানের সাথে সম্পর্কিত।
যাইহোক, শুধুমাত্র একটি এনাম ব্যবহার করে একই ধারণাটি উপস্থাপন করা আরও সংক্ষিপ্ত: একটি স্ট্রাকটের ভিতরে একটি এনামের পরিবর্তে, আমরা সরাসরি প্রতিটি এনাম ভেরিয়েন্টের মধ্যে ডেটা রাখতে পারি। IpAddr
এনামের এই নতুন সংজ্ঞাটি বলে যে V4
এবং V6
উভয় ভেরিয়েন্টের সাথেই String
মান যুক্ত থাকবে:
fn main() { enum IpAddr { V4(String), V6(String), } let home = IpAddr::V4(String::from("127.0.0.1")); let loopback = IpAddr::V6(String::from("::1")); }
আমরা সরাসরি এনামের প্রতিটি ভেরিয়েন্টের সাথে ডেটা সংযুক্ত করি, তাই একটি অতিরিক্ত স্ট্রাকটের প্রয়োজন নেই। এখানে, এনামগুলো কীভাবে কাজ করে তার আরেকটি বিশদ বিবরণ দেখাও সহজ: আমরা যে প্রতিটি এনাম ভেরিয়েন্টের নাম সংজ্ঞায়িত করি সেটিও একটি ফাংশন হয়ে যায় যা এনামের একটি ইন্সট্যান্স তৈরি করে। অর্থাৎ, IpAddr::V4()
হল একটি ফাংশন কল যা একটি String
আর্গুমেন্ট নেয় এবং IpAddr
টাইপের একটি ইন্সট্যান্স রিটার্ন করে। এনাম সংজ্ঞায়িত করার ফলে আমরা স্বয়ংক্রিয়ভাবে এই কনস্ট্রাক্টর ফাংশনটি সংজ্ঞায়িত করি।
একটি স্ট্রাকটের পরিবর্তে একটি এনাম ব্যবহার করার আরেকটি সুবিধা রয়েছে: প্রতিটি ভেরিয়েন্টের বিভিন্ন টাইপ এবং পরিমাণের ডেটা থাকতে পারে। ভার্সন চার IP অ্যাড্রেসগুলোতে সর্বদা চারটি সংখ্যাসূচক উপাদান থাকবে যার মান 0 থেকে 255 এর মধ্যে থাকবে। যদি আমরা V4
অ্যাড্রেসগুলোকে চারটি u8
মান হিসাবে সংরক্ষণ করতে চাইতাম কিন্তু এখনও V6
অ্যাড্রেসগুলোকে একটি String
মান হিসাবে প্রকাশ করতে চাইতাম, তাহলে আমরা একটি স্ট্রাকট দিয়ে তা করতে পারতাম না। এনামগুলো এই ক্ষেত্রটি সহজে পরিচালনা করে:
fn main() { enum IpAddr { V4(u8, u8, u8, u8), V6(String), } let home = IpAddr::V4(127, 0, 0, 1); let loopback = IpAddr::V6(String::from("::1")); }
আমরা ভার্সন চার এবং ভার্সন ছয় IP অ্যাড্রেস সংরক্ষণ করার জন্য ডেটা স্ট্রাকচার সংজ্ঞায়িত করার বিভিন্ন উপায় দেখিয়েছি। যাইহোক, দেখা যাচ্ছে যে, IP অ্যাড্রেস সংরক্ষণ করা এবং সেগুলো কোন ধরনের তা এনকোড করা এতটাই সাধারণ যে স্ট্যান্ডার্ড লাইব্রেরিতে আমাদের ব্যবহারের জন্য একটি সংজ্ঞা রয়েছে! চলুন দেখি কিভাবে স্ট্যান্ডার্ড লাইব্রেরি IpAddr
সংজ্ঞায়িত করে: এতে ঠিক সেই এনাম এবং ভেরিয়েন্টগুলো রয়েছে যা আমরা সংজ্ঞায়িত করেছি এবং ব্যবহার করেছি, কিন্তু এটি অ্যাড্রেস ডেটাকে দুটি ভিন্ন স্ট্রাকটের আকারে ভেরিয়েন্টগুলোর ভিতরে এমবেড করে, যা প্রতিটি ভেরিয়েন্টের জন্য ভিন্নভাবে সংজ্ঞায়িত করা হয়েছে:
#![allow(unused)] fn main() { struct Ipv4Addr { // --snip-- } struct Ipv6Addr { // --snip-- } enum IpAddr { V4(Ipv4Addr), V6(Ipv6Addr), } }
এই কোডটি ব্যাখ্যা করে যে আপনি একটি এনাম ভেরিয়েন্টের ভিতরে যেকোনো ধরনের ডেটা রাখতে পারেন: উদাহরণস্বরূপ, স্ট্রিং, সাংখ্যিক টাইপ বা স্ট্রাকট। আপনি এমনকি অন্য একটি এনামও অন্তর্ভুক্ত করতে পারেন! এছাড়াও, স্ট্যান্ডার্ড লাইব্রেরির টাইপগুলো প্রায়শই আপনার তৈরি করা জিনিসের চেয়ে বেশি জটিল হয় না।
লক্ষ্য করুন যে যদিও স্ট্যান্ডার্ড লাইব্রেরিতে IpAddr
-এর জন্য একটি সংজ্ঞা রয়েছে, তবুও আমরা কোনো বিরোধ ছাড়াই আমাদের নিজস্ব সংজ্ঞা তৈরি এবং ব্যবহার করতে পারি কারণ আমরা স্ট্যান্ডার্ড লাইব্রেরির সংজ্ঞাটিকে আমাদের স্কোপে আনিনি। আমরা চ্যাপ্টার ৭-এ স্কোপে টাইপ আনার বিষয়ে আরও কথা বলব।
Listing 6-2-তে এনামের আরেকটি উদাহরণ দেখা যাক: এটির ভেরিয়েন্টগুলোতে বিভিন্ন ধরনের ডেটা এমবেড করা আছে।
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() {}
এই এনামটিতে চারটি ভেরিয়েন্ট রয়েছে যাদের বিভিন্ন টাইপ রয়েছে:
Quit
-এর সাথে কোনো ডেটা যুক্ত নেই।Move
-এর নামযুক্ত ফিল্ড রয়েছে, যেমন একটি স্ট্রাকটে থাকে।Write
একটি এককString
অন্তর্ভুক্ত করে।ChangeColor
-এ তিনটিi32
মান রয়েছে।
Listing 6-2-এর মতো ভেরিয়েন্ট সহ একটি এনাম সংজ্ঞায়িত করা বিভিন্ন ধরণের স্ট্রাকট সংজ্ঞা সংজ্ঞায়িত করার মতোই, পার্থক্য হল এনামটি struct
কীওয়ার্ড ব্যবহার করে না এবং সমস্ত ভেরিয়েন্ট Message
টাইপের অধীনে একত্রিত হয়। নিম্নলিখিত স্ট্রাকটগুলো পূর্ববর্তী এনাম ভেরিয়েন্টগুলোর মতো একই ডেটা ধারণ করতে পারে:
struct QuitMessage; // unit struct struct MoveMessage { x: i32, y: i32, } struct WriteMessage(String); // tuple struct struct ChangeColorMessage(i32, i32, i32); // tuple struct fn main() {}
কিন্তু যদি আমরা বিভিন্ন স্ট্রাকট ব্যবহার করতাম, যেগুলোর প্রত্যেকের নিজস্ব টাইপ রয়েছে, তাহলে আমরা Listing 6-2-তে সংজ্ঞায়িত Message
এনামের মতো এই সমস্ত ধরণের মেসেজ নিতে পারে এমন একটি ফাংশন সংজ্ঞায়িত করতে পারতাম না, যেটি একটি একক টাইপ।
এনাম এবং স্ট্রাকটগুলোর মধ্যে আরও একটি মিল রয়েছে: যেমন আমরা impl
ব্যবহার করে স্ট্রাকটগুলোতে মেথড সংজ্ঞায়িত করতে পারি, তেমনই আমরা এনামগুলোতেও মেথড সংজ্ঞায়িত করতে পারি। এখানে call
নামে একটি মেথড রয়েছে যা আমরা আমাদের Message
এনামে সংজ্ঞায়িত করতে পারি:
fn main() { enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } impl Message { fn call(&self) { // method body would be defined here } } let m = Message::Write(String::from("hello")); m.call(); }
মেথডের বডি self
ব্যবহার করে সেই মানটি পেতে পারে যেটিতে আমরা মেথডটি কল করেছি। এই উদাহরণে, আমরা একটি ভেরিয়েবল m
তৈরি করেছি যার মান Message::Write(String::from("hello"))
, এবং m.call()
রান করার সময় call
মেথডের বডিতে self
হবে এটি।
আসুন স্ট্যান্ডার্ড লাইব্রেরির আরেকটি এনাম দেখি যা খুব সাধারণ এবং দরকারী: Option
।
Option
এনাম এবং নাল (Null) মানের উপর এর সুবিধা (The Option
Enum and Its Advantages Over Null Values)
এই বিভাগে Option
-এর একটি কেস স্টাডি অন্বেষণ করা হয়েছে, যেটি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সংজ্ঞায়িত আরেকটি এনাম। Option
টাইপটি খুব সাধারণ পরিস্থিতিকে এনকোড করে যেখানে একটি মান কিছু হতে পারে বা কিছুই নাও হতে পারে।
উদাহরণস্বরূপ, আপনি যদি একটি খালি নয় এমন তালিকার প্রথম আইটেমটির অনুরোধ করেন, তাহলে আপনি একটি মান পাবেন। আপনি যদি একটি খালি তালিকার প্রথম আইটেমটির অনুরোধ করেন, তাহলে আপনি কিছুই পাবেন না। টাইপ সিস্টেমের পরিপ্রেক্ষিতে এই ধারণাটি প্রকাশ করার অর্থ হল কম্পাইলার পরীক্ষা করতে পারে যে আপনি যে সমস্ত ক্ষেত্রগুলো হ্যান্ডেল করা উচিত সেগুলো হ্যান্ডেল করেছেন কিনা; এই কার্যকারিতা অন্যান্য প্রোগ্রামিং ল্যাঙ্গুয়েজে অত্যন্ত সাধারণ বাগগুলো প্রতিরোধ করতে পারে।
প্রোগ্রামিং ল্যাঙ্গুয়েজ ডিজাইন প্রায়শই কোন ফিচারগুলো আপনি অন্তর্ভুক্ত করেন তার পরিপ্রেক্ষিতে ভাবা হয়, তবে আপনি যে ফিচারগুলো বাদ দেন সেগুলোও গুরুত্বপূর্ণ। Rust-এর নাল (null) ফিচার নেই যা অন্য অনেক ভাষার আছে। নাল হল এমন একটি মান যার অর্থ সেখানে কোনো মান নেই। নাল সহ ভাষাগুলোতে, ভেরিয়েবলগুলো সর্বদাই দুটি অবস্থার মধ্যে একটিতে থাকতে পারে: নাল বা নাল-নয়।
২০০৯ সালে তার উপস্থাপনা “Null References: The Billion Dollar Mistake,”-এ নালের উদ্ভাবক টনি হোর (Tony Hoare) এটি বলেছেন:
আমি এটিকে আমার বিলিয়ন-ডলার ভুল বলি। সেই সময়ে, আমি একটি অবজেক্ট-ওরিয়েন্টেড ভাষায় রেফারেন্সের জন্য প্রথম ব্যাপক টাইপ সিস্টেম ডিজাইন করছিলাম। আমার লক্ষ্য ছিল নিশ্চিত করা যে রেফারেন্সের সমস্ত ব্যবহার যেন একেবারে নিরাপদ হয়, কম্পাইলার দ্বারা স্বয়ংক্রিয়ভাবে পরীক্ষা করা হয়। কিন্তু আমি একটি নাল রেফারেন্স রাখার প্রলোভন প্রতিরোধ করতে পারিনি, কারণ এটি বাস্তবায়ন করা খুব সহজ ছিল। এটি অসংখ্য এরর, দুর্বলতা এবং সিস্টেম ক্র্যাশের দিকে পরিচালিত করেছে, যা সম্ভবত গত চল্লিশ বছরে এক বিলিয়ন ডলারের কষ্ট এবং ক্ষতির কারণ হয়েছে।
নাল মানগুলোর সমস্যা হল যে আপনি যদি একটি নাল মানকে নাল-নয় মান হিসাবে ব্যবহার করার চেষ্টা করেন, তাহলে আপনি কোনো না কোনো ধরনের এরর পাবেন। যেহেতু এই নাল বা নাল-নয় বৈশিষ্ট্যটি ব্যাপক, তাই এই ধরনের এরর করা অত্যন্ত সহজ।
যাইহোক, নাল যে ধারণাটি প্রকাশ করার চেষ্টা করছে সেটি এখনও একটি দরকারী ধারণা: একটি নাল হল এমন একটি মান যা বর্তমানে কোনো কারণে অবৈধ বা অনুপস্থিত।
সমস্যাটি আসলে ধারণার সাথে নয়, নির্দিষ্ট বাস্তবায়নের সাথে। সেই অনুযায়ী, Rust-এর নাল নেই, তবে এটিতে একটি এনাম রয়েছে যা একটি মান উপস্থিত বা অনুপস্থিত থাকার ধারণাটিকে এনকোড করতে পারে। এই এনামটি হল Option<T>
, এবং এটি স্ট্যান্ডার্ড লাইব্রেরি দ্বারা সংজ্ঞায়িত করা হয়েছে এইভাবে:
#![allow(unused)] fn main() { enum Option<T> { None, Some(T), } }
Option<T>
এনামটি এতটাই দরকারী যে এটি প্রেলিউডে (prelude) অন্তর্ভুক্ত করা হয়েছে; আপনাকে এটিকে স্পষ্টতই স্কোপে আনতে হবে না। এর ভেরিয়েন্টগুলোও প্রেলিউডে অন্তর্ভুক্ত করা হয়েছে: আপনি সরাসরি Option::
উপসর্গ ছাড়াই Some
এবং None
ব্যবহার করতে পারেন। Option<T>
এনামটি এখনও একটি নিয়মিত এনাম এবং Some(T)
এবং None
এখনও Option<T>
টাইপের ভেরিয়েন্ট।
<T>
সিনট্যাক্স হল Rust-এর একটি ফিচার যা নিয়ে আমরা এখনও কথা বলিনি। এটি একটি জেনেরিক টাইপ প্যারামিটার এবং আমরা চ্যাপ্টার ১০-এ জেনেরিকগুলো নিয়ে আরও বিস্তারিত আলোচনা করব। আপাতত, আপনাকে শুধু জানতে হবে যে <T>
মানে হল Option
এনামের Some
ভেরিয়েন্টটি যেকোনো টাইপের এক টুকরো ডেটা ধারণ করতে পারে এবং T
-এর পরিবর্তে ব্যবহৃত প্রতিটি কংক্রিট টাইপ সামগ্রিক Option<T>
টাইপটিকে একটি ভিন্ন টাইপ করে তোলে। এখানে সংখ্যা টাইপ এবং অক্ষর টাইপ ধারণ করতে Option
মান ব্যবহারের কিছু উদাহরণ দেওয়া হল:
fn main() { let some_number = Some(5); let some_char = Some('e'); let absent_number: Option<i32> = None; }
some_number
-এর টাইপ হল Option<i32>
। some_char
-এর টাইপ হল Option<char>
, যেটি একটি ভিন্ন টাইপ। Rust এই টাইপগুলো অনুমান করতে পারে কারণ আমরা Some
ভেরিয়েন্টের ভিতরে একটি মান নির্দিষ্ট করেছি। absent_number
-এর জন্য, Rust আমাদের সামগ্রিক Option
টাইপটি অ্যানোটেট করতে বলে: কম্পাইলার শুধুমাত্র একটি None
মান দেখে সংশ্লিষ্ট Some
ভেরিয়েন্টটি কী টাইপ ধারণ করবে তা অনুমান করতে পারে না। এখানে, আমরা Rust-কে বলি যে আমরা চাই absent_number
-এর টাইপ Option<i32>
হোক।
যখন আমাদের কাছে একটি Some
মান থাকে, তখন আমরা জানি যে একটি মান উপস্থিত রয়েছে এবং মানটি Some
-এর মধ্যে রয়েছে। যখন আমাদের কাছে একটি None
মান থাকে, তখন এক অর্থে এর অর্থ নালের মতোই: আমাদের কাছে একটি বৈধ মান নেই। তাহলে Option<T>
থাকা নাল থাকার চেয়ে ভালো কেন?
সংক্ষেপে, যেহেতু Option<T>
এবং T
(যেখানে T
যেকোনো টাইপ হতে পারে) ভিন্ন টাইপ, তাই কম্পাইলার আমাদের একটি Option<T>
মানকে নিশ্চিতভাবে একটি বৈধ মান হিসাবে ব্যবহার করতে দেবে না। উদাহরণস্বরূপ, এই কোডটি কম্পাইল হবে না, কারণ এটি একটি i8
-এর সাথে একটি Option<i8>
যোগ করার চেষ্টা করছে:
fn main() {
let x: i8 = 5;
let y: Option<i8> = Some(5);
let sum = x + y;
}
যদি আমরা এই কোডটি চালাই, তাহলে আমরা এইরকম একটি এরর মেসেজ পাব:
$ cargo run
Compiling enums v0.1.0 (file:///projects/enums)
error[E0277]: cannot add `Option<i8>` to `i8`
--> src/main.rs:5:17
|
5 | let sum = x + y;
| ^ no implementation for `i8 + Option<i8>`
|
= help: the trait `Add<Option<i8>>` is not implemented for `i8`
= help: the following other types implement trait `Add<Rhs>`:
`&i8` implements `Add<i8>`
`&i8` implements `Add`
`i8` implements `Add<&i8>`
`i8` implements `Add`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `enums` (bin "enums") due to 1 previous error
দারুণ! আসলে, এই এরর মেসেজটির অর্থ হল Rust বুঝতে পারছে না কিভাবে একটি i8
এবং একটি Option<i8>
যোগ করতে হয়, কারণ তারা ভিন্ন টাইপ। যখন Rust-এ আমাদের i8
-এর মতো একটি টাইপের মান থাকে, তখন কম্পাইলার নিশ্চিত করবে যে আমাদের কাছে সর্বদাই একটি বৈধ মান রয়েছে। আমরা সেই মানটি ব্যবহার করার আগে নালের জন্য পরীক্ষা না করেই আত্মবিশ্বাসের সাথে এগিয়ে যেতে পারি। শুধুমাত্র যখন আমাদের কাছে একটি Option<i8>
থাকে (অথবা আমরা যে মানের টাইপ নিয়ে কাজ করছি) তখনই আমাদের একটি মান নাও থাকতে পারে এমন বিষয়ে চিন্তা করতে হবে এবং কম্পাইলার নিশ্চিত করবে যে আমরা মানটি ব্যবহার করার আগে সেই ক্ষেত্রটি হ্যান্ডেল করেছি।
অন্য কথায়, আপনি Option<T>
-এর সাথে T
অপারেশনগুলো সম্পাদন করার আগে আপনাকে এটিকে T
-তে রূপান্তর করতে হবে। সাধারণত, এটি নালের সাথে সবচেয়ে সাধারণ সমস্যাগুলোর মধ্যে একটি ধরতে সাহায্য করে: এমন কিছুকে নাল নয় বলে ধরে নেওয়া যা আসলে নাল।
একটি নাল-নয় মান ভুলভাবে ধরে নেওয়ার ঝুঁকি দূর করা আপনাকে আপনার কোডে আরও আত্মবিশ্বাসী হতে সাহায্য করে। এমন একটি মান থাকতে যা সম্ভবত নাল হতে পারে, আপনাকে অবশ্যই স্পষ্টতই অপ্ট ইন করতে হবে সেই মানটির টাইপ Option<T>
করে। তারপর, যখন আপনি সেই মানটি ব্যবহার করেন, তখন আপনাকে স্পষ্টতই সেই ক্ষেত্রটি হ্যান্ডেল করতে হবে যখন মানটি নাল হয়। যেখানেই একটি মানের টাইপ Option<T>
নয়, সেখানে আপনি নিরাপদে ধরে নিতে পারেন যে মানটি নাল নয়। Rust-এর জন্য এটি একটি ইচ্ছাকৃত ডিজাইনের সিদ্ধান্ত ছিল নালের ব্যাপকতা সীমিত করতে এবং Rust কোডের নিরাপত্তা বাড়াতে।
তাহলে আপনি কীভাবে একটি Option<T>
টাইপের মান থাকলে Some
ভেরিয়েন্ট থেকে T
মানটি বের করবেন যাতে আপনি সেই মানটি ব্যবহার করতে পারেন? Option<T>
এনামের বিভিন্ন পরিস্থিতিতে দরকারী প্রচুর সংখ্যক মেথড রয়েছে; আপনি এর ডকুমেন্টেশনে সেগুলো দেখতে পারেন। Option<T>
-এর মেথডগুলোর সাথে পরিচিত হওয়া আপনার Rust যাত্রায় অত্যন্ত দরকারী হবে।
সাধারণভাবে, একটি Option<T>
মান ব্যবহার করার জন্য, আপনি প্রতিটি ভেরিয়েন্ট হ্যান্ডেল করার জন্য কোড রাখতে চান। আপনি কিছু কোড চান যা শুধুমাত্র তখনই চলবে যখন আপনার কাছে একটি Some(T)
মান থাকবে এবং এই কোডটিকে ভিতরের T
ব্যবহার করার অনুমতি দেওয়া হয়। আপনার কাছে যদি একটি None
মান থাকে তবে আপনি অন্য কিছু কোড চালাতে চান এবং সেই কোডে একটি T
মান উপলব্ধ নেই। match
এক্সপ্রেশন হল একটি কন্ট্রোল ফ্লো কনস্ট্রাক্ট যা এনামগুলোর সাথে ব্যবহার করার সময় ঠিক এটিই করে: এটি এনামের কোন ভেরিয়েন্ট রয়েছে তার উপর নির্ভর করে ভিন্ন কোড চালাবে এবং সেই কোডটি মিলিত মানের ভিতরের ডেটা ব্যবহার করতে পারে।