Skip to content

Tổng hợp các lưu ý về các kiểu dữ liệu tập hợp trong C#

Notifications You must be signed in to change notification settings

unityvn/csharp-collection-notes

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 

Repository files navigation

Tổng hợp các lưu ý về kiểu dữ liệu dạng tập hợp

1. ArrayList

  • Array và List đều là những kiểu dữ liệu được sử dụng nhiều và phổ biến

1.1. Array

  • Có kích thước cố định
  • Truy cập phần tử nhanh (O(1))
  • Không có sẵn phương thức thêm/xoá phần tử
  • Read more docs
int[] numbers = new int[3] { 1, 2, 3 };
Debug.Log(numbers[0]); // 1

1.2. List

  • Kích thước linh hoạt
  • Thêm/xoá các phần tử dễ dàng
  • Tìm kiếm phần tử chậm hơn HashSet.
  • Read more docs
List<string> players = new List<string>() { "Alice", "Bob" };
players.Add("Charlie");
Debug.Log(players[1]); // Bob

1.3. Lưu ý khi sử dụng vòng lặp for/foreach (Read more)

  • Đối với Array, vòng lặp foreach đôi khi có thể nhanh như, hoặc thậm chí nhanh hơn, vòng lặp for. Điều này là do trình biên dịch C# tối ưu hóa foreach cho mảng, khiến nó gần như tương đương với vòng lặp for. Vì mảng có kích thước cố định và các phần tử của chúng được lưu trữ trong một khối bộ nhớ liền kề, trình biên dịch có thể tạo mã IL (Ngôn ngữ trung gian) rất hiệu quả cho foreach trên mảng, thường khiến nó hiệu quả như vòng lặp for.
  • Đối với List, vòng lặp for thường nhanh hơn foreach khi lặp qua các đối tượng List. Điều này là do foreach tạo ra một đối tượng Enumerator, có thể thêm một chút chi phí, trong khi for truy cập trực tiếp các phần tử theo chỉ mục. Việc phân bổ thêm này trong foreach có thể ảnh hưởng một chút đến hiệu suất, đặc biệt là trong các tình huống mà chi phí thu gom rác (GC) là một mối quan tâm.
  • Tóm lại:
    • Sử dụng for với Lists: Đối với các trường hợp bạn đang lặp lại các danh sách lớn hoặc có mã nhạy cảm về hiệu suất, for thường được ưu tiên cho List. Điều này có thể tránh việc phân bổ enumerator bổ sung đi kèm với foreach.
    • Sử dụng foreach với Mảng: Nếu bạn đang lặp qua các mảng, foreach thường có hiệu suất ngang bằng với for và có thể giúp mã của bạn dễ đọc hơn.

2. HashSet

  • HashSet rất hữu ích khi bạn cần lưu các phần tử duy nhất và thực hiện các tháo tác tập hợp hiệu quả
  • Read more docs
HashSet<string> enemyNames = new HashSet<string>();

enemyNames.Add("Zombie");
enemyNames.Add("Skeleton");
enemyNames.Add("Orc");

// Thêm phần tử trùng lặp sẽ không có tác dụng
enemyNames.Add("Zombie"); // Không được thêm vào

2.1 Ưu điểm của HashSet

  • Hiệu suất cao: Các thao tác Contains, Add, Remove có độ phức tạp O(1)
  • Tự động loại bỏ trùng lặp: Đảm bảo mọi phần tử là duy nhất

2.2 So sánh với List

Tính năng HashSet List
Tốc độ tìm kiếm O(1) O(n)
Cho phép trùng lặp Không
Thứ tự phần tử Không đảm bảo Được đảm bảo
Tốc độ thêm/xóa Nhanh Chậm hơn (đặc biệt khi xóa)

==> Tóm lại: HashSet là sự lựa chọn tuyệt vời khi bạn cần lưu trữ các phần tử duy nhất và thường xuyên kiểm tra sự tồn tại của phần tử

3. Dictionary

  • Là cấu trúc dữ liệu cho phép lưu trữ dữ liệu theo dạng cặp key-value với hiệu suất truy xuất cao
  • Read more docs
using System.Collections.Generic;

// Khởi tạo Dictionary cơ bản
Dictionary<string, int> playerScores = new Dictionary<string, int>();

// Khởi tạo với giá trị ban đầu
Dictionary<string, string> weaponTypes = new Dictionary<string, string>() {
    {"sword", "melee"},
    {"bow", "ranged"},
    {"staff", "magic"}
};
playerScores.Add("Player1", 100);
playerScores.Add("Player2", 150);

// Hoặc sử dụng indexer
playerScores["Player3"] = 200;

int score = playerScores["Player1"]; // Lấy giá trị

// Kiểm tra tồn tại key trước khi truy cập
if (playerScores.ContainsKey("Player2")) {
    Debug.Log("Score của Player2: " + playerScores["Player2"]);
}

3.1 Ưu điểm

  • Truy xuất nhanh: Độ phức tạp O(1) cho các thao tác tìm kiếm, thêm, xóa
  • Tổ chức linh hoạt: Dễ dàng ánh xạ giữa các đối tượng game
  • Hiệu suất cao: Lý tưởng cho hệ thống truy câp nhanh như inventory, player data, ...

3.2 Lưu ý quan trọng

  • Key phải là duy nhất: Mỗi key chỉ xuất hiện một lần trong Dictionary
  • Key không được null: Sẽ gây ra exception nếu cố gắng sử dụng null key
  • Kiểm tra tồn tại: Luôn kiểm tra ContainsKey trước khi truy cập để tránh KeyNotFoundException
  • Hiệu suất: Dictionary chiếm nhiều bộ nhớ hơn List/Array nhưng truy xuất nhanh hơn

3.3 So sánh với các cấu trúc khác

Tính năng Dictionary<TKey,TValue> List Array (T[]) HashSet
Truy cập bằng key ✅ O(1) ❌ Không hỗ trợ ❌ Không hỗ trợ ❌ Chỉ kiểm tra tồn tại
Truy cập bằng index ❌ Không hỗ trợ ✅ O(1) ✅ O(1) ❌ Không hỗ trợ
Tìm kiếm phần tử ✅ O(1) (by key) ⚠️ O(n) ⚠️ O(n) ✅ O(1)
Thêm phần tử ✅ O(1) ✅ O(1)* ❌ Kích thước cố định ✅ O(1)
Xóa phần tử ✅ O(1) ⚠️ O(n) ❌ Không hỗ trợ ✅ O(1)
Lưu trữ key-value ✅ Có ❌ Không ❌ Không ❌ Chỉ lưu giá trị
Cho phép trùng lặp ❌ Key phải duy nhất ✅ Có ✅ Có ❌ Giá trị duy nhất
Bảo toàn thứ tự ❌ Không đảm bảo ✅ Có ✅ Có ❌ Không đảm bảo
Tốt cho Tra cứu nhanh bằng key Danh sách có thứ tự Dữ liệu cố định Kiểm tra tồn tại nhanh

*Ghi chú:

  • ✅ = Hỗ trợ tốt
  • ⚠️ = Có thể chậm với dữ liệu lớn
  • ❌ = Không hỗ trợ
  • O(1)* với List: O(1) nếu Capacity chưa đầy, O(n) khi cần mở rộng

==> Tóm lại: Dictionary là công cụ mạnh mẽ để quản lý dữ liệu trong Unity, đặc biệt khi bạn cần ánh xạ giữa các định danh và giá trị với hiệu suất cao.

4. Queue

  • Queue là cấu trúc dữ liệu hàng đợi (FIFO - First In First Out), nghĩa là phần tử nào được thêm vào trước thì sẽ được lấy ra trước. Nó hữu ích khi bạn cần quản lý một danh sách các hành động, sự kiện hoặc nhiệm vụ cần xử lý theo thứ tự.
  • Read more docs
Queue<int> queue = new Queue<int>(); // Khởi tạo queue chứa số nguyên

void Add(){
    queue.Enqueue(1);
    queue.Enqueue(2);
    queue.Enqueue(3);
}

int Get(){
    int firstElement = queue.Dequeue(); // Lấy ra 1 và xóa khỏi hàng đợi
    return firstElement; // kết quả trả về bằng 1
}

int Peek(){
    int front = queue.Peek(); // Xem phần tử đầu tiên nhưng không xóa
    return front
}

Ví dụ sử dụng Queue để quản lý nhiệm vụ trong game

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TaskManager : MonoBehaviour
{
    Queue<string> taskQueue = new Queue<string>();

    void Start()
    {
        // Thêm nhiệm vụ vào hàng đợi
        taskQueue.Enqueue("Nhặt vũ khí");
        taskQueue.Enqueue("Tấn công kẻ địch");
        taskQueue.Enqueue("Thu thập vật phẩm");

        StartCoroutine(ProcessTasks());
    }

    IEnumerator ProcessTasks()
    {
        while (taskQueue.Count > 0)
        {
            string currentTask = taskQueue.Dequeue();
            Debug.Log("Thực hiện nhiệm vụ: " + currentTask);
            yield return new WaitForSeconds(2); // Giả lập thời gian thực hiện nhiệm vụ
        }

        Debug.Log("Hoàn thành tất cả nhiệm vụ!");
    }
}

📌 Giải thích:

  • Danh sách nhiệm vụ được quản lý bằng Queue.
  • Dùng Coroutine để thực hiện nhiệm vụ tuần tự (mỗi nhiệm vụ mất 2 giây).
  • Khi taskQueue.Count == 0, tất cả nhiệm vụ đã hoàn thành.

Khi nào ta nên dùng Queue trong unity?

  • Quản lý danh sách nhiệm vụ tuần tự (như AI, NPC thực hiện hành động theo thứ tự).
  • Xử lý hàng đợi sự kiện (như log chat, thông báo hệ thống).
  • Tạo hệ thống Pooling (quản lý đối tượng tái sử dụng để tối ưu hiệu năng).
  • Tính toán đường đi AI (bằng BFS - Breadth First Search).

5. Stack

  • Stack là cấu trúc dữ liệu ngăn xếp (LIFO - Last In First Out), tức là phần tử nào được thêm vào sau sẽ được lấy ra trước. Nó thích hợp sử dụng để quản lý các trạng thái, hệ thống Undo/Redo hoặc giải quyết bài toán đệ quy.
  • Read more docs
Stack<int> stack = new Stack<int>(); // Khởi tạo stack chứa số nguyên

void Push(){
    stack.Push(10);
    stack.Push(20);
    stack.Push(30);
}

int Pop(){
    int lastElement = stack.Pop(); // Lấy ra 30 và xóa khỏi stack
    return lastElement;
}

int Peek(){
    int topElement = stack.Peek(); // Xem phần tử trên cùng nhưng không xóa
    return topElement;
}

Ví dụ về hệ thống Undo/Redo bằng Stack

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class UndoRedoManager : MonoBehaviour
{
    Stack<string> undoStack = new Stack<string>();
    Stack<string> redoStack = new Stack<string>();

    void Start()
    {
        PerformAction("Di chuyển lên");
        PerformAction("Di chuyển phải");
        PerformAction("Tấn công");

        Undo();
        Undo();
        Redo();
    }

    void PerformAction(string action)
    {
        undoStack.Push(action);
        redoStack.Clear(); // Khi thực hiện hành động mới, không thể Redo nữa
        Debug.Log("Hành động: " + action);
    }

    void Undo()
    {
        if (undoStack.Count > 0)
        {
            string lastAction = undoStack.Pop();
            redoStack.Push(lastAction);
            Debug.Log("Undo: " + lastAction);
        }
        else
        {
            Debug.Log("Không thể Undo!");
        }
    }

    void Redo()
    {
        if (redoStack.Count > 0)
        {
            string lastUndoneAction = redoStack.Pop();
            undoStack.Push(lastUndoneAction);
            Debug.Log("Redo: " + lastUndoneAction);
        }
        else
        {
            Debug.Log("Không thể Redo!");
        }
    }
}

📌 Giải thích:

  • undoStack lưu trữ các hành động đã thực hiện.
  • redoStack lưu trữ các hành động đã Undo để có thể Redo.
  • Khi thực hiện hành động mới, redoStack bị xóa để tránh Redo lỗi.

So sánh Stack và Queue

Đặc điểm Stack<T> (LIFO) Queue<T> (FIFO)
Cách hoạt động Lấy phần tử cuối cùng trước Lấy phần tử đầu tiên trước
Phương thức chính Push(), Pop(), Peek() Enqueue(), Dequeue(), Peek()
Ứng dụng Hệ thống Undo/Redo, đệ quy, duyệt cây Quản lý hàng đợi, xử lý nhiệm vụ tuần tự

Khi nào nên dùng Stack trong unity?

  • Hệ thống Undo/Redo (ví dụ: chỉnh sửa bản đồ, hành động nhân vật).
  • Duyệt cây đệ quy (DFS - Depth First Search).
  • Hệ thống xử lý trạng thái nhân vật (State Machine).
  • Quản lý lịch sử hành động của AI.

6. LinkedList

  • Là một cấu trúc dữ liệu giúp quản lý danh sách liên kết 2 chiều
  • Read more docs

Thêm phần tử

  • Có nhiều cách để thêm phần tử
LinkedList<int> numbers = new LinkedList<int>();

// Thêm vào đầu danh sách
numbers.AddFirst(10);

// Thêm vào cuối danh sách
numbers.AddLast(20);

// Thêm một phần tử sau một node cụ thể
LinkedListNode<int> node = numbers.AddLast(30); 
numbers.AddAfter(node, 40);

// Thêm một phần tử trước một node cụ thể
numbers.AddBefore(node, 25);

Duyệt LinkedList

  • Sử dụng foreach
foreach (int number in numbers)
{
    Debug.Log(number);
}
  • Sử dụng while thông qua LinkedListNode
LinkedListNode<int> current = numbers.First;
while (current != null)
{
    Debug.Log(current.Value);
    current = current.Next;
}

Xóa phần tử

  • Có nhiều cách để xóa phần tử
numbers.Remove(25); // Xóa giá trị cụ thể
numbers.RemoveFirst(); // Xóa phần tử đầu tiên
numbers.RemoveLast(); // Xóa phần tử cuối cùng
numbers.Clear(); // Xóa toàn bộ danh sách

Ứng dụng LinkedList trong Unity

  • Quản lý danh sách hành động của AI (các hành động liên tục như di chuyển, tấn công).
  • Quản lý đối tượng trong game (danh sách quái vật, đạn trong game bắn súng, checkpoint).
  • Triển khai Undo/Redo (lịch sử thao tác của người chơi).

Khi nào nên dùng LinkedList thay vì List?

Tiêu chí List LinkedList
Thêm/Xóa ở giữa Chậm (O(n)) Nhanh (O(1))
Truy cập phần tử Nhanh (O(1)) Chậm (O(n))
Duyệt danh sách Nhanh Trung bình
Bộ nhớ sử dụng Ít hơn Nhiều hơn (do lưu Node)
  • Dùng List nếu bạn cần truy cập phần tử nhanh bằng index.
  • Dùng LinkedList nếu bạn cần thêm/xóa phần tử thường xuyên ở giữa danh sách.

Kết luận

  • LinkedList hữu ích khi cần thêm/xóa phần tử nhanh ở giữa danh sách.
  • Tuy nhiên, nó tốn bộ nhớ hơn so với List.
  • Trong Unity, nên dùng cho các hệ thống hàng đợi, undo/redo, hoặc quản lý pool đối tượng.

About

Tổng hợp các lưu ý về các kiểu dữ liệu tập hợp trong C#

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published