một số khái niệm cơ bản trong rust

27 0 0
Tài liệu đã được kiểm tra trùng lặp
một số khái niệm cơ bản trong rust

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

-Tuy nhiên thì trong một số trường hợp ta cần liên tục thay đổi giá trị cho 1 biến và trong rust ta có thể để biến thay đổi được bằng cách sử dụng từ khoá mut khi khởi tạo biến.-Chương t

Trang 1

Mục lục Phần 1: Giới thiệu

1 Chương trình đầu tiên2 Cargo

3 Packages, crates

Phần 2 : Một số khái niệm cơ bản trong Rust

Phần 3: OwnershipPhần 4: StructPhần 5: Enum

Phần 6: Pattern matchingPhần 7: Error handling

Phần 8: Trait, generic data typePhần 9: Iterators, closure

Phần 1: Những khái niệm cơ bản

Trang 2

1.1 Hello world:

- Để tạo 1 chương trình trong Rust, đầu tiên cần tạo ra 1 project để lưu trữ code:

- Tiếp theo, vào file main.rs và viết câu lệnh đơn giản như sau:

- Khi chạy ta sẽ được kết quả:

1.2 Cargo:

- Cargo là một công cụ của rust giúp xây dựng và quản lý chương trình Cargo giúp xây dựng code, tải các thư viện dùng trong đoạn code.

- Tạo 1 dự án sử dụng Cargo bằng cách chạy lệnh:

- Cargo sẽ tự tạo cho ta 1 file Cargo.toml và 1 thư mục src, trong thư mục này thì cargo cũng tạo sẵn 1 file main.rs.

- File Cargo.toml sẽ cho ta biết format chương trình vừa được tạo:

Khi chương trình được tạo bằng cargo thì các file rs sẽ được đặt vào thư mục src.

Ta cũng có thể sử dụng cargo để xây dựng chương trình :

Và chạy :

Trang 3

Khi chương trình gần được hoàn thành thì có thể thêm lựa chọn –release vào câu lệnh cargo build để cargo tự động tối ưu chương trình giúp chương trình chạy nhanh hơn.

1.3 Modules, crates và package:

- Crates là phần code nhỏ nhất mà rust compiler có thể xử lý được trong 1 thời điểm, gồm binary crates và library crates.

- Package là tập hợp của nhiều crates cùng thực hiện 1 mục đích nào đó

- Crate là các thực thể đã được biên dịch và phân phối trên máy tính để có thể chạy được Mặc định Rust sẽ quy định các đoạn code một crate được “chứa” trong một module có tên là root một cách ngầm định Sau đó các code này có thể được các coder phân chia thành các module nhỏ hơn, gọi là submodule nằm bên dưới module root.

Module cũng có thể được định nghĩa bên trong một module khác, lúc này module đó được gọi là nested Khi biên dịch thì tất cả các module nằm trong một crate sẽ được biên dịch cùng với nhau.

Phần 2: Những khái niệm cơ bản trong Rust

Trang 4

-Tuy nhiên thì trong một số trường hợp ta cần liên tục thay đổi giá trị cho 1 biến và trong rust ta có thể để biến thay đổi được bằng cách sử dụng từ khoá mut khi khởi tạo biến.

-Chương trình không còn báo lỗi:

Trang 5

- Đầu tiên biến x gán cho 5, sau đó nó tạo một biến x mới rồi cộngthêm giá trị của biến cũ ban đầu thêm 1 Trong phạm vi của cặp dấu ngoặc thứ 2 một biến x mới được tạo ra và nhân giá trị trước đó là 6 với 2

Ví dụ:fnmain() { let x: u32= ; 2}

x là biến không âm có giá trị bằng 2 với kích thước 32 bit- Rust phân biệt số nguyên âm và số không âm Nếu biến được gán kiểu i là âm và u là không âm.

Trong rust thì cho phép lựa chọn độ dài của biến như trong bảng Đặc biệt với đồ dài là isize với số có dấu và usize với kiểu không dấu sẽ dựa vào kiến trúc của máy tính bạn sử dụng Ví dụ: biến sẽcó độ dài là 64bit nếu bạn sử dụng máy tính với CPU là 64 bit.

* Kiểu thực: có hai kiểu là f32 và f64.

Trang 6

- Ví dụ:

* Kiểu Boolean:

*Kiểu kí tự:

* Kiểu dữ liệu tập hợp

Trong rust có hai kiểu dữ liệu tập hợp là mảng và bộ

- Bộ: bộ là tập hợp các giá trị khác nhau, trong rust thì độ dài của bộ có thể thay đổi.

Ta có thể gán bộ bằng dấu “:” hoặc “=”.Ví dụ:

Trang 7

Truy cập phần tử mảng:

Phần 3: Ownership

Ownership là một khái niệm hoàn toàn mới được Rust giới thiệu, có chức năng đảm bảo tính an toàn, tối ưu cho bộ nhớ mà không cần đến trình dọn rác (garbage collector) như các ngôn ngữ khác.Các ngôn ngữ lập trình khác nhau thì có những cách tiếp cận khác nhau để quản lý bộ nhớ (memory) Một số ngôn ngữ như Java, Golang , thì sử dụng garbage collection sẽ tự động giải phóng các ô nhớ không còn được sử dụng, các ngôn ngữ khác thì yêu cầu người dùng phải tự giải phóng bộ nhớ bằng tay (điển hình là C/C++) Với Rust, đội ngũ phát triển đã đề xuất một cách tiếp cận mới cho vấn đề quản lý bộ nhớ, đó

là ownership Ownership được định nghĩa là một bộ các quy tắc chỉ định cách quản lý bộ nhớ trong Rust, nếu bất kỳ quy tắc nào bị vi phạm, chương trình sẽ không được biên dịch.

Các quy tắc ownership

Mỗi giá trị trong Rust có một biến được gọi là chủ sở hữu (ownership) của nó.

Chỉ có thể có một chủ sở hữu tại một thời điểm.

Khi chủ sở hữu đi ra khỏi phạm vi, bộ nhớ lưu giá trị đó sẽ được giải phóng.

1 Mỗi giá trị trong Rust có một biến được coi là chủ sở hữu:

2 Chỉ có thể có một chủ sở hữu tại một thời điểm:

Trang 8

- Giả sử một biến phải truyền giá trị cho một hàm, ownership sẽ truyền cho đối số của hàm

=> Nên sau khi gọi hàm xong, biến sẽ không có lại ownership vì giá trị được lưu đã được giải phóng.

- Tương tự như trường hợp sau:

- Nếu chạy ta sẽ gặp lỗi như sau :

Trong những trường hợp khi biến không có trait Copy, để tránh việc 2 biến cùng truy cập vào 1 vùng dữ liệu và gây ra lỗi (double free memory, …) khi truyền giá trị vào hàm hay tạo ra 1 biến mới có giá trị như biến cũ thì Rust sẽ vô hiệu hóa biến lúc đầu và coi như là biến đó không còn sở hữu giá trị nào.

- Ownership có thể được trả lại từ 1 hàm khi trả giá trị về Đây chính là khái niệm move trong Rust.

Trang 9

*Chú ý: 1 số kiểu dữ liệu nguyên thủy không có phép move là vì chúng được lưu trữ trên Stack nên khi cần tạo ra biến mới có giá trị như biến cũ thì chúng tạo ra cả biến mới và vùng lưu trữ có giá trị bằng giá trị biến cũ.

3 Khi chủ sở hữu ra khỏi phạm vi, thì vùng nhớ lưu giá trị đó sẽ được giải phóng:

Trang 10

- Tạo ra 1 referece đến 1 biến được gọi là borrowing Ta chỉ mượn giá trị của biến đó chứ ko sở hữu chúng Nên ta sẽ phải trả lại vào 1 lúc nào đó

- Để có quyền thay đổi giá trị biến với Reference, ta cần sử dụng toán tử &mut:

Các quy tắc của Reference:

- Chỉ có 1 mutable reference hoặc bao nhiêu immutable reference cũng được tại bất kỳ thời điểm nào.

- Reference phải luôn hợp lệ (tức luôn có giá trị để nó trỏ vào).

Phần 4: Struct

Trang 11

1 Khởi tạo struct:

Để định nghĩa một struct, chúng ta nhập từ khóa struct và đặt tên cho toàn bộ cấu trúc Sau đó, bên trong dấu ngoặc nhọn, chúng ta xác định tên và kiểu của các phần dữ liệu mà chúng ta gọi là trường.

Để sử dụng một cấu trúc sau khi đã xác định nó, chúng ta tạo một đối tượng của cấu trúc đó bằng cách chỉ định các giá trị cụ thể cho từng trường Chúng ta tạo một đối tượng bằng cách nêu tên của cấu trúc và sau đó thêm dấu ngoặc nhọn chứa các cặp khóa: giá trị, trong đó các khóa là tên của các trường và giá trị là dữ liệu mà chúng ta muốn lưu trữ trong các trường đó Chúng ta không phải chỉ định các trường theo cùng thứ tự mà đã khai báo chúng trong cấu trúc Nói cách khác, định nghĩa struct giống như một mẫu chung cho kiểu và các đối tượng điền vào mẫu đó với dữ liệu cụ thể để tạo các giá trị của kiểu.

Để nhận một giá trị cụ thể từ một cấu trúc, chúng ta có thể sử dụngký hiệu dấu chấm Nếu chúng tôi chỉ muốn địa chỉ email của người dùng này, chúng tôi có thể sử dụng user1.email ở bất cứ nơi nào chúng tôi muốn sử dụng giá trị này Nếu trường hợp có thể thay đổi, chúng ta có thể thay đổi giá trị bằng cách sử dụng ký hiệu dấu chấm và gán vào một trường cụ thể

Trang 12

Lưu ý rằng toàn bộ đối tượng phải có thể thay đổi được; Rust không cho phép chúng tôi đánh dấu chỉ một số trường nhất định là có thể thay đổi

Trang 13

=> Về cơ bản, struct tương tự như class trong java.Phần 5: Enum

- enum là từ khóa cho phép tạo kiểu dữ liệu mới có giá trị nằm trong các phần tử của enum.

VD:

* 1 kiểu enum hay được sử dụng trong Rust : Option- Nó được sử dụng để diễn tả các giá trị null trong Rust:

Trang 17

- Khi dùng từ khóa impl:

2 Trait:

- Trait định nghĩa các hàm mà 1 kiểu dữ liệu có thể có, khá tương tự với interface(abstract class) trong java mặc dù có vài khác biệt nhỏ.

Trang 18

Phần 9: Iterators, closure1 Iterators:

Iterator là một lớp đại diện cho các phần tử trong các kiểu dữ liệu danh sách như array, vector, slice… từ phần tử đầu tiên tới phần tử cuối cùng Trong Rust, để lấy các đối tượng này thì chúng ta dùng phương thức next() trong các đối tượng danh sách.

Trang 19

Ví dụ:fn main() {

let langs = ["Rust", "C++", "Java", "Python"];let mut langs_iter = langs.iter();

println!("{:?}", langs_iter.next());println!("{:?}", langs_iter.next());println!("{:?}", langs_iter.next());println!("{:?}", langs_iter.next());println!("{:?"} langs_iter.next());}

Đầu tiên để lấy ra đối tượng Iterator thì chúng ta dùng hàm iter() có sẵn trong mảng Sau đó để lấy phần tử tiếp theo trong Iterator thì chúng ta gọi hàm next(), hàm next() sẽ trả về đối tượng Some hoặc None chứ không phải là các giá trị thông thường.

Đối tượng Some sẽ được trả về khi Iterator vẫn lấy được giá trị, khi đã lấy ra phần tử cuối cùng trong danh sách thì hàm next() sẽ trả về đối tượng None.

Iterator thường được dùng trong vòng lặp như sau:fn main() {

let langs = ["Rust", "C++", "Java", "Python"];for lang in langs.iter() {

println!("{}", lang);}

Việc lặp thông qua Iterator giúp chúng ta tránh được một số lỗi như lỗi Index out of bound – tức lỗi chỉ số nằm ngoài phạm vi mảng.

Output

Trang 20

Ngoài việc dùng hàm iter() thì Rust có một cú pháp rút gọn khác làdùng kí tự & như sau:

fn main() {

let langs = ["Rust", "C++", "Java", "Python"];for lang in &langs {

println!("{}", lang);}

PythonJavaC++Rust2 Closure:

- Closures là các hàm ẩn danh có thể truy cập vào một biến local ngay cả khi hàm đó đã thực hiện xong.

- Closures có 2 đặc điểm chính: luôn là hàm ẩn danh (hoặc hàm được viết trong dòng); và chức năng chính là cho chúng ta quyền truy cập vào local scope.

- Cú pháp:

VD về 1 closure:

Trang 21

Và kết quả khi chạy:

-> Ta thấy rằng closure giữ lại giá trị của biến my_val sau mỗi lần chạy hàm Ngoài ra, với biến lưu trữ closure increment_closure ta phải có từ khóa mut để cho Rust biết hàm closure sẽ thay đổi giá trị biến.

- Nói chung, closures tạo ra một tham chiếu đến các thực thể trongphạm vi của nó, nhưng có một loại closure khác được gọi là move closure chiếm ownership của tất cả các biến mà nó sử dụng.Ta sử dụng từ khóa move để xác định move closure Đây là cú pháp:

Phần 10: Macro

Trong các bài trước chúng ta đã làm việc rất nhiều với macro Mỗi lần chúng ta gọi một hàm mà có dấu chấm than ! ở sau tên thì đó chính là một macro có sẵn trong Rust Những macro mà chúng ta đã sử dụng

là println!, panic!, vec!, assert!… Kể từ bài này mình sẽ gọichúng ta macro chứ không gọi là hàm như những bài trước nữa.

Trang 22

Tại sao cần đến macro?

Macro cho phép chúng ta tạo nên các cú pháp gọn nhẹ nhưng mạnh mẽ, nhờ đó mà việc lập trình trở nên dễ dàng hơn rất nhiều Chẳng hạn như trong Rust có macro regex! cho phép chúng ta định nghĩa các biểu thức chính quy, các biểu thức chính quy này sẽ được dịch và sử dụng.

Macro giúp giảm đi việc lặp lại code nhiều lần, chúng ta cóthể định nghĩa một macro thực hiện một công việc nào đó và gọi mỗi khi cần.

Giống như với các ngôn ngữ khác, chúng ta cũng có thể định nghĩa macro của riêng chúng ta trong Rust.

Cú pháp

Chúng ta định nghĩa một macro theo như mẫu dưới đây:macro_rules! mac1 {

(pattern) => (expansion);(pattern) => (expansion);…

Đầu tiên chúng ta ghi từ khóa macro_rules!, sau đó là tên macro, trong đoạn code trên thì tên macro là mac1, rồi đến cặp dấu {}, và danh sách các quy luật, mỗi luật kết thúc bằng dấu chấm phẩy ;

Quy luật mà chúng ta nói tới ở đây có thể hiểu như trong câu lệnh match mà chúng ta đã học (hay câu

lệnh switch của C++, Java…), tức là chúng ta có thể truyền vào các tham số, và Rust sẽ so sánh xem tham số nào phù hợp và chạy đoạn code phía sau đó Ví dụ:fn main() {

macro_rules! hello {() => {

Trang 23

println!("Hello World");}

Trong đoạn code trên có một macro tên là hello, bên trongchúng ta định nghĩa một luật, luật này không nhận vào tham số nào Khi chúng ta gọi macro này thì chúng ta ghi là hello!() và luật đầu tiên khớp sẽ chạy, ở đây chuỗi HelloWorld sẽ được in ra.

Output1Hello World

Khi muốn đưa tham số vào thì chúng ta đưa với cấu trúc sau:

Trong đó $arg chỉ là tên biến bình thường như khi định nghĩa hàm, đối với tham số cho macro thì phải có dấu $ Còn frag có thể là một trong những giá trị sau:

expr: biểu thức cho ra một giá trị (vd 2+2, ‘phocode’)item: một thành phần của một crate (chẳng

hạn struct Blog)

block: một khối lệnh (vd { println!(); return 12; } )stmt: một câu lệnh (vd let x = 3)

pat: mẫu hay patter (vd (17, 'a') )

ty: biến theo sau bởi dấu => hoặc : => asident: tên một biến (vd x; str)

path: tên một struct, interface… hợp lệ (vd Blog::PhoCode)

tt: token tree (bạn có thể tự tìm hiểu thêm)Ví dụ:

Trang 24

fn main() {

macro_rules! hello_name {($arg:expr) => {

println!("Hello {}", $arg);}

hello_name!("Pho Code");}

Trong đoạn code trên, macro hello_name nhận vào một luật là nhận một biến kiểu expr, tức là chúng ta có thể truyền vào một biểu thức hay một giá trị Bên trong chúng ta cho in giá trị của biểu thức đó ra.

$($arg:expr), *) => ({$

( println!("{}", $arg); );*}

arguments_list!("Pho Code", 2017, 3.14);}

Cú pháp này của Rust hơi lạ mắt và khó đọc, bạn có thể tự suy ra, ở đây mình sẽ không giải thích Bạn có thể viết liền trên một hàng chứ không nhất thiết phải xuống dòng như mình.

Trang 25

Pho Code20173.14Tạo hàm mới

Chúng ta có thể dùng macro để tạo một hàm mới như sau:

fn main() {

macro_rules! create_a_function {($arg:ident) => {

fn $arg() {

println!("Calling the {} function", stringify!($arg));}

Trong đoạn code trên, chúng ta tạo một macro có tên create_a_function, macro này nhận vào tên hàm, bên trong chúng ta định nghĩa một hàm mới với tên lấy từ $arg Khi gọi create_a_function!() là chúng ta đã tạo được một hàm mới và có thể gọi một cách bình thường.Ngoài ra ở đây chúng ta còn dùng đến macro stringify để chuyển $arg thành một chuỗi.

Trang 26

#[macro_use]extern crate log;

extern crate env_logger;fn main() {

extern crate log;

extern c<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>rate env_logger;fn main() {

Đóng gói:

Rust hỗ trợ tính đóng gói bằng cách tạo ra các đối tượng với thuộc tính bằng struct và các phương thức được khai triển sử dụng từ khóa “impl”.

Trang 27

Tham số đầu tiên của bất cứ phương thức nào phải là một biến tham chiếu đến đối tượng gọi hàm

(e.g instanceOfObj.foo()) Rust sử dụng:&self - biến tham chiếu không thay đổi được.&mut self - biến tham chiếu thay đổi được.

Trừu tượng hóa :

Rust có thể xóa đi những khai triển bên trong của đối tượng.

Mặc định trường struct và phương thức chỉ được truy cập vào module mà nó nằm trong.

Từ khóa pub cho phép chúng truy cập vào các module bênngoài.

Đa hình :

Rust hỗ trợ tính đa hình bằng cách sử dụng trait Traits hoạt động tương tự như interface trong Java, cho phép nhiều struct khai triển cùng 1 phương thức.

Khi 1 struct khai triển 1 trait, nó cho phép chúng ta tương tác gián tiếp với struct thông qua kiểu của trait mà không cần biết kiểu thực sự là gì.

Thừa kế :

Rust không hỗ trợ thừa kế các biến hay hàm của 1 structvà vì vậy, không có thừa kế 1 cách chính thống như Java Tuy nhiên, 1 số hành động cần thừa kế trong các ngôn ngữ khác có thể được thay thế bằng trait object.

Ngày đăng: 14/05/2024, 16:06

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan