หากคุณติดตามฉันมาสักระยะ คุณจะรู้ว่าฉันเริ่มเพลิดเพลินกับ 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