Bài 17 - Phương thức trong Go


  • Trùm cuối

    Method

    3d31a4f5-9732-4648-890c-80fe00f92f54-image.png

    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):
    func (<vật nhận>) <tên phương thức>(<danh sách tham số>) (<danh sách trả về>) { <Khối lệnh> }
    Chúng ta cùng xem ví dụ như sau:

    package main 
      
    import "fmt" 
      
    type Int int 
      
    func (x Int) square() Int { 
        return x * x 
    } 
      
     func main() { 
        x := Int(5) 
        fmt.Printf("Bình phương của %d là %d", x, x.square()) // Bình phương của 5 là 25
     } 
    
    • 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.
    • 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:

    func (x *Int) square() int { 
        return *x * *x 
    }
    

    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.

    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:

    type Rect struct {
        length, width int
    }
    

    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:

    func (r Rect) area() int { 
        return r.length * r.width 
    } 
      
    func (r Rect) perim() int { 
        return (r.length + r.width) * 2
    }
    

    Đố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ỏ.

    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
    }
    

    Ở cấu trúc bên dưới, ta thấy ColorRect có trường là cấu trúc Rect đã khai báo bên trên. Với cách khai báo này, muốn truy xuất thông tin chiều dài, chiều rộng hay các phương thức của hình chữ nhật phải thông qua trường r. Trong khi đó, ColorRectRect đều là mô tả hình chữ nhật chứ không phải ColorRect chứa hình chữ nhật được mô tả bởi Rect. Để giải quyết tình huống này, Go đề xuất khái niệm trường vô danh như sau:

    type ColorRect struct {
        Rect
        color color.RGBA
    }
    

    Như chúng ta thấy ColorRect khai báo Rect là trường của nó mà không có tên trường. Vì vậy nó được gọi là trường vô danh. Lúc này, ColorRect có thể sử dụng trường và phương thức của Rect như là trường và phương thức của nó. Tức là, với c là biến cấu trúc ColorRect, chúng ta có thể sử dụng c.width hay c.Area() hợp lệ:

    func 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     
    }
    
    • Ở hàm main:
      🎅 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.

    Bài tiếp theo chúng ta sẽ tìm hiểu về kiểu giao diện (Interface) trong Go.

    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ó thể bạn cũng quan tâm

.
DMCA.com Protection Status