آموزش زبان برنامهنویسی Rust – قسمت۱۴- پترنها
ما در Rust دستورات مختلفی را برای کنترل جریان اجرای برنامه در اختیار داریم. قبل از این با if و حلقهها آشنا شدیم. چیزهایی که تقریباً در تمام زبانها وجود دارند.
امّا امروز میخواهیم با یک دستور متفاوت آشنا بشویم. دستوری فوقالعاده قدرتمند که هم انعطافپذیری بالایی را هنگام کدنویسی به شما میدهد، و هم خیلی وقتها جلوی اینکه برنامهی باگدار بنویسید را میگیرد.
ما میخواهیم در این جلسه به صورت کامل همهچیز را درمورد دستور match و الگوها (pattern) یادبگیریم. بعد از اینکه مدّتی با آنها کارکردید، حسرت میخورید که چرا بقیهی زبانها چنین قابلیّتی را ندارند.
این نوشته چون قرار است تمام جنبههای این دستور و پترنها را پوشش بدهد کمی طولانی است، پس سعی کنید با دقّت تمام و حوصلهی زیاد مفاهیمی که درونش وجود دارد را یادبگیرید.
توجّه: شاید عنوان بخشها الان برایتان کمی گنگ به نظر برسد. ولی با خواندن هر بخش دقیقاً متوجّه منظور عنوان میشوید و بعداً موقعی که نیاز بود تا دوباره به این نوشته مراجعهکنید تا مباحث را یادآوری کنید، به محض دیدن عنوان هر بخش میفهمید که دارد درمورد چه چیزی صحبت میکند. در این قسمت باید کمی صبر داشته باشید تا همهچیز خوب پیش برود. 😉
خب این شما و این کاملترین آموزش پترنهای زبان Rust در کهکشان راه شیری و حومه.
فهرست مطالب
دستور match چیست؟
فرضکنید که میخواهید یک مقدار را در برابر حالتهای مختلف امتحان کنید. در سادهترین سناریو، قرار است اگر مقدار ورودی برابر با ۱ بود پیام خوشآمدگویی به مدیر پرینت بشود. اگر برابر با ۲ بود باید پیام خوشآمدگویی به کاربر برنامه پرینت بشود و اگر برابر با ۳ بود باید پیام درخواست عضویت را به کاربر مهمان نمایش داد.
ساختار switch، نزدیکترین ساختار در زبانهای دیگر به ساختار match
ساختاری که خیلی از زبانها برای این کار پیشنهاد میدهند، ساختار switch است. مثلاً اگر بخواهیم یک برنامهی ساده را با همین منطقی که گفتیم در زبان C بنویسیم، به چیزی شبیه به این خواهیم رسید:
#include "stdio.h" /* * This function gets userLevel integer and prints proper message according to the input value. */ void printMessage(int userLevel) { // Checking userLevel parameter for different probabilities switch (userLevel) { case 0: // Is userLevel value equals to 0? printf("Welcome dear admin.\n"); // userLevel is zero, so user is an admin. break; // We found what we wanted, so we breaking decision structure. case 1: // Is userLevel value equals to 1? printf("Welcome back our best member.\n"); // userLevel is one, so user is a member. break; // We found what we wanted, so we breaking decision structure. case 2: // Is userLevel value equals to 2? printf("Hello. Please register first.\n"); // userLevel is two, so user is a guest. break; // We found what we wanted, so we breaking decision structure. } } /* * This function gets an integer from user and returns it. */ int getUserInput() { int userValue; printf("Please enter your level. enter -1 for exit.\n"); scanf("%d", &userValue); return userValue; } int main() { int userInput = getUserInput(); // Getting input from user for the first time. while (userInput != -1) { // While user input is not equal to the -1, the program will repeat. printMessage(userInput); // Showing proper message for user according to his/hers type. userInput = getUserInput(); // Getting new input from the user. } return 0; }
بیایید اوّل اجرای برنامه را ببینیم و بعد سراغ توضیحش برویم:
ما اوّل از همه یک تابع به نام getUserInput
داریم که پیام زیر را به کاربر نمایش میدهد:
Please enter your level. enter -1 for exit.
بعد از نمایش این پیام، یک عدد صحیح را از کاربر میگیرد.
ما درون تابع main
ابتدا همین تابع را فراخوانی میکنیم. سپس بررسی میکنیم که مقداری که کاربر وارد کرده عدد ۱- نباشد. تا زمانی که ورودی عددی به جز ۱- باشد، ما درون حلقهی while
به اجرای برنامه ادامه میدهیم.
حالا میرسیم به جایی که اصلاً موضوع بحث ما است و این مثال را برای رسیدن به آن زدهایم: تابع printMessage
.
این تابع از ساختار switch برای بررسی مقدار ورودی استفاده میکند. ما داخل پرانتزهای مقابل کلمهی کلیدی switch
میگوییم که میخواهیم مقدار userLevel
را برای حالات مختلف بررسی کنیم.
هر حالت درون یک بخش case
مشخّص میشود.
اوّلین حالت این است که سطح کاربر ۰ باشد. اینطوری باید پیام مربوط به مدیر نمایش داده بشود. دومین حالت برای سطح کاربری ۱ و سومی هم برای سطح کاربری ۲ است.
خب، حالا سؤال این جاست که برتری استفاده از switch
به جای چند if else
متوالی چیست؟ درست است که در برخی زبانها، برخی کامپایلرها موقع استفاده از switch
با حالات زیاد، کارهایی برای افزایش سرعت اجرای برنامه میکنند، امّا از لحاظ ساختاری این ساختار ساده اصلاً کمکی به منطق برنامه میکند؟
برخی زبانهای جدیدتر مثل پایتون به همین خاطر کلاً قید استفاده از switch را زده اند و در این زبانها اصلاً چنین ساختاری وجود ندارد. در این زبانها شما میتوانید در حالت ساده این ساختار را با همان if و else ها پیادهسازی کنید. (توضیحات بیشتری در این مورد در داکیومنتهای پایتون وجود دارد که با کلیک روی این نوشته میتوانید آنها را بخوانید.)
مشکل بعدی با این ساختار این است که انعطافپذیری ندارد. شما ورودی را دقیقاً با یک مقدار خاص میتوانید بررسی کنید. یعنی مثلاً درون case
نمیتوانید بگوید که ببین ورودی در یک بازهی مشخّص قرار دارد یا نه. باید تمامی اعضای بازه را یک به یک مشخّص کنید و برای هرکدام همان کد را تکرار کنید.
به علاوه شما اگر از ساختارهای دادهی پیچیدهای مثل struct استفاده کنید، نمیتوانید مستقیماً از case
ها برای بررسی مقادیر درون آنها استفاده کنید و باید خودتان خارج از کد به صورت کاملاً اختصاصی هر مقدار را در یک متغیّر جدید قرار بدهید.
شاید بیدلیل نیست که نسبت به خیلی از ساختارهای دیگر، switch
ها را خیلی کمتر در کدهای مختلف میبینم.
حالا برویم سراغ زبان Rust.
ساختار کلّی match و برتری آن نسبت به switch
زبان Rust هم ساختاری به نام switch ندارد. به جای آن یک ساختار بسیار قدرتمند به نام match آماده کرده است.
در match ما یک مقدار را با یک الگو (pattern) مقایسه میکنیم و اگر مقدارمان با آن الگو مطابقت داشت، کد مربوطه را اجرا میکنیم.
شاید بپرسید پس فرقش با switch چه شد؟ خب، فرقش در همان الگو است. اینجا به جای اینکه فقط یک مقدار دقیق برای مقایسه داشته باشیم، یک الگو را در اختیار داریم.
قبل از اینکه در اعماق الگوها به جست و جو بپردازیم، بیایید ساختار کلّی دستور match
را ببینیم:
match expression { pattern => expression, pattern2 => expression2, . . . }
دستور match به شکل واقعاً غافلگیرکنندهای با کلمهی کلیدی match
آغاز میشود. مقابل آن یک expression قرار میگیرد (یادت نیست که expression ها چه چیزی بودند؟ روی این نوشته کلیک کن و خیلی سریع تعریف آن را بخوان). Expression میتواند یک مقدار ساده مثل یک عدد، رشته یا هرچیز دیگری باشد.
داخل آکولادها، ترکیب: pattern => expression
قرار میگیرد. به هرکدام از این ترکیبها یک match arm میگویند.
ابتدا الگویی که قرار است مقدارمان را با آن مقایسه کنیم قرار میگیرد. بعد از آن یک مساوی و علامت بزرگتر به شکل <=
قرارمیگیرد. بعد از این نشانه هم کدی که قرار است در صورت تطبیق الگو اجرا شود قرار میگیرد.
تعداد match arm هایی که درون یک ساختار match قرار میگیرد میتواند یک یا بیشتر باشد. به همین دلیل آن ۳نقطهی انتهایی را در الگوی بالا اضافه کردم.
خب حالا برویم سراغ الگوها و شیوههای مختلف استفاده از match.
الگو یاpattern چیست؟
الگو (pattern) برای تطبیق مقادیر با ساختارها به کار برده میشود. یعنی برای بررسی اینکه یک مقدار از الگوی مشخّصی پیروی میکند یا نه. امّا این تمام ماجرا نیست. شما میتوانید الگو را متضاد expression درنظر بگیرید.
Expressionها مقادیر را تولید میکنند، امّا الگوها مقادیر را مصرف میکنند. کمی که جلوتر برویم میفهمید که منظورمان از مصرفکردن چیست.
اگر منابع دیگر را هم برای یادگیری زبان Rust مطالعه کرده باشید، دیدهاید که برخی الگو (pattern) ها را مشابه regex تعریف میکنند. از آنجایی که این دو مفهوم از زمین تا آسمان با هم فرق میکنند، ما اینجا از این کارهای اشتباه نمیکنیم. شما هم سعیکنید آن تصوّر اشتباه را دور بیندازید و به الگوها به عنوان یک مفهوم مجزا نگاه کنید.
الگوها دو ویژگی خیلی مهم دارند که باید همیشه موقع کار با الگوها آن دو را در نظر بگیرید:
۱) جامع بودن (Refutability) الگو. یعنی مجموعهی الگوهایی که در match قرار دارند، باید تمامی حالات ممکن را برای مقدار ورودی match تحت پوشش قرار بدهند. البته ترجمهی کلمهی Refutability ربطی به جامعبودن ندارد، ولی به نظرم عبارت «جامعبودن» منظور سازندگان Rust را بهتر به زبان فارسی میرساند.
۲) تخریبکنندگی (Destructuring). یعنی الگو میتواند ساختار را به اجزای سازندهاش تجزیه کند.
هر دو ویژگی را در ادامهی آموزش با هم بررسی میکنیم. ویژگی Destructuring را در بخشهایی که در عنوانشان از کلمهی «استخراج» استفاده شده است و ویژگی Refutability را در بخشی مجزا در اواخر آموزش.
خب دیگر حرفزدن کافی است. وقت آن است که دست به کد بشویم و ببینیم دقیقاً اینجا چه خبر است.
تطبیق literal ها
ما خیلی وقتها نیاز داریم که یک مقدار را برابر literal های مختلف بررسی کنیم. شبیه به کاری که ساختار switch انجام میدهد.
ما میتوانیم الگو را طوری تعریف کنیم که صرفاً شامل یک literal بشود. (اینجا منظورمان از literal یک مقدار ثابت و مشخّص است. مثل یک عدد، یکی از حالاتenum با مقدار مشخص، یک String ثابت یا …)
بیایید تابع printMessage مثال ابتدای نوشته را در زبان Rust با استفاده از match پیادهسازی کنیم:
fn print_message(user_level: u8) { match user_level { 0 => println!("Welcome dear admin."), 1 => println!("Welcome back our best member."), 2 => println!("Hello. Please register first.") } } fn main() { let user_inputs: [u8;5] = [0, 1, 2, 3, 4]; for value in user_inputs.iter() { print_message(*value); } }
از آنجایی که هنوز نحوهی ورودیگرفتن از کاربر را یادنگرفتهایم، به جای اینکه ورودیها را از کاربر بگیریم، خودمان آنها را به صورت hard code شده درون متغیّر user_inputs
قرار داده ایم.
حالا با یک حلقهی for روی مقادیر این آرایه میچرخیم. فقط باید یادتان باشد که وقتی با متد iter
روی مقادیر یک آرایه میچرخیم، مقادیری که خروجی داده میشدند رفرنس هستند. به همین خاطر موقع فراخوانی تابع print_message
، قبل از value
علامت *
را قرار داده ایم تا با dereference کردن، به خود مقدار به جای رفرنسی به آن دسترسی داشته باشیم.
حالا میرسیم به خود تابع print_message
که بحث اصلی ما است.
درون تابع با کلمهی کلیدی match
اعلام کرده ایم که میخواهیم از ساختار match استفاده کنیم. مقابل آن نام ورودی تابع را قرار داده ایم تا مشخّص شود قرار است حالتهای مختلف آن را بررسی کنیم.
در اوّلین match arm، الگوی ما تنها عدد ۰ است. یعنی این پترن تنها literal صفر را مورد بررسی قرار میدهد.
Match expression آن هم یک پرینت ساده است که تا الان بارها آن را دیدهایم.
در match arm دوم هم همین اتّفاق میافتد. با این تفاوت که الگوی ما literal یک است. در سومی هم از الگویی که تنها literal دو را مورد بررسی قرار میدهد استفاده کردهایم.
تا اینجای کار دقیقاً همان تابع printMessage
که به زبان C نوشته بودیم را پیادهسازی کردهایم.
خب، این پیادهسازی در C که جواب داد. ببینیم نتیجهاش در Rust چه میشود:
error[E0004]: non-exhaustive patterns: `3u8..=255u8` not covered --> src/main.rs:2:11 | 2 | match user_level { | ^^^^^^^^^^ pattern `3u8..=255u8` not covered
خب کامپایلر Rust با یک ارور جلوی ما را گرفت. امّا دلیلش چیست؟
همانطوری که در متن پیام خطا میبینید، کامپایلر به ما میگوید که شما تمامی حالات را در پترنها درنظر نگرفتهاید. یعنی ویژگی جامعبودن که بالاتر معرّفیاش کردیم نقض شده است. در مورد این ویژگی بیشتر صحبت میکنیم.
فعلاً بیایید با افزودن یک الگوی خیلی رایج این مشکل را برطرف کنیم:
fn print_message(user_level: u8) { match user_level { 0 => println!("Welcome dear admin."), 1 => println!("Welcome back our best member."), 2 => println!("Hello. Please register first."), _ => println!("Bad Input") } }
الگوی آخری که به match اضافهکردیم با نام wildcard شناخته میشود که در بخش مخصوص به خودش درموردش توضیح دادهایم.
تا اینجا شما صرفاً میتوانید آن را مانند else
در دنبالهای از if
ها درنظر بگیرید. یعنی ورودی هرچیزی که باشد، اگر با پترنهای قبلی match نشده باشد، این الگو با آن تطبیق پیدا میکند و کد مربوط به آن اجرا میشود.
پس اینطوری با اضافهکردن این الگوی جدید، ما تمامی حالات ممکن را پوششدادهایم.
حالا برنامه را کامپایل و اجرا میکنیم:
Welcome dear admin. Welcome back our best member. Hello. Please register first. Bad Input Bad Input
همانطوری که گفتیم، literal میتواند هر مقدار دقیق دیگری هم باشد. برای مثال همان enum مربوط به status های پروتکل http را که در قسمت قبلی دیدیم درنظربگیرید:
enum HttpStatus { Ok = 200, NotFound = 404, InternalServerError = 500 }
میخواهیم یک برنامه بنویسیم که با توجّه به status درخواست http، یک پیام مناسب را چاپ کند:
fn print_status(http_status: HttpStatus) { match http_status { HttpStatus::Ok => println!("status: 200 Ok"), HttpStatus::NotFound => println!("status: 404 Not Found"), HttpStatus::InternalServerError => println!("status: 500 Internal Server Error") } } fn main() { let a = HttpStatus::Ok; let b = HttpStatus::NotFound; let c = HttpStatus::InternalServerError; print_status(a); print_status(b); print_status(c); }
همانطوری که میبینید، در تابع print_status
، ما ورودیای از نوع HttpStatus
که همان enumمان است گرفتهایم. الگوهای ما برای literal matching همان حالتهای موجود در enum هستند. فقط حواستان باشد که باید دقیقاً خود حالت را به شکل EnumName::Variant
مشخّص کنیم. یعنی اگر مقدار حالت، مثلاً ۲۰۰ برای Ok
، را به عنوان الگو قرار بدهید، با خطا مواجه میشوید. بیایید برنامه را اجرا کنیم:
status: 200 Ok status: 404 Not Found status: 500 Internal Server Error
همانطوری که میبینید برنامه به خوبی اجرا میشود و مثل دفعهی پیش مشکلی به خاطر پوشش ندادن تمامی حالات پیش نمیآید. حتّی با وجود اینکه ما از wildcard استفاده نکردیم. دلیلش این است که تعداد حالات ممکن یک enum مشخّص است.
در اینجا HttpStatus
تنها همین ۳ حالت را دارد و ما هر ۳ تا را در الگوها پوشش داده ایم. بنابراین اصلاً امکان ندارد که مقداری که حالت دیگری به جز این ۳ تا دارد وارد این match
بشود.
فراموش نکنید که Rust روی type ها به شدّت حساس است و اصلاً امکان ندارد وقتی که نوع ورودی تابع HttpStatus
است اجازه بدهد مقدار دیگری واردش بشود. خب این از توضیح سادهترین نوع الگوها. حالا برویم سراغ حالتهای پیچیدهتر.
استخراج مقادیر با استفاده از متغیّرها
حالا ما یک الگو نوشتهایم و مقدارمان هم با آن منطبق شده است. اگر بخواهیم از مقدار منطبق شده استفاده کنیم باید چه کار کنیم؟
همانطوری که در جلسهی پیش در توضیح enum ها گفتیم، شما نمیتوانید به مقدار ذخیره شده در یک enum به صورت مستقیم دسترسی داشته باشید. حالا ما میخواهیم با استفاده از متغیّرهایی که درون الگوها تعریف میشوند، از مقدار ذخیره شده در یک enum استفاده کنیم.
این بار enumی که دفعهی پیش برای نگهداری رنگ ساخته بودیم را به خاطر بیاورید:
enum Colour { RGB(u16, u16, u16), Hex(String) }
حالا میخواهیم برنامهای بنویسیم که رنگهایی که از نوع این enum هستند را چاپ کند. برنامهی ما این شکلی است:
fn print_colour(colour_value: Colour) { match colour_value { Colour::RGB(red, green, blue) => { println!("The colour is in rgb format. red value: {}, green value: {}, blue value: {}", red, green, blue); }, Colour::Hex(hex) => println!("The colour is in hex format: #{}", hex) } } fn main() { let a = Colour::RGB(255, 123, 30); let b = Colour::Hex(String::from("ff7b1e")); print_colour(a); print_colour(b); }
کد اصلی در تابع print_colour
قرار دارد. در اوّلین match arm، ما پترنی برای تطبیق با نوع RGB
نوشتهایم. همهچیز شبیه به قبل است، با این تفاوت که ما این بار درون الگوهایمان از متغیّرهایی برای استخراج مقدار مطابقت داده شده استفاده کرده ایم.
یعنی چی؟ ما در Colour
تعریف کرده ایم که حالت RGB
یک tuple like struct است که ۳ مقدار عددی میگیرد.
حالا درون الگو ما درست با همان سینتکس، سه متغیّر جدید را به نامهای red، green و blue تعریف کردهایم.
اگر ورودی با این الگو match بشود، اتّفاقی که میافتد این است که هر کدام از عددهای ذخیره شده در آن tuple like struct، به ترتیب در این متغیّرهای جدید قرار میگیرند.
حالا ما میتوانیم از این متغیّر ها در expressionی که بعد از علامت <= میآید استفاده کنیم.
یک تفاوت دیگر این کد با کدهای قبلی این است که این بار چون کدمان طولانیتر بود، expression بعد از الگو را درون آکولاد قرار دادهایم.
برای حالت Hex هم دقیقاً همین کار را کردهایم. چون Hex تنها یک مقدار داشت، ما هم فقط یک متغیّر را برای استخراج مقدارش تعریف کردهایم.
همانطوری که میبینید خود کامپایلر Rust از روی enum اصلی میتواند type متغیّرها را تشخیص بدهد و دیگر لازم نیست ما خودمان آن را مشخّص کنیم.
خروجی برنامهای بالا این شکلی میشود:
The colour is in rgb format. red value: 255, green value: 123, blue value: 30 The colour is in hex format: #ff7b1e
استفاده از literal ها و متغیّر ها به صورت همزمان در یک match expression
ما میتوانیم در match
حالتها را با دقّتهای متفاوت از هم متمایز کنیم. مثلاً چه میشود اگر ما بخواهیم برای رنگ قرمز rgb، کد کاملاً متفاوتی نسبت به بقیهی رنگها اجرا شود؟
برای این کار میتوانیم خیلی راحت الگوهایی که برای literalها نوشته میشوند را با الگوهای دارای متغیّر ترکیب کنیم.
اوّل کد زیر را ببینید:
fn print_colour(colour_value: Colour) { match colour_value { Colour::RGB(255, 0, 0) => println!("RED Colour. Here is a completely different code."), Colour::RGB(red, green, blue) => { println!("The colour is in rgb format. red value: {}, green value: {}, blue value: {}", red, green, blue); }, Colour::Hex(hex) => println!("The colour is in hex format: #{}", hex) } }
ما قبل از الگوی قبلی، یک الگوی جدید اضافه کردهایم. در این الگو، ما تلاش میکنیم که مقدار ورودی را با یک literal تطبیق بدهیم.
اگر ورودی یک رنگ از نوع RGB
بود که مقدار اوّلش برابر با ۲۵۵ است و دو مقدار بعدی برابر با ۰، این الگو با آن ورودی مطابقت پیدا میکند و کد مربوط به آن اجرا میشود.
اگر ورودی از نوع RGB
بود ولی مقادیرش دقیقاً مثل بالا نبود، با الگوی دوم match میشود و کد مربوط به آن اجرا میشود.
fn main() { let a = Colour::RGB(255, 123, 30); let b = Colour::Hex(String::from("ff7b1e")); let red = Colour::RGB(255, 0, 0); print_colour(a); print_colour(b); print_colour(red); }
حالا برنامه را اجرا میکنیم:
The colour is in rgb format. red value: 255, green value: 123, blue value: 30 The colour is in hex format: #ff7b1e RED Colour. Here is a completely different code.
اینطوری ما قدرت انعطاف خیلی بیشتری داریم و میتوانیم حالات مختلف را با جزئیات متفاوت پیادهسازی کنیم.
فقط حواستان باشد که متغیّرهای red
، green
و… که درون الگو تعریف شدهاند، تنها در scope همان match arm تعریف میشوند. یعنی شما نمیتوانید از آنها در هیچکجای دیگری به جز همانجا استفادهکنید (اگر مطالب مربوط به scope ها را فراموش کردهاید، با کلیک روی این نوشته به قسمت مربوط به آن بروید و خیلی سریع همهچیز را به خاطر بیاورید).
استفاده از بازهها در الگو
ما قبلاً یادگرفتیم که range یا همان بازه چیست و چطوری میتوان از آن در جاهایی مثل حلقههای for استفاده کرد. بازهها به ما انعطافپذیری خوبی میدهند، چون به جای اینکه تمامی مقادیر یک بازه را بنویسیم، میتوانیم بازهای که مقادیر مورد نظرمان در آن قرار دارند را مشخّص کنیم.
خبر خوب این است که ما میتوانیم از بازهها در الگوهایمان هم استفاده کنیم.
فرض کنید میخواهیم برنامهای بنویسیم که کاراکتر ورودی را تست کند تا برای حروف کوچک انگلیسی، حروف بزرگ و اعداد پیامهای مختلفی را نمایش بدهد:
fn print_character_type(character: char) { match character { 'a'...'z' => println!("{} is a lowercase english character.", character), 'A'...'Z' => println!("{} is a uppercase english character.", character), '0'...'9' => println!("{} is an english digit.", character), _ => println!("This character is not an english character.") } } fn main() { let characters = ['X', 'y', '4', 'س']; for ch in characters.iter() { print_character_type(*ch); } }
ما در اوّلین الگو گفتهایم که تمامی کاراکترهایی که از a تا z هستند را مورد بررسی قرار بده. اگر ورودی یکی از این کاراکترها بود، پیام مقابل را چاپ کن.
در الگوی دوم همین کار را برای حروف بزرگ کردهایم. در سومی هم بازهای از کاراکترهای مرتبط به اعداد انگلیسی را از 0 تا 9 به عنوان الگو استفاده کردهایم.
فقط همانطوری که قبلاً گفتیم، در Rust ما از Unicode استفاده میکنیم. پس نوع char هم شامل مقادیر Unicode scalar است. پس به جز حروف و اعداد انگلیسی، تعداد زیادی کاراکتر دیگر هم وجود دارد.
به همین خاطر ما یک الگوی wildcard هم در آخر اضافه کرده ایم تا بقیهی کاراکترهای Unicode را هم تحت پوشش قرار بدهیم و match expression ما «جامع» باشد و تمامی حالات را پوشش بدهد.
نکته: بازه (range) ها در الگوها با بازهها در جاهای دیگر تفاوت دارند. یک بازه درون الگو شامل آخرین مقدار هم میشود و مشابه حالت =..
در بازههای عادی است (برای یادآوری بیشتر، با کلیک روی این نوشته به آموزش بازهها در Rust بروید).
خب حالا بیایید برنامه را کامپایل و اجرا کنیم تا ببینیم خروجی چه میشود.
X is a uppercase english character. y is a lowercase english character. 4 is an english digit. This character is not an english character.
همانطوری که میبینید، به جای اینکه ۵۲ الگو برای حروف و ۱۰ الگو برای ارقام بنویسیم، با ۳ تا الگو توانستیم تمامی اعداد و حروف انگلیسی را پوشش بدهیم.
استفاده از چند پترن در کنار هم
خب حالا شاید ما لازم داشته باشیم که برای چندین حالت مختلف که با الگوهای متفاوت match میشوند، کد یکسانی را اجرا کنیم.
فرضکنید که ما یک فروشگاه داریم. این فروشگاه از ساعت ۸ صبح تا ۶ بعد از ظهر کار میکند، امّا بین ۱۲ تا ۱۳ برای ناهار و استراحت بسته است.
حالا ما میخواهیم برنامهای را بنویسیم که ساعت را بگیرد و وضعیّت بسته یا باز بودن فروشگاه را نمایش بدهد:
fn show_store_status(hour: f32) { match hour { 8.0...12.0 | 13.0...18.0 => println!("The store is open."), 12.0...13.0 => println!("We will start working again at 13"), _ => println!("closed") } } fn main() { let hours = [8.5, 12.0, 12.25, 13.0, 19.0]; for hour in hours.iter() { show_store_status(*hour); } }
فروشگاه از ساعت ۸ تا ۱۲ باز است. به علاوه از ساعت ۱۳ تا ۱۸ هم فروشگاه مشغول به کار است. پس دقیقاً همان کدی که وضعیّت فروشگاه را در ساعت ۸ تا ۱۲ نشان میدهد، میتواند برای وقتی که ساعت بین ۱۳ تا ۱۸ قرار دارد هم کار کند.
به همین خاطر ما در اوّلین الگو (pattern )، دو الگوی مختلف را در کنار هم استفاده کردهایم.
اوّلین الگو ساعتهایی که بین ۸ تا ۱۲ قرار دارند را مشخّص میکند.
بعد از این الگو، ما علامت یا منطقی(|
)را قرار داده ایم. این علامت مشخّص میکند که قرار است بعد از این الگو، یک الگوی دیگر هم قرار بگیرد و هر کدام از این الگوها که با ورودی منطبق شدند، کد مربوط به این match arm اجرا میشود.
حالا بعد از علامت |
دومین الگو را که زمانهای بین ۱۳ تا ۱۸ را نمایش میدهند، قرار میدهیم.
در match arm بعدی، ما یک الگوی ساده نوشتهایم که ساعات بین ۱۲ تا ۱۳ را مورد بررسی قرار میدهد.
ما ساعت را به عنوان یک عدد اعشاری f32
تعریف کردهایم. طبیعتاً تعداد اعداد خیلی زیادی علاوه بر ۲۴ ساعت وجود دارند. به همین خاطر، ما باید یک match arm دیگر هم اضافه کنیم که تمامی اعداد دیگر (ساعاتی که فروشگاه بسته است به علاوهی تمامی اعداد دیگری که به f32
قابل نمایش هستند امّا به عنوان عدد ساعت قابل پذیرش نیستند) را مورد بررسی قرار میدهد.
حالا برنامه را اجرا میکنیم:
The store is open. The store is open. We will start working again at 13 The store is open. closed
حالا علاوه بر بازهها، امکان ترکیب هر نوع الگویی را با یکدیگر داریم.
استخراج مقادیر از داخل یک tuple
یکی از تواناییهای الگوها این است که وقتی که ورودی با آنها match شد، میتوانند بخشهایی از ساختار ورودی را در متغیّرهای جدید قراربدهند تا در کدی که مربوط به آن match arm است، از آن استفاده کنند.
درست مثل چیزی که در بخش استخراج با متغیّرها دیدیم.
وقتی که ورودی یک tuple است، شما میتوانید المانهای آن را با استفاده از متغیّرها استخراج کنید.
مثلاً فرضکنید که یک تابع داریم که یک tuple را به عنوان ورودی میگیرد. این tuple دارد یک رنگ را در فرمت rgb نمایش میدهد.
اگر رنگ ورودی تدارجی از رنگ قرمز بود (مقادیر green و blue آن برابر با صفر بود)، قرار است یک پیام نمایش داده بشود. در غیر این صورت سه مقدارِ رنگ را به کاربر نمایش میدهیم:
fn print_rgb_tuple(rgb: (u16, u16, u16)) { match rgb { (red, 0, 0) => println!("This colour is a gradient of red. red value: {}", red), (red, green, blue) => println!("rgb({}, {}, {})", red, green, blue) } } fn main() { let r1 = (113, 0, 0); let r2 = (123, 221, 0); print_rgb_tuple(r1); print_rgb_tuple(r2); }
همانطوری که میبینید ما ورودی تابع print_rgb_tuple
را به صورت یک تاپل سهتایی از نوع u16
تعریف کرده ایم.
در اوّلین الگو (pattern)، ما حالتی را بررسی میکنیم که مقادیر دوم و سوم ورودی برابر با صفر باشد. در این صورت مقدار اوّل این tuple را درون متغیّر red
میریزیم.
امّا در حالت دوم تمامی مقادیر تاپل را استخراج کردهایم و آنها را به ترتیب در متغیّرهای red
و green
و blue
ریختهایم.
حواستان باشد که این متغیّرها تنها در scope همان match arm تعریف شده اند. به همین خاطر متغیّر red
اوّلین الگو یک متغیّر کاملاً جدا نسبت به متغیّر red
در الگوی دوم است.
بیایید برنامه را کامپایل و اجرا کنیم:
This colour is a gradient of red. red value: 113 rgb(123, 221, 0)
چرا برنامه بدون مشکلی بر سر Refutability اجرا شد؟ چون ما در حقیقت تمامی حالات را پوشش داده ایم.
الگوی دوم هر ۳ مقدار tuple را درون ۳ متغیّر از نوع u16
قرار میدهد. پس وقتی ورودی به این الگو میرسد، در هر صورت موردپذیرش قرار میگیرد.
یعنی این الگو تمامی tuple های سهتایی را پوشش میدهد. پس الگویی جامع است.
استخراج مقادیر از داخل یک struct
خب حالا ما میخواهیم مقادیر را از درون struct ها هم استخراج کنیم. بیایید دقیقاً همین مثال قبلی را درنظر بگیریم. این بار فقط به جای اینکه برای نگهداری رنگ rgb از یک tuple استفاده کنیم، میخواهیم از یک struct استفاده کنیم:
struct RGB { red: u16, green: u16, blue: u16 }
ابتدا یک struct خیلی ساده به نام RGB
تعریف میکنیم که ۳ فیلد به نامهای red
، green
و blue
از نوع u16
دارد.
حالا میخواهیم همان عملکرد قبلی را برای این struct پیادهسازی کنیم:
fn print_rgb_struct(rgb: RGB) { match rgb { RGB {red: r, green: 0, blue: 0} => println!("This colour is a gradient of red. red value: {}", r), RGB {red, blue, green} => println!("rgb({}, {}, {})", red, green, blue) } } fn main() { let r1 = RGB { red: 113, green: 0, blue: 0 }; let r2 = RGB { red: 123, green: 221, blue: 0 }; print_rgb_struct(r1); print_rgb_struct(r2); }
ما در تابع print_rgb_struct
، یک ورودی از نوع ساختار RGB
میگیریم.
درون match
، در اوّلین الگو میخواهیم تنها رنگهایی که تدارجی از قرمز اند را انطباق بدهیم.
الگوی ما درست به شکل یک struct است. ابتدا نام struct (در اینجا RGB
) را مینویسم. سپس دورن آکولادها فیلدها را مشخّص میکنیم.
ما میخواهیم مقدار مربوط به key های green
و blue
صفر باشد. پس مقدار آنها را مشخّص میکنیم.
به علاوه ما نیاز داریم که value مرتبط با key مربوط به رنگ قرمز، red
، را هم در یک متغیّر جدید ذخیره کنیم. اینطوری میتوانیم از آن درون expression مربوط به این match arm از آن استفاده کنیم.
پس ابتدا red
که key مورد نظرمان است را مینویسم. این بار مقابل علامت :
نام متغیّری که میخواهیم value در آن ذخیره بشود را قرار میدهیم.
اینطوری میتوانیم درون کد مربوط به این بخش به آن مقدار دسترسی داشته باشیم.
الگوی دوم کمی متفاوت به نظر میرسد. ما تنها اسم key ها را نوشتهایم و متغیّری که value ها قرار است در آنها قراربگیرند را مشخّص نکرده ایم.
همانطوری که در قسمت مربوط به توضیح struct ها دیدیم، این یکجور مختصرنویسی است. کامپایلر وقتی به این کد میرسد، خودش یک متغیّر جدید و همنام با key میسازد و value را درون آن میریزد.
یعنی در حقیقت این کد کوتاه شدهی کد زیر است:
RGB {red: red, blue: blue, green: green} => println!("rgb({}, {}, {})", red, green, blue),
در الگوی اوّل هم میتوانستیم برای کمتر نوشتن از همین شیوه استفاده کنیم.
خب حالا برنامه را اجرا کنیم تا ببینم درست کار میکند یا نه:
This colour is a gradient of red. red value: 113 rgb(123, 221, 0)
استخراج مقادیر از داخل struct ها و enum های تودرتو
ما حالت عادی استخراج مقادیر enum ها را در بخش استخراج مقادیر با استفاده از متغیّرها دیدیم. حالا میخواهیم با یک مثال ترکیبی، نحوهی استخراج حالتهای تودرتو را هم ببینم.
میخواهیم enum رنگمان را کاملتر کنیم. برای این کار یک حالت transparent به آن اضافه میکنیم. به علاوه فرمت CMYK را هم به آن اضافه میکنیم تا فرمتهای بیشتری را برای نمایش رنگ دراختیار داشته باشد:
struct RgbStruct { red: u16, green: u16, blue: u16 } enum Colour { Transparent, RGB(RgbStruct), Hex(String), CMYK {cyan: f32, magenta: f32, yellow: f32, black: f32} }
ما یک struct به نام RgbStruct
تعریف کرده ایم که قرار است رنگ را در فرمت rgb نمایش بدهد. چرا از struct استفاده کردهایم؟ چون هر رنگ در فرمت rgb حتماً هر سهتا مقدار را دارد و به کار بردن اسم هر بخش به خواناتر شدن برنامه کمک میکند.
حالا میرسیم به خود Colour
. در این enum ما ابتدا یک مقدار ساده به نام Transparent
را اضافه کرده ایم. این مقدار به صورت صریح هیچ مقداری ندارد. چون وقتی چیزی شفاف است، شفاف است. دادهی اضافیای برای نمایش شفاف بودنش نیاز نداریم.
حالت RGB
در این enum هم یک نمونه از ساختار RgbStruct
را در خودش نگهداری میکند. مقدار Hex
مثل همان کدی است که در جلسهی قبلی دیدیم و تغییری نکرده است. در آن تنها یک String
ذخیره میشود.
حالت CMYK
هم یک struct را نگهداری میکند، امّا این struct یک ساختار بدون نام است که درون خود enum و تنها برای همین حالت تعریف شده است. یعنی مثل RgbStruct
ما نمیتوانیم به صورت مجزا نمونههایی از آن بسازیم.
خب حالا ببینیم که چطوری میتوان مقادیر مختلف را با استفاده از الگوها از درون نمونههای ساخته شده از این enum استخراج کرد:
fn print_nested_structures(colour: Colour) { match colour { Colour::Transparent => println!("This is Transparent! You can not see anything"), Colour::RGB(RgbStruct{red, green, blue}) => { println!("Colour is in rgb format: ({}, {}, {})", red, green, blue) }, Colour::Hex(hex_value) => println!("The colour in the hex format: #{}", hex_value), Colour::CMYK {cyan: c, magenta: m, yellow: y, black: k} => { println!("Colour is in cmyk format: ({}, {}, {}, {})", c, m, y, k) } } } fn main() { let t = Colour::Transparent; let rgb = Colour::RGB(RgbStruct{ red: 255, green: 255, blue: 255 }); let hex = Colour::Hex(String::from("ffffff")); let cmyk = Colour::CMYK { cyan: 0.0, magenta: 0.0, yellow: 0.0, black: 0.0 }; print_nested_structures(t); print_nested_structures(rgb); print_nested_structures(hex); print_nested_structures(cmyk); }
اصل کار ما با تابع print_nested_structures
است. البته اینجا منظورم از structure در اسم این تابع نوعدادههای متفاوت بود، نه struct ها.
در الگوی اوّل ما ورودی را بررسی میکنیم تا ببینیم حالت Transparent
است یا نه. اگر بود کد مخصوص به آن را اجرا میکنیم.
Colour::RGB(RgbStruct{red, green, blue}) => { println!("Colour is in rgb format: ({}, {}, {})", red, green, blue) },
در الگوی دوم، ما میخواهیم که هر سه عدد مربوط به rgb را از ورودی استخراج کنیم. به همین خاطر درون الگو، مانند حالت استخراج از struct، نام struct را نوشتهایم و درون آکولادها نام کلیدها را آوردهایم تا به صورت خلاصه متغیّرهایی با همان نامها بسازیم.
اگر ورودی یک رنگ از نوع RGB
باشد، با این الگو منطبق میشود، مقادیرش درون متغیّرهای جدید قرار میگیرد و کد مربوط به آن اجرا میشود.
الگوی سوم دقیقاً همان کدی است که در بخش استخراج مقادیر با استفاده از متغیّرها با هم دیدیم.
امّا میرسیم به سومین match arm:
Colour::CMYK {cyan: c, magenta: m, yellow: y, black: k} => { println!("Colour is in cmyk format: ({}, {}, {}, {})", c, m, y, k) }
همانطوری که دیدیم، ما برای حالت CMYK
از یک struct بدون نام که درون خود enum تعریف شده بود استفاده کردیم. پس برخلاف حالت دوم که برای استخراج مقادیر، ابتدا نام struct را مینوشتیم و بعد دوتایی key: variable
را درون آکولادها مشخّص میکردیم، این بار خبری از اسم struct نیست.
تنها کاری که ما اینجا میکنیم این است که مقابل نام حالت (CMYK
) و درون آکولاد دوتایی key: variable
را مشخّص میکنیم. مثل کاری که برای تعریف خود این struct هنگام معرّفیکردن enum کردیم.
اگر به کد درون تابع main
هم دقّت کنید، میبینید که آنجا هم دقیقاً همین اتّفاق افتاده است.
یعنی ما موقعی که میخواستیم متغیّر cmyk
را تعریف کنیم، پس از نوشتن Colour::CMYK
که حالت مورد نظر را از enum مشخّص میکند، مستقیماً آکولاد را بازکردهایم و مقادیر struct بینام و نشانمان را درونش مشخّص کردهایم.
خب بیایید برنامه را کامپایل و اجرا کنیم تا ببینم همهچیز خوب پیش میرود یا نه:
This is Transparent! You can not see anything Colour is in rgb format: (255, 255, 255) The colour in the hex format: #ffffff Colour is in cmyk format: (0, 0, 0, 0)
خب هیچ مشکلی وجود ندارد. نکتهی اصلی این است که کد ما برای استخراج مقادیر، باید دقیقاً منطبق بر روش تعریف آن مقدار باشد. یعنی اگر مقدار موردنظرمان یک struct معمولی است، باید دقیقاً همانطوری که یک struct ساخته میشود رفتار کنیم. اگر یک struct بینام است، همانطوری که به آن مقداردهی شده است.
تطبیق رفرنسها در الگوها
حالا ما فقط نمیخواهیم که یک مقدار را بگیریم و همه یا بخشی از آن را درون یک متغیّر جدید استخراج کنیم. ما این بار میخواهیم یک رفرنس را با یک الگو مقایسه کنیم و در صورت تطابق، مقدار اصلی را به expression مربوط به match arm پاس بدهیم.
یعنی این بار ما میخواهیم dereferencing را هم درون پترن انجام بدهیم و با مقداری که آن رفرنس به آن اشاره میکند درون کدمان کار کنیم.
بیایید دوباره برویم سراغ کد. ما یک تابع به نام print_number
تعریف میکنیم که یک عدد از نوع u16
را دریافت میکند و آن را پرینت میکند:
fn print_number(n: u16) { println!("number is: {}", n); }
حالا میخواهیم یک آرایه از ساختارهای RgbStruct
بسازیم، روی مقادیر آن iterate کنیم و اگر رنگ ورودی یک تدارج از رنگ قرمز بود، مقدار red
را با استفاده از تابع print_number
چاپ کنیم.
struct RgbStruct { red: u16, green: u16, blue: u16 } fn print_number(n: u16) { println!("number is: {}", n); } fn main() { let colours = [ RgbStruct { red: 112, green: 0, blue: 0 }, RgbStruct { red: 123, green: 124, blue: 8 }, RgbStruct { red: 0, green: 41, blue: 223 } ]; for rgb_reference in colours.iter() { match rgb_reference { RgbStruct {red, blue: 0, green: 0} => { println!("This is a kind of red colour."); print_number(red); }, RgbStruct {red, green, blue} => println!("rgb({}, {}, {})", red, green, blue) } } }
ما یک آرایه از نوع RgbStruct
ساختهایم و آن را درون متغیّر colours
ذخیرهکردهایم. سپس درون حلقهی for
با فراخوانی متد iter
روی آن، شروع به چرخیدن روی این آرایه کردهایم.
متد iter
همیشه یک رفرنس از المنتهای collection را برمیگرداند. به همین خاطر متغیّر rgb_reference
از نوع RgbStruct&
است، نه RgbStruct
(اگر کمی گیجشدهاید بهتر است که با کلیک روی این نوشته یک سر به قسمت مربوط به توضیح رفرنسها بزنید).
ما در الگو (pattern) اوّل بررسی میکنیم که آیا در مقدار ورودی match
، مقادیر مربوط به blue
و green
برابر با صفر اند یا نه. اگر بودند کد درون آکولادها اجرا میشود.
در این کد اوّل یک متن ساده چاپ میشود تا بفهمیم که کدهای این block درحال اجرا شدن اند. بعدش مقدار red
را که از طریق الگو از درون struct استخراج کرده ایم به تابع print_number
میدهیم تا آن را چاپ کند.
خب بیایید کد را کامپایل کنیم:
error[E0308]: mismatched types --> src/main.rs:237:30 | 237 | print_number(red); | ^^^ | | | expected u16, found &u16 | help: consider dereferencing the borrow: `*red` | = note: expected type `u16` found type `&u16`
برنامه به ارور خورد. چرا؟ چون تابع print_number
یک مقدار از نوع u16
را دریافت میکند. حالا rgb_reference
یک رفرنس است، پس وقتی مقادیر آن را هم استخراج میکنیم به یک سری رفرنس میرسیم.
یعنی متغیّر red
در الگوی اوّل به جای آنکه یک u16
باشد، یک u16&
است. ما هم نمیتوانیم به تابعی که خود مقدار را میگیرد، رفرنسی از آن مقدار را بفرستیم.
سؤال اینجاست که چطوری درون الگو به جای اینکه با رفرنس ورودی کار کنیم، از خود مقداری که آن رفرنس دارد به آن اشاره میکند استفاده کنیم؟
این بار ما الگویمان را کمی تغییر میدهیم:
for rgb_reference in colours.iter() { match rgb_reference { &RgbStruct {red, blue: 0, green: 0} => { println!("This is a kind of red colour."); print_number(red); }, RgbStruct {red, green, blue} => println!("rgb({}, {}, {})", red, green, blue) } }
خب چه اتّفاقی افتاد؟ ما در الگوی اوّل، پیش از مشخّص کردن struct برای استخراج مقادیر آن، علامت &
را قرار دادیم. چرا؟
همانطوری که گفتیم، الگوها درست برعکس expression ها هستند. وقتی در حالت عادی و در جایی خارج از الگوها از علامت &
استفاده میکنیم، درحقیقت یک رفرنس را ایجاد میکنیم. امّا وقتی که درون یک الگو از علامت &
استفاده میکنیم، یک رفرنس را dereference میکنیم. یعنی به مقدار اصلیای که رفرنس به آن اشاره میکرد دسترسی پیدا میکنیم.
حالا وقتی ما در الگوی اوّل علامت &
را پیش از تعریف ساختار struct قرار دادیم، به کامپایلر میگوییم که رفرنس ورودی را dereference کند و برای ما مقدار اصلی را قرار بدهد. اینطوری متغیّر red
به جای اینکه مقدار red
ورودی را به عنوان یک رفرنس (u16&
) بگیرد، آن را به عنوان یک عدد از نوع u16
دریافت میکند. برنامهی کلّی این شکلی میشود:
struct RgbStruct { red: u16, green: u16, blue: u16 } fn print_number(n: u16) { println!("number is: {}", n); } fn main() { let colours = [ RgbStruct { red: 112, green: 0, blue: 0 }, RgbStruct { red: 123, green: 124, blue: 8 }, RgbStruct { red: 0, green: 41, blue: 223 } ]; for rgb_reference in colours.iter() { match rgb_reference { &RgbStruct {red, blue: 0, green: 0} => { println!("This is a kind of red colour."); print_number(red); }, RgbStruct {red, green, blue} => println!("rgb({}, {}, {})", red, green, blue) } } }
خب حالا برنامه را کامپایل و اجرا میکنیم:
Red value of this colour is: number is: 112 rgb(123, 124, 8) rgb(0, 41, 223)
اینطوری میتوانیم خیلی راحت در الگوها با رفرنسها کار کنیم.
خسته نشدهاید که؟ تا اینجای کار خیلی چیزها را درمورد الگوها یادگرفتیم. ولی هنوز چیزهای هیجانانگیز زیادی مانده است.
الگویwildcard
ما تا اینجا زیاد این حضرت را زیارت کردهایم. الگوی Wildcard که با _
نمایش داده میشود، الگویی است که با همهچیز match میشود. شما یک دستور if
را درنظر بگیر که شرطش مقدار true
باشد. یا میتوانید آن را معادل حالت default
دستور switch
درنظر بگیرید.
البته حواستان باشد که الگوی wildcard هیچجوره مقدار را برای شما استخراج نمیکند. یعنی شما نمیتوانید به واسطهی آن به مقدار دسترسی داشته باشید.
این الگو به درد آخرین match arm میخورد تا حالتهایی که در الگوهای قبلی پوشش نداده شده اند را پوشش بدهد و الگوی ما جامع باشد.
جامع بودن الگو
مجموعهی الگوهای موجود در یک دستور match باید جامع (irrefutable) باشند. یعنی باید تمامی حالاتی که ورودی match ممکن است داشته باشد را پوشش بدهند.
اینطوری برنامهی ما ایمن خواهد بود. چون امکان ندارد در جریان اجرای برنامه حالتی رخ بدهد که درون برنامه در نظر گرفته نشده باشد.
فراموش نکنید که در Rust یکی از مهمترین چیزهایی که در سرتاسر زبان درنظر گرفته شده است همین بحث ایمنی است.
مثلاً الگوی زیر جامع است:
let a = 12; match a { var => println!("{}", var) }
این الگو جامع (irrefutable) است. چون var
در این الگو یک متغیّر از نوع i32
میشود و هر مقداری که a
داشته باشد را میپذیرد.
حالا الگوی زیر را ببینید:
let a = 12; match a { 0...12 => println!("A number between 0 and 12") }
این الگو جامع نیست و طبق اصطلاح سازندگان Rust یک الگوی refutable است. چرا؟ چون a
یک متغیّر از نوع i32
است و بازهی بزرگی از اعداد مثبت و منفی را شامل میشود. امّا ما در الگویمان تنها حالتی که بین 0 و 12 باشد را بررسی کردهایم.
اگر برنامهای که شامل این الگو است را اجرا کنیم، کامپایلر جلوی ما را میگیرد. سادهترین راه برای جامعکردن مجموعهی الگوهای موجود در این match این است که یک الگوی wildcard به آن اضافهکنیم:
let a = 12; match a { 0...12 => println!("A number between 0 and 12"), _ => println!("Other numbers") }
حالا match جامع است. چون اعداد ۰ تا ۱۲ را الگوی اوّل و بقیهی اعداد را الگوی دوم پوشش میدهند.
چشمپوشیکردن از مقادیر اضافی
تا اینجای کار انواع و اقسام روشهای استخراج مقادیر را از درون یک ساختار ورودی به match
یادگرفتیم. امّا خیلی وقتها ما به تمامی مقادیر موجود در ساختار ورودی احتیاجی نداریم.
مثلاً برنامهی زیر را ببینید:
fn ignore_tuple(input_tuple: (u8, u8, u8, u8)) { match input_tuple { (0, _, _, val4) => println!("4th value: {}", val4), (_, val2, val3, _) => println!("second and third values are: {} and {}", val2, val3) } } fn main() { ignore_tuple((10, 12, 13, 14)); ignore_tuple((0, 1, 2, 3)); }
در اوّلین الگو، اگر اوّلین مقدار tuple ورودی صفر باشد، مقادیر دوم و سوم آن برایمان اهمّیّتی ندارند. صرفاً مقدار چهارم را درون متغیّر val4
قرار میدهیم و چاپ میکنیم.
در الگوی دوم میگوییم که اصلاً مقادیر اول و آخر اهمّیّتی ندارند. صرفاً مقادیر دوم و سوم را میگیریم و پرینت میکنیم.
همانطوری که میبینید برای اینکه بیخیال مقادیر درونی ساختارها بشویم از همان wildcard استفاده میکنیم.
از این روش همهجا میتوانیم استفاده کنیم، امّا چه میشود اگر بخواهیم بیخیال یکسری مقادیر یک struct بشویم؟ مثلاً ما یک struct با دهتا فیلد مختلف داریم که فقط یکی یا دو تا از آنها برایمان درون این match اهمّیّت دارد.
خبر خوب این است که در Rust حتّی فکر اینجایش را هم کرده اند. کد زیر را ببینید:
struct BigStruct { key1: u16, key2: u16, key3: u16, key4: u16, key5: u16, key6: u16, key7: u16, } fn main() { let a = BigStruct { key1: 0, key2: 1, key3: 2, key4: 3, key5: 4, key6: 5, key7: 6 }; match a { BigStruct {key1: 0, key7: x, ..} => println!(" x = {}", x), BigStruct {key6: y, ..} => println!(" y = {}", y) } }
ما یک struct بزرگ به نام BigStruct
تعریف کرده ایم که ۷ فیلد مختلف دارد. بعد درون تابع main
یک نمونه از آن را ساختهایم و درون متغیّر a
قرار داده ایم.
حالا میخواهیم با یک دستور match
آن را مورد بررسی قرار بدهیم.
در الگوی اوّل، اگر مقدار مرتبط به key1
برابر با ۰ بود، میخواهیم مقدار مرتبط به key7
درون متغیّر x
قرار بگیرد. بقیهی فیلدهای struct هم برایمان اهمّیّتی ندارند.
در الگوی دوم هم صرفاً مقدار مرتبط با key6
را درون متغیّر y
میریزیم و به کامپایلر میگویم که بقیهی فیلدها برایمان اهمّیّتی ندارند.
از ..
برای درنظرنگرفتن مقادیر موجود در tuple ها هم میتوان استفاده کرد. ولی استفاده از آنها در struct ها رایجتر است.
فقط حواستان باشد که در بازه (range) ها ما ۳ تا نقطه میگذاشتیم. برای نادیدهگرفتن مقادیر ۲ تا. یکوقت این دو تا را با هم قاطی نکنید.
انعطافپذیری بیشتر با استفاده از Guardها
حالا تمام کارهایی که میشد با الگوها کرد را دیدید؟ ما در Rust میتوانیم حتّی انعطافپذیری بیشتری را هم تجربه کنیم.
با استفاده از guard ها میتوانیم یک شرط اضافی را هم به الگو اضافه کنیم. در حقیقت guard یک if
اضافی است که بعد از الگو قرار میگیرد و باعث میشود که علاوه بر مطابقت ورودی با الگو، درست بودن شرط آن هم پیش از اجرای کد بررسی شود.
اوّل کد زیر را ببینید:
struct Foo(u8, u8); fn print_guarded_pattern(input: Foo) { match input { Foo(x, _) if x % 2 == 0 => println!("{} is an even number", x), Foo(x, y) => println!("first value is not even. pair is: ({}, {})", x, y) } } fn main() { print_guarded_pattern(Foo(2, 10)); print_guarded_pattern(Foo(7, 8)); }
ما اوّل یک tuple like struct به نام Foo
تعریف کردهایم. Foo
دو مقدار از نوع u8
را میگیرد.
دستور match
درون تابع print_guarded_pattern
قرار دارد. در اوّلین الگو تنها مقدار اوّل ورودی را درون متغیّر x
میریزیم. مقدار دوم ورودی برایمان اهمّیّتی ندارد. به همین خاطر با _
از آن صرف نظر میکنیم.
هدف ما این است که اگر مقدار اوّل Foo
زوج بود، کد مربوط به match arm اوّل اجرا بشود. در غیر این صورت مقدار دوتایی ورودی را پرینت کنیم.
برای این کار در الگو، مقدار اوّل را درون متغیّر x
استخراج کرده ایم. حالا درون guard مربوط به الگوی اوّل مشخّص کردهایم که باید باقیماندهی تقسیم x
بر عدد ۲ برابر با صفر شود. در این حالت میفهمیم که مقدار اوّل دوتایی Foo
که درون match
دارد بررسی میشود یک عدد زوج است.
خب برنامه را کامپایل و اجرا میکنیم:
2 is an even number first value is not even. pair is: (7, 8)
شرط موجود در Guard دقیقاً مثل شرط دستور if
معمولی است. شما میتوانید در آن هم از متغیّرهایی که درون الگو (pattern) تعریف شدهاند استفاده کنید و هم میتوانید هر متغیّر یا مقداری که در آن scope وجود دارد را به کار ببرید.
الگو میتواند هر کدام از الگوهایی که دیدیم باشد. فرقی نمیکند چند تا الگو را با استفاده از | با هم or کرده باشید، از بازه (range) ها استفاده کرده باشید یا هر چیزی دیگری. تا زمانی که یک الگوی معتبر باشد، شما میتوانید برایش یک guard بنویسید.
عملگر @ در الگوها
فرضکنید با تکنیکهایی که یادگرفتیم یک مقدار را از ورودی دستور match
استخراج کردهایم. حالا چطوری خود آن مقدار را با یک الگو مطابقت بدهیم؟
دوباره ساختار RgbStruct
را درنظر بگیرید:
struct RgbStruct { red: u16, green: u16, blue: u16 }
ما میخواهیم یک تابع بنویسیم که یک رنگ rgb را دریافت کند. اگر مقدار red بین ۰ تا ۱۰۰ بود و مقدار green صفر بود، پیام موفّقیّت را به کاربر نمایش بدهد. در غیر این صورت پیام پیشفرض را چاپ کند.
fn extract_and_match(colour: RgbStruct) { match colour { RgbStruct {red: r @ 0...100, green: 0, blue} => println!("This is my colour:rgb({}, 0, {})", r, blue), _ => println!("Not desired colour.") } } fn main() { let colour1 = RgbStruct { red: 120, green: 0, blue: 255 }; let colour2 = RgbStruct { red: 50, blue: 20, green: 0 }; extract_and_match(colour1); extract_and_match(colour2); }
ما میخواهیم مقدار red
این ساختار را با یک الگوی مشخّص تطابق بدهیم. در حالت عادی اگر این کار را بکنیم دیگر به خود مقدار دسترسی نخواهیم داشت.
برای اینکه مقدار را مقابل یک بازه (range) مورد بررسی قرار بدهیم، مقابل red
که یکی از key های struct مان است، اسم متغیّر جدید را که همان r
است مشخّص میکنیم. بعد از نام آن علامت @
را قرار میدهیم.
حالا بعد از این علامت میتوانیم بازهی مورد نظرمان را وارد کنیم. اینطوری ابتدا مقدار مربوط به key رنگ قرمز، red
، درون متغیّر r
ریخته میشود و سپس بررسی میشود که آیا متغیّر r
در بازهی اعلام شده قرار دارد یا نه.
خروجی برنامه این شکلی خواهد بود:
Not desired colour. This is my colour: rgb(50, 0, 20)
عملگر @
تمام مقدار را میتواند درون یک متغیّر جدید کپی یا move کند. مثلاً میخواهیم کل ورودی را درون یک متغیّر جدید درون الگو بریزیم:
fn extract_and_match(colour: RgbStruct) { match colour { new_colour @ RgbStruct {..} => println!("This is my colour: rgb({}, {}, {})", new_colour.red, new_colour.green, new_colour.blue), } }
الان مالکیّت مقدار متعلّق به متغیّر colour
به متغیّر جدید new_colour
منتقل میشود. این یعنی خارج از scope متعلّق به این match arm دیگر این مقدار وجود ندارد.
یعنی اگر دقیقاً بعد از همین دستور match
بخواهیم از این مقدار استفاده کنیم با ارور روبهرو میشویم. چون مالکیّت ورودی به متغیّر new_colour
منتقل شده است و با خروج از scope آن match arm، آن متغیّر هم drop شده است (اگر مالکیّت و نحوهی مدیریت حافظه را در Rust فراموشکردهاید، با کلیک روی این نوشته خیلی سریع همهچیز را به خاطر بیاورید).
مقدار {..}RgbStruct
که بعد از علامت @
قرارگرفته است در واقع الگوی ما است. چون اعلام کردهایم که key: value
های struct برایمان اهمّیّتی ندارند، هر مقداری که از نوع RgbStruct
باشد با این الگو match میشود.
عملگر ref در الگوها
در نسخههای قدیمیتر Rust، همیشه درون match به صورت پیشفرض مالکیّت مقادیر منتقل میشد. به همین خاطر اگر ما میخواستیم بگوییم که مالکیّت این مقدار را منتقل نکن، باید کلمهی کلیدی ref
را قبل از آن درون الگو قرار میدادیم.
کد زیر را ببینید:
enum Colour { Transparent, Hex(String), } fn print_reference(colour: &Colour) { match colour { Colour::Transparent => println!("Transparent"), &Colour::Hex(hex) => println!("hex {}", hex), } }
اگر در نسخههای قدیمی این کد را اجرا میکردیم، مالکیّت رشتهی درون enum در حالت Hex
به متغیّر hex
منتقل میشد. اینطوری دیگر خارج از این قطعه کد نمیتوانستیم به آن مقدار دسترسی داشته باشیم.
اگر میخواستیم این مشکل رفع بشود، باید قبل از متغیّر hex
درون الگوی دوم، کلمهی کلیدی ref
را قرار میدادیم. اینطوری صرفاً یک رفرنس از آن String
به متغیّر hex
منتقل میشود. حالا دیگر بدون هیچ مشکلی میتوان از مقداری که به این تابع پاس داده شده است در جاهای دیگر هم استفاده کرد:
fn print_reference(colour: &Colour) { match colour { Colour::Transparent => println!("Transparent"), &Colour::Hex(ref hex) => println!("hex {}", hex), } }
از الان به بعد در متغیّر hex
دیگر خود مقدار String
قرار نمیگیرد، بلکه تنها یک رفرنس به آن درون این متغیّر قرار میگیرد. اینطوری دیگر مالکیّت آن String
منتقل نمیشود.
به هر حال این بخش از الگوها دیگر منقضی شده اند. یعنی در نسخههای جدید Rust اصلاً لازم نیست از چنین چیزی استفاده کنید.
در نسخههای جدید Rust اگر یک رفرنس را به match
وارد کنید، تمامی مقادیری که از آن گرفته میشوند به صورت رفرنس خواهند بود.
یعنی در نسخههای جدید Rust کد زیر به خوبی و خوشی اجرا میشود. بدون اینکه مالکیّت String یا خود ورودی به جایی منتقل بشود.
fn print_reference(colour: &Colour) { match colour { Colour::Transparent => println!("Transparent"), Colour::Hex(hex) => println!("hex {}", hex), } }
درست است که ref
کلاً منقضی شده است و دیگر لازم نیست از آن استفاده کنیم، ولی چون Rust پشتیبانی از نسخههای قبلی را حفظ میکند، ممکن است این الگو را در کدهایی که با نسخههای قدیمیتر نوشته شده اند ببینید.
پس هرچند که به کار بردن این الگو دیگر کار درستی نیست، ولی دانستنش باعث میشود که کدهای قبلی را بتوانید بفهمید.
استفاده از الگوها روی slice ها
ما میتوانیم از الگوها برای استخراج مقادیر slice ها هم استفاده کنیم.
بیایید اوّل با هم کدش را ببینیم:
fn get_pair(slice: &[i32]) -> (i32, i32) { match slice { [e1] => (*e1, *e1), [e1, e2, .., e3, e4] => { let average1 = (e1 + e2).pow(2); let average2 = (e3 + e4).pow(2); (average1, average2) } [e1, .., e2] => (*e1, *e2), [] => (0, 0) } }
ما اینجا یک تابع داریم که یک slice را به عنوان ورودی دریافت میکند. قصد ما این است که با توجّه به ورودی، یک تاپل دوتایی را از آن درست کنیم.
منطق ما این شکلی است:
اگر slice
تنها یک عضو داشت، مقادیر تاپل برابر با همان یک عضو خواهند بود. این اتّفاقی است که در اوّلین بازوی match
دارد میافتد.
الگویی که با یک رفرنس دارای یک عضو مطابقت پیدا میکند [e1]
است. همانطوری که تا اینجا دیدیم e1
یک اسم متغیّر است که ما برای دسترسی به آن یک عضو از آن استفاده میکنید. قلّابهای باز و بسته هم (]
و [
) نشاندهندهی یک slice در الگوها هستند.
شرط بعدی ما این است که اگر slice شامل ۴ عضو یا بیشتر بود، خانهی اوّل تاپل میشود مجموع دو عضو اوّل به توان ۲ و خانهی دوم تاپل هم میشود مجموع دو عضو آخر به توان ۲.
ما در Rust برای اینکه یک عدد را به توان برسانیم، میتوانید از متد pow
که برای تمامی مقادیر عددی پیادهسازی شده است استفاده کنیم. مقدار ورودی این متد عدد توان است.
همانطوری که در الگوی بازوی دوم match
میبینید، ما برای اینکه صرفاً دو رقم اوّل و آخر را بگیریم و بیخیال عناصر میانی slice بشویم، از علامت ..
استفاده کردهایم.
در الگوهای مربوط به slice ها، علامت ..
مشخّص میکند که ما قرار است از تعداد نامشخّصی از عناصر صرف نظر کنیم.
جالبی این الگو این است که میتواند هرجایی حضور داشته باشد. یعنی مثلاً
اگر بخواهیم بیخیال تمامی عناصر به جز آخری شویم، کافی است که بنویسیم: [..e]
. اینطوری عنصر آخر در e
ریخته میشود و بقیهی عناصر هم اهمّیّتی نخواهند داشت.
الگوی سوم در مثال بالا عناصر اوّل و آخر slice را به عنوان مقادیر تاپل خروجی در نظر میگیرد و کلاً بیخیال تمامی عناصری که این وسط هستند میشود.
الگوی آخر به معنی یک slice خالی است. اگر درون slice ما هیچ عنصری قرار نداشته باشد، این الگو (pattern) با آن جور در میآید و یک تاپل با عناصر ۰ خروجی داده خواهد شد.
خب حالا بیایید کدی که نوشتهایم را امتحان کنیم:
fn main() { let a = [1, 2, 3, 4, 5, 6, 7, 8, 9]; let mut slice = &a[..]; let mut pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); slice = &a[..=1]; pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); slice = &a[..=4]; pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); slice = &a[0..1]; pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); slice = &[]; pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); slice = &a[..3]; pair = get_pair(slice); println!("Created pair from {:?} is: {:?}", slice, pair); }
اگر برنامهی بالا را اجرا کنیم به خروجی زیر میرسیم:
Created pair from [1, 2, 3, 4, 5, 6, 7, 8, 9] is: (9, 289) Created pair from [1, 2] is: (1, 2) Created pair from [1, 2, 3, 4, 5] is: (9, 81) Created pair from [1] is: (1, 1) Created pair from [] is: (0, 0) Created pair from [1, 2, 3] is: (1, 3) Process finished with exit code 0
همانطوری که میبینید در فراخوانی اوّل ما داریم یک slice به طول ۱۰ را به تابع میدهیم. پس الگوی دوم با ورودی مطابقت پیدا میکند.
در فراخوانی دوم ما داریم یک slice با ۲ مقدار را به تابع ارسال میکنیم. پس سومین الگو با ورودی مطابقت پیدا میکند و مقادیر اول و دوم به عنوان مقادیر تاپل خروجی قرار میگیرند.
در فراخوانی چهارم slice ما تنها یک عضو دارد. پس اوّلین الگو با آن مطابقت پیدا میکند.
در پنجمین فراخوانی ما داریم یک slice خالی را به تابع میفرستیم. پس الگوی آخر با این ورودی مطابقت خواهد داشت.
همانطوری که از این مثال متوجّه شدید، ..
میتواند شامل هیچ عنصری نشود (مثل فراخوانی دوم). یا میتواند شامل تعداد زیادی عنصر شود.
استفاده از @ به عنوان الگو در slice ها
ما میتوانیم از @
هم در الگوهای مربوط به slice ها استفاده کنیم. این کار باعث میشود که یک بخشی از slice را به یک متغیّر متّصل کنیم.
مثلاً با نوشتن الگوی زیر، ما تمام اعضای slice را به جز آخری به متغیّر subslice
متّصل میکنیم.
[subslice @ .., _]
اهمّیّت ترتیب قرارگیری الگوها
حالا ما همهچیز را درمورد خود الگو (pattern) ها میدانیم. نکتهای که باید هنگام کار با الگوها حتماً به خاطر داشته باشید، این است که ترتیب قرارگیری آنها خیلی اهمّیّت دارد.
درون یک دستور match
، الگوها به ترتیب از بالا به پایین با مقدار ورودی تطابق داده میشوند. کد مربوط به الگویی اجرا میشود که ورودی زودتر با آن مطابقت پیدا کرده باشد.
fn main() { let a = (10, 11); match a { (v1, v2) => println!("This pattern match every (i32, i32)"), (10, 11) => println!("Desired pattern.") } }
این match
مشکل سینتکسی ندارد، امّا اشتباه است. چرا؟ چون اوّلین الگو تمامی تاپلهای دوتایی ممکن را قبول میکند. پس متغیّر a هرچیزی که باشد تنها کد مربوط به match arm اوّل اجرا میشود. در صورتی که الگویی که مدنظر ما است و باید با ورودی مطابقت پیداکند الگوی دوم است.
اگر این برنامه را اجرا کنیم کامپایلر از ما ایرادی نمیگیرد، امّا اخطار زیر را برایمان نمایش میدهد:
warning: unreachable pattern --> src/main.rs:394:9 | 393 | (v1, v2) => println!("This pattern match every (i32, i32)"), | -------- matches any value 394 | (10, 11) => println!("Desired pattern.") | ^^^^^^^^ unreachable pattern | = note: #[warn(unreachable_patterns)] on by default
اگر با دقّت به کدهای قبلی نگاهکنید، میبینید که ما همه جا این اولویتبندی را رعایت کردهایم. همیشه ترتیب قرارگیری الگوها باید طوری باشد که از خاصترین و دقیقترین الگو به عمومیترین برسیم.
از الگوها در کجا میتوان استفاده کرد؟
خب خسته نباشید. تا اینجا خیلی چیزها یادگرفتیم. استفاده از الگوها درون match
خیلی هیجانانگیز و کار راهانداز است. امّا match
تنها جایی نیست که در آن میتوان از الگوها استفاده کرد.
در بخش انتهایی این قسمت میخواهیم ببینیم که در چه جاهای دیگری هم میتوان از الگوها استفاده کرد.
ساختار if let
ما کدی نوشتهایم که در آن تنها یک حالت از تمامی الگوهای ممکن ورودی مهم است. یکی از راهها این است که یک match
بنویسیم و در آن دو الگو را مورد بررسی قرار بدهیم. یکی آن چیزی که میخواهیم، یکی از الگوی wildcard.
fn main() { let hour: u8 = 10; match hour { 0...24 => println!("A valid hour"), _ => println!("hour value is not a valid hour!") } }
خب ما میتوانیم این کار را بدون استفاده از match
هم انجام بدهیم:
fn main() { let hour: u8 = 10; if let 0...24 = hour { println!("a valid hour"); } }
ساختار if let
با کلمهی کلیدی if
شروع میشود. بعد از آن کلمهی کلیدی let
را قرار میدهیم.
بعد از let
باید الگو (pattern) مورد نظر را بنویسیم. حالا درست بعد از الگو علامت مساوی را قرار میدهیم. سپس درست بعد از علامت مساوی مقداری که قرار است با الگو مطابقت داده بشود را مینویسیم.
کدی را که قرار است در صورت مطابقت مقدار با الگو اجرا بشود درون آکولادها قرار میدهیم.
مثلاً در همین برنامهای که بالاتر گذاشتیم، مقدار متغیّر hour
با الگوی 24...0
مطابقت داده میشود. در صورت مطابقت، کد درون آکولاد اجرا میشود:
a valid hour
اگر هم ورودی با الگو مطابقت نداشته باشد هیچ اتّفاقی نمیافتد.
ساختار if let
میتواند یک بلاک else
هم داشته باشد که کدهای آن در صورت عدم مطابقت مقدار با الگو اجرا بشوند.
if let 0...24 = hour { println!("a valid hour"); } else { println!("hour value is not a valid hour."); }
مزیّت if let
نسبت به match
این است که وقتی تنها یک حالت برایمان مهم است، میتوانیم با کد کمتری به نتیجهای که میخواهیم برسیم.
امّا این ساختار یک عیب بزرگ دارد. ما هنگام استفاده از match
مطمئن میشدیم که کدمان جامع است. امّا حالا هیچ راهی برای اطمینان از این موضوع وجود ندارد.
ساختار while let
ما میتوانیم برای شرط حلقهی while هم از الگوها استفاده کنیم. اینطوری تا زمانی که مقدار ورودی شرط حلقه با الگو مطابقت داشته باشد، حلقه اجرا خواهد شد.
fn main() { let mut counter = 0; while let 0 | 1 | 2 | 3 = counter { println!("counter: {}", counter); counter += 1; } }
برای این کار ابتدا مثل حالت عادی کلمهی کلیدی while
را مینویسیم. سپس کلمهی کلیدی let
را قرار میدهیم تا مشخّص شود که قرار است از الگوها استفاده کنیم.
بعد از let
الگویی که میخواهیم ورودی شرط حلقه با آن مطابقت داده شود را مینویسیم. وقتی که الگو تمام شد، مثل ساختار قبلی علامت مساوی را قرار میدهیم و مقابلش مقداری که قرار است با الگو مطابقت داده شود را مینویسیم.
تا زمانی که متغیّر counter
با الگویی که مشخّص کردهایم مطابقت داشته باشد حلقه اجرا میشود. اوّلین باری که مقدار متغیّر و الگو با هم مطابقت نداشته باشند، اجرای حلقه متوقّف میشود.
مثالی که این جا زدهایم خیلی ساده و کمی نامربوط است. این قابلیّت موقع کار با فایلها، وکتورها و… خیلی خیلی کاربردی است. امّا چون هنوز آنها را یادنگرفتهایم به همین مثال قناعت میکنیم.
حلقهی for
با حلقهی for زیاد کار کردهایم. خب به نظرتان کجای حلقهی for از الگوها استفاده میشود؟
اصلاً الان میخواهم یک تست حافظهی کوچولو بگیرم. همانطوری که در قسمت آموزش slicing دیدیم، چطوری میتوان روی خروجی متد enumerate
چرخید؟ خیلی خوب. حالا اینقدر هم لازم نیست برای به خاطر آوردنش به خودتان زحمت بدهید. این خود کد:
fn main() { let array = [-5, -3, -10, 0, 1, 8, 9]; for (index, &item) in array.iter().enumerate() { println!("{} item is in the {} index of the array.", item, index); } }
خروجی متد enumerate
یک تاپل است که مقدار اوّل آن یک عدد نشاندهندهی ایندکس element، و مقدار دومش یک رفرنس به خود element است.
مقدار حلقه یا همان چیزی که دقیقاً بعد از کلمهی کلیدی for
قرار میگیرد همیشه یک الگو است. ما میتوانیم تمامی کارهایی که برای استخراج مقادیر در الگوها میکردیم را اینجا هم انجام بدهیم.
در همین مثال بالا، ما مقادیر موجود در تاپل خروجی از تابع enumerate
را با استفاده از الگویی که نوشتهایم در دو متغیّر index
و item
استخراج کردهایم.
تعریف متغیّر با let
جالبترین چیز این است که اصلاً هنگام تعریف متغیّر با let
ما داریم از یک الگو استفاده میکنیم. حتّی در کد خیلی سادهی: let a = 10
ما در حقیقت داریم از یک الگو استفاده میکنیم. اینجا a
یک الگو است که با مقدار ۱۰ میتواند match بشود.
یک کار جالبی که میتوان انجام داد این است که مقادیر یک tuple یا struct را در یک خط کد درون متغیّرهای مختلف ذخیره کرد.
let (a, b, c) = (10, String::from("A String"), false);
در این کدی که میبینید، ما مقادیر موجود در tuple سمت راست تساوی را به ترتیب درون متغیّرهای سمت چپ ریختهایم.
یعنی a
یک متغیّر از نوع i32
است که مقداری برابر با ۱۰ دارد. B
یک متغیّر از نوع String
است که مقدارش رشتهی: A String
است و c
یک متغیّر از نوع bool
که مقدار false
درونش قرار دارد.
اینطوری میتوانید با کد خیلی کمتری خروجیهای توابع و متدهای مختلف را درون متغیّرهای مختلف قرار بدهید.
پارامترهای ورودی تابع
پارامترهای ورودی تابع هم مثل تعریف متغیّرها الگو هستند. یعنی شما میتوانید با راهکارهای استخراج مقادیر در الگوها، پارامترهای توابع را هم تعریف کنید.
مثلاً کد زیر را ببینید:
struct Point(u8, u8); fn print_point(Point(x, y): Point) { println!("x value of the point: {}", x); println!("y value of the point: {}", y); } fn main() { let a = Point(10, 50); print_point(a); }
ما ابتدا یک tuple like struct تعریف کرده ایم. برای تعریف کردن پارامترهای ورودی تابع print_point
که قرار است مقادیر x
و y
را چاپ کند، دقیقاً همان کاری را کردهایم که برای استخراج مقادیر یک struct درون الگوها کردیم. چرا؟ چون همانطوری که گفتم پارامترهای تابع هم الگو اند.
تابع یک نمونه از ساختار Point
را به عنوان ورودی میگیرد، امّا همان موقع آن مقادیر آن را درون دو پارامتر x
و y
میریزد. یعنی درون تابع، ما صرفاً به مقادیر استخراجشدهی x
و y
از ورودی اصلی دسترسی داریم و دیگر خود آن درون تابع وجود ندارد.
اینطوری ورودیگرفتن از توابع و ورودی دادن به آنها هم خیلی خیلی ساده میشود. شما خیلی راحت فقط یک ورودی به تابع میدهید. تابع هم درون همان بخش تعریف پارامترهای ورودی مقادیر مورد نیاز را از مقدار ورودی جدا میکند.
حالا درون تابع میتوانیم بدون نوشتن کد اضافی با مقادیر نهایی کار کنیم.
خب واقعاً خسته نباشید. با اینکه هم شما زمان زیادی برای خواندن این نوشته گذاشتهاید و هم خود من ساعتهای خیلی زیادی را صرف آن کردم، به نظرم ارزشش را داشت.
حالا به خوبی همه چیز را درمورد الگوها و کاربردهای آنها میدانیم. مطمئناً بخشی از چیزهایی که اینجا خواندید را فراموش میکنید. اصلاً جای نگرانی ندارد. این نوشته یک منبع کامل است که میتوانید همیشه به آن مراجعه کنید.
تا اینجای کار ما خیلی چیزها را درمورد زبان Rust میدانیم و پیشرفت خیلی خوبی را در این مورد داشته ایم. دیگر باید کمکم آماده شوید که بعد از چند قسمت سراغ نوشتن پروژههای کامل و معنیدار برویم.
تا آن موقع من پیشنهاد میکنم که یک دور دیگر قسمتهای مختلف این مجموعه را مرور کنید و سعی کنید کدهای مختلف آن را خودتان بنویسید.
قسمتهای بعدی را به هیچ عنوان از دست نده.
برای دیدن کدهای مربوط به این قسمت در مخزن گیتهاب آن، روی این نوشته کلیک کنید.
یا حرفهای شو یا برنامهنویسی را رها کن.
چطور میشود در بازار کار خالی از نیروی سنیور ایران، بدون کمک دیگران و وقت تلفکردن خودت را تبدیل به یک نیروی باتجربه بکنی؟
پاسخ ساده است. باید حرفهای بشوی. چیزی که من در این کتاب به تو یاد خواهم داد.
rust شاهکار دنیای برنامه نویسیه واقعا
دست شما درد نکنه واقعا عالی بود این مطلب
خواهش میکنم.
از زحمات شما بینهایت ممنونم. فکرشو نمیکردم اینقدر خوب از پسش بربیاین.اونجایی که فکرشو نمیکنید کاری بشه که اینقدر خوب باشه براتون.
خیلی ممنونم.