Paths for Referring to an Item in the Module Tree

Để chỉ cho Rust biết nơi tìm thấy một item trong cây module, chúng ta sử dụng một đường dẫn (path) tương tự như chúng ta sử dụng một đường dẫn trong một hệ thống tập tin. Để gọi một hàm, chúng ta cần biết đường dẫn của nó.

Một đường dẫn có thể có hai dạng:

  • Đường dẫn tuyệt đối (absolute path) là đường dẫn đầy đủ bắt đầu từ một crate root; đối với code từ một crate bên ngoài, đường dẫn tuyệt đối bắt đầu với tên crate, và đối với code từ crate hiện tại, nó bắt đầu với từ khoá crate.
  • Đường dẫn tương đối (relative path) bắt đầu từ module hiện tại và sử dụng self, super, hoặc một định danh (identifier) trong module hiện tại.

Cả đường dẫn tuyệt đối và tương đối đều được theo sau bởi một hoặc nhiều định danh (identifier) được phân tách bởi hai dấu hai chấm (::).

Trở lại Listing 7-1, giả sử chúng ta muốn gọi hàm add_to_waitlist. Điều này tương đương với câu hỏi: đường dẫn của hàm add_to_waitlist là gì? Listing 7-3 chứa Listing 7-1 với một số module và hàm bị xóa. Chúng ta sẽ hiển thị hai cách gọi hàm add_to_waitlist từ một hàm mới eat_at_restaurant được định nghĩa trong crate root. Hàm eat_at_restaurant là một phần của API công khai của library crate của chúng ta, vì vậy chúng ta đánh dấu nó với từ khoá pub. Trong phần “Exposing Paths with the pub Keyword”, chúng ta sẽ đi sâu vào chi tiết hơn về pub. Lưu ý rằng ví dụ này tạm thời sẽ không được biên dịch; chúng ta sẽ giải thích tại trong ít phút nữa.

Filename: src/lib.rs

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Listing 7-3: Gọi hàm add_to_waitlist sử dụng đường dẫn tuyệt đối và tương đối

Lần đầu tiên chúng ta gọi hàm add_to_waitlist trong eat_at_restaurant, chúng ta sử dụng đường dẫn tuyệt đối. Hàm add_to_waitlist được định nghĩa trong cùng một crate với eat_at_restaurant, điều này có nghĩa là chúng ta có thể sử dụng từ khoá crate để bắt đầu một đường dẫn tuyệt đối. Sau đó, chúng ta sẽ đi qua từng tên của các module cho đến khi chúng ta bắt gặp add_to_waitlist. Bạn có thể tưởng tượng một hệ thống tệp tin với cùng cấu trúc: chúng ta sẽ chỉ định đường dẫn /front_of_house/hosting/add_to_waitlist để chạy chương trình add_to_waitlist; sử dụng từ khoá crate để bắt đầu từ crate root giống như sử dụng / để bắt đầu từ thư mục gốc của hệ thống tệp tin trong shell của bạn.

Lần thứ hai chúng ta gọi add_to_waitlist trong eat_at_restaurant, chúng ta sử dụng đường dẫn tương đối. Đường dẫn bắt đầu với front_of_house, tên của module được định nghĩa cùng cấp với module tree của eat_at_restaurant. Ở đây đường dẫn tương đương với hệ thống tệp tin sẽ là sử dụng đường dẫn front_of_house/hosting/add_to_waitlist. Bắt đầu với tên của module có nghĩa rằng đường dẫn là tương đối.

Việc lựa chọn sử dụng đường dẫn tương đối hay tuyệt đối là một quyết định bạn sẽ đưa ra dựa trên dự án, và phụ thuộc vào việc bạn có thể di chuyển định nghĩa code của item riêng biệt hay cùng với code sử dụng item. Ví dụ, nếu chúng ta di chuyển module front_of_house và hàm eat_at_restaurant vào một module có tên customer_experience, chúng ta sẽ cần cập nhật đường dẫn tuyệt đối đến add_to_waitlist, nhưng đường dẫn tương đối vẫn là hợp lệ. Tuy nhiên, nếu chúng ta di chuyển hàm eat_at_restaurant riêng biệt vào một module có tên dining, đường dẫn tuyệt đối đến add_to_waitlist vẫn giữ nguyên, nhưng đường dẫn tương đối sẽ cần được cập nhật. Nhìn chung, chúng tôi thích dùng đường dẫn tuyệt đối vì nó có thể giúp chúng tôi di chuyển code định nghĩa và code gọi item độc lập với nhau.

Hãy thử biên dịch Listing 7-3 và tìm hiểu tại sao nó vẫn chưa biên dịch được! Lỗi mà chúng ta nhận được được hiển thị trong Listing 7-4.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
 --> src/lib.rs:9:28
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                            ^^^^^^^ private module
  |
note: the module `hosting` is defined here
 --> src/lib.rs:2:5
  |
2 |     mod hosting {
  |     ^^^^^^^^^^^

error[E0603]: module `hosting` is private
  --> src/lib.rs:12:21
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                     ^^^^^^^ private module
   |
note: the module `hosting` is defined here
  --> src/lib.rs:2:5
   |
2  |     mod hosting {
   |     ^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Listing 7-4: Lỗi biên dịch từ việc biên dịch code trong Listing 7-3

Các thông báo lỗi nói rằng module hosting là riêng tư. Nói cách khác, chúng ta có đúng đường dẫn cho module hosting và hàm add_to_waitlist, nhưng Rust không cho phép chúng ta sử dụng chúng vì nó không có quyền truy cập vào các phần riêng tư. Trong Rust, tất cả các item (hàm, phương thức, struct, enum, module, và hằng số) là private đối với module cha theo mặc định. Nếu bạn muốn tạo một item như một hàm hoặc struct private, bạn cần đặt nó trong một module.

Các item trong module cha không thể sử dụng các item private bên trong module con, nhưng các item trong module con có thể sử dụng các item trong các module cha của nó. Điều này là vì module đã gói các chi tiết code của nó và ẩn chúng đi nhưng module con có thể thấy được ngữ cảnh mà nó được định nghĩa. Để tiếp tục với ví dụ của chúng ta, hãy nghĩ về quy tắc riêng tư như là một phòng họp của một nhà hàng: những gì xảy ra bên trong phòng họp là riêng tư đối với khách hàng (tức là họ, khách hàng, không thể truy cập vào phòng họp), nhưng quản lý nhà hàng có thể thấy và làm mọi thứ trong nhà hàng mà họ quản lý.

Rust đã chọn để hệ thống module hoạt động theo cách này để ẩn các chi tiết code bên trong một cách mặc định. Theo cách này, bạn biết được những phần của code bên trong mà bạn có thể thay đổi mà không làm hỏng code bên ngoài. Tuy nhiên, Rust cũng cho phép bạn lựa chọn để tiết lộ các phần bên trong của module con đến các module cha bên ngoài bằng cách sử dụng từ khóa pub để tạo một item public.

Exposing Paths with the pub Keyword

Cùng trở lại lỗi trong Listing 7-4 mà nói cho chúng ta rằng module hosting là private. Chúng ta muốn hàm eat_at_restaurant trong module cha có thể truy cập vào hàm add_to_waitlist trong module con, vì vậy chúng ta đánh dấu module hosting với từ khóa pub, như trong Listing 7-5.

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Listing 7-5: Đánh dấu module hostingpub để sử dụng nó từ eat_at_restaurant

Rất tiếc, code trong Listing 7-5 vẫn dẫn đến lỗi, như trong Listing 7-6.

$ cargo build
   Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
 --> src/lib.rs:9:37
  |
9 |     crate::front_of_house::hosting::add_to_waitlist();
  |                                     ^^^^^^^^^^^^^^^ private function
  |
note: the function `add_to_waitlist` is defined here
 --> src/lib.rs:3:9
  |
3 |         fn add_to_waitlist() {}
  |         ^^^^^^^^^^^^^^^^^^^^

error[E0603]: function `add_to_waitlist` is private
  --> src/lib.rs:12:30
   |
12 |     front_of_house::hosting::add_to_waitlist();
   |                              ^^^^^^^^^^^^^^^ private function
   |
note: the function `add_to_waitlist` is defined here
  --> src/lib.rs:3:9
   |
3  |         fn add_to_waitlist() {}
   |         ^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` due to 2 previous errors

Listing 7-6: Lỗi của compiler khi build code trong Listing 7-5

Điều gì đã xảy ra? Việc thêm từ khóa pub trước mod hosting làm cho module public. Với thay đổi này, nếu chúng ta có thể truy cập front_of_house, chúng ta có thể truy cập hosting. Nhưng nội dung của hosting vẫn là private; việc làm module public không làm cho nội dung của nó public. Từ khóa pub trên module chỉ cho phép code ở module cha tham chiếu đến nó, không cho phép truy cập code bên trong. Vì module là container, việc làm cho module public; không thực sự giúp chúng ta việc gì, chúng ta cần đi xa hơn và chọn để làm public một hoặc nhiều item bên trong module.

Lỗi trong Listing 7-6 nói rằng hàm add_to_waitlist là private. Quy tắc privacy áp dụng cho struct, enum, function, và method cũng như module.

Hãy cũng làm cho hàm add_to_waitlist public bằng cách thêm từ khóa pub trước định nghĩa của nó, như trong Listing 7-7.

Filename: src/lib.rs

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // Absolute path
    crate::front_of_house::hosting::add_to_waitlist();

    // Relative path
    front_of_house::hosting::add_to_waitlist();
}

Listing 7-7: Thêm từ khóa pub vào mod hostingfn add_to_waitlist để chúng ta gọi hàm từ eat_at_restaurant

Bây giờ code sẽ compile! Để biết tại sao thêm từ khóa pub cho phép chúng ta sử dụng những đường dẫn này trong add_to_waitlist với quy tắc privacy, hãy xem đường dẫn tuyệt đối và đường dẫn tương đối.

Trong đường dẫn tuyệt đối, chúng ta bắt đầu với crate, gốc của cây module của crate. Module front_of_house được định nghĩa trong crate root. Khi front_of_house không phải là public, vì hàm eat_at_restaurant được định nghĩa trong cùng một module với front_of_house (tức là eat_at_restaurantfront_of_house là anh em), chúng ta có thể tham chiếu đến front_of_house từ eat_at_restaurant. Tiếp theo là module hosting được đánh dấu với pub. Chúng ta có thể truy cập module cha của hosting, vì vậy chúng ta có thể truy cập hosting. Cuối cùng, hàm add_to_waitlist được đánh dấu với pub và chúng ta có thể truy cập module cha của nó, vì vậy cuộc gọi hàm này hoạt động!

Trong đường dẫn tương đối, logic là giống như đường dẫn tuyệt đối ngoại trừ bước đầu tiên: thay vì bắt đầu từ crate root, đường dẫn bắt đầu từ front_of_house. Module front_of_house được định nghĩa trong cùng một module với eat_at_restaurant, vì vậy đường dẫn tương đối bắt đầu từ module mà eat_at_restaurant được định nghĩa hoạt động. Sau đó, vì hostingadd_to_waitlist được đánh dấu với pub, phần còn lại của đường dẫn hoạt động, và cuộc gọi hàm này là hợp lệ!

Nếu bạn dự định chia sẻ library crate để các dự án khác có thể sử dụng mã của bạn, API công khai của bạn là interface mà người dùng của crate của bạn tương tác với mã của bạn. Có nhiều điều cần xem xét về việc quản lý các thay đổi trong API công khai của bạn để làm cho việc phụ thuộc vào crate của bạn dễ dàng hơn. Những điều này nằm ngoài phạm vi của cuốn sách này; nếu bạn quan tâm đến chủ đề này, hãy xem The Rust API Guidelines.

Best Practices for Packages with a Binary and a Library

Chúng ta đã nói rằng một package có thể chứa cả src/main.rs binary crate root cũng như src/lib.rs library crate root, và cả hai crate sẽ có tên package mặc định. Thông thường, các package dạng này sẽ chứa đủ code trong binary crate để khởi động một chương trình thực thi, code này sẽ gọi đến code tron library crate. Điều này cho phép các dự án khác có thể tận dụng tối đa các chức năng mà package cung cấp, vì code trong library crate có thể được chia sẻ.

Cây module nên được định nghĩa trong src/lib.rs. Sau đó, bất kỳ item công khai nào cũng có thể được sử dụng trong binary crate bằng cách bắt đầu đường dẫn với tên của package. Binary crate trở thành một người dùng của library crate giống như một crate hoàn toàn bên ngoài sẽ sử dụng library crate: nó chỉ có thể sử dụng API công khai. Điều này giúp bạn thiết kế một API tốt; không chỉ là bạn là người viết, bạn cũng là người dùng!

Trong Chapter 12, chúng ta sẽ minh họa cách tổ chức này với một chương trình CLI sẽ chứa cả binary crate và library crate.


Starting Relative Paths with super

Chúng ta có thể khai báo đường dẫn tương đối bắt đầu từ module cha, thay vì module hiện tại hoặc module gốc, bằng cách sử dụng super ở đầu đường dẫn. Điều này giống như bắt đầu một đường dẫn hệ thống tập tin với cú pháp ... Điều này cho phép chúng ta tham chiếu đến một item mà chúng ta biết nằm trong module cha, điều này sẽ giúp chúng ta dễ dàng sắp xếp lại cây module khi module đó liên quan gần với module cha, nhưng module cha có thể được di chuyển đến một nơi khác trong cây module một lúc nào đó trong tương lai.

Xem code trong Listing 7-8 mô hình hóa tình huống trong đó một nhà bếp sửa lỗi đơn hàng và mang đến cho khách hàng cá nhân. Hàm fix_incorrect_order được định nghĩa trong module back_of_house gọi hàm deliver_order được định nghĩa trong module cha bằng cách chỉ định đường dẫn đến deliver_order bắt đầu bằng super:

Filename: src/lib.rs

fn deliver_order() {}

mod back_of_house {
    fn fix_incorrect_order() {
        cook_order();
        super::deliver_order();
    }

    fn cook_order() {}
}

Listing 7-8: Gọi một hàm sử dụng đường dẫn tương đối bắt đầu bằng super

Hàm fix_incorrect_order nằm trong module back_of_house, vì vậy chúng ta có thể sử dụng super để đi đến module cha của back_of_house, trong trường hợp này là crate, gốc. Từ đó, chúng ta tìm kiếm deliver_order và tìm thấy nó. Chúng ta cho rằng module back_of_house và hàm deliver_order có thể ở trong mối quan hệ tương tự và được di chuyển cùng nhau dù chúng ta quyết định tổ chức lại cây module của crate. Do đó, chúng ta đã sử dụng super để chúng ta sẽ có ít chỗ cần cập nhật code trong tương lai nếu code này được di chuyển đến một module khác.


Making Structs and Enums Public

Chúng ta cũng có thể sử dụng pub để đánh dấu struct và enum là public, nhưng có một số chi tiết khác về cách sử dụng pub với struct và enum. Nếu chúng ta sử dụng pub trước một định nghĩa struct, chúng ta sẽ làm public struct, nhưng các trường của struct vẫn sẽ là private. Chúng ta có thể làm public hoặc không cho mỗi trường một cách riêng biệt. Trong Listing 7-9, chúng ta đã định nghĩa một struct back_of_house::Breakfast public với một trường toast public nhưng một trường seasonal_fruit private. Điều này mô hình trường hợp trong một nhà hàng nơi khách hàng có thể chọn loại bánh mì mà họ muốn kèm theo một bữa ăn, nhưng bếp trưởng quyết định loại quả tươi sẽ đi kèm theo bữa ăn dựa trên mùa và hàng tồn kho. Các loại quả tươi thay đổi nhanh chóng, vì vậy khách hàng không thể chọn loại quả tươi hoặc thậm chí xem được loại quả tươi mà họ sẽ nhận được.

Filename: src/lib.rs

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,
        seasonal_fruit: String,
    }

    impl Breakfast {
        pub fn summer(toast: &str) -> Breakfast {
            Breakfast {
                toast: String::from(toast),
                seasonal_fruit: String::from("peaches"),
            }
        }
    }
}

pub fn eat_at_restaurant() {
    // Order a breakfast in the summer with Rye toast
    let mut meal = back_of_house::Breakfast::summer("Rye");
    // Change our mind about what bread we'd like
    meal.toast = String::from("Wheat");
    println!("I'd like {} toast please", meal.toast);

    // The next line won't compile if we uncomment it; we're not allowed
    // to see or modify the seasonal fruit that comes with the meal
    // meal.seasonal_fruit = String::from("blueberries");
}

Listing 7-9: Một struct với một số trường public và một số trường private

Bởi vì trường toast trong struct back_of_house::Breakfast là public, trong eat_at_restaurant chúng ta có thể viết và đọc trường toast sử dụng dấu chấm . Lưu ý rằng chúng ta không thể sử dụng trường seasonal_fruit trong eat_at_restaurantseasonal_fruit là private. Hãy thử bỏ comment dòng sửa đổi giá trị trường seasonal_fruit để xem có lỗi gì xảy ra!

Ngoài ra, lưu ý rằng vì back_of_house::Breakfast có một trường private, struct cần phải cung cấp một hàm liên quan public để tạo một instance của Breakfast (chúng ta đã đặt tên là summer). Nếu Breakfast không có hàm như vậy, chúng ta không thể tạo một instance của Breakfast trong eat_at_restaurant vì chúng ta không thể thiết lập giá trị của trường private seasonal_fruit trong eat_at_restaurant.

Trái ngược với struct, nếu chúng ta đặt một enum là public, tất cả các variant của nó sẽ là public. Chúng ta chỉ cần đặt pub trước từ khóa enum, như trong Listing 7-10.

Filename: src/lib.rs

mod back_of_house {
    pub enum Appetizer {
        Soup,
        Salad,
    }
}

pub fn eat_at_restaurant() {
    let order1 = back_of_house::Appetizer::Soup;
    let order2 = back_of_house::Appetizer::Salad;
}

Listing 7-10: Đánh dấu một enum là public sẽ làm cho tất cả các variant của nó là public

Bởi vì chúng ta đã đánh dấu enum Appetizer là public, chúng ta có thể sử dụng variant SoupSalad trong eat_at_restaurant.

Enum không có ích gì nếu các variant của nó không phải là public; sẽ rất phiền phức phải đánh dấu tất cả các variant của enum với pub trong mọi trường hợp, vì vậy mặc định cho variant của enum là public. Struct thường có ích mà không cần các trường của nó là public, vì vậy các trường của struct theo quy tắc chung của mọi thứ là private trừ khi được đánh dấu với pub.

Có một trường hợp nữa liên quan đến pub mà chúng ta chưa bàn đến, đó là tính năng cuối cùng của module system: từ khóa use. Chúng ta sẽ bàn use một mình trước, và sau đó chúng ta sẽ hiển thị cách kết hợp pubuse.