وبلاگ شخصی
محمّدرضا
علی حسینی

جایی که تجربیات, علایق
و چیزهایی که یادگرفته‌ام را
با هم مرور می‌کنیم.

آموزش زبان برنامه‌نویسی Rust – قسمت۱۴- پترن‌ها

آموزش زبان برنامه‌نویسی 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 می‌دانیم و پیشرفت خیلی خوبی را در این مورد داشته ایم. دیگر باید کم‌کم آماده شوید که بعد از چند قسمت سراغ نوشتن پروژه‌های کامل و معنی‌دار برویم.

تا آن موقع من پیشنهاد می‌کنم که یک دور دیگر قسمت‌های مختلف این مجموعه را مرور کنید و سعی کنید کدهای مختلف آن را خودتان بنویسید.

قسمت‌های بعدی را به هیچ عنوان از دست نده.

برای دیدن کدهای مربوط به این قسمت در مخزن گیتهاب آن، روی این نوشته کلیک کنید.

در قسمت قبلی درمورد enum ها صحبت کردیم. اگر آن قسمت را نخوانده‌اید با کلیک روی این نوشته همین الان آن را هم بخوان.

دفعه‌ی اوّل است که با این مجموعه‌ی آموزشی روبه‌رو می‌شوی؟ با کلیک روی این نوشته به اوّلین قسمت برو و آموزش کامل، حرفه‌ای و رایگان زبان برنامه‌نویسی Rust را از صفر شروع کن.

یا حرفه‌ای شو یا برنامه‌نویسی را رها کن.

چطور می‌شود در بازار کار خالی از نیروی سنیور ایران، بدون کمک دیگران و وقت تلف‌کردن خودت را تبدیل به یک نیروی باتجربه بکنی؟

پاسخ ساده است. باید حرفه‌ای بشوی. چیزی که من در این کتاب به تو یاد خواهم داد.

«نوشته‌های مرتبط»

5 پاسخ به “آموزش زبان برنامه‌نویسی Rust – قسمت۱۴- پترن‌ها”

  1. احسان شه بخش گفت:

    rust شاهکار دنیای برنامه نویسیه واقعا

    دست شما درد نکنه واقعا عالی بود این مطلب

  2. حسین کاوسی گفت:

    از زحمات شما بینهایت ممنونم. فکرشو نمیکردم اینقدر خوب از پسش بربیاین.اونجایی که فکرشو نمیکنید کاری بشه که اینقدر خوب باشه براتون.

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

«نوشته‌های ویژه»

«نوشته‌های محبوب»

«دیدگاه کاربران»