Pattern Syntax
In this section, we gather all the syntax that is valid in patterns and discuss why and when you might want to use each one.
Matching Literals
همانطور که در فصل 6 دیدید، میتوانید الگوها را مستقیماً با مقادیر ثابت (literals) تطبیق دهید. کد زیر برخی از مثالها را نشان میدهد:
fn main() { let x = 1; match x { 1 => println!("one"), 2 => println!("two"), 3 => println!("three"), _ => println!("anything"), } }
این کد one را چاپ میکند زیرا مقدار در x برابر با 1 است. این نحو زمانی مفید است که بخواهید کد شما در صورت دریافت یک مقدار مشخص خاص اقدامی انجام دهد.
Matching Named Variables
متغیرهای نامگذاریشده الگوهای غیرقابلرد هستند که با هر مقداری مطابقت دارند، و ما بارها در این کتاب از آنها استفاده کردهایم. با این حال، زمانی که از متغیرهای نامگذاریشده در عبارات match، if let، یا while let استفاده میکنید، یک پیچیدگی وجود دارد. زیرا هر یک از این نوع عبارات یک دامنه جدید را شروع میکنند، متغیرهایی که بهعنوان بخشی از یک الگو در داخل عبارت تعریف میشوند، متغیرهایی با همان نام در خارج را پوشش میدهند، همانطور که برای همه متغیرها صدق میکند. در فهرست 19-11، یک متغیر به نام x با مقدار Some(5) و یک متغیر y با مقدار 10 تعریف میکنیم. سپس یک عبارت match روی مقدار x ایجاد میکنیم. به الگوها در بازوهای match و دستور println! در انتها نگاه کنید و سعی کنید قبل از اجرای این کد یا خواندن بیشتر، حدس بزنید که کد چه چیزی را چاپ خواهد کرد.
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(y) => println!("Matched, y = {y}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
match با بازویی که یک متغیر جدید معرفی میکند که متغیر موجود y را پوشش میدهدبیایید بررسی کنیم که وقتی عبارت match اجرا میشود چه اتفاقی میافتد. الگوی موجود در بازوی اول match با مقدار تعریفشده x مطابقت ندارد، بنابراین کد ادامه مییابد.
الگوی موجود در بازوی دوم match یک متغیر جدید به نام y معرفی میکند که با هر مقداری درون یک Some مطابقت خواهد داشت. از آنجا که ما در یک دامنه جدید داخل عبارت match هستیم، این یک متغیر جدید y است، نه متغیری که در ابتدا با مقدار 10 تعریف کردیم. این binding جدید y با هر مقداری درون یک Some مطابقت دارد، که همان چیزی است که ما در x داریم. بنابراین، این y جدید به مقدار داخلی Some در x متصل میشود. آن مقدار 5 است، بنابراین عبارت برای آن بازو اجرا میشود و Matched, y = 5 را چاپ میکند.
اگر x به جای Some(5) یک مقدار None بود، الگوهای موجود در دو بازوی اول مطابقت نداشتند، بنابراین مقدار به علامت زیرخط (_) مطابقت داده میشد. ما متغیر x را در الگوی بازوی زیرخط معرفی نکردیم، بنابراین x در عبارت همچنان همان x خارجی است که پوشش داده نشده است. در این حالت فرضی، عبارت match پیام Default case, x = None را چاپ میکرد.
وقتی عبارت match تمام میشود، دامنه آن نیز پایان مییابد، و همینطور دامنه y داخلی. دستور println! آخر پیام at the end: x = Some(5), y = 10 را تولید میکند.
برای ایجاد یک عبارت match که مقادیر x و y بیرونی را مقایسه کند، بهجای معرفی یک متغیر جدید که متغیر y موجود را سایهبان (shadow) کند، باید از یک شرط نگهبان match (match guard) استفاده کنیم. دربارهی نگهبانهای match بعداً در بخش «شرطهای اضافی با Match Guards» صحبت خواهیم کرد.
چند الگو (Multiple Patterns)
در عبارات match میتوانید چندین الگو را با استفاده از سینتکس | که عملگر یا (or) برای الگوها است، با هم مقایسه کنید. برای مثال، در کد زیر مقدار x را با شاخههای match مقایسه میکنیم که اولین شاخه شامل گزینهی یا است، یعنی اگر مقدار x با هر یک از مقادیر موجود در آن شاخه مطابقت داشته باشد، کد آن شاخه اجرا خواهد شد:
fn main() { let x = 1; match x { 1 | 2 => println!("one or two"), 3 => println!("three"), _ => println!("anything"), } }
این کد one or two را چاپ میکند.
Matching Ranges of Values with ..=
نحو ..= به ما اجازه میدهد یک بازه شامل مقادیر را مطابقت دهیم. در کد زیر، وقتی یک الگو با هر کدام از مقادیر در بازه دادهشده مطابقت داشته باشد، آن بازو اجرا خواهد شد:
fn main() { let x = 5; match x { 1..=5 => println!("one through five"), _ => println!("something else"), } }
اگر مقدار x برابر با 1، 2، 3، 4 یا 5 باشد، شاخهی اول در match تطابق خواهد داشت. این نحو برای مقادیر متعدد در match بسیار راحتتر از استفادهی مکرر از عملگر | است؛ زیرا اگر از | استفاده کنیم، باید به صورت 1 | 2 | 3 | 4 | 5 آنها را مشخص کنیم. استفاده از بازه (range) بسیار کوتاهتر است، بهویژه اگر بخواهیم مثلاً هر عددی بین ۱ تا ۱۰۰۰ را تطبیق دهیم!
کامپایلر بررسی میکند که بازه در زمان کامپایل خالی نیست، و چون تنها نوعهایی که راست میتواند تشخیص دهد که آیا یک بازه خالی است یا نه char و مقادیر عددی هستند، بازهها فقط برای مقادیر عددی یا char مجاز هستند.
در اینجا یک مثال با استفاده از بازههایی از مقادیر char آمده است:
fn main() { let x = 'c'; match x { 'a'..='j' => println!("early ASCII letter"), 'k'..='z' => println!("late ASCII letter"), _ => println!("something else"), } }
راست میتواند تشخیص دهد که 'c' در بازه الگوی اول است و پیام early ASCII letter را چاپ میکند.
Destructuring to Break Apart Values
ما همچنین میتوانیم از الگوها برای تخریب (destructure) ساختارها (structs)، enums، و tupleها استفاده کنیم تا از بخشهای مختلف این مقادیر استفاده کنیم. بیایید به هر نوع مقدار نگاهی بیندازیم.
Destructuring Structs
فهرست 19-12 یک struct به نام Point را با دو فیلد، x و y نشان میدهد که میتوانیم با استفاده از یک الگو در یک عبارت let آن را تخریب کنیم.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x: a, y: b } = p; assert_eq!(0, a); assert_eq!(7, b); }
این کد متغیرهای a و b را ایجاد میکند که با مقادیر فیلدهای x و y از struct p مطابقت دارند. این مثال نشان میدهد که نام متغیرها در الگو نیازی به مطابقت با نام فیلدهای struct ندارند. با این حال، معمولاً نام متغیرها با نام فیلدها مطابقت داده میشوند تا یادآوری اینکه کدام متغیرها از کدام فیلدها آمدهاند آسانتر شود. بهدلیل این استفاده معمول و بهدلیل اینکه نوشتن let Point { x: x, y: y } = p; تکرار زیادی دارد، راست یک نحو کوتاه برای الگوهایی که فیلدهای struct را مطابقت میدهند فراهم میکند: فقط کافی است نام فیلد struct را لیست کنید و متغیرهایی که از الگو ایجاد میشوند همان نامها را خواهند داشت. فهرست 19-13 به همان روشی که کد در فهرست 19-12 عمل میکند، اما متغیرهای ایجادشده در الگوی let بهجای a و b، x و y هستند.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; let Point { x, y } = p; assert_eq!(0, x); assert_eq!(7, y); }
این کد متغیرهای x و y را ایجاد میکند که با فیلدهای x و y از متغیر p مطابقت دارند. نتیجه این است که متغیرهای x و y مقادیر از ساختار p را شامل میشوند.
ما همچنین میتوانیم با مقادیر ثابت (literals) بهعنوان بخشی از الگوی struct تخریب کنیم، بهجای ایجاد متغیرهایی برای همه فیلدها. انجام این کار به ما اجازه میدهد برخی از فیلدها را برای مقادیر خاصی تست کنیم، در حالی که متغیرهایی برای تخریب فیلدهای دیگر ایجاد میکنیم.
در فهرست 19-14، یک عبارت match داریم که مقادیر Point را به سه حالت تقسیم میکند: نقاطی که مستقیماً روی محور x قرار دارند (که در صورتی درست است که y = 0)، روی محور y (x = 0)، یا هیچکدام.
struct Point { x: i32, y: i32, } fn main() { let p = Point { x: 0, y: 7 }; match p { Point { x, y: 0 } => println!("On the x axis at {x}"), Point { x: 0, y } => println!("On the y axis at {y}"), Point { x, y } => { println!("On neither axis: ({x}, {y})"); } } }
بازوی اول هر نقطهای که روی محور x قرار دارد را با مشخص کردن اینکه فیلد y در صورتی مطابقت دارد که مقدار آن با مقدار ثابت 0 مطابقت داشته باشد، تطبیق میدهد. الگو همچنان یک متغیر x ایجاد میکند که میتوانیم در کد این بازو از آن استفاده کنیم.
بهطور مشابه، بازوی دوم هر نقطه روی محور y را با مشخص کردن اینکه فیلد x در صورتی که مقدار آن 0 باشد مطابقت دارد و یک متغیر y برای مقدار فیلد y ایجاد میکند. بازوی سوم هیچ مقدار ثابتی را مشخص نمیکند، بنابراین هر Point دیگری را مطابقت میدهد و متغیرهایی برای هر دو فیلد x و y ایجاد میکند.
در این مثال، مقدار p به لطف x که مقدار 0 دارد، با بازوی دوم مطابقت دارد، بنابراین این کد پیام On the y axis at 7 را چاپ میکند.
به یاد داشته باشید که یک عبارت match پس از یافتن اولین الگوی مطابقت متوقف میشود، بنابراین حتی اگر Point { x: 0, y: 0 } روی محور x و محور y باشد، این کد فقط پیام On the x axis at 0 را چاپ خواهد کرد.
Destructuring Enums
ما در این کتاب enums را تخریب کردهایم (برای مثال، فهرست 6-5 در فصل 6)، اما هنوز بهطور خاص بحث نکردهایم که الگوی تخریب یک enum مطابق با نحوه تعریف دادههای ذخیرهشده درون enum است. بهعنوان مثال، در فهرست 19-15 از enum Message از فهرست 6-2 استفاده میکنیم و یک match با الگوهایی مینویسیم که هر مقدار داخلی را تخریب میکنند.
enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(i32, i32, i32), } fn main() { let msg = Message::ChangeColor(0, 160, 255); match msg { Message::Quit => { println!("The Quit variant has no data to destructure."); } Message::Move { x, y } => { println!("Move in the x direction {x} and in the y direction {y}"); } Message::Write(text) => { println!("Text message: {text}"); } Message::ChangeColor(r, g, b) => { println!("Change color to red {r}, green {g}, and blue {b}"); } } }
این کد پیام Change the color to red 0, green 160, and blue 255 را چاپ میکند. مقدار msg را تغییر دهید تا کد از بازوهای دیگر اجرا شود.
برای متغیرهای enum بدون هیچ دادهای، مانند Message::Quit، نمیتوان مقدار را بیشتر تخریب کرد. فقط میتوان روی مقدار ثابت Message::Quit مطابقت داد، و هیچ متغیری در آن الگو وجود ندارد.
برای متغیرهای enum شبیه به struct، مانند Message::Move، میتوانیم از الگویی مشابه الگوی مشخصشده برای تطبیق structs استفاده کنیم. پس از نام متغیر، آکولاد باز میکنیم و سپس فیلدها را با متغیرها لیست میکنیم تا بخشها را برای استفاده در کد این بازو تجزیه کنیم. در اینجا از فرم کوتاه همانطور که در فهرست 19-13 استفاده کردیم استفاده میکنیم.
برای متغیرهای enum شبیه به tuple، مانند Message::Write که یک tuple با یک عنصر دارد و Message::ChangeColor که یک tuple با سه عنصر دارد، الگو مشابه الگویی است که برای تطبیق tupleها مشخص میکنیم. تعداد متغیرها در الگو باید با تعداد عناصر در متغیر که تطبیق میدهیم مطابقت داشته باشد.
Destructuring Nested Structs and Enums
تاکنون، مثالهای ما همه تطبیق ساختارها یا enums در یک سطح عمیق بودهاند، اما تطبیق میتواند روی آیتمهای تو در تو نیز کار کند! برای مثال، میتوانیم کد در فهرست 19-15 را بازسازی کنیم تا از رنگهای RGB و HSV در پیام ChangeColor پشتیبانی کند، همانطور که در فهرست 19-16 نشان داده شده است.
enum Color { Rgb(i32, i32, i32), Hsv(i32, i32, i32), } enum Message { Quit, Move { x: i32, y: i32 }, Write(String), ChangeColor(Color), } fn main() { let msg = Message::ChangeColor(Color::Hsv(0, 160, 255)); match msg { Message::ChangeColor(Color::Rgb(r, g, b)) => { println!("Change color to red {r}, green {g}, and blue {b}"); } Message::ChangeColor(Color::Hsv(h, s, v)) => { println!("Change color to hue {h}, saturation {s}, value {v}"); } _ => (), } }
الگوی بازوی اول در عبارت match یک متغیر enum به نام Message::ChangeColor را تطبیق میدهد که شامل یک متغیر Color::Rgb است؛ سپس الگو به سه مقدار داخلی i32 متصل میشود. الگوی بازوی دوم نیز یک متغیر enum به نام Message::ChangeColor را تطبیق میدهد، اما enum داخلی به جای آن Color::Hsv را مطابقت میدهد. ما میتوانیم این شرایط پیچیده را در یک عبارت match مشخص کنیم، حتی اگر دو enum درگیر باشند.
Destructuring Structs and Tuples
ما میتوانیم الگوهای تخریب را به روشهای پیچیدهتر ترکیب، تطبیق و تو در تو کنیم. مثال زیر یک تخریب پیچیده را نشان میدهد که در آن ساختارها و tupleها را داخل یک tuple تو در تو میکنیم و تمام مقادیر اولیه را تخریب میکنیم:
fn main() { struct Point { x: i32, y: i32, } let ((feet, inches), Point { x, y }) = ((3, 10), Point { x: 3, y: -10 }); }
این کد به ما اجازه میدهد انواع پیچیده را به اجزای سازنده آنها بشکنیم تا بتوانیم مقادیری که به آنها علاقه داریم را جداگانه استفاده کنیم.
تخریب با الگوها یک روش راحت برای استفاده از بخشهایی از مقادیر، مانند مقدار هر فیلد در یک ساختار، بهصورت جداگانه است.
Ignoring Values in a Pattern
گاهی اوقات مفید است که مقادیر را در یک الگو نادیده بگیرید، مانند بازوی آخر یک match، برای دریافت یک catchall که هیچ کاری انجام نمیدهد اما تمام مقادیر باقیمانده ممکن را در نظر میگیرد. چندین روش برای نادیده گرفتن مقادیر کامل یا بخشهایی از مقادیر در یک الگو وجود دارد: استفاده از الگوی _ (که دیدهاید)، استفاده از الگوی _ درون یک الگوی دیگر، استفاده از نامی که با یک زیرخط شروع میشود، یا استفاده از .. برای نادیده گرفتن بخشهای باقیمانده یک مقدار. بیایید بررسی کنیم چگونه و چرا از هر یک از این الگوها استفاده کنیم.
An Entire Value with _
ما از زیرخط بهعنوان یک الگوی wildcard استفاده کردهایم که با هر مقداری مطابقت دارد اما به مقدار متصل نمیشود. این بهویژه بهعنوان بازوی آخر در یک عبارت match مفید است، اما ما همچنین میتوانیم آن را در هر الگویی استفاده کنیم، از جمله پارامترهای تابع، همانطور که در فهرست 19-17 نشان داده شده است.
fn foo(_: i32, y: i32) { println!("This code only uses the y parameter: {y}"); } fn main() { foo(3, 4); }
_ در یک امضای تابعاین کد مقدار 3 را که بهعنوان آرگومان اول ارسال شده است، کاملاً نادیده میگیرد و پیام This code only uses the y parameter: 4 را چاپ میکند.
در بیشتر موارد، زمانی که دیگر نیازی به یک پارامتر تابع خاص ندارید، امضای تابع را تغییر میدهید تا آن پارامتر استفادهنشده را شامل نشود. نادیده گرفتن یک پارامتر تابع میتواند بهویژه در مواردی مفید باشد که، برای مثال، شما در حال پیادهسازی یک trait هستید و به یک امضای خاص نیاز دارید، اما بدنه تابع در پیادهسازی شما نیازی به یکی از پارامترها ندارد. در این صورت، از دریافت هشدار کامپایلر درباره پارامترهای استفادهنشده جلوگیری میکنید، همانطور که اگر به جای آن از یک نام استفاده میکردید، هشدار دریافت میکردید.
Parts of a Value with a Nested _
ما همچنین میتوانیم از _ در داخل یک الگوی دیگر استفاده کنیم تا فقط بخشی از یک مقدار را نادیده بگیریم. برای مثال، وقتی میخواهیم فقط بخشی از یک مقدار را تست کنیم اما نیازی به استفاده از بخشهای دیگر در کدی که میخواهیم اجرا کنیم نداریم. فهرست 19-18 کدی را نشان میدهد که مسئول مدیریت مقدار یک تنظیم است. نیازمندیهای تجاری این است که کاربر نباید اجازه داشته باشد یک سفارشیسازی موجود برای یک تنظیم را بازنویسی کند، اما میتواند تنظیم را لغو کند و به آن یک مقدار بدهد اگر در حال حاضر لغو شده باشد.
fn main() { let mut setting_value = Some(5); let new_setting_value = Some(10); match (setting_value, new_setting_value) { (Some(_), Some(_)) => { println!("Can't overwrite an existing customized value"); } _ => { setting_value = new_setting_value; } } println!("setting is {setting_value:?}"); }
Some مطابقت دارند وقتی نیازی به استفاده از مقدار داخل Some نداریماین کد پیام Can't overwrite an existing customized value را چاپ میکند و سپس setting is Some(5) را چاپ میکند. در بازوی اول match، نیازی به مطابقت یا استفاده از مقادیر داخل هر یک از متغیرهای Some نداریم، اما باید حالتهایی را که در آنها setting_value و new_setting_value در حالت Some هستند، تست کنیم. در این صورت، دلیل تغییر ندادن setting_value را چاپ میکنیم و این مقدار تغییر نمیکند.
در تمام موارد دیگر (اگر setting_value یا new_setting_value مقدار None داشته باشند) که توسط الگوی _ در بازوی دوم بیان شده است، میخواهیم اجازه دهیم new_setting_value به setting_value تبدیل شود.
ما همچنین میتوانیم از زیرخطها در مکانهای مختلف در یک الگو برای نادیده گرفتن مقادیر خاص استفاده کنیم. فهرست 19-19 مثالی از نادیده گرفتن مقادیر دوم و چهارم در یک tuple پنج آیتمی را نشان میدهد.
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, _, third, _, fifth) => { println!("Some numbers: {first}, {third}, {fifth}"); } } }
این کد پیام Some numbers: 2, 8, 32 را چاپ میکند و مقادیر 4 و 16 نادیده گرفته میشوند.
An Unused Variable by Starting Its Name with _
اگر یک متغیر ایجاد کنید اما از آن در هیچ جایی استفاده نکنید، راست معمولاً یک هشدار صادر میکند زیرا یک متغیر استفادهنشده ممکن است یک باگ باشد. با این حال، گاهی اوقات مفید است که بتوانید متغیری ایجاد کنید که هنوز از آن استفاده نمیکنید، مانند زمانی که در حال نمونهسازی یا تازه شروع یک پروژه هستید. در این وضعیت، میتوانید به راست بگویید که درباره متغیر استفادهنشده هشدار ندهد، با شروع نام متغیر با یک زیرخط. در فهرست 19-20، دو متغیر استفادهنشده ایجاد میکنیم، اما وقتی این کد را کامپایل میکنیم، باید فقط یک هشدار درباره یکی از آنها دریافت کنیم.
fn main() { let _x = 5; let y = 10; }
اینجا درباره استفاده نکردن از متغیر y یک هشدار دریافت میکنیم، اما درباره استفاده نکردن از _x هشدار نمیگیریم.
توجه داشته باشید که تفاوت ظریفی بین استفاده از فقط _ و استفاده از نامی که با یک زیرخط شروع میشود وجود دارد. نحو _x همچنان مقدار را به متغیر متصل میکند، در حالی که _ اصلاً متصل نمیشود. برای نشان دادن موردی که این تفاوت اهمیت دارد، فهرست 19-21 به ما یک خطا ارائه میدهد.
fn main() {
let s = Some(String::from("Hello!"));
if let Some(_s) = s {
println!("found a string");
}
println!("{s:?}");
}
ما یک خطا دریافت خواهیم کرد زیرا مقدار s همچنان به _s منتقل میشود، که مانع از استفاده دوباره از s میشود. با این حال، استفاده از زیرخط بهتنهایی هرگز به مقدار متصل نمیشود. فهرست 19-22 بدون هیچ خطایی کامپایل خواهد شد زیرا s به _ منتقل نمیشود.
fn main() { let s = Some(String::from("Hello!")); if let Some(_) = s { println!("found a string"); } println!("{s:?}"); }
این کد بهخوبی کار میکند زیرا ما هرگز s را به چیزی متصل نمیکنیم؛ بنابراین انتقال داده نمیشود.
Remaining Parts of a Value with ..
برای مقادیری که بخشهای زیادی دارند، میتوانیم از نحو .. برای استفاده از بخشهای خاص و نادیده گرفتن باقی بخشها استفاده کنیم، و نیازی به لیست کردن زیرخطها برای هر مقدار نادیده گرفتهشده نخواهیم داشت. الگوی .. هر بخشی از یک مقدار را که بهطور صریح در بقیه الگو مطابقت داده نشده نادیده میگیرد. در فهرست 19-23، یک struct به نام Point داریم که یک مختصات در فضای سهبعدی نگه میدارد. در عبارت match، میخواهیم فقط روی مختصات x عمل کنیم و مقادیر موجود در فیلدهای y و z را نادیده بگیریم.
fn main() { struct Point { x: i32, y: i32, z: i32, } let origin = Point { x: 0, y: 0, z: 0 }; match origin { Point { x, .. } => println!("x is {x}"), } }
Point بهجز x با استفاده از ..ما مقدار x را فهرست میکنیم و سپس فقط الگوی .. را اضافه میکنیم. این سریعتر از این است که y: _ و z: _ را فهرست کنیم، بهویژه زمانی که با ساختارهایی کار میکنیم که فیلدهای زیادی دارند و فقط یکی یا دو فیلد مهم هستند.
نحو .. به هر تعداد مقداری که نیاز باشد گسترش مییابد. فهرست 19-24 نشان میدهد که چگونه از .. با یک tuple استفاده کنیم.
fn main() { let numbers = (2, 4, 8, 16, 32); match numbers { (first, .., last) => { println!("Some numbers: {first}, {last}"); } } }
در این کد، مقدار اول و آخر با first و last مطابقت داده میشوند. الگوی .. تمام مقادیر میانی را مطابقت داده و نادیده میگیرد.
با این حال، استفاده از .. باید بدون ابهام باشد. اگر مشخص نباشد کدام مقادیر برای تطبیق و کدام برای نادیده گرفتن در نظر گرفته شدهاند، راست به ما خطا میدهد. فهرست 19-25 مثالی از استفاده از .. به شکلی مبهم را نشان میدهد، بنابراین کامپایل نخواهد شد.
fn main() {
let numbers = (2, 4, 8, 16, 32);
match numbers {
(.., second, ..) => {
println!("Some numbers: {second}")
},
}
}
.. به شکلی مبهموقتی این مثال را کامپایل میکنیم، این خطا را دریافت میکنیم:
$ cargo run
Compiling patterns v0.1.0 (file:///projects/patterns)
error: `..` can only be used once per tuple pattern
--> src/main.rs:5:22
|
5 | (.., second, ..) => {
| -- ^^ can only be used once per tuple pattern
| |
| previously used here
error: could not compile `patterns` (bin "patterns") due to 1 previous error
برای راست امکانپذیر نیست که تعیین کند چند مقدار در tuple باید نادیده گرفته شود قبل از اینکه یک مقدار را با second تطبیق دهد و سپس چند مقدار دیگر را بعد از آن نادیده بگیرد. این کد میتواند به این معنا باشد که میخواهیم 2 را نادیده بگیریم، second را به 4 متصل کنیم، و سپس 8، 16 و 32 را نادیده بگیریم؛ یا اینکه میخواهیم 2 و 4 را نادیده بگیریم، second را به 8 متصل کنیم، و سپس 16 و 32 را نادیده بگیریم؛ و غیره. نام متغیر second برای راست معنی خاصی ندارد، بنابراین به دلیل استفاده از .. در دو مکان به این شکل مبهم، خطای کامپایل دریافت میکنیم.
Extra Conditionals with Match Guards
یک match guard یک شرط اضافی if است که پس از الگو در یک بازوی match مشخص میشود و باید برای انتخاب آن بازو نیز مطابقت داشته باشد. Match guardها برای بیان ایدههای پیچیدهتر از آنچه که یک الگو بهتنهایی اجازه میدهد، مفید هستند. این قابلیت فقط در عبارات match در دسترس است، نه در عبارات if let یا while let.
شرط میتواند از متغیرهایی که در الگو ایجاد شدهاند استفاده کند. فهرست 19-26 یک match را نشان میدهد که بازوی اول آن دارای الگوی Some(x) است و همچنین دارای یک match guard if x % 2 == 0 است (که در صورتی که عدد زوج باشد، true خواهد بود).
fn main() { let num = Some(4); match num { Some(x) if x % 2 == 0 => println!("The number {x} is even"), Some(x) => println!("The number {x} is odd"), None => (), } }
این مثال پیام The number 4 is even را چاپ میکند. وقتی num با الگوی بازوی اول مقایسه میشود، مطابقت دارد، زیرا Some(4) با Some(x) مطابقت دارد. سپس match guard بررسی میکند که آیا باقیمانده تقسیم x بر 2 برابر با 0 است یا نه، و چون این شرط برقرار است، بازوی اول انتخاب میشود.
اگر مقدار num برابر با Some(5) بود، match guard در بازوی اول false میشد زیرا باقیمانده تقسیم 5 بر 2 برابر با 1 است که برابر با 0 نیست. راست سپس به بازوی دوم میرود که مطابقت دارد زیرا بازوی دوم match guard ندارد و بنابراین با هر متغیر Some مطابقت دارد.
هیچ راهی برای بیان شرط if x % 2 == 0 در داخل یک الگو وجود ندارد، بنابراین match guard به ما امکان بیان این منطق را میدهد. نقطه ضعف این قابلیت اضافی این است که کامپایلر سعی نمیکند بررسی کند که آیا تمام موارد پوشش داده شدهاند یا نه وقتی که match guardها درگیر هستند.
در فهرست 19-11 اشاره کردیم که میتوانیم از match guardها برای حل مشکل shadowing الگو استفاده کنیم. به یاد بیاورید که ما یک متغیر جدید در داخل الگو در عبارت match ایجاد کردیم به جای استفاده از متغیر بیرون از match. آن متغیر جدید به این معنا بود که نمیتوانستیم مقدار متغیر بیرونی را تست کنیم. فهرست 19-27 نشان میدهد که چگونه میتوانیم از یک match guard برای رفع این مشکل استفاده کنیم.
fn main() { let x = Some(5); let y = 10; match x { Some(50) => println!("Got 50"), Some(n) if n == y => println!("Matched, n = {n}"), _ => println!("Default case, x = {x:?}"), } println!("at the end: x = {x:?}, y = {y}"); }
این کد اکنون پیام Default case, x = Some(5) را چاپ میکند. الگوی بازوی دوم match یک متغیر جدید y که متغیر بیرونی y را shadow کند معرفی نمیکند، به این معنا که میتوانیم از متغیر بیرونی y در match guard استفاده کنیم. به جای مشخص کردن الگو بهعنوان Some(y) که متغیر بیرونی y را shadow میکرد، ما Some(n) را مشخص میکنیم. این یک متغیر جدید n ایجاد میکند که هیچ چیزی را shadow نمیکند زیرا هیچ متغیر n در خارج از match وجود ندارد.
Match guard if n == y یک الگو نیست و بنابراین متغیرهای جدیدی را معرفی نمیکند. این y همان متغیر بیرونی y است و یک متغیر جدید که آن را shadow کند نیست، و میتوانیم با مقایسه n با y به دنبال مقداری باشیم که با مقدار بیرونی y یکسان باشد.
همچنین میتوانید از عملگر یا | در یک match guard استفاده کنید تا چندین الگو مشخص کنید؛ شرط match guard برای تمام الگوها اعمال خواهد شد. فهرست 19-28 تقدم هنگام ترکیب یک الگو که از | استفاده میکند با یک match guard را نشان میدهد. بخش مهم این مثال این است که match guard if y برای 4، 5، و 6 اعمال میشود، حتی اگر ممکن است به نظر برسد که if y فقط برای 6 اعمال میشود.
fn main() { let x = 4; let y = false; match x { 4 | 5 | 6 if y => println!("yes"), _ => println!("no"), } }
شرط مطابقت بیان میکند که بازو فقط زمانی مطابقت دارد که مقدار x برابر با 4، 5، یا 6 و مقدار y برابر با true باشد. وقتی این کد اجرا میشود، الگوی بازوی اول مطابقت دارد زیرا x برابر با 4 است، اما match guard if y برابر با false است، بنابراین بازوی اول انتخاب نمیشود. کد به بازوی دوم میرود که مطابقت دارد، و این برنامه no را چاپ میکند. دلیل این است که شرط if برای کل الگوی 4 | 5 | 6 اعمال میشود، نه فقط برای مقدار آخر 6. به عبارت دیگر، تقدم یک match guard نسبت به یک الگو به این شکل رفتار میکند:
(4 | 5 | 6) if y => ...
و نه به این شکل:
4 | 5 | (6 if y) => ...
بعد از اجرای کد، رفتار تقدم آشکار میشود: اگر match guard فقط برای مقدار نهایی در لیست مقادیر مشخصشده با استفاده از عملگر | اعمال میشد، بازو مطابقت میداشت و برنامه پیام yes را چاپ میکرد.
@ Bindings
عملگر at (@) به ما امکان میدهد یک متغیر ایجاد کنیم که یک مقدار را نگه میدارد و همزمان آن مقدار را برای تطبیق با الگو آزمایش میکند. در فهرست 19-29، ما میخواهیم بررسی کنیم که آیا فیلد id در Message::Hello در بازه 3..=7 قرار دارد یا نه. همچنین میخواهیم مقدار را به متغیر id_variable متصل کنیم تا بتوانیم در کد مرتبط با بازو از آن استفاده کنیم. میتوانستیم این متغیر را id بنامیم، مشابه فیلد، اما برای این مثال از نام متفاوتی استفاده خواهیم کرد.
fn main() { enum Message { Hello { id: i32 }, } let msg = Message::Hello { id: 5 }; match msg { Message::Hello { id: id @ 3..=7 } => { println!("Found an id in range: {id}") } Message::Hello { id: 10..=12 } => { println!("Found an id in another range") } Message::Hello { id } => println!("Found some other id: {id}"), } }
@ برای اتصال به یک مقدار در یک الگو و همزمان آزمایش آناین مثال پیام Found an id in range: 5 را چاپ میکند. با مشخص کردن id_variable @ قبل از بازه 3..=7، ما هر مقداری که با بازه مطابقت داشت را ذخیره میکنیم و همزمان بررسی میکنیم که آیا مقدار با الگوی بازه مطابقت دارد.
در بازوی دوم، جایی که فقط یک بازه در الگو مشخص شده است، کدی که با بازو مرتبط است متغیری ندارد که مقدار واقعی فیلد id را شامل شود. مقدار فیلد id میتوانست 10، 11، یا 12 باشد، اما کدی که با آن الگو مرتبط است نمیداند مقدار چیست. کد بازو نمیتواند از مقدار فیلد id استفاده کند، زیرا ما مقدار id را در یک متغیر ذخیره نکردهایم.
در بازوی آخر، جایی که یک متغیر بدون بازه مشخص کردهایم، مقدار برای استفاده در کد بازو در متغیری به نام id در دسترس است. دلیل این است که ما از نحو کوتاه فیلدهای struct استفاده کردهایم. اما در این بازو هیچ آزمایشی برای مقدار در فیلد id اعمال نکردهایم، همانطور که در دو بازوی اول انجام دادیم: هر مقداری با این الگو مطابقت خواهد داشت.
استفاده از @ به ما امکان میدهد یک مقدار را آزمایش کنیم و همزمان آن را در یک متغیر ذخیره کنیم، همه در یک الگو.
Summary
الگوهای راست در تشخیص بین انواع مختلف داده بسیار مفید هستند. وقتی در عبارات match استفاده میشوند، راست اطمینان حاصل میکند که الگوهای شما تمام مقادیر ممکن را پوشش میدهند، وگرنه برنامه شما کامپایل نخواهد شد. الگوها در عبارات let و پارامترهای تابع این ساختارها را مفیدتر میکنند و تخریب مقادیر به بخشهای کوچکتر را همزمان با تخصیص به متغیرها ممکن میسازند. ما میتوانیم الگوهای ساده یا پیچیدهای ایجاد کنیم که نیازهای ما را برآورده کنند.
در فصل ماقبل آخر این کتاب، به برخی از جنبههای پیشرفته از ویژگیهای مختلف راست خواهیم پرداخت.