Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ประเภทข้อมูลแบบเจเนอริก (Generic Data Types)

เราใช้เจเนอริกเพื่อสร้างคำนิยามสำหรับสิ่งต่าง ๆ เช่น ซิกเนเจอร์ของฟังก์ชัน หรือ struct ซึ่งเราจะสามารถนำไปใช้งานร่วมกับประเภทข้อมูลรูปธรรมที่หลากหลายได้ในภายหลัง ก่อนอื่นเรามาศึกษาวิธีการนิยามฟังก์ชัน, struct, enum และเมธอดต่าง ๆ โดยใช้เจเนอริกกัน จากนั้นเราจะอภิปรายถึงเรื่องเจเนอริกส่งผลต่อประสิทธิภาพการทำงานของโค้ดอย่างไร

ในนิยามของฟังก์ชัน (In Function Definitions)

เมื่อเรานิยามฟังก์ชันที่ใช้เจเนอริก เราจะนำเจเนอริกนั้นไปวางไว้ในส่วนซิกเนเจอร์ของฟังก์ชัน ซึ่งปกติแล้วเป็นจุดที่เราต้องระบุประเภทข้อมูลของพารามิเตอร์และข้อมูลส่งคืน การเขียนเช่นนี้จะช่วยให้โค้ดของเรายืดหยุ่นขึ้น และมอบฟังก์ชันการทำงานที่ครอบคลุมแก่ผู้เรียกฟังก์ชัน ในขณะเดียวกันก็ช่วยป้องกันไม่ให้เกิดโค้ดซ้ำซ้อน

มาต่อกันที่ฟังก์ชัน largest ของเรากันครับ ตัวอย่างที่ 10-4 แสดงฟังก์ชันสองตัวที่ต่างก็ทำหน้าที่ค้นหาค่าที่มากที่สุดใน slice จากนั้นเราจะนำสองฟังก์ชันนี้มารวมกันให้เหลือเพียงฟังก์ชันเดียวโดยอาศัยเจเนอริก

fn largest_i32(list: &[i32]) -> &i32 {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn largest_char(list: &[char]) -> &char {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest_i32(&number_list);
    println!("The largest number is {result}");
    assert_eq!(*result, 100);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest_char(&char_list);
    println!("The largest char is {result}");
    assert_eq!(*result, 'y');
}

ฟังก์ชัน largest_i32 คือตัวที่เราสกัดออกมาในตัวอย่างที่ 10-3 เพื่อหาตัวเลข i32 ที่ใหญ่ที่สุดใน slice ส่วนฟังก์ชัน largest_char ทำหน้าที่หาตัวอักษร char ที่ใหญ่ที่สุดใน slice ตัวโครงสร้างภายในของฟังก์ชันมีโค้ดที่เหมือนกันทุกประการ ดังนั้นเรามากำจัดจุดซ้ำซ้อนนี้โดยเพิ่มพารามิเตอร์ประเภทข้อมูลเจเนอริกเข้าไปในฟังก์ชันเดียว

ในการกำหนดประเภทพารามิเตอร์ของประเภทข้อมูลให้กับฟังก์ชันเดี่ยวตัวใหม่ เราจำเป็นต้องตั้งชื่อให้กับพารามิเตอร์ประเภทข้อมูลนั้นด้วย เช่นเดียวกับการตั้งชื่อตัวแปรพารามิเตอร์ทั่วไปของฟังก์ชัน คุณสามารถใช้ชื่อระบุตัวแปรใดก็ได้เป็นชื่อพารามิเตอร์ประเภทข้อมูล แต่เราจะเลือกใช้ตัวอักษร T เนื่องจากตามขนบข้อตกลงแล้ว ชื่อพารามิเตอร์ประเภทข้อมูลในภาษา Rust มักจะสั้น และมักจะใช้ตัวอักษรตัวเดียว โดยเขียนในรูปแบบตัวพิมพ์ใหญ่ (UpperCamelCase) โดยตัวอักษร T ย่อมาจากคำว่า type และมักจะเป็นทางเลือกเริ่มต้นที่นักพัฒนา Rust ส่วนใหญ่หยิบมาใช้

เมื่อเราใช้งานตัวแปรพารามิเตอร์ภายในฟังก์ชัน เราจำเป็นต้องประกาศชื่อพารามิเตอร์นั้นในส่วนซิกเนเจอร์เพื่อให้คอมไพเลอร์เข้าใจความหมายของชื่อดังกล่าว ในทำนองเดียวกัน เมื่อเราใช้ชื่อพารามิเตอร์ประเภทข้อมูลในส่วนซิกเนเจอร์ของฟังก์ชัน เราก็ต้องประกาศตัวแปรพารามิเตอร์ประเภทข้อมูลนั้นก่อนนำไปใช้งานจริง สำหรับการนิยามฟังก์ชันเจเนอริก largest เราจะวางประกาศประเภทข้อมูลนี้ไว้ภายในเครื่องหมายวงเล็บสามเหลี่ยม <> ระหว่างชื่อของฟังก์ชันกับรายการตัวแปรพารามิเตอร์ ดังนี้:

fn largest<T>(list: &[T]) -> &T {

เราจะอ่านคำนิยามบรรทัดนี้ได้ว่า “ฟังก์ชัน largest ทำหน้าที่เป็นเจเนอริกครอบคลุมประเภทข้อมูล T ใด ๆ” ฟังก์ชันนี้มีพารามิเตอร์ตัวหนึ่งชื่อว่า list ซึ่งมีรูปแบบเป็น slice ของค่าข้อมูลประเภท T และตัวฟังก์ชัน largest จะส่งคืนข้อมูลอ้างอิงของค่าข้อมูลประเภท T ชนิดเดียวกันนั้นกลับมา

ตัวอย่างที่ 10-5 แสดงนิยามของฟังก์ชัน largest ที่รวมกันแล้วโดยใช้ประเภทข้อมูลเจเนอริกในส่วนซิกเนเจอร์ ตัวอย่างนี้ยังแสดงวิธีเรียกใช้ฟังก์ชันกับ slice ของข้อมูลประเภท i32 หรือข้อมูลประเภท char ได้อย่างอิสระ สังเกตว่าโค้ดชุดนี้จะยังคอมไพล์ไม่ผ่าน ณ ตอนนี้

fn largest<T>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {result}");

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {result}");
}

หากเราสั่งคอมไพล์โค้ดนี้ตอนนี้ เราจะพบข้อความแจ้งข้อผิดพลาดดังนี้:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `&T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- &T
  |            |
  |            &T
  |
help: consider restricting type parameter `T` with trait `PartialOrd`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> &T {
  |             ++++++++++++++++++++++

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

ข้อความแนะนำได้ระบุคำว่า std::cmp::PartialOrd ซึ่งเป็นข้อมูลทริต (trait) และเรากำลังจะพูดถึงเรื่องทริตกันต่อในส่วนถัดไป สำหรับตอนนี้ ขอให้เข้าใจว่าข้อผิดพลาดนี้ระบุว่า โค้ดหลักภายในฟังก์ชัน largest จะไม่ทำงานครอบคลุมประเภทข้อมูลทั้งหมดที่ T สามารถเป็นได้ เนื่องจากเราต้องการนำค่าประเภท T ไปทำการเปรียบเทียบขนาด (comparison) ภายในฟังก์ชัน เราจึงจำเป็นต้องใช้ประเภทข้อมูลที่สามารถระบุลำดับก่อนหลังได้เท่านั้น เพื่อเปิดโอกาสให้ประมวลผลคำสั่งเปรียบเทียบได้ ไลบรารีมาตรฐานจึงได้กำหนดทริต std::cmp::PartialOrd ขึ้นมาเพื่อนำเสนอให้กับประเภทข้อมูลต่าง ๆ ได้สืบทอด (ดูภาคผนวก C สำหรับรายละเอียดทริตนี้) ในการแก้ไขโค้ดจากตัวอย่างที่ 10-5 เราทำตามคำแนะนำโดยจำกัดขอบเขตของประเภทข้อมูล T ให้ยอมรับเฉพาะประเภทข้อมูลที่มีการสืบทอดทริต PartialOrd เท่านั้น จากนั้นตัวโปรแกรมจะสามารถคอมไพล์ผ่านได้ เนื่องจากไลบรารีมาตรฐานได้สืบทอดทริต PartialOrd ไว้ให้กับประเภทข้อมูลทั้ง i32 และ char อยู่แล้ว

ในนิยามของ Struct (In Struct Definitions)

เราสามารถกำหนดนิยามให้ struct ใช้ประโยชน์จากพารามิเตอร์ประเภทข้อมูลเจเนอริกสำหรับฟิลด์ข้อมูลต่าง ๆ ได้โดยใช้ไวยากรณ์ <> เช่นเดียวกัน ตัวอย่างที่ 10-6 นิยาม struct Point<T> เพื่อทำหน้าที่จัดเก็บพิกัดแกน x และ y ของประเภทข้อมูลใด ๆ ก็ตาม

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let integer = Point { x: 5, y: 10 };
    let float = Point { x: 1.0, y: 4.0 };
}

ไวยากรณ์สำหรับเขียนใช้งานเจเนอริกในนิยาม struct จะมีโครงสร้างคล้ายกับกรณีของฟังก์ชัน ขั้นแรก ประกาศชื่อพารามิเตอร์ประเภทข้อมูลไว้ในวงเล็บสามเหลี่ยมต่อท้ายชื่อของ struct ทันที จากนั้นนำประเภทข้อมูลเจเนอริกนี้ไปกำหนดตรงส่วนฟิลด์ต่าง ๆ ภายใน struct ซึ่งปกติแล้วเป็นจุดระบุประเภทข้อมูลรูปธรรม

ข้อพึงสังเกตคือ เนื่องจากเราเลือกใช้ประเภทข้อมูลเจเนอริกเพียงตัวเดียวในนิยามของ Point<T> นิยามนี้จึงระบุว่า struct Point<T> จะเป็นเจเนอริกครอบคลุมชนิดประเภทข้อมูล T ใด ๆ และทั้งฟิลด์ x และ y ต้องเป็น ชนิดข้อมูลเดียวกันนั้นเสมอไม่ว่าข้อมูลดังกล่าวจะเป็นชนิดใดก็ตาม หากเราพยายามสร้างอินสแตนซ์ของ Point<T> โดยใส่ค่าข้อมูลที่ต่างประเภทกันดังแสดงในตัวอย่างที่ 10-7 โค้ดของเราจะไม่สามารถคอมไพล์ผ่านได้

struct Point<T> {
    x: T,
    y: T,
}

fn main() {
    let wont_work = Point { x: 5, y: 4.0 };
}

ในตัวอย่างนี้ เมื่อเรากำหนดจำนวนเต็ม 5 ให้กับ x เราจะบอกให้คอมไพเลอร์รู้ว่าประเภทข้อมูลเจเนอริก T ของอินสแตนซ์ Point<T> ตัวนี้จะเป็นชนิดจำนวนเต็ม แต่เมื่อเรากำหนดค่า 4.0 ให้กับ y ซึ่งเราได้นิยามให้เป็นชนิดข้อมูลเดียวกันกับ x เราจะเจอกับข้อผิดพลาดประเภทข้อมูลไม่ตรงกัน (type mismatch error) ดังต่อไปนี้:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0308]: mismatched types
 --> src/main.rs:7:38
  |
7 |     let wont_work = Point { x: 5, y: 4.0 };
  |                                      ^^^ expected integer, found floating-point number

For more information about this error, try `rustc --explain E0308`.
error: could not compile `chapter10` (bin "chapter10") due to 1 previous error

หากเราต้องการกำหนดนิยาม struct Point โดยระบุให้แกน x และ y ต่างก็ใช้เจเนอริกแต่ยอมให้เป็นชนิดประเภทข้อมูลที่ต่างกันได้ เราสามารถเลือกประกาศพารามิเตอร์ประเภทข้อมูลเจเนอริกเพิ่มได้หลายตัว ตัวอย่างเช่น ในตัวอย่างที่ 10-8 เราปรับเปลี่ยนนิยามของ Point ให้ครอบคลุมเจเนอริกของประเภทข้อมูล T และ U โดยกำหนดให้ x เป็นประเภท T และ y เป็นประเภท U

struct Point<T, U> {
    x: T,
    y: U,
}

fn main() {
    let both_integer = Point { x: 5, y: 10 };
    let both_float = Point { x: 1.0, y: 4.0 };
    let integer_and_float = Point { x: 5, y: 4.0 };
}

คราวนี้ อินสแตนซ์ต่าง ๆ ของ Point ทั้งหมดข้างต้นก็จะได้รับการอนุญาตให้คอมไพล์ผ่านได้แล้ว! คุณสามารถระบุพารามิเตอร์ประเภทข้อมูลเจเนอริกเพิ่มได้มากเท่าที่คุณต้องการในนิยามการทำงาน แต่อย่างไรก็ตาม การใส่พารามิเตอร์เจเนอริกมากเกินไปจะส่งผลให้โค้ดของคุณอ่านและทำความเข้าใจยากขึ้น หากคุณพบว่าโค้ดมีความจำเป็นต้องดึงเจเนอริกมาเขียนใช้จำนวนมาก นั่นอาจเป็นสัญญาณบ่งชี้ว่าโค้ดต้องการการปรับโครงสร้างใหม่เพื่อหั่นซอยออกเป็นส่วนที่เล็กลง

ในนิยามของ Enum (In Enum Definitions)

ในลักษณะเดียวกับ struct เราสามารถกำหนดนิยาม enum เพื่อเก็บประเภทข้อมูลเจเนอริกในส่วนตัวแปรย่อย (variants) ของมันได้ ลองย้อนกลับมาทบทวนนิยามของ enum Option<T> ที่มีเตรียมไว้ในไลบรารีมาตรฐานซึ่งเราได้ใช้งานไปแล้วในบทที่ 6:

#![allow(unused)]
fn main() {
enum Option<T> {
    Some(T),
    None,
}
}

คำนิยามบรรทัดนี้น่าจะดูเข้าใจง่ายขึ้นสำหรับคุณแล้วในตอนนี้ ดังที่คุณเห็น enum Option<T> เป็นเจเนอริกครอบคลุมประเภทข้อมูล T และมีตัวแปรย่อยสองตัว ได้แก่ Some ซึ่งห่อหุ้มค่าข้อมูลชนิด T เอาไว้หนึ่งค่า และตัวแปรย่อย None ซึ่งไม่ได้เก็บค่าข้อมูลใด ๆ การใช้งาน enum Option<T> ช่วยให้เราสามารถระบุคุณสมบัตินามธรรมของค่าข้อมูลที่อาจจะมีอยู่หรือไม่มีก็ได้ (optional value) และเนื่องจาก Option<T> ถูกระบุให้ทำงานแบบเจเนอริก เราจึงนำคุณสมบัตินามธรรมนี้ไปประยุกต์ใช้ได้กับข้อมูลทุกชนิดโดยไม่เกี่ยงว่าข้อมูลตัวเลือกนั้นจะเป็นประเภทใด

ตัวแปร enum สามารถประกาศใช้งานเจเนอริกเพิ่มได้หลายตัวแปรเช่นกัน ตัวอย่างหนึ่งคือคำนิยามของ enum Result ที่เราเคยใช้งานกันไปในบทที่ 9:

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}
}

ตัวแปร enum Result นี้ใช้เจเนอริกร่วมกันสองตัว ได้แก่ T และ E และมีตัวแปรย่อยสองตัวคือ Ok ซึ่งเก็บค่าข้อมูลประเภท T และ Err ซึ่งเก็บค่าข้อผิดพลาดประเภท E โครงสร้างนิยามนี้ช่วยให้เรานำ Result ไปเรียกใช้งานได้อย่างสะดวกในการทำงานต่าง ๆ ที่อาจประสบผลสำเร็จ (และส่งผลลัพธ์ข้อมูลประเภท T กลับมา) หรือล้มเหลว (และส่งข้อมูลข้อผิดพลาดประเภท E กลับคืนมา) ตัวอย่างเช่น โค้ดสำหรับเปิดไฟล์ในตัวอย่างที่ 9-3 ซึ่งกำหนดให้ T ถูกแทนที่ด้วยประเภทข้อมูล std::fs::File เมื่อเปิดไฟล์สำเร็จ และ E ถูกแทนที่ด้วยประเภทข้อมูล std::io::Error เมื่อตรวจพบข้อผิดพลาดระหว่างกระบวนการเปิดไฟล์

เมื่อคุณเริ่มสังเกตเห็นสถานการณ์ในโค้ดที่มีการสร้าง struct หรือ enum หลาย ๆ ชิ้นที่มีโครงสร้างคำสั่งแทบจะเหมือนกันทว่ามีความแตกต่างกันเฉพาะแค่ประเภทข้อมูลที่ฟิลด์ภายในเลือกใช้ คุณจะสามารถลดโค้ดซ้ำซ้อนเหล่านั้นได้โดยการเปลี่ยนมาใช้นิยามแบบเจเนอริกทดแทน

ในนิยามของ เมธอด (In Method Definitions)

เราสามารถเขียนคำสั่งนำเสนอเมธอดต่าง ๆ ให้กับ struct และ enum (แบบที่เราเรียนกันไปในบทที่ 5) และเรียกใช้งานประเภทข้อมูลเจเนอริกในการระบุนิยามได้เช่นเดียวกัน ตัวอย่างที่ 10-9 แสดงการสืบทอด struct Point<T> จากตัวอย่างที่ 10-6 โดยมีการสร้างเมธอดชื่อว่า x เข้าไปด้วย

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

ที่จุดนี้ เรานิยามเมธอดชื่อว่า x ให้กับ Point<T> ซึ่งจะคอยส่งคืนข้อมูลอ้างอิงของข้อมูลในฟิลด์ x กลับออกไป

สังเกตว่าเราต้องระบุตัวแปร T ไว้ถัดจากคำสำคัญ impl ทันที เพื่อให้ระบบนำ T ไปประมวลผลต่อว่าเราต้องการนำเสนอเมธอดให้กับประเภทข้อมูล Point<T> การประกาศระบุ T เป็นชนิดเจเนอริกต่อท้าย impl จะช่วยให้ Rust วิเคราะห์ได้ถูกต้องว่าข้อมูลภายในเครื่องหมายวงเล็บสามเหลี่ยมของคำว่า Point คือเจเนอริก ไม่ใช่ประเภทข้อมูลรูปธรรมคงที่ ความจริงแล้วเราสามารถกำหนดชื่อพารามิเตอร์เจเนอริกตรงจุดนี้ให้ต่างจากชื่อเจเนอริกในส่วนนิยาม struct ก็ได้ แต่ขนบทั่วไปจะเลือกใช้ชื่อเดียวกันเพื่อความไม่งง หากคุณเขียนสร้างเมธอดภายในบล็อก impl ที่ประกาศใช้เจเนอริก เมธอดดังกล่าวจะพร้อมถูกเรียกใช้งานในทุกอินสแตนซ์ของประเภทข้อมูลนั้น ๆ โดยไม่เกี่ยวว่าตัวแปรเจเนอริกดังกล่าวจะถูกแทนที่ด้วยประเภทรูปธรรมชนิดใดในภายหลัง

นอกจากนี้เรายังสามารถระบุข้อจำกัด (constraints) เพิ่มเติมให้กับเจเนอริกของประเภทข้อมูลเมื่อเราจะนิยามเมธอดได้ด้วย ตัวอย่างเช่น เราอาจต้องการเขียนสร้างเมธอดเฉพาะให้กับอินสแตนซ์ Point<f32> เท่านั้น แทนที่จะนำเสนอให้กับ Point<T> ทั่วไปของข้อมูลทุกชนิด ในตัวอย่างที่ 10-10 เราเลือกกำหนดชนิดข้อมูลรูปธรรมเป็น f32 ส่งผลให้เราไม่ต้องระบุประเภทเจเนอริกใด ๆ ต่อท้ายคำสั่ง impl เลย

struct Point<T> {
    x: T,
    y: T,
}

impl<T> Point<T> {
    fn x(&self) -> &T {
        &self.x
    }
}

impl Point<f32> {
    fn distance_from_origin(&self) -> f32 {
        (self.x.powi(2) + self.y.powi(2)).sqrt()
    }
}

fn main() {
    let p = Point { x: 5, y: 10 };

    println!("p.x = {}", p.x());
}

โค้ดนี้ส่งผลให้เฉพาะประเภทข้อมูล Point<f32> เท่านั้นที่จะมีเมธอด distance_from_origin ไว้ใช้งาน ส่วนอินสแตนซ์ของ Point<T> อื่น ๆ ที่ T ไม่ได้มีประเภทเป็น f32 จะไม่มีการนิยามเมธอดนี้ไว้ เมธอดดังกล่าวทำหน้าที่คำนวณวัดระยะห่างระหว่างจุดพิกัดปัจจุบันกับจุดพิกัดเริ่มต้น (0.0, 0.0) ซึ่งมีการใช้ตรรกะกระบวนการคำนวณทางคณิตศาสตร์ที่มีสิทธิ์เรียกใช้เฉพาะกับประเภทตัวเลขทศนิยมเท่านั้น

พารามิเตอร์ประเภทข้อมูลเจเนอริกในนิยามของ struct ไม่จำเป็นต้องเหมือนกับพารามิเตอร์เจเนอริกที่ใช้ในส่วนซิกเนเจอร์ของเมธอดภายใน struct เสมอไป ตัวอย่างที่ 10-11 เลือกใช้ตัวแปรประเภทข้อมูลเจเนอริก X1 และ Y1 สำหรับ struct Point และเลือกใช้ X2 และ Y2 สำหรับซิกเนเจอร์ของเมธอด mixup เพื่ออธิบายตัวอย่างให้แจ่มชัด เมธอดนี้ทำหน้าที่สร้างอินสแตนซ์ Point ตัวใหม่ขึ้นมาโดยดึงค่า x มาจาก self Point (ซึ่งมีชนิดข้อมูลเป็น X1) และดึงค่า y มาจากอินพุต Point ที่ส่งเข้ามา (ซึ่งมีชนิดข้อมูลเป็น Y2)

struct Point<X1, Y1> {
    x: X1,
    y: Y1,
}

impl<X1, Y1> Point<X1, Y1> {
    fn mixup<X2, Y2>(self, other: Point<X2, Y2>) -> Point<X1, Y2> {
        Point {
            x: self.x,
            y: other.y,
        }
    }
}

fn main() {
    let p1 = Point { x: 5, y: 10.4 };
    let p2 = Point { x: "Hello", y: 'c' };

    let p3 = p1.mixup(p2);

    println!("p3.x = {}, p3.y = {}", p3.x, p3.y);
}

ในฟังก์ชัน main เรานิยาม Point ตัวแรกให้มีฟิลด์ x เป็นประเภท i32 (เก็บค่า 5) และฟิลด์ y เป็นประเภท f64 (เก็บค่า 10.4) ส่วนตัวแปร p2 เป็น struct Point ที่มีแกน x เป็นประเภท string slice (เก็บข้อความ "Hello") และแกน y เป็นประเภท char (เก็บอักษร c) การเรียกใช้เมธอด mixup บนตัวแปร p1 โดยส่งอาร์กิวเมนต์เป็น p2 จะส่งผลให้ได้ผลลัพธ์กลับมาเป็น p3 ซึ่งจะมีแกน x เป็นประเภท i32 เนื่องจากได้ข้อมูลตั้งต้นมาจาก p1 และตัวแปร p3 จะมีแกน y เป็นประเภท char เนื่องจากดึงข้อมูลต้นทางมาจาก p2 มาโคร println! จะแสดงผลลัพธ์ลู่เข้าหาค่า p3.x = 5, p3.y = c

เป้าหมายของโค้ดตัวอย่างนี้เพื่อแสดงสถานการณ์ที่มีการประกาศพารามิเตอร์เจเนอริกบางส่วนไว้กับคำสั่ง impl และประกาศเจเนอริกอีกส่วนซ้อนอยู่กับการกำหนดนิยามเมธอดแยกเฉพาะ ในจุดนี้ พารามิเตอร์เจเนอริก X1 และ Y1 ถูกประกาศไว้ต่อท้าย impl เนื่องจากมีความสัมพันธ์โดยตรงกับนิยามของ struct หลัก ส่วนพารามิเตอร์เจเนอริก X2 และ Y2 จะประกาศไว้หลัง fn mixup เพราะมีบทบาทเกี่ยวข้องเฉพาะกับตัวเมธอดนี้เท่านั้น

ประสิทธิภาพการประมวลผลของโค้ดเมื่อมีการใช้งานเจเนอริก (Performance of Code Using Generics)

คุณอาจจะกำลังสงสัยอยู่ว่ามีค่าใช้จ่ายด้านประสิทธิภาพการทำงานในตอนประมวลผลโปรแกรม (runtime cost) หรือไม่เมื่อเลือกใช้งานพารามิเตอร์ประเภทข้อมูลเจเนอริก ข่าวดีคือการใช้งานประเภทข้อมูลเจเนอริกจะไม่ทำให้โปรแกรมของคุณทำงานช้าลงเลยเมื่อเทียบกับการเขียนระบุประเภทข้อมูลรูปธรรมแบบคงที่

ภาษา Rust ประสบผลสำเร็จในแง่นนี้ได้โดยทำกระบวนการแปลงโค้ดจากรูปแบบนามธรรมให้เป็นโค้ดเฉพาะเจาะจง (monomorphization) ในระหว่างขั้นตอนการคอมไพล์โค้ด Monomorphization คือกระบวนการแปลงโค้ดเจเนอริกเชิงนามธรรมให้กลายเป็นโค้ดเฉพาะทางโดยนำประเภทข้อมูลรูปธรรมจริง ๆ ที่ตรวจพบในโปรแกรมมาเติมใส่ตำแหน่งยึดในขั้นตอนคอมไพล์ ในกระบวนการนี้ คอมไพเลอร์จะดำเนินขั้นตอนย้อนกลับกับสิ่งที่เราทำตอนเขียนฟังก์ชันเจเนอริกในตัวอย่างที่ 10-5 กล่าวคือ: คอมไพเลอร์จะไล่สำรวจดูตำแหน่งทั้งหมดในโค้ดที่มีการเรียกใช้งานโค้ดเจเนอริก และสร้างชุดคำสั่งเฉพาะขึ้นมารองรับแต่ละชนิดประเภทข้อมูลรูปธรรมที่มีการเรียกใช้งานจริงขึ้นมาแทน

ลองมาศึกษาการทำงานจริงโดยอ้างอิงจาก enum Option<T> แบบเจเนอริกของไลบรารีมาตรฐาน:

#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}

เมื่อ Rust สั่งคอมไพล์โค้ดชุดนี้ ระบบจะดำเนินการ monomorphization ในระหว่างกระบวนการนั้น คอมไพเลอร์จะอ่านข้อมูลค่าตัวแปรต่าง ๆ ที่มีการเรียกใช้งานในอินสแตนซ์ของ Option<T> และวิเคราะห์ว่าตรวจพบ Option<T> สองประเภท: ชนิดแรกเป็น i32 และชนิดที่สองเป็น f64 จากนั้นระบบจะขยายร่างนิยามโครงสร้างเจเนอริกของ Option<T> ออกมาเป็นคำนิยามแยกเฉพาะสองรูปแบบสำหรับ i32 และ f64 ส่งผลให้นิยามแบบเจเนอริกเดิมถูกแปลงเปลี่ยนเป็นนิยามชนิดข้อมูลเจาะจงทดแทนในทันที

ตัวโปรแกรมเวอร์ชันที่ผ่านกระบวนการ monomorphization เรียบร้อยแล้วจะมีหน้าตาโครงสร้างประมาณนี้ (แน่นอนว่าชื่อของระบบที่คอมไพเลอร์สร้างขึ้นมาจริงจะต่างไปจากชื่อที่เราใช้สมมติในตัวอย่างนี้):

enum Option_i32 {
    Some(i32),
    None,
}

enum Option_f64 {
    Some(f64),
    None,
}

fn main() {
    let integer = Option_i32::Some(5);
    let float = Option_f64::Some(5.0);
}

คำนิยามแบบเจเนอริก Option<T> จะถูกเขียนแทนที่ด้วยคำนิยามแบบเฉพาะทางที่คอมไพเลอร์คอยดำเนินการสร้างให้โดยอัตโนมัติ เนื่องจาก Rust สั่งคอมไพล์โค้ดเจเนอริกออกมาเป็นภาษาเครื่องระดับโค้ดที่ระบุประเภทข้อมูลเฉพาะเจาะจงในแต่ละอินสแตนซ์แล้ว เราจึงไม่ต้องจ่ายต้นทุนประสิทธิภาพในการรันโปรแกรมเลยสำหรับการใช้งานเจเนอริก เมื่อโค้ดเริ่มรันการประมวลผล ประสิทธิภาพจะดีเท่ากับกรณีที่เราเขียนคัดลอกนิยามฟังก์ชันเหล่านั้นทีละฟังก์ชันด้วยตนเอง กระบวนการ monomorphization นี้ทำให้ระบบเจเนอริกของ Rust มีความเสถียรและทรงพลังในแง่ความเร็วรันไทม์เป็นอย่างมาก