Giải pháp thay thế cho phương thức tổng quát
Thuộc phần Lập trình hướng đối tượng trong hành trình GO của Coddy. Bài học 71 trên 107.
Go có một hạn chế đáng chú ý: bạn không thể định nghĩa phương thức có tham số kiểu riêng trên một struct. Dù struct tổng quát hoạt động rất tốt, bạn không thể thêm phương thức có tham số kiểu mới.
Đoạn code này sẽ không biên dịch:
type Box[T any] struct {
Value T
}
// ERROR: phương thức không được có tham số kiểu
func (b Box[T]) Convert[U any](fn func(T) U) U {
return fn(b.Value)
}Cách khắc phục là dùng một hàm tổng quát độc lập thay vì một phương thức:
type Box[T any] struct {
Value T
}
// Phương thức thông thường dùng tham số kiểu của struct
func (b Box[T]) Get() T {
return b.Value
}
// Hàm độc lập có thể có các tham số kiểu riêng
func Convert[T, U any](b Box[T], fn func(T) U) U {
return fn(b.Value)
}
func main() {
intBox := Box[int]{Value: 42}
// Dùng phương thức
fmt.Println(intBox.Get()) // 42
// Dùng hàm độc lập
strResult := Convert(intBox, func(n int) string {
return fmt.Sprintf("Number: %d", n)
})
fmt.Println(strResult) // Số: 42
}Các phương thức trên struct tổng quát vẫn có thể dùng tham số kiểu T của struct. Hạn chế này chỉ áp dụng khi khai báo thêm tham số kiểu trong chữ ký phương thức.
Khi cần sự linh hoạt đó, một hàm tổng quát nhận struct làm đối số đầu tiên sẽ mang lại kết quả tương tự.
Thử thách
DễHãy cùng xây dựng một bộ công cụ chuyển đổi dữ liệu để minh họa cách khắc phục hạn chế của Go đối với phương thức generic! Vì phương thức không thể khai báo tham số kiểu mới, bạn sẽ tạo các hàm generic độc lập để có được sự linh hoạt tương tự.
Bạn sẽ sắp xếp code trong hai tệp:
wrapper.go: Định nghĩa vùng chứa generic và các hàm chuyển đổi.Tạo một struct generic
Wrapper[T any]có một trường duy nhất làValuevới kiểuT.Thêm phương thức
GetvàoWrapper[T]để trả về giá trị được bọc. Phương thức này dùng tham số kiểu của struct, điều này được phép.Tạo hàm generic độc lập
Transform[T, U any](w Wrapper[T], fn func(T) U) Uđể áp dụng hàm chuyển đổi lên giá trị trong wrapper và trả về kết quả. Hàm này cần tham số kiểu riêngUcho kiểu đầu ra, vì vậy nó phải là hàm độc lập thay vì phương thức.Tạo một hàm độc lập khác,
TransformToString[T any](w Wrapper[T]) string, để chuyển giá trị được bọc thành chuỗi bằngfmt.Sprintf("%v", ...).main.go: Minh họa cách khắc phục này với các phép chuyển đổi khác nhau.Đọc một chỉ báo kiểu (
inthoặcstring), sau đó đọc một giá trị. Tạo mộtWrappercó kiểu phù hợp.Với đầu vào
int: Tạo mộtWrapper[int], sau đó dùngTransformđể nhân đôi giá trị (trả về một int) và dùngTransformToStringđể lấy biểu diễn chuỗi.Với đầu vào
string: Tạo mộtWrapper[string], sau đó dùngTransformđể lấy độ dài của chuỗi (trả về một int) và dùngTransformToStringđể lấy biểu diễn chuỗi.In kết quả theo định dạng này:
Original: [value] Transformed: [transformed value] As String: [string representation]
Đầu vào sẽ có dạng sau:
- Dòng 1: Chỉ báo kiểu (
inthoặcstring) - Dòng 2: Giá trị
Ví dụ, với đầu vào:
int
25Đầu ra của bạn sẽ là:
Original: 25
Transformed: 50
As String: 25Và với đầu vào:
string
Hello WorldĐầu ra của bạn sẽ là:
Original: Hello World
Transformed: 11
As String: Hello WorldĐiểm mấu chốt là Transform nhận một Wrapper[T] làm đối số đầu tiên và khai báo thêm tham số kiểu mới U cho kiểu trả về. Struct không thể thực hiện điều này nếu đó là một phương thức. Mẫu này giúp bạn linh hoạt thực hiện các phép chuyển đổi generic mà vẫn tuân theo những giới hạn của hệ thống kiểu Go.
Tự làm thử
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Đọc chỉ báo kiểu dữ liệu
typeIndicator, _ := reader.ReadString('\n')
typeIndicator = strings.TrimSpace(typeIndicator)
// Đọc giá trị
value, _ := reader.ReadString('\n')
value = strings.TrimSpace(value)
if typeIndicator == "int" {
// Phân tích giá trị số nguyên
num, _ := strconv.Atoi(value)
// TODO: Tạo Wrapper[int] với số đã phân tích
// TODO: Dùng Transform để nhân đôi giá trị (gợi ý: truyền vào một hàm (function) thực hiện phép nhân đôi)
// TODO: Dùng TransformToString để lấy dạng biểu diễn chuỗi
// TODO: In kết quả theo định dạng yêu cầu:
// Original: [value]
// Transformed: [transformed value]
// As String: [string representation]
_ = num // Xóa dòng này khi bạn dùng num
} else if typeIndicator == "string" {
// TODO: Tạo Wrapper[string] với giá trị
// TODO: Dùng Transform để lấy độ dài chuỗi (gợi ý: truyền vào một hàm (function) trả về len())
// TODO: Dùng TransformToString để lấy dạng biểu diễn chuỗi
// TODO: In kết quả theo định dạng yêu cầu:
// Original: [value]
// Transformed: [transformed value]
// As String: [string representation]
_ = value // Xóa dòng này khi bạn dùng value
}
}
Bài học này có một bài trắc nghiệm ngắn. Bắt đầu bài học để trả lời và theo dõi tiến độ.
Tất cả bài học trong Lập trình hướng đối tượng
1Nền tảng OOP trong Go
Tệp bên ngoàiWorkspace và module trong GoPackage và importTên được export và không được exportGiới thiệu về OOP trong GoStruct như lớpĐịnh nghĩa phương thức cho structReceiver kiểu con trỏ và kiểu giá trịKhởi tạo structHàm khởi tạoÔn tập: máy tính đơn giản4Interface
Giới thiệu về interfaceTriển khai ngầm địnhInterface như một hợp đồngInterface rỗng (any)Khẳng định kiểuswitch kiểuKết hợp interfaceInterface Stringer và ErrorÔn tập: Máy tính hình học7Tính đóng gói
Trường được xuất và không được xuấtĐóng gói ở cấp packagePhương thức getter và setterẨn thông tin trong GoÔn tập: hồ sơ học sinh10Generics (Go 1.18 trở lên)
Giới thiệu về GenericsTham số kiểuRàng buộc kiểuStruct tổng quátGiải pháp thay thế cho phương thức tổng quátÔn tập: tập hợp tổng quát13Các mẫu thiết kế phần 1
Giới thiệu về mẫu thiết kếMẫu SingletonMẫu FactoryMẫu Abstract FactoryMẫu ObserverMẫu Strategy2Tìm hiểu chuyên sâu về kiểu dữ liệu và struct
Kiểu cơ bản và kiểu tổng hợpĐịnh nghĩa kiểu dữ liệu tùy chỉnhThẻ structStruct ẩn danhStruct lồng nhauGiá trị zero và giá trị mặc địnhÔn tập: sổ liên lạc5Ưu tiên kết hợp hơn kế thừa
Vì sao Go không có kế thừaNhập môn nhúng structTự động nâng phương thứcNhúng nhiều structNhúng và kết tậpChe khuất phương thức được nhúngÔn tập: hệ phân cấp nhân viên8Xử lý lỗi và lập trình hướng đối tượng
Giao diện errorCác kiểu lỗi tùy chỉnhBọc lỗi với fmt.ErrorfLỗi sentinelerrors.Is() và errors.As()Panic, defer và recoverÔn tập: trình phân tích tệp3Con trỏ và bộ nhớ
Cơ bản về con trỏ trong GoCon trỏ đến structTruyền theo giá trị và tham chiếuHàm new()Thu gom rác trong GoÔn tập: xây dựng danh sách liên kết6Đa hình trong Go
Đa hình qua interfaceĐịnh kiểu vịt trong GoQuy tắc thỏa mãn interfaceCác tập hợp đa hìnhTiêm phụ thuộcÔn tập: bộ xử lý thanh toán9Lập trình đồng thời và lập trình hướng đối tượng
Cơ bản về goroutineChannel và giao tiếpChannel có bộ đệm và không có bộ đệmCâu lệnh selectsync.Mutex và sync.RWMutexsync.WaitGroupThiết kế struct an toàn luồngÔn tập: worker poolTự luyện tập: Trình biên dịch Go online