Menu
CoddyTech

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ự.

challenge icon

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à Value với kiểu T.

    Thêm phương thức Get vào Wrapper[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êng U cho 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ằng fmt.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 (int hoặc string), sau đó đọc một giá trị. Tạo một Wrapper có kiểu phù hợp.

    Với đầu vào int: Tạo một Wrapper[int], sau đó dùng Transform để nhân đôi giá trị (trả về một int) và dùng TransformToString để lấy biểu diễn chuỗi.

    Với đầu vào string: Tạo một Wrapper[string], sau đó dùng Transform để lấy độ dài của chuỗi (trả về một int) và dùng TransformToString để 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 (int hoặc string)
  • 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: 25

Và 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
	}
}
quiz iconTự kiểm tra

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

Tự luyện tập: Trình biên dịch Go online