CodeGym /Các khóa học /C# SELF /Các trường hợp so sánh phức tạp và best practice

Các trường hợp so sánh phức tạp và best practice

C# SELF
Mức độ, Bài học
Có sẵn

1. Sắp xếp nhiều cấp (phân cấp)

Tại sao so sánh không phải lúc nào cũng đơn giản?

Khi phỏng vấn C#, người ta hay hỏi về sort và so sánh object phức tạp. Không phải trend đâu, mà là vấn đề thực tế mà dev nào cũng gặp. Tìm kiếm record trong database, sort bảng hiển thị, kiểm tra unique trong collection — tất cả đều liên quan trực tiếp đến việc so sánh object cho đúng.

So sánh số thì dễ. Nhưng nếu muốn sort user theo họ, rồi đến tên, mà lại có trường có thể bị rỗng (null) hoặc string viết bằng nhiều ngôn ngữ... Lúc này phải có cách tiếp cận hệ thống.

Yêu cầu phổ biến: sort theo một tiêu chí, nếu bằng nhau thì sort tiếp theo tiêu chí khác, nếu vẫn bằng thì lại tiếp tục.

Ví dụ: User với tên, họ và ngày sinh


public class User
{
    public string FirstName { get; set; }
    public string LastName  { get; set; }
    public DateTime BirthDate { get; set; }

    // Cho đẹp — in thông tin user
    public override string ToString()
        => $"{LastName} {FirstName} ({BirthDate:yyyy-MM-dd})";
}

Tại sao cần phân cấp?

Giả sử có list user, muốn in ra theo thứ tự alphabet: trước tiên theo họ, rồi đến tên. Nếu họ và tên đều trùng thì so ngày sinh.

Logic so sánh: "chuỗi trách nhiệm"

Nó giống như sort thí sinh olympic: đầu tiên theo điểm, nếu bằng thì theo thời gian nộp bài, nếu vẫn bằng thì theo alphabet. Trong code thì làm như này:


public class UserComparer : IComparer<User>
{
    public int Compare(User x, User y)
    {
        // So sánh họ
        int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);

        if (result != 0) return result; // Nếu khác — xong luôn

        // Nếu họ bằng — so sánh tên
        result = string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
        if (result != 0) return result;

        // Nếu họ và tên bằng — so ngày sinh
        return x.BirthDate.CompareTo(y.BirthDate);
    }
}

Cách dùng:


var users = new List<User>
{
    new User { FirstName = "Ivan", LastName = "Ivanov", BirthDate = new DateTime(1990, 1, 1) },
    new User { FirstName = "Petr", LastName = "Ivanov", BirthDate = new DateTime(1992, 5, 1) },
    new User { FirstName = "Anna", LastName = "Petrova", BirthDate = new DateTime(1985, 8, 30) }
};

users.Sort(new UserComparer());
users.ForEach(Console.WriteLine);
// Petrova Anna (1985-08-30)
// Ivanov Ivan (1990-01-01)
// Ivanov Petr (1992-05-01)

2. So sánh string? Đặc điểm văn hóa

So sánh string: Ordinal, CurrentCulture, InvariantCulture

String tưởng như ở đâu cũng giống nhau, nhưng không phải vậy! Ví dụ, tiếng Nga ё và е, tiếng Đức ss và ß, khác biệt hoa/thường...

Trong .NET, so sánh string có quy tắc riêng, set qua StringComparison. Nó ảnh hưởng đến sort và tìm kiếm.

Ví dụ so sánh string:


// trong tiếng Đức ß gần như tương đương ss
string a = "straße";
string b = "STRASSE";

bool eq1 = string.Equals(a, b, StringComparison.Ordinal); // false
bool eq2 = string.Equals(a, b, StringComparison.OrdinalIgnoreCase); // false
bool eq3 = string.Equals(a, b, StringComparison.CurrentCultureIgnoreCase); // true 

Hai trường hợp đầu so sánh theo byte — không quan tâm culture và ngôn ngữ, nên ß và SS là khác nhau. Nhưng trường hợp thứ ba dùng culture hiện tại (ví dụ, tiếng Đức), và string được hiểu như người bản xứ: ß coi như ss, không phân biệt hoa/thường. Vì vậy eq3 trả về true.

Chọn cách so sánh nào?

  • Ordinal — nhanh, so sánh byte, hợp cho kỹ thuật (ví dụ, so sánh id).
  • CurrentCulture / InvariantCulture — cho text người dùng, theo quy tắc ngôn ngữ của OS hoặc culture set sẵn.

Trong Sort, Compare và các method khác nên chỉ rõ kiểu so sánh:


string.Compare(x, y, StringComparison.CurrentCultureIgnoreCase)

Đặc điểm sort ở các ngôn ngữ khác nhau

Sort tiếng Nga — ё có thể đứng sau е, hoặc coi như bằng nhau (tùy culture!). Nếu làm gì nghiêm túc (ví dụ, danh bạ), hãy hỏi khách hàng hoặc BA xem sort "chữ đặc biệt" thế nào cho đúng.

3. Bảo vệ khỏi null: không phải object nào cũng ngoan

Làm gì nếu trường bị null?

Trong thực tế, kiểu gì cũng có người quên điền trường, lúc so sánh thì — bùm! — lỗi NullReferenceException. Nhiệm vụ của mình là phải chuẩn bị trước.

Ví dụ không bảo vệ khỏi null:


public int Compare(User x, User y)
{
    return x.LastName.CompareTo(y.LastName); // nếu LastName == null, sẽ lỗi!
}

Ví dụ có bảo vệ (null nhỏ hơn mọi non-null):


public int Compare(User x, User y)
{
    // Dùng comparer đặc biệt cho string, xử lý null
    int byLastName = Comparer<string>.Default.Compare(x.LastName, y.LastName);
    if (byLastName != 0) return byLastName;

    // tiếp tục...
}

Còn ngắn hơn nữa:


public int Compare(User x, User y)
{
    return string.Compare(x?.LastName, y?.LastName, StringComparison.OrdinalIgnoreCase);
}

"Null" ở đầu hay cuối?

Có thể cho user có họ "rỗng" lên đầu hoặc xuống cuối list — tùy bài toán.


public int Compare(User x, User y)
{
    if (x.LastName == null && y.LastName == null) return 0;
    if (x.LastName == null) return 1;   // null — xuống cuối
    if (y.LastName == null) return -1;  // null — xuống cuối
    return string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
}

4. So sánh theo nhiều tiêu chí

Lỗi newbie hay gặp — dùng toán học thay vì "chuỗi trách nhiệm" khi so sánh, ví dụ:


// Đừng làm thế này!
public int Compare(User x, User y)
{
    // Ví dụ sai
    return (x.Age - y.Age) + string.Compare(x.FirstName, y.FirstName, StringComparison.Ordinal);
}

Cách này không đảm bảo sort đúng: nếu chênh lệch tuổi là -100, còn so sánh string trả về 1, kết quả là -99, không đúng logic sort mong muốn.

Đúng là phải dùng thứ tự rõ ràng:
nếu đã có khác biệt — trả về luôn, nếu không thì xét tiếp tiêu chí sau.

5. Một số lưu ý hay

Làm gì nếu object bằng nhau ở các tiêu chí chính?

Nếu object "bằng nhau", quan trọng là thuật toán sort phải ổn định: không đổi thứ tự các phần tử bằng nhau theo so sánh. List<T>.Sort() mặc định không đảm bảo ổn định. Nếu cần (ví dụ, sort bảng nhiều cấp cho user), hãy dùng LINQ OrderBy/ThenBy — chúng ổn định.

So sánh với trường optional/nullable

Trong .NET hay gặp model có trường kiểu DateTime? (Nullable<DateTime>) hoặc int?. Logic đơn giản: null nhỏ hơn non-null, hoặc ngược lại — tùy bài toán. Có thể dùng helper của thư viện chuẩn:


int result = Nullable.Compare<DateTime>(u1.BirthDate, u2.BirthDate);

So sánh với quy tắc bổ sung

Đôi khi cần so sánh theo "trọng số", ví dụ, khách VIP luôn lên đầu. Giải pháp — thêm "VIP-flag" vào đầu sort.


public int Compare(User x, User y)
{
    // VIP lên đầu
    int vipResult = y.IsVip.CompareTo(x.IsVip); // true = 1, false = 0; sort giảm dần
    if (vipResult != 0) return vipResult;

    // Còn lại — như thường lệ
    int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase);
    if (result != 0) return result;
    return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase);
}

6. Tips và best practice

Luôn kiểm tra null
C# hiện đại ngày càng hướng tới "an toàn null", nhưng code cũ thì không. Luôn thêm bảo vệ hoặc dùng method phù hợp.

Tránh "magic number"
Đừng viết return x.Field - y.Field; cho trường có thể bị tràn kiểu (overflow). Nếu là long — dễ lỗi lắm.

Dùng StringComparison
Đừng tin vào so sánh string mặc định. Hãy chỉ rõ StringComparison.OrdinalIgnoreCase hoặc cái phù hợp.

Tách biệt so sánh và bằng nhau
Interface IComparable<T> và IEqualityComparer<T> dùng cho mục đích khác nhau. Sort thì dùng comparer, tìm unique thì dùng equality. Đôi khi kết quả khác nhau đấy!

Thêm test cho case "dị"
Kiểm tra sort chạy đúng khi trường bằng nhau, trường rỗng, string khác hoa/thường hoặc khác ngôn ngữ.

2
Nhiệm vụ
C# SELF, mức độ, bài học
Đã khóa
So sánh chuỗi với các đặc điểm văn hóa
So sánh chuỗi với các đặc điểm văn hóa
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION