หากคุณติดตามฉันมาสักระยะ คุณจะรู้ว่าฉันเริ่มเพลิดเพลินกับ Rust จริงๆ ในปีที่แล้ว Rust มีคุณสมบัติที่ยอดเยี่ยมมากมาย และการจับคู่รูปแบบก็เป็นหนึ่งในนั้น หากคุณใช้ภาษาอื่นเช่น Haskell หรือ Standard ML คุณจะสังเกตเห็นความคล้ายคลึงบางประการ เช่นเดียวกับการจับคู่รูปแบบพื้นฐานที่สมบูรณ์กับเมื่ออยู่ใน Kotlin (มีงานเล็กน้อย) การจับคู่รูปแบบใน Rust ทำให้โค้ดมีความชัดเจน อ่านง่าย และชัดเจน ฉันยอมรับว่าวิธีการของ Rust เป็นสิ่งที่ฉันชอบเป็นการส่วนตัว ในบทความนี้ เราจะมาดูหัวข้อนี้ และบางทีคุณจะเห็นว่าทำไมฉันถึงคิดว่ามันเยี่ยมมาก!
พื้นฐานของรูปแบบ
หักล้างได้ VS หักล้างไม่ได้
รูปแบบใน Rust มีสองประเภท; หักล้างได้และหักล้างไม่ได้ รูปแบบที่ตรงตามเงื่อนไขเรียกว่าหักล้างได้ ในขณะที่รูปแบบที่ตรงกับค่าที่เป็นไปได้เรียกว่าหักล้างไม่ได้ อันไหนที่คุณสามารถใช้ได้จะขึ้นอยู่กับบริบท ตัวอย่างเช่น คำสั่ง Let จะต้องมีรูปแบบที่หักล้างไม่ได้ เพราะจะเกิดอะไรขึ้นหากตัวแปรในคำสั่ง Let ไม่ได้รับค่า
// Irrefutable patterns let x = 2; let (x, y) = (2, 3); // WILL NOT COMPILE // let does not allow refutable patterns let Ok(x) = someString.parse::<i32>() // trying to parse a string will return a Result, and can not guarantee that a result and not an Err is returned
ในทางกลับกัน ประโยค “ถ้าให้” อาจมีรูปแบบที่หักล้างได้ เนื่องจากร่างกายได้รับการประเมินตามเงื่อนไข:
if let Ok(x) = someString.parse::<i32>() { // … do something if someString can be parsed as a 32 bit integer … } // if let can have a refutable pattern, so we can also use a value for x: if let Ok(64) = someString.parse::<i32>() { // … do something if someString can be parsed as a 32 bit integer … }
เราจะดูตัวอย่างเพิ่มเติมของข้อความและรูปแบบประเภทนี้ด้านล่าง หากคุณยังคงคิดว่าหัวข้อ “refutable vs irrefuable” นั้นยาก ให้อ่านหัวข้อนี้อีกครั้งหลังจากอ่านบทความที่เหลือแล้ว “เอกสารประกอบของ Rust” ยังมีตัวอย่างและคำอธิบายที่คุ้มค่าที่จะลองดู
การทำลายล้าง
หลายลายเป็นลายที่ทำลายโครงสร้างประเภทต่างๆ และยังสามารถผสมและจับคู่กันได้ ลองดูที่บางส่วนของพวกเขา
สิ่งอันดับ
เราเห็นตัวอย่างการทำลายล้างสิ่งอันดับแล้วในส่วนสุดท้ายแล้ว แต่ลองมาดูกันอีกครั้ง:
// myTuple is of type (i32, i32, &str) let my_tuple = (1, 2, "hellothere"); let (x, y, my_str) = my_tuple;
ที่นี่เราเห็นในบรรทัดสุดท้ายว่า my_tuple ถูกทำลายออกเป็น 3 ตัวแปรใหม่: x, y และ my_str สิ่งนี้สามารถทำได้กับสิ่งอันดับทุกประเภท ตราบใดที่ประเภทที่ทำลายโครงสร้างตรงกัน
คุณยังสามารถจับคู่องค์ประกอบด้วย “..” (อาจเป็นหลายรายการ) หรือ “_” (เดี่ยว) ซึ่งมักใช้เพื่อข้ามองค์ประกอบ:
// ignore my_str let (x, y, _) = my_tuple; // ignore everything after x let (x, ..) = my_tuple; // bigger tuple let bigger_tuple = (1, 2, 3, 4, 5); // get first and last let (first, .., last) = bigger_tuple; // ambiguous! NOT ALLOWED // How would the compiler even know which element you wanted let (.., middle, ..) = bigger_tuple;
(สังเกตว่ารูปแบบต้องไม่คลุมเครือ)
คุณอาจลองตัวอย่าง tuple แรกด้านบนด้วยโครงสร้าง tuple และได้รับข้อความแสดงข้อผิดพลาด นั่นเป็นเพราะว่าโครงสร้างทูเพิลมีความเหมือนกันกับการทำลายโครงสร้างมากกว่า ดังนั้นจึงจำเป็นต้องมีไวยากรณ์พิเศษ:
// Defining a tuple struct that looks like the tuple in the previous example: struct MyTuple(i32, i32, String); // Destructure it let my_tuple = TupleStr(1, 2, "hellothere".to_string()); let TupleStr(x, y, my_str) = my_tuple;
(สังเกตว่าเราต้องเปลี่ยนประเภท &str เป็น String เนื่องจากคอมไพเลอร์ไม่สามารถอนุมานขนาดของ &str ใดๆ ที่เราอาจต้องการได้ ใช้ในโครงสร้าง tuple ของเรา สตริงจะถูกบันทึกไว้ในฮีปเพื่อแก้ปัญหานั้น)
โครงสร้าง Tuple เป็นโครงสร้างทางเทคนิค ซึ่งนำเราไปสู่การทำลายโครงสร้างประเภทถัดไป...
โครงสร้าง
โครงสร้างไม่ได้แตกต่างกันมากนัก และตัวอย่างอาจแสดงให้เห็นอย่างชัดเจน:
#+BEGIN_SRC สนิม
#+END_SRC
// define a simple struct struct Point { x: f32, y: f32, z: f32 } // create a variable to use let myPoint = Point { x: 1.0, y: 0.5, z: -1.0 }; // destructure it! let Point { x, y, z} = my_point; // Maybe we just want x and y? let Point { x, y, .. } = my_point; // or maybe just z let Point { z, .. } = my_point;
สิ่งหนึ่งที่คุณควรสังเกตเมื่อทำลายโครงสร้างคือชื่อจะต้องตรงกับชื่อที่พบในโครงสร้าง และ “..” จะต้องอยู่หลังสุด “..” ในกรณีนี้หมายถึงเพียงจับคู่ส่วนที่เหลือและละเว้นผลลัพธ์
Enums
กรณีที่ง่ายที่สุดสำหรับ enum คือการจับคู่รายการที่ไม่มีข้อมูล:
// define a simple enum enum Color { Red, Blue, Green } // match if our color is green if let Color::Green = my_color { // .. do something is color is green .. }
สิ่งนี้ไม่น่าสนใจนัก เนื่องจาก Rust enums มีประสิทธิภาพมากกว่ามาก หากคุณไม่คุ้นเคยก็สามารถมีข้อมูลได้! ให้เราดูตัวอย่าง:
// More advanced enum enum HttpRequest { Get, Post(String) } // match the post request if let HttpRequest::Post(data) = my_request { // .. do something with the post request data … } // can also ignore data if let HttpRequest::Post(_) = my_request { // .. do something when post request … }
ถ้า enum มีหลายข้อโต้แย้ง คุณสามารถทำสิ่งที่คุณคุ้นเคยจากสิ่งอันดับด้านบนได้เกือบทั้งหมด คุณสามารถใช้ช่วง (เช่น “1..=2”) หรือข้ามองค์ประกอบบางอย่างด้วย “..” เพียงอย่างเดียว (เช่น “MyEnum( firstElem, .., LastElem)”)
รวมกัน
คุณยังสามารถรวมทั้งหมดที่กล่าวมาข้างต้นเป็นรูปแบบของคุณเองได้! โครงสร้างภายใน enums, enums ภายใน tuples ฯลฯ รายการดำเนินต่อไป!
// Define some nested structure enum Color { Red, Blue, Green } // imagine old OpenGL from the early 2000s where colors of points were interpolated across the shape struct Point { x: f32, y: f32, z: f32, color: Color } struct Triangle(Point, Point, Point); // A destructuring based upon the data we want // get only x for the first point when the first points color is blue if let Triangle( Point { x, color: Color::Blue, .. }, .., ) = my_triangle { // .. do something with the data we wanted for some reason .. }
รูปแบบอื่นๆ
นอกจากนี้ยังมีรูปแบบประเภทอื่นๆ อีกด้วย และส่วนใหญ่จะใช้ร่วมกับรูปแบบที่ตรงกัน ซึ่งคุณจะเห็นรูปแบบเพิ่มเติมด้านล่าง อันแรกคือ or-matcher:
// matches 1, 2 or 3 1 | 2 | 3 // Matches one of the strings "first" | "second"
เราเห็นช่วงสั้นๆ ข้างต้น:
// matches 1 to 10 (inclusive) 1..=10 // matches 1 to 10 (non-inclusive) 1..10
(ช่วงยังสามารถใช้เป็นดัชนีสำหรับอาร์เรย์เพื่อดึงข้อมูลหลายองค์ประกอบ)
รูปแบบสุดท้ายที่ฉันต้องการแสดงคือรูปแบบที่ใช้สำหรับการทดสอบและบันทึกค่า ใช่ คุณสามารถมีได้ทั้งสองอย่าง! ฟังก์ชันนี้มักใช้สำหรับบันทึกค่าภายในรูปแบบที่ตรงตามเงื่อนไขที่กำหนด และสามารถนำมาใช้ในเชิงลึกภายในรูปแบบได้ ให้เราดูตัวอย่างที่มีช่วง:
// Integer version of point struct MyData { x: i32, y: i32, z: i32, } // Match data x value when it's between 1 and 20 (inclusive) if let MyData { x: my_x @ 1..=20, .. } = my_data { // .. do something with the my_x that is 1<=20 .. }
(สังเกตว่าตัวแปรที่เราใช้ตอนนี้เรียกว่า my_x)
สิ่งเหล่านี้สามารถใช้กับ if-let และตำแหน่งอื่นๆ ที่อนุญาตให้มีรูปแบบที่หักล้างได้ แต่ส่วนใหญ่จะใช้ร่วมกับรูปแบบอื่นๆ ที่ตรงกัน ฉันไม่ค่อยได้ใช้สิ่งเหล่านี้กับ if-let เลย (อาจจะยกเว้นช่วง)
นั่นควรเป็นหัวข้อที่สำคัญที่สุด แต่อาจมีบางสิ่งที่ฉันพลาดไป ท้ายที่สุดแล้ว Rust เป็นภาษาที่มีการพัฒนา และอาจมีการเพิ่มฟีเจอร์ใหม่ๆ เข้าไป อาจมีฟีเจอร์ที่ฉันไม่ค่อยได้ใช้มากนัก ก็เลยลืมไป คุณสามารถดู เอกสารประกอบของ Rust ในหัวข้อนี้ สำหรับข้อมูลเพิ่มเติม
คำเตือน
ข้อแม้เล็กๆ น้อยๆ ประการหนึ่งที่ฉันพบก็คือ Rust นั้นจู้จี้จุกจิกเกี่ยวกับจุดลอยตัวในรูปแบบ ไม่เกี่ยวกับส่วนที่จับแต่ถ้าคุณลองใช้มันในระยะและอื่นๆ มันเตือนคุณว่ามันไม่รองรับ เราทุกคนรู้ดีว่าจุดลอยตัวมีพฤติกรรมอย่างไร และพวกมันไวต่อสเกลต่างๆ มากและไม่ค่อยแม่นยำ (ถ้าเคย) นั่นอาจอธิบายได้ว่าทำไมจึงไม่แนะนำให้ใช้กับรูปแบบ Rust ขั้นสูงกว่านี้
จะใช้การจับคู่รูปแบบได้ที่ไหน (เวลาตัวอย่าง!)
เราได้เห็นตัวอย่างพื้นฐานบางส่วนแล้วข้างต้น แต่มาเจาะลึกอีกสักหน่อยแล้วดูว่าจะใช้การจับคู่รูปแบบอย่างไรและที่ไหน เราได้เห็นคำสั่ง Let และ If-Let มากมายแล้ว ดังนั้นเรามาดูการจับคู่และรูปแบบใน Signature ของฟังก์ชันกัน!
จับคู่
นี่คือในมุมมองของฉันเกี่ยวกับคำสั่ง switch/case ที่คุณคุ้นเคยในภาษาอื่น แต่ใช้กับสเตียรอยด์! การจับคู่ช่วยให้คุณเขียนการจับคู่ที่ทรงพลังอย่างบ้าคลั่ง เราได้ดูรูปแบบที่แตกต่างกันทั้งหมดข้างต้นแล้ว ดังนั้นเรามาดูตัวอย่างสั้นๆ กัน:
// A more extensive version of httprequest enum HttpRequest { Get, Post(String), Put(String), Custom, Unknown } // a match expression match my_request { Get => { // .. do something with get .. } Post(data) | Put(data) => { // .. do something with the data .. } _ => {} }
(การจับคู่จะต้องละเอียดถี่ถ้วนและครอบคลุมทุกกรณี คุณจะเห็นว่าเราจัดการเรื่องนี้ด้วยไวด์การ์ด “_” ด้านบน นี่เป็นข้อได้เปรียบเช่นกัน เนื่องจากคอมไพเลอร์ช่วยให้คุณจดจำทุกกรณี!)
การแข่งขันยังมีสิ่งที่ฉลาดอีกอย่างหนึ่งที่เรียกว่าการ์ดป้องกันการแข่งขัน! โดยพื้นฐานแล้วมันเป็นประโยคเสริมในกรณี มาเพิ่มลงในกรณีที่สองด้านบน:
match my_request { HttpRequest::Get => { // .. do something with get .. } HttpRequest::Post(data) | HttpRequest::Put(data) if !data.is_empty() => { // .. do something with the data .. } _ => {} };
ที่นี่เราได้เพิ่มการตรวจสอบเพื่อดูว่าข้อมูลสตริงที่ส่งเข้ามาไม่ว่างเปล่า! สวยเก๋!
ฟังก์ชั่น
คุณสามารถใช้รูปแบบในคำจำกัดความของฟังก์ชันได้! มันเจ๋งจริงๆ ใช่มั้ย? คุณต้องจำไว้ว่ารูปแบบจะต้องหักล้างไม่ได้เช่นการอนุญาต อย่าเพิ่งเศร้า! มันยังหมายความว่าคุณสามารถใช้การดำเนินการทำลายล้างได้มากมายที่เราได้เห็นข้างต้น! ลองดูตัวอย่างบางส่วน:
// test data struct Vector2d { x: f32, y: f32 } // destructured Vector2d fn length(Vector2d { x, y }: Vector2d) -> f32 { (x.powi(2) + y.powi(2)).sqrt() } fn x_val(Vector2d { x, .. }: Vector2d) -> f32 { x }
สิ่งเหล่านี้สามารถเจาะลึกได้เช่นกันหากคุณต้องการ แต่อย่าลืมอย่าทำให้โค้ดของคุณอ่านง่ายเกินไป
อย่าลังเลที่จะแบ่งปันการจับคู่รูปแบบที่ชาญฉลาดที่คุณได้ทำในลายเซ็นฟังก์ชันของคุณในความคิดเห็น! ใช่ ฉันรู้ว่าคุณมีมันใน JavaScript เช่นกัน
ตอนนี้คุณคงได้เห็นแล้วว่ารูปแบบใน Rust สามารถทำได้บ้างแล้ว หากคุณยังใหม่กับ Rust บางทีนี่อาจเป็นแรงบันดาลใจให้คุณเรียนภาษา? :)
เผยแพร่ครั้งแรกที่ https://themkat.net เมื่อวันที่ 6 ตุลาคม 2022