Mỗi kiểu dữ liệu chứa các dữ liệu khác nhau và các hành động tương ứng với dữ liệu đó cũng khác nhau. Go đã tạo sẵn cho chúng ta nhiều hàm để thực hiện các hành động này. Ví dụ các kiểu dữ liệu số luôn đi liền với các phép toán còn lấy chiều dài, sức chứa, thêm phần tử, sao chép phần tử đi kèm với slice, v.v... Khi các biến của từng kiểu dữ liệu được tạo, chúng trở thành những thực thể, có đời sống riêng. Lúc này nhu cầu tạo ra thêm nhiều hành động khác cho các thực thể này là có tùy theo mục đích chúng ta muốn. Bên cạnh việc tạo ra các hàm để đáp ứng nhu cầu này với thực thể là tham số truyền vào thì Go hỗ trợ một cách thức tạo hành động mới. Đó là các phương thức (methods).
Phương thức là hàm được khai báo cho riêng một kiểu dữ liệu để đáp ứng nhu cầu tạo hành động mới cho kiểu dữ liệu đó. Cách khai báo phương thức tương tự như khai báo hàm, có thêm tham số là biến kiểu dữ liệu cần tạo hành động. Tham số này thường gọi là vật nhận (receiver):
Phương thức là hàm được khai báo cho riêng một kiểu dữ liệu để đáp ứng nhu cầu tạo hành động mới cho kiểu dữ liệu đó. Cách khai báo phương thức tương tự như khai báo hàm, có thêm tham số là biến kiểu dữ liệu cần tạo hành động. Tham số này thường gọi là vật nhận (receiver):
func (<vật nhận>) <tên phương thức>(<danh sách tham số>) (<danh sách trả về>) {Chúng ta cùng xem ví dụ như sau:
<Khối lệnh>
}
1 package main- Dòng 5 là khai báo kiểu dữ liệu mới Int, thực nhất là int. Chúng ta không thể khai báo phương thức trực tiếp trên int vì Go quy định chỉ những kiểu dữ liệu nào được khai báo trong package mới có thể tạo phương thức. Nếu bạn tạo phương thức trên các kiểu dữ liệu do Go tạo sẵn như int, string hay kiểu dữ liệu do package khác tạo, Go sẽ báo lỗi: "cannot define new methods on non-local type X" với X là kiểu dữ liệu khai phương thức. Quy định này nhằm tránh sự phức tạp nếu chúng ta có thể tạo phương thức cho một kiểu dữ liệu từ một package khác. Tuy vậy về bản chất Int vẫn là int nên khẳng định Go hỗ trợ tạo phương thức trên mọi kiểu dữ liệu là không sai.
2
3 import "fmt"
4
5 type Int int
6
7 func (x Int) square() Int {
8 return x * x
9 }
10
11 func main() {
12 x := Int(5)
13 fmt.Printf("Bình phương của %d là %d", x, x.square()) // Bình phương của 5 là 2514 }
- Dòng 7-9 là khai báo phương thức tính bình phương của số nguyên Int. Chúng ta thấy vật nhận x kiểu Int được khai báo trước tên phương thức và được sử dụng trực tiếp trong phương thức như là tham số.
- Dòng 12 khai báo biến x kiểu Int. Ở đây chúng ta phải ép kiểu bởi mặc định gán x:=5 thì Go sẽ gán x kiểu int chứ không phải kiểu Int.
- Dòng 13 in chuỗi ra màn hình. Ở đay dùng hàm fmt.Printf do chúng ta có truyền thêm các tham số vào trong lòng chuỗi. %d sẽ được hàm Printf thay thế thành số nguyên tương ứng ở các tham số phía sau chuỗi. Ta thấy khi cần truy xuất phương thức square() cho biến x kiểu Int, ta đơn giản gọi x.square().
Tương tự như hàm, Go cũng không cho phép tạo phương thức cùng tên. Tuy nhiên chúng ta có quyền tạo phương thức cùng tên cho các kiểu dữ liệu khác nhau. Ví dụ, chúng ta hoàn toàn có quyền tạo phương thức square() cho kiểu dữ liệu Complex, định nghĩa lại từ complex128.
Vật nhận là con trỏ
Go cũng hỗ trợ vật nhận là con trỏ để tăng hiệu quả khi truyền tham số như với hàm. Ví dụ như ở trên, chúng ta có thể khai báo lại hàm tính bình phương của Int như sau:
7 func (x *Int) square() int {Lúc này, khi cần sử dụng phương thức square(), ta có thể dùng (&x).square(). Tuy nhiên để thuận tiện, Go quy định chúng ta có thể dùng x.square() trong trường hợp này. Như vậy khi vật nhận là con trỏ, thì biến hay biến con trỏ đều có thể gọi trực tiếp phương thức mà không cần chuyển thành con trỏ cho đúng. Go sẽ làm ngầm việc này giúp chúng ta. Tương tự nếu khai báo biến con trỏ p = &x thì ta có thể gọi p.square() thay vì (*p).square(). Go cũng thêm ngầm giúp chúng ta.
8 return *x * *x
9 }
Với việc hỗ trợ đối tượng nhận là con trỏ, để tránh nhập nhằng, Go cấm tạo phương thức cho kiểu dữ liệu con trỏ.
Phương thức là dạng hàm đặc biệt nên nó có thể được sử dụng như tham số kiểu dữ liệu hàm trong các hàm liên quan. Ví dụ sau thực hiện việc gọi phương thức doSomething() sau 10 giây:
type Task struct { /* Khai báo cấu trúc Task */ }
func (r *Task) doSongthing() { /* Khai báo phương thức doSomething */ }
t := new(Task)
time.AfterFunc(10 * time.Second, t.doSomething)
Hàm AfterFunc(d Duration, f func()) của package time sẽ thực hiện hàm f sau khoảng thời gian d. Như chúng ta thấy là t.doSomething có thể dùng như là biến kiểu dữ liệu hàm làm tham số cho time.AfterFunc.
Tương tự, chúng ta có thể gán ds := r.doSomething và sau đó sử dụng ds() để gọi thực hiện phương thức này của r.
Phương thức cho cấu trúc và trường vô danh
Giả sử bây giờ chúng ta muốn tạo một cấu trúc mô tả hình chữ nhật (đặc trưng bởi chiều dài và chiều rộng). Chúng ta sẽ khai báo như sau:
Tiếp theo chúng ta tạo 2 phương thức tính chu vi và diện tích cho cấu trúc Rect này như sau:
Bây giờ chúng ta cần cấu trúc mô tả hình chữ nhật có màu. Chúng ta có thể mô tả theo 2 cách như sau:
type Rect struct {
length, width int
}
func (r Rect) area() int {Đối với cấu trúc, Go không cho phép đặt tên phương thức trùng tên trường. Phương thức trùng tên trường sẽ bị loại bỏ.
return r.length * r.width
}
func (r Rect) perim() int {
return (r.length + r.width) * 2
}
Bây giờ chúng ta cần cấu trúc mô tả hình chữ nhật có màu. Chúng ta có thể mô tả theo 2 cách như sau:
type ColorRect struct {
length, width int
color color.RGBA
}
hoặc
type ColorRect struct {
r Rect
color color.RGBA
}
type ColorRect struct {
Rect
color color.RGBA
}
func main() {- Ở hàm main:
r := Rect{width: 5, length: 10}
fmt.Println("area: ", r.area()) // area: 50
fmt.Println("perim:", r.perim()) // perim: 30
red := color.RGBA{255, 0, 0, 255}
c := ColorRect{Rect{10, 5}, red}
c.width += 5
fmt.Println("area: ", c.area()) // area: 100
fmt.Println("perim:", c.perim()) // perim: 40
}
+ Dòng 4 khai báo biến cấu trúc RGBA red chứa thông tin màu đỏ từ package color thuộc image/color.
+ Dòng 5 khai báo biến cấu trúc ColorRect c và thiết lập giá trị ban đầu cho nó. Lưu ý là khi khai báo, cấu trúc Rect cần được nêu rõ ràng thay vì chỉ nêu {10,5}.
Một số lưu ý khi sử dụng trường vô danh:
- Do khai báo trường vô danh nên khi cần truy xuất trường này chúng ta bối rối không biết dùng như thế nào. Go đề xuất sử dụng chính tên của cấu trúc làm trường vô danh đó. Ví dụ ở trên có thể dùng c.Rect để lấy biến cấu trúc Rect và c.Rect.width sẽ cho giá trị chiều rộng.
- Trong trường hợp cấu trúc ColorRect ở trên và cấu trúc gốc Rect có trường cùng tên border thì khi sử dụng ở ColorRect, trường border ở Rect sẽ bị che đi. Muốn sử dụng border của Rect ở biến c thuộc cấu trúc ColorRect, ta sử dụng c.Rect.border. Với phương thức cũng tương tự, phương thức cùng tên của cấu trúc sử dụng sẽ che phương thức của cấu trúc gốc.
- Trong trường hợp một cấu trúc sử dụng nhiều trường vô danh có trường hoặc phương thức cùng tên thì việc sử dụng trực tiếp trường đó trong cấu trúc này sẽ bị Go báo lỗi do không biết truy xuất của trường vô danh nào. Lúc đó, chúng ta cần nêu rõ dữ liệu của trường vô danh cần lấy.
Tóm tắt:
- Go cho phép tạo phương thức của bất kỳ kiểu dữ liệu nào trong cùng package (trừ kiểu con trỏ). Cách khai báo phương thức là thêm tham số là vật nhận trước tên hàm: func (<vật nhận>)<tên hàm>(<tham số>) (<trả về>).- Tên phương thức trùng tên trường sẽ bị loại bỏ và đặt tên phương thức giống nhau của cùng kiểu dữ liệu sẽ bị báo lỗi.- Khi vật nhận là con trỏ, có thể truy xuất phương thức trực tiếp mà không cần chuyển nếu biến gọi không phải con trỏ. Ngược lại vẫn đúng.- Phương thức có thể được truyền là tham số và gán cho một biến để sử dụng sau này y như hàm.- Để sử dụng trực tiếp thông tin của một cấu trúc khác, cấu trúc cần khai báo dưới dạng trường vô danh cho cấu trúc cần sử dụng.
Câu này là sao a "Với việc hỗ trợ đối tượng nhận là con trỏ, để tránh nhập nhằng, Go cấm tạo phương thức cho kiểu dữ liệu con trỏ."
ReplyDeleteNghĩa là nếu bạn có khai báo 1 kiểu dữ liệu là kiểu dữ liệu con trỏ thì bạn sẽ không thể tạo phương thức cho kiểu dữ liệu mới này.
ReplyDeleteCảm ơn anh. Cho em hỏi thêm là khi nào thì mình sử dụng vật nhận kiểu pointer vậy anh?
DeleteKhi mà vật nhận chiếm bộ nhớ lớn thì dùng con trỏ để tránh chiếm nhiều bộ nhớ trên stack khi thực thi phương thức. Thường thì các phương thức của struct đều dùng vậy nhận con trỏ.
DeleteThanks a. Em có tìm hiểu thêm thì khi sử dụng vật nhận kiểu pointer sẽ có thể gán giá trị cho vật nhận trong phương thức.
Delete"Phương thức cho cấu trúc và trường vô danh" Cài này giống với kế thừa nhỉ
ReplyDelete