Những điều tôi đã học được khi viết lib với Rust
Tuần rồi có ngồi viết một wrapper lại api của Facebook Accountkit bằng Rust , đây là lần đầu viết lib nên mình chọn cái đơn giản này chỉ có 4 endpoint thôi, 3 GET và 1 DELETE thông qua url, không có nhiều require, nên khá đơn giản cho ai tập tành viết lib như mình. Cứ tưởng ...
Tuần rồi có ngồi viết một wrapper lại api của Facebook Accountkit bằng Rust, đây là lần đầu viết lib nên mình chọn cái đơn giản này chỉ có 4 endpoint thôi, 3 GET và 1 DELETE thông qua url, không có nhiều require, nên khá đơn giản cho ai tập tành viết lib như mình. Cứ tưởng là đơn giản, nhưng mà khí bắt tay vào làm thì có nhiều vấn đề xảy ra, không đơn giản như những ngôn ngữ khác javascript, php, ruby, …
Mọi công việc đều đơn giản cho tới khi bạn bắt đầu làm nó. Ngay cả khi viết bài
Chọn &str hoặc là String
Nếu những ngôn ngữ khác bạn có thể tuỳ ý truyền kiểu String vào, tuy nhiên ở Rust sẽ phân biệt &str là một string slices (fixed len) và String a UTF-8 encoded, growable string, bạn có thể append một str hoặc một char vào một String bất kỳ, còn với str do là fixed len nên bạn không thể append ra được, hiểu rộng ra hơn nếu khai bao kiểu String khi bạn append vào String thì sẽ có quá trình realloccation String đó, nghe có vẻ là không tốt mấy.
String cung cấp cho ta một function là with_capacity
1 2 3 4 |
let will_append = String::with_capacity(20); will_append.push_str("hello"); |
với hàm with_capacity thì sẽ không có quá trìnhreallocation cho đến khi capacity vượt qua giá trị chúng ta khai báo ở đây là 20.
Vậy chọn kiểu gì dể dùng bây giờ &str hay là String, điều đó tuỳ thuộc vào mục đích của bạn, nếu bạn không muốn ownership của params truyền vào thì nên dùng kiểu là &str, nếu muốn là owner của params thì nên dùng kiểu String tuy nhiên sẽ tốt hơn nếu chúng ta biết được len của String chúng ta dùng là gì để dùng với with_capacity sẽ tốt hơn rất nhiều.
Cho nên không muốn kiểu gì thì dùng đại kiểu đó nhé, tất cả điều có lý do.
Functions có thể nhận nhiều kiểu dữ liệu
Điều này giúp cho function của chúng ta trở nên generic hơn, cũng một phần làm hạn chế lỗi xảy ra nếu truyền sai type. Nói hơi khó hiểu để lấy ví dụ minh hoạ.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
pub struct AccountKit<'a> { auth: &'a str, appsecret_proof: Option<&'a str>, } impl<'a> AccountKit<'a> { pub fn with_token<S: Into<Option<&'a str>>>(access_token: &'a str, appsecret_proof: S) -> AccountKit<'a> { AccountKit { auth: access_token, appsecret_proof: appsecret_proof.into(), } } } |
mình khai báo property appsecret_proof là kiểu Option<str>, xét trường hợp impl của AccountKit thay vì chọn kiểu generic S, ở đây mình sửa lại chút
1 2 3 4 5 6 7 8 9 10 11 12 |
impl<'a> AccountKit<'a> { pub fn with_token(access_token: &'a str, appsecret_proof: Option<&'a str>) -> AccountKit<'a> { AccountKit { auth: access_token, appsecret_proof: appsecret_proof, } } } |
Nếu như trường hợp ở trên chúng ta chỉ có thể truyền vào kiểu Option, nếu truyền vào kiểu &str thì fail ngay, function tới đây thì có thể dùng ổn được rồi. Tuy nhiên mở rộng ra chúng ta muốn truyền kiểu &str hoặc kiểu Option thì hàm không dùng generic không đáp ứng yêu cầu của chúng tay.
Quay trở lại với hàm generic ở trên, tại sao lại làm được như vậy thì đơn giản là từ bản 1.12 rust có một feature nhỏ là
Option implements From for its contained type
Chúng ta có thể convert từ kiểu &str sang kiểu Option<&str> nếu có một
implement từ &str cho Option<&str>, thế thì tra ở trong trait From xem có không? Rất may mắn có implement đó
1 2 3 |
impl<'a> From<&'a str> for Cow<'a, str> |
Dó đó từ bản 1.12 thì khi function của hcungs ta flexible hơn kiểu nhận vào. Ví dụ
Xử lý lỗi
Thông thường chúng ta có thể handling error với if và else, hoặc ngay cả dùng unwrap. Nếu dùng unwrap thì chỉ phù hợp với các code muốn quick test, còn production không nên dùng vì nó có panic làm crash luôn chương trình đang chạy, còn dùng if, else nếu với đoạn code dài thì làm cho chúng ta không kiểm soát được đủ hết.
Còn một cách khác là dùng macro try!, khi dung try! thì bắt buộc chúng ta phai khai báo kiểu Error trả về, vì bản thân try! sẽ return về lỗi và được catch ở phần khai bao lỗi của ta trước đó. Và try! được dùng với những function có kiểu trả về là Result.
Như implement dưới đây
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
pub trait DoRequest: BaseRequest { fn retriev_json(&self) -> Result<AccountKitResponse, AccountKitError> { let url = try!(Url::parse(self.url())); let fresh_net = try!(request::Request::new(self.method(), url)); let streaming_req = try!(fresh_net.start()); let mut response = try!(streaming_req.send()); let mut s = String::new(); let status = response.status; let status_code = status.to_u16(); let reason = status.canonical_reason(); try!(response.read_to_string(&mut s)); Ok(AccountKitResponse { body: s, status_code: status_code, canonical_reason: reason, }) } } |
Bỏ qua những thứ khác chú ý tới phần AccountKitError, đây là enum tập hợp tất cả error chúng ta tự định nghĩa.
1 2 3 4 5 6 7 |
pub enum AccountKitError { HttpError(error::Error), UrlError(ParseError), IoError(io::Error), } |
Thích khai báo gì thì khai báo ở đây, trường hợp này chúng tự định nghĩa lại error ở lib chúng ta nhưng dựa trên error đã được xử lý ở third party.
Khi dùng try! thì nó sẽ tự động return về đúng case lỗi của chúng ta như ví dụ try!(Url::parse(self.url())) nó sẽ trả về enum AccountKitError::UrlError(ParseError) để thấy được chi tiết lỗi là gì chúng ta cần implements 2 trait fmt::Display và convert::From
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
impl From<error::Error> for AccountKitError { fn from(err: error::Error) -> AccountKitError { AccountKitError::HttpError(err) } } impl From<ParseError> for AccountKitError { fn from(err: ParseError) -> AccountKitError { AccountKitError::UrlError(err) } } impl From<io::Error> for AccountKitError { fn from(err: io::Error) -> AccountKitError { AccountKitError::IoError(err) } } impl fmt::Display for AccountKitError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { AccountKitError::HttpError(ref err) => write!(f, "{}", err), AccountKitError::UrlError(ref err) => write!(f, "{}", err), AccountKitError::IoError(ref err) => write!(f, "{}", err), } } } |
Nếu có một trường hợp lỗi khác thì chúng ta chỉ cần khai báo kiểu lỗi của chúng ta là gì.
Ngoài ra với trường hợp trên chúng ta có thể code theo kiểu
1 2 3 4 5 6 |
let url = match Url::parse(self.url())) { Ok(url) => url, Err(err) => return Err(AccountKit::UrlError(e)) }; |
Tuy nhiên khi so sánh với đoạn code này với việc dùng try! thì ai cũng chọn try! vì nó ngắn gọn và flexible trong việc return Error
Tóm lại
Code Rust khá là thú vị, phải suy nghĩ nhiều và lựa chọn nhiều, nhưng khi đã code được bạn sẽ rất thích nó. Ngoài ra bạn có thể dùng thử lib này rust-accountkit và nếu bạn contribute nó thì thật là tuyệt vời.
Techtalk via kipalog