Series C# hay ho: So sánh 2 object trong C# (Deep compare)

Lâu rồi không viết bài về technical nên phải viết 1 bài cho thiên hạ biết mình vẫn code :D. Ở bài viết này, mình sẽ nói về một chuyện khá đơn giản trong C#: So sánh 2 object. Đây là một vấn đề ai cũng tưởng là dễ, mình sẽ nâng dần vấn đề lên từ đơn giản đến phức tạp. Cách giải quyết cũng sẽ từ đơn giản trở nên phức tạp, sau đó sẽ trở lại đơn giản. Nếu chịu khó đọc bài viết này từ đầu đến cuối, các bạn sẽ ngộ ra nhiều điều, khả năng technical cũng sẽ tăng kha khá đấy.

Are-You-Up-For-The-Challenge

Cấp độ 1: Class đơn giản

Chúng ta bắt đầu bài toán với một class đơn giản nhé. Class này có 3 properties, khi ta gọi hàm Equals, C# chỉ so sánh reference, do đó kết quả là False


public class Student
{
   public string FirstName { get; set; }
   public string LastName { get; set; }
   public int Age { get; set; }
}

var student1 = new Student
{
   FirstName = "Pham",
   LastName = "Hoang",
   Age = 15,
};

var student2 = new Student
{
   FirstName = "Pham",
   LastName = "Hoang",
   Age = 15,
};

student1.Equals(student2); //False

Để xử lý chuyện này, chúng ta chỉ cần override lại hàm Equals là xong, không phức tạp, chắc bạn nào cũng đã học ở trường nhỉ


public override bool Equals(object obj)
{
  if (obj == null || GetType() != obj.GetType()) return false;

  var student = (Student) obj;

  return FirstName.Equals(student.FirstName)
         && LastName.Equals(student.LastName)
         && Age == student.Age
         && BirthDate.Equals(student.BirthDate);
}

Cấp độ 2: Sử dụng extension method

Hàm Equals vẫn còn khuyết điểm, đó là student1 không được null. Ta có thể xử lý vấn đề này bằng cách sử dụng extension method như sau. Giờ ta có thể gọi student1.Equals(student2) dù student1 có null đi nữa:


public static bool DeepEquals(this Student obj, Student another)
{
 //Nếu null hoặc giống nhau, trả true
 if (ReferenceEquals(obj, another))  return true;

 //Nếu 1 trong 2 là null, trả false
 if ((obj == null) || (another == null))  return false;

 return obj.FirstName.Equals(another.FirstName)
        && obj.LastName.Equals(another.LastName)
        && obj.Age == another.Age
        && obj.BirthDate.Equals(another.BirthDate);
}

Cấp độ 3: Viết method để so sánh 2 object nói chung

Hãy nghĩ tới trường hợp ứng dụng của bạn có khoảng vài chục class, mỗi class có vài chục property. Bạn sẽ làm gì? Hì hục viết tay hàm Equals cho từng class? Hãy sử dụng Refection trong C# để viết 1 hàm so sánh object có thể dùng cho mọi object.


public static bool DeepEquals(this object obj, object another)
 {

  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  //So sánh class của 2 object, nếu khác nhau thì trả fail
  if (obj.GetType() != another.GetType()) return false;

  var result = true;
  //Lấy toàn bộ các properties của obj
  //sau đó so sánh giá trị của từng property
  foreach (var property in obj.GetType().GetProperties())
  {
      var objValue = property.GetValue(obj);
      var anotherValue = property.GetValue(another);
      if (!objValue.Equals(anotherValue)) result = false;
  }

  return result;
 }

Hàm này chạy khá OK. Bạn đang tự hỏi: Ối dào, dễ thế này mà cũng viết? Chưa đâu, hãy xem tiếp phía dưới, nhiều thứ “đáng sợ” hơn đang chờ đấy.

Cấp độ 4: Trường hợp object chứa object hoặc struct như DateTime

Ở cấp độ trên, ta viết hàm so sánh từng trường. Thế nhưng giả sử trong class Student, ta có chứa DateTime hoặc 1 class khác thì sao nhỉ??


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

public class Teacher
{
   public string Name { get; set; }
   public string Subject { get; set; }
}

var student1 = new Student
{
   FirstName = "Pham",
   LastName = "Hoang",
   BirthDate = new DateTime(1992, 12, 5),
   Teacher = new Teacher { Name = "Le Minh", Subject = "Math"}
};

var student2 = new Student
{
   FirstName = "Pham",
   LastName = "Hoang",
   BirthDate = new DateTime(1992, 12, 5),
   Teacher = new Teacher { Name = "Le Minh", Subject = "Math" }
};

Suy nghĩ 1 tí nào. Để giải quyết, ta cũng sẽ so sánh từng trường, nhưng nếu trường đó là một object thì ta sẽ so sánh bằng hàm DeepEquals đã viết. Một thuật giải đệ quy cơ bản thôi mà 😀


public static bool DeepEquals(this object obj, object another)
{
 if (ReferenceEquals(obj, another)) return true;
 if ((obj == null) || (another == null)) return false;
 if (obj.GetType() != another.GetType()) return false;

 //Nếu property không phải class, chỉ là int, double, DateTime v...v
 //Gọi hàm equal thông thường
 if (!obj.GetType().IsClass) return obj.Equals(another);

 var result = true;
 foreach (var property in obj.GetType().GetProperties())
 {
    var objValue = property.GetValue(obj);
    var anotherValue = property.GetValue(another);
    //Tiếp tục đệ quy
    if (!objValue.DeepEquals(anotherValue))   result = false;
 }
 return result;
}

Wow, thế là đã xong, mọi vấn đề đã được giải quyết, bạn đang tự khen mình giỏi. Ồ, thế còn trường hợp không phải một object, mà là một List thì sao nhỉ, căng đây 😦

Cấp độ 5: So sánh 2 list

May thay, ta có thể viết extension method cho list như sau (Chữ <T> là generic nhé, các bạn có thể đọc bài này để xem lại).


public static bool DeepEquals<T>(this IEnumerable<T> obj, IEnumerable<T> another)
{
 if (ReferenceEquals(obj, another)) return true;
 if ((obj == null) || (another == null)) return false;

 bool result = true;
//Duyệt từng phần tử trong 2 list đưa vào
 using (IEnumerator<T> enumerator1 = obj.GetEnumerator())
 using (IEnumerator<T> enumerator2 = another.GetEnumerator())
 {
   while (true)
   {
     bool hasNext1 = enumerator1.MoveNext();
     bool hasNext2 = enumerator2.MoveNext();

     //Nếu có 1 list hết, hoặc 2 phần tử khác nhau, thoát khoải vòng lặp
     if (hasNext1 != hasNext2 || !enumerator1.Current.DeepEquals(enumerator2.Current))
     {
        result = false;
        break;
     }

      //Dừng vòng lặp khi 2 list đều hết
      if (!hasNext1) break;
   }
 }

 return result;
}

Phù, tạm xong. Chắc không còn gì nữa đâu nhỉ?


var list1 = new List<Student> { student1, student2 };
var list2 = new List<Student> { student1, student2};

list1 == list2; //True

Cấp độ 6: Một đống những thứ tả pín lù khác

Bạn chợt nhớ ra rằng, trong C# còn hằng hà sa số những thứ tương tự List như Dictionary, HashSet,… chẳng lẽ phải viết hết. Còn một vài trường hợp tréo ngoe hơn, vd như class Student sẽ chứa 1 list cái Teacher, method chúng ta viết không chạy được


var teacherA = new Teacher { Name = "Le Minh", Subject = "Math"};
var teacherB = new Teacher { Name = "Tai Phu", Subject = "Physics"};

var student1 = new Student
{
 FirstName = "Pham",
 LastName = "Hoang",
 Age = 15,
 BirthDate = new DateTime(1992, 12, 5),
 Teacher = new List<Teacher> { teacherA, teacherB}
};

var student2 = new Student
{
 FirstName = "Pham",
 LastName = "Hoang",
 Age = 15,
 BirthDate = new DateTime(1992, 12, 5),
 Teacher = new List<Teacher> { teacherA, teacherB}
};

Tới đây mình cũng bó tay rồi, con đường phía trước khá rắc rối gian nan và phức tạp :'(. Bạn được chọn 1 trong 2 lựa chọn sau:

  1. Với những class khó quá, hãy tự viết hàm Equals, cũng không nhiều lắm
  2. Theo đuổi hàm generic tới cùng, bằng cách đọc tiếp bài này.

Cấp độ cuối cùng: Cầu cứu anh JSON

Cách giải quyết vấn đề thật ra đơn giản hơn bạn nghĩ. Hãy serialize 2 object đó dưới dạng chuỗi JSON, so sánh 2 chuỗi được tạo ra là xong. (Việc serialize ra json đã giải quyết 99% vấn đề phức tạp liên quan đến các kiểu dữ liệu rồi, may quá :D).

Các bước thực hiện:

1. Add Reference Newtonsoft.JSON theo hướng dẫn, được kết quả như hình 3 là ok.

img2

2. Viết 1 hàm so sánh gọn nhẹ đơn giản:


public static bool JSONEquals(this object obj, object another)
{
  if (ReferenceEquals(obj, another)) return true;
  if ((obj == null) || (another == null)) return false;
  if (obj.GetType() != another.GetType()) return false;

  var objJson = JsonConvert.SerializeObject(obj);
  var anotherJson = JsonConvert.SerializeObject(another);

  return objJson == anotherJson;
}

Có 1 vài thư viện khác, các bạn có thể search trên google với từ khóa: Deep Compare C#.

 

Vì bài viết khá dài, lại hơi thiên về technical nên mình đã cố gắng khiến nó hấp dẫn hơn. Chúc mừng các bạn nếu các bạn chịu khó đọc được đến cuối cùng. Phần thưởng cho các bạn kiên nhẫn đây: 5 bạn comment đầu tiên trong bài viết này có quyền request mình viết 1 bài về 1 khía cạnh trong C#, MVC hoặc javascript mà bạn muốn tìm hiểu nhé. Chúc các bạn may mắn.

Rate this:

Like this:

Like

Loading…