Generic Data Type
เราใช้ generic สร้าง definition สำหรับ item อย่าง signature ฟังก์ชันหรือ struct ซึ่งเราใช้กับ type ข้อมูลคอนกรีตหลายตัวได้ ก่อนอื่นมาดูวิธีประกาศ ฟังก์ชัน struct enum และเมธอดโดยใช้ generic จากนั้น เราจะพูดถึงวิธีที่ generic กระทบ performance ของโค้ด
ในการประกาศฟังก์ชัน
เมื่อประกาศฟังก์ชันที่ใช้ generic เราวาง generic ใน signature ของฟังก์ชัน ตรงที่โดยปกติเราจะระบุ type ข้อมูลของ parameter และ return value การทำ เช่นนั้นทำให้โค้ดของเรายืดหยุ่นขึ้นและให้ functionality เพิ่มแก่ผู้เรียก ฟังก์ชันของเรา ขณะป้องกันการซ้ำของโค้ด
ต่อจากฟังก์ชัน largest ของเรา Listing 10-4 แสดงสองฟังก์ชันที่ทั้งคู่หา
ค่าที่ใหญ่ที่สุดใน slice เราจะรวมพวกมันเป็นฟังก์ชันเดียวที่ใช้ generic
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 คือตัวที่เราดึงใน Listing 10-3 ที่หา i32 ที่
ใหญ่ที่สุดใน slice ฟังก์ชัน largest_char หา char ที่ใหญ่ที่สุดใน
slice body ของฟังก์ชันมีโค้ดเดียวกัน มากำจัดการซ้ำโดยแนะนำ generic type
parameter ในฟังก์ชันเดียว
ในการ parameterize type ในฟังก์ชันเดียวใหม่ เราต้องตั้งชื่อ type parameter
เช่นเดียวกับที่เราทำสำหรับ value parameter ของฟังก์ชัน คุณใช้ identifier
ใดเป็นชื่อ type parameter ได้ แต่เราจะใช้ T เพราะตาม convention ชื่อ
type parameter ใน Rust สั้น มักแค่ตัวอักษรเดียว และ convention การตั้งชื่อ
type ของ Rust คือ UpperCamelCase ย่อจาก type T เป็นตัวเลือก default
ของโปรแกรมเมอร์ Rust ส่วนใหญ่
เมื่อเราใช้ parameter ใน body ของฟังก์ชัน เราต้องประกาศชื่อ parameter ใน
signature เพื่อให้ compiler รู้ว่าชื่อนั้นหมายถึงอะไร ในทำนองเดียวกัน
เมื่อเราใช้ชื่อ type parameter ใน signature ฟังก์ชัน เราต้องประกาศชื่อ
type parameter ก่อนเราใช้ ในการประกาศฟังก์ชัน largest แบบ generic เรา
วางการประกาศชื่อ type ภายใน angle bracket <> ระหว่างชื่อฟังก์ชันและ
list parameter ดังนี้:
fn largest<T>(list: &[T]) -> &T {
เราอ่าน definition นี้ว่า “ฟังก์ชัน largest เป็น generic บน type T
บางตัว” ฟังก์ชันนี้มี parameter หนึ่งตัวชื่อ list ซึ่งเป็น slice ของ
ค่า type T ฟังก์ชัน largest จะ return reference ของค่า type T
เดียวกัน
Listing 10-5 แสดง definition ฟังก์ชัน largest ที่รวมโดยใช้ generic data
type ใน signature listing ยังแสดงวิธีเราเรียกฟังก์ชันด้วย slice ของค่า
i32 หรือค่า char หมายเหตุว่าโค้ดนี้จะยัง compile ไม่ผ่าน
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}");
}
largest ที่ใช้ generic type parameter — ยัง compile ไม่ผ่านถ้าเรา compile โค้ดนี้ตอนนี้ เราจะได้ error นี้:
$ 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
ข้อความ help เอ่ย std::cmp::PartialOrd ซึ่งเป็น trait และเราจะพูดถึง
trait ในส่วนถัดไป ตอนนี้ รู้ว่า error นี้ระบุว่า body ของ largest จะ
ไม่ทำงานสำหรับ type ที่เป็นไปได้ทั้งหมดที่ T เป็น เพราะเราอยาก
เปรียบเทียบค่า type T ใน body เราใช้ได้แค่ type ที่ค่าของพวกมัน
เรียงลำดับได้ เพื่อเปิดทางการเปรียบเทียบ standard library มี trait
std::cmp::PartialOrd ที่คุณ implement บน type ได้ (ดูภาคผนวก C สำหรับ
ข้อมูลเพิ่มเติมเรื่อง trait นี้) ในการแก้ Listing 10-5 เราตามคำแนะนำของ
ข้อความ help และจำกัด type ที่ valid สำหรับ T ให้เป็นแค่ตัวที่
implement PartialOrd Listing จะ compile ผ่านตอนนั้น เพราะ standard
library implement PartialOrd บนทั้ง i32 และ char
ในการประกาศ Struct
เรายังประกาศ struct ให้ใช้ generic type parameter ในหนึ่งหรือมากกว่าหนึ่ง
field โดยใช้ syntax <> ได้ Listing 10-6 ประกาศ struct Point<T> เพื่อ
เก็บค่าพิกัด x และ y ของ type ใด ๆ
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 };
}
Point<T> ที่เก็บค่า x และ y ของ type Tsyntax สำหรับใช้ generic ใน definition struct คล้ายกับที่ใช้ใน definition ฟังก์ชัน ก่อนอื่น เราประกาศชื่อ type parameter ภายใน angle bracket หลัง ชื่อ struct จากนั้น เราใช้ generic type ใน definition struct ตรงที่ มิฉะนั้นเราจะระบุ type ข้อมูลคอนกรีต
หมายเหตุว่าเพราะเราใช้แค่ generic type หนึ่งตัวประกาศ Point<T>
definition นี้บอกว่า struct Point<T> เป็น generic บน type T บางตัว
และ field x และ y ทั้งคู่ เป็น type เดียวกัน ไม่ว่า type นั้นจะ
เป็นอะไร ถ้าเราสร้าง instance ของ Point<T> ที่มีค่า type ต่างกัน
อย่างใน Listing 10-7 โค้ดของเราจะ compile ไม่ผ่าน
struct Point<T> {
x: T,
y: T,
}
fn main() {
let wont_work = Point { x: 5, y: 4.0 };
}
x และ y ต้องเป็น type เดียวกันเพราะทั้งคู่มี generic data type T เดียวกันในตัวอย่างนี้ เมื่อเรา assign ค่า integer 5 ให้ x เราให้ compiler
รู้ว่า generic type T จะเป็น integer สำหรับ instance นี้ของ Point<T>
จากนั้น เมื่อเราระบุ 4.0 สำหรับ y ซึ่งเราประกาศให้มี type เดียวกับ
x เราจะได้ error type mismatch แบบนี้:
$ 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 เป็น generic ทั้งคู่ แต่มี
type ต่างกันได้ เราใช้ generic type parameter หลายตัวได้ เช่น ใน
Listing 10-8 เราเปลี่ยน definition ของ Point ให้เป็น generic บน type
T และ U ที่ x เป็น type T และ y เป็น type 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<T, U> generic บนสอง type เพื่อให้ x และ y เป็นค่าของ type ต่างกันได้ตอนนี้ instance ทั้งหมดของ Point ที่แสดงอนุญาตแล้ว! คุณใช้ generic
type parameter มากเท่าที่อยากใน definition ได้ แต่ใช้มากกว่าไม่กี่ตัวทำ
ให้โค้ดของคุณอ่านยาก ถ้าคุณพบว่าต้องการ generic type หลายตัวในโค้ด มัน
อาจบ่งบอกว่าโค้ดต้องการการ restructure เป็นชิ้นเล็ก
ในการประกาศ Enum
อย่างที่เราทำกับ struct เราประกาศ enum ให้เก็บ generic data type ใน
variant ได้ ลองมาดู enum Option<T> ที่ standard library ให้อีกครั้ง
ซึ่งเราใช้ในบทที่ 6:
#![allow(unused)]
fn main() {
enum Option<T> {
Some(T),
None,
}
}
Definition นี้ควรสมเหตุสมผลมากขึ้นกับคุณตอนนี้ อย่างที่คุณเห็น enum
Option<T> เป็น generic บน type T และมีสอง variant — Some ที่เก็บ
ค่าหนึ่งของ type T และ variant None ที่ไม่เก็บค่าใด ๆ ด้วยการใช้
enum Option<T> เราแสดงแนวคิดนามธรรมของค่าทางเลือกได้ และเพราะ
Option<T> เป็น generic เราใช้ abstraction นี้ได้ไม่ว่า type ของค่า
ทางเลือกจะเป็นอะไร
Enum ใช้ generic type หลายตัวได้ด้วย Definition ของ enum Result ที่เรา
ใช้ในบทที่ 9 เป็นตัวอย่างหนึ่ง:
#![allow(unused)]
fn main() {
enum Result<T, E> {
Ok(T),
Err(E),
}
}
Enum Result เป็น generic บนสอง type T และ E และมีสอง variant —
Ok ที่เก็บค่าของ type T และ Err ที่เก็บค่าของ type E Definition
นี้ทำให้สะดวกในการใช้ enum Result ที่ใดก็ตามที่เรามี operation ที่อาจ
สำเร็จ (return ค่าของ type T) หรือล้มเหลว (return error ของ type E)
จริง ๆ นี่คือสิ่งที่เราใช้เปิดไฟล์ใน Listing 9-3 ที่ T ถูกเติมด้วย
type std::fs::File เมื่อไฟล์ถูกเปิดสำเร็จ และ E ถูกเติมด้วย type
std::io::Error เมื่อมีปัญหาเปิดไฟล์
เมื่อคุณจับสถานการณ์ในโค้ดของคุณที่มี definition struct หรือ enum หลาย ตัวที่ต่างกันแค่ที่ type ของค่าที่พวกมันเก็บ คุณหลีกเลี่ยงการซ้ำได้โดย ใช้ generic type แทน
ในการประกาศเมธอด
เรา implement เมธอดบน struct และ enum ได้ (อย่างที่เราทำในบทที่ 5) และ
ใช้ generic type ใน definition ของพวกมันด้วย Listing 10-9 แสดง struct
Point<T> ที่เราประกาศใน Listing 10-6 พร้อมเมธอดชื่อ x ที่ implement
บนมัน
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 บน struct Point<T> ที่จะ return reference ของ field x type Tที่นี่ เราประกาศเมธอดชื่อ x บน Point<T> ที่ return reference ของ
ข้อมูลใน field x
หมายเหตุว่าเราต้องประกาศ T หลัง impl ตรง ๆ เพื่อให้เราใช้ T ระบุ
ว่าเรากำลัง implement เมธอดบน type Point<T> โดยประกาศ T เป็น generic
type หลัง impl Rust ระบุได้ว่า type ใน angle bracket ใน Point เป็น
generic type แทน type คอนกรีต เราเลือกชื่อต่างสำหรับ generic parameter
นี้ที่ต่างจาก generic parameter ที่ประกาศใน definition struct ได้ แต่
ใช้ชื่อเดียวกันเป็น convention ถ้าคุณเขียนเมธอดภายใน impl ที่ประกาศ
generic type เมธอดนั้นจะถูกประกาศบน instance ใด ๆ ของ type ไม่ว่า type
คอนกรีตอะไรลงเอยแทน generic type
เรายังระบุข้อจำกัดบน generic type เมื่อประกาศเมธอดบน type ได้ เช่น เรา
implement เมธอดแค่บน instance Point<f32> แทนบน instance Point<T>
ด้วย generic type ใดก็ได้ ใน Listing 10-10 เราใช้ type คอนกรีต f32
หมายความว่าเราไม่ประกาศ type ใด ๆ หลัง 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());
}
impl ที่ใช้แค่กับ struct ที่มี type คอนกรีตเฉพาะสำหรับ generic type parameter Tโค้ดนี้หมายความว่า type Point<f32> จะมีเมธอด distance_from_origin
instance อื่นของ Point<T> ที่ T ไม่ใช่ type f32 จะไม่มีเมธอดนี้
ประกาศ เมธอดวัดว่าจุดของเราอยู่ห่างจากจุดที่พิกัด (0.0, 0.0) เท่าไร และ
ใช้ operation คณิตศาสตร์ที่มีให้แค่สำหรับ type floating-point
Generic type parameter ใน definition struct ไม่ใช่ตัวเดียวกับที่คุณใช้
ใน signature เมธอดของ struct นั้นเสมอ Listing 10-11 ใช้ generic type
X1 และ Y1 สำหรับ struct Point และ X2 และ Y2 สำหรับ signature
เมธอด mixup เพื่อทำให้ตัวอย่างชัดเจนขึ้น เมธอดสร้าง instance Point
ใหม่ด้วยค่า x จาก self Point (type X1) และค่า y จาก Point
ที่ส่งเข้า (type 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 ที่มี i32 สำหรับ x (ค่า 5) และ f64
สำหรับ y (ค่า 10.4) ตัวแปร p2 เป็น struct Point ที่มี string
slice สำหรับ x (ค่า "Hello") และ char สำหรับ y (ค่า c) การเรียก
mixup บน p1 ด้วย argument p2 ให้เรา p3 ซึ่งจะมี i32 สำหรับ x
เพราะ x มาจาก p1 ตัวแปร p3 จะมี char สำหรับ y เพราะ y มาจาก
p2 การเรียก println! macro จะพิมพ์ p3.x = 5, p3.y = c
จุดประสงค์ของตัวอย่างนี้คือแสดงสถานการณ์ที่ generic parameter บางตัว
ประกาศกับ impl และบางตัวประกาศกับ definition เมธอด ที่นี่ generic
parameter X1 และ Y1 ประกาศหลัง impl เพราะพวกมันไปกับ definition
struct generic parameter X2 และ Y2 ประกาศหลัง fn mixup เพราะพวก
มันเกี่ยวข้องแค่กับเมธอด
Performance ของโค้ดที่ใช้ Generic
คุณอาจสงสัยว่ามีต้นทุน runtime เมื่อใช้ generic type parameter ไหม ข่าวดีคือการใช้ generic type จะไม่ทำให้โปรแกรมของคุณรันช้ากว่าที่จะรัน ด้วย type คอนกรีต
Rust ทำสิ่งนี้สำเร็จโดยทำ monomorphization ของโค้ดที่ใช้ generic ตอน compile time Monomorphization คือกระบวนการเปลี่ยนโค้ด generic เป็น โค้ดเฉพาะโดยเติม type คอนกรีตที่ใช้เมื่อ compile ในกระบวนการนี้ compiler ทำตรงข้ามกับขั้นตอนที่เราใช้สร้าง generic function ใน Listing 10-5 — compiler ดูทุกที่ที่ generic code ถูกเรียก และ generate โค้ดสำหรับ type คอนกรีตที่ generic code ถูกเรียกด้วย
มาดูว่ามันทำงานยังไงโดยใช้ enum Option<T> generic ของ standard library:
#![allow(unused)]
fn main() {
let integer = Some(5);
let float = Some(5.0);
}
เมื่อ Rust compile โค้ดนี้ มันทำ monomorphization ระหว่างกระบวนการนั้น
compiler อ่านค่าที่ถูกใช้ใน instance Option<T> และระบุสองชนิดของ
Option<T> — หนึ่งคือ i32 และอีกหนึ่งคือ f64 ดังนั้น มันขยาย
definition generic ของ Option<T> เป็นสอง definition พิเศษสำหรับ i32
และ f64 จึงแทน definition generic ด้วยตัวเฉพาะ
version monomorphize ของโค้ดดูคล้ายต่อไปนี้ (compiler ใช้ชื่อต่างจากที่ เราใช้ที่นี่เพื่อแสดง):
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> generic ถูกแทนด้วย definition เฉพาะที่ compiler สร้าง เพราะ
Rust compile generic code เป็นโค้ดที่ระบุ type ในแต่ละ instance เราจ่าย
ต้นทุน runtime ไม่มีสำหรับการใช้ generic เมื่อโค้ดรัน มันทำเหมือนเรา
คัดลอกแต่ละ definition ด้วยมือ กระบวนการ monomorphization ทำให้ generic
ของ Rust มีประสิทธิภาพมากตอน runtime