Pattern Syntax
در این بخش، تمام نحوهایی که در الگوها معتبر هستند را جمعآوری کرده و بحث میکنیم که چرا و چه زمانی ممکن است بخواهید از هر کدام استفاده کنید.
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
را پوشش میدهد، باید از یک نگهبان شرطی (match guard) استفاده کنیم. ما درباره نگهبانهای شرطی در بخش “Extra Conditionals with Match Guards” صحبت خواهیم کرد.
Multiple Patterns
میتوانید با استفاده از نحو |
، که عملگر یا (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 باشد، بازوی اول مطابقت خواهد داشت. این نحو برای مقادیر مطابقت چندگانه راحتتر از استفاده از عملگر |
برای بیان همان ایده است؛ اگر بخواهیم از |
استفاده کنیم، باید 1 | 2 | 3 | 4 | 5
را مشخص کنیم. مشخص کردن یک بازه بسیار کوتاهتر است، بهویژه اگر بخواهیم، برای مثال، هر عدد بین 1 و 1,000 را مطابقت دهیم!
کامپایلر بررسی میکند که بازه در زمان کامپایل خالی نیست، و چون تنها نوعهایی که راست میتواند تشخیص دهد که آیا یک بازه خالی است یا نه 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 the 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 که هیچ کاری انجام نمیدهد اما تمام مقادیر باقیمانده ممکن را در نظر میگیرد. چندین روش برای نادیده گرفتن مقادیر کامل یا بخشهایی از مقادیر در یک الگو وجود دارد: استفاده از الگوی _
(که دیدهاید)، استفاده از الگوی _
درون یک الگوی دیگر، استفاده از نامی که با یک زیرخط شروع میشود، یا استفاده از ..
برای نادیده گرفتن بخشهای باقیمانده یک مقدار. بیایید بررسی کنیم چگونه و چرا از هر یک از این الگوها استفاده کنیم.
Ignoring 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 هستید و به یک امضای خاص نیاز دارید، اما بدنه تابع در پیادهسازی شما نیازی به یکی از پارامترها ندارد. در این صورت، از دریافت هشدار کامپایلر درباره پارامترهای استفادهنشده جلوگیری میکنید، همانطور که اگر به جای آن از یک نام استفاده میکردید، هشدار دریافت میکردید.
Ignoring 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 نادیده گرفته میشوند.
Ignoring 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
را به چیزی متصل نمیکنیم؛ بنابراین انتقال داده نمیشود.
Ignoring 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_variable @ 3..=7, } => println!("Found an id in range: {id_variable}"), 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
و پارامترهای تابع این ساختارها را مفیدتر میکنند و تخریب مقادیر به بخشهای کوچکتر را همزمان با تخصیص به متغیرها ممکن میسازند. ما میتوانیم الگوهای ساده یا پیچیدهای ایجاد کنیم که نیازهای ما را برآورده کنند.
در فصل ماقبل آخر این کتاب، به برخی از جنبههای پیشرفته از ویژگیهای مختلف راست خواهیم پرداخت.