Menu
CoddyTech

Struct 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 70 trên 107.

Giống như hàm, struct cũng có thể có tham số kiểu. Struct tổng quát cho phép bạn tạo cấu trúc dữ liệu hoạt động với mọi kiểu dữ liệu mà vẫn đảm bảo an toàn kiểu. Điều này rất phù hợp để xây dựng các vùng chứa có thể tái sử dụng như ngăn xếp, hàng đợi hoặc cặp khóa-giá trị.

Để định nghĩa một cấu trúc tổng quát, hãy đặt tham số kiểu sau tên cấu trúc:

type Box[T any] struct {
    Value T
}

func main() {
    intBox := Box[int]{Value: 42}
    strBox := Box[string]{Value: "hello"}
    
    fmt.Println(intBox.Value)  // 42
    fmt.Println(strBox.Value)  // hello
}

Không giống như các hàm thông thường, bạn phải chỉ định rõ kiểu khi tạo một instance của struct tổng quát. Go không thể suy luận kiểu chỉ dựa vào ngữ cảnh.

Các struct tổng quát có thể có nhiều tham số kiểu, nên rất phù hợp với những cấu trúc như cặp hoặc bản đồ:

type Pair[K, V any] struct {
    Key   K
    Value V
}

p := Pair[string, int]{Key: "age", Value: 25}
fmt.Printf("%s: %d\n", p.Key, p.Value)  // age: 25

Bạn cũng có thể áp dụng các ràng buộc để bảo đảm kiểu dữ liệu được lưu trữ hỗ trợ những phép toán cụ thể:

type NumberBox[T int | float64] struct {
    Value T
}

nb := NumberBox[float64]{Value: 3.14}

Các cấu trúc generic là nền tảng để xây dựng cấu trúc dữ liệu có kiểu an toàn và có thể tái sử dụng trong Go. Trong bài học tiếp theo, bạn sẽ học cách định nghĩa phương thức cho các kiểu generic này.

challenge icon

Thử thách

Dễ

Hãy xây dựng một hệ thống quản lý kho tổng quát có thể theo dõi nhiều loại mặt hàng! Bạn sẽ tạo các cấu trúc vùng chứa có thể tái sử dụng, hoạt động với mọi kiểu dữ liệu mà vẫn đảm bảo an toàn kiểu dữ liệu.

Bạn sẽ sắp xếp code trong hai tệp:

  • inventory.go: Định nghĩa các cấu trúc vùng chứa tổng quát.

    Tạo một struct tổng quát Item[T any] với hai trường: Name (string) và Data (kiểu T). Struct này đại diện cho bất kỳ mặt hàng nào có dữ liệu đi kèm thuộc một kiểu linh hoạt.

    Tạo một struct tổng quát Container[T any] với một trường duy nhất là Items, chứa một slice gồm các phần tử Item[T]. Vùng chứa này có thể lưu trữ nhiều mặt hàng cùng kiểu dữ liệu.

    Tạo một struct tổng quát có ràng buộc PricedItem[T int | float64] với ba trường: Name (string), Quantity (int) và Price (kiểu T). Ràng buộc này đảm bảo giá luôn có kiểu số.

    Triển khai một hàm NewContainer[T any]() *Container[T] để tạo và trả về con trỏ đến một Container rỗng.

    Triển khai phương thức Add trên *Container[T]. Phương thức này nhận tên (string) và dữ liệu (T), tạo một Item rồi thêm Item đó vào slice Items của vùng chứa.

    Triển khai phương thức Count trên Container[T] để trả về số lượng mặt hàng trong vùng chứa.

  • main.go: Minh họa các cấu trúc tổng quát với nhiều kiểu dữ liệu khác nhau.

    Đọc loại mặt hàng (string, int hoặc priced), sau đó đọc số lượng rồi đọc thông tin chi tiết của từng mặt hàng.

    Với kiểu string: Đọc từng cặp tên và dữ liệu chuỗi. Tạo một Container[string], thêm tất cả mặt hàng, rồi in từng mặt hàng theo định dạng [Name]: [Data], sau đó in tổng số lượng.

    Với kiểu int: Đọc từng cặp tên và dữ liệu số nguyên. Tạo một Container[int], thêm tất cả mặt hàng, rồi in từng mặt hàng theo định dạng [Name]: [Data], sau đó in tổng số lượng.

    Với kiểu priced: Đọc từng bộ ba gồm tên, số lượng và giá (dạng số thực). Tạo trực tiếp các instance PricedItem[float64] rồi in từng mặt hàng theo định dạng [Name] x[Quantity] @ [Price], trong đó giá hiển thị một chữ số thập phân.

    In dòng số lượng theo định dạng: Total items: [count]

Chương trình sẽ nhận đầu vào như sau:

  • Dòng 1: Loại mặt hàng (string, int hoặc priced)
  • Dòng 2: Số lượng mặt hàng (số nguyên)
  • Các dòng tiếp theo: Thông tin chi tiết của mặt hàng, tùy theo loại

Ví dụ, với đầu vào:

string
3
Book
Fiction Novel
Pen
Blue Ink
Notebook
Lined Paper

Kết quả đầu ra sẽ là:

Book: Fiction Novel
Pen: Blue Ink
Notebook: Lined Paper
Total items: 3

Với đầu vào:

int
2
Apples
50
Oranges
30

Kết quả đầu ra sẽ là:

Apples: 50
Oranges: 30
Total items: 2

Với đầu vào:

priced
2
Widget
10
19.99
Gadget
5
49.50

Kết quả đầu ra sẽ là:

Widget x10 @ 19.9
Gadget x5 @ 49.5
Total items: 2

Lưu ý rằng cùng một cấu trúc Container có thể hoạt động liền mạch với cả chuỗi lẫn số nguyên, trong khi PricedItem dùng ràng buộc để đảm bảo chỉ có thể dùng kiểu số cho giá. Bạn phải chỉ định rõ tham số kiểu khi tạo instance của các struct tổng quát này.

Tự làm thử

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
)

func main() {
	scanner := bufio.NewScanner(os.Stdin)
	
	// Đọc kiểu dữ liệu của mục
	scanner.Scan()
	itemType := scanner.Text()
	
	// Đọc số lượng
	scanner.Scan()
	count, _ := strconv.Atoi(scanner.Text())
	
	switch itemType {
	case "string":
		// TODO: Tạo Container[string] bằng NewContainer
		// TODO: Đọc 'count' mục, mỗi mục có tên ở một dòng và dữ liệu ở dòng tiếp theo
		// TODO: Thêm từng mục vào vùng chứa
		// TODO: In từng mục theo dạng "[Name]: [Data]"
		// TODO: Dùng phương thức (method) Count để in "Total items: [count]"
		
	case "int":
		// TODO: Tạo Container[int] bằng NewContainer
		// TODO: Đọc 'count' mục, mỗi mục có tên ở một dòng và dữ liệu số nguyên ở dòng tiếp theo
		// TODO: Thêm từng mục vào vùng chứa
		// TODO: In từng mục theo dạng "[Name]: [Data]"
		// TODO: Dùng phương thức (method) Count để in "Total items: [count]"
		
	case "priced":
		// TODO: Tạo một lát cắt (slice) để chứa các instance PricedItem[float64]
		// TODO: Đọc 'count' mục, mỗi mục có tên, số lượng và giá trên các dòng riêng
		// TODO: Tạo PricedItem[float64] cho từng mục rồi thêm vào lát cắt
		// TODO: In từng mục theo dạng "[Name] x[Quantity] @ [Price]", với giá hiển thị một chữ số thập phân
		// TODO: In "Total items: [count]"
	}
	
	_ = scanner // Dùng scanner để đọc đầu vào
	_ = count   // Dùng count cho vòng lặp
}
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