611 Chương 16: Các giao diện và mẫu thông dụng • Đối tượng StreamingContext cung cấp các thông tin về chủ định và đích của dữ liệu được-tuần-tự-hóa, cho phép bạn chọn tuần tự hóa dữ liệu nào. Ví dụ, bạn có thể cần tuần tự hóa dữ liệu riêng nếu nó được dành cho một miền ứng dụng khác trong cùng tiến trình, nhưng không cần nếu dữ liệu sẽ được ghi ra file. Trong quá trình giải tuần tự hóa, formatter sẽ gọi phương thức khởi dựng việc giải tuần tự hóa, lại truyền cho nó các tham chiếu SerializationInfo và StreamingContext . • Kiểu của bạn phải trích dữ liệu đã-được-tuần-tự-hóa từ đối tượng SerializationInfo bằng một trong các phương thức SerializationInfo.Get* , ví dụ: GetString , GetInt32 , hay GetBoolean . • Đối tượng StreamingContext cung cấp các thông tin về nguồn gốc của dữ liệu đã-được- tuần-tự-hóa, phản ánh logic mà bạn đã hiện thực cho việc tuần tự hóa. Trong quá trình tuần tự hóa chuẩn, formatter không sử dụng khả năng của đối tượng StreamingContext để cho biết các chi tiết về nguồn gốc, đích, và chủ định của dữ liệu được-tuần-tự-hóa. Tuy nhiên, nếu muốn thực hiện quá trình tuần tự hóa tùy biến, bạn có thể cấu hình đối tượng StreamingContext của formatter trước khi bắt đầu quá trình tuần tự hóa và giải tuần tự hóa. Tham khảo tài liệu .NET Framework SDK để có thêm thông tin về lớp StreamingContext . Ví dụ dưới đây trình bày phiên bản đã được chỉnh sửa của lớp Employee , có hiện thực giao diện ISerializable . Trong phiên bản này, lớp Employee không tuần tự hóa trường address nếu đối tượng StreamingContext chỉ định rằng đích của dữ liệu được-tuần-tự-hóa là file. Phương thức Main sẽ giải thích việc tuần tự hóa và giải tuần tự hóa của một đối tượng Employee . using System; using System.Runtime.Serialization; [Serializable] public class Employee : ISerializable { private string name; private int age; private string address; // Phương thức khởi dựng đơn giản. public Employee(string name, int age, string address) { this.name = name; this.age = age; this.address = address; } 612 Chương 16: Các giao diện và mẫu thông dụng // Phương thức khởi dựng dùng để kích hoạt formatter thực hiện việc // giải tuần tự hóa một đối tượng Employee. Bạn nên khai báo // phương thức khởi dựng này là private, hay ít nhất cũng là // protected để bảo đảm nó không bị gọi quá mức cần thiết. private Employee(SerializationInfo info, StreamingContext context) { // Trích xuất tên và tuổi của Employee (sẽ luôn hiện diện // trong dữ liệu đã-được-tuần-tự-hóa bất chấp giá trị // của StreamingContext). name = info.GetString("Name"); age = info.GetInt32("Age"); // Thực hiện trích xuất địa chỉ của Employee // (thất bại nếu không có). try { address = info.GetString("Address"); } catch (SerializationException) { address = null; } } // Các thuộc tính Name, Age, và Address (đã trình bày ở trên). § // Được khai báo bởi giao diện ISerializable, phương thức // GetObjectData cung cấp cơ chế để formatter thu lấy // dữ liệu sẽ-được-tuần-tự-hóa. public void GetObjectData(SerializationInfo inf, StreamingContext con){ // Luôn tuần tự hóa tên và tuổi của Employee. inf.AddValue("Name", name); inf.AddValue("Age", age); // Không tuần tự hóa địa chỉ của Employee nếu StreamingContext // cho biết rằng dữ liệu được-tuần-tự-hóa sẽ được ghi ra file. 613 Chương 16: Các giao diện và mẫu thông dụng if ((con.State & StreamingContextStates.File) == 0) { inf.AddValue("Address", address); } } // Chép đè Object.ToString để trả về chuỗi mô tả Employee. public override string ToString() { StringBuilder str = new StringBuilder(); str.AppendFormat("Name: {0}\n\r", Name); str.AppendFormat("Age: {0}\n\r", Age); str.AppendFormat("Address: {0}\n\r", Address); return str.ToString(); } public static void Main(string[] args) { // Tạo một đối tượng Employee mô tả Phuong. Employee phuong = new Employee("Phuong", 23, "HCM"); // Hiển thị Phuong. Console.WriteLine(phuong); // Tuần tự hóa Phuong với đích là một miền ứng dụng khác. // Địa chỉ của Phuong sẽ được tuần tự hóa. Stream str = File.Create("phuong.bin"); BinaryFormatter bf = new BinaryFormatter(); bf.Context = new StreamingContext(StreamingContextStates.CrossAppDomain); bf.Serialize(str, phuong); str.Close(); // Giải tuần tự hóa và hiển thị Phuong. str = File.OpenRead("phuong.bin"); 614 Chương 16: Các giao diện và mẫu thông dụng bf = new BinaryFormatter(); phuong = (Employee)bf.Deserialize(str); str.Close(); Console.WriteLine(phuong); // Tuần tự hóa Phuong với đích là file. Trong trường hợp này, // địa chỉ của Phuong sẽ không được tuần tự hóa. str = File.Create("phuong.bin"); bf = new BinaryFormatter(); bf.Context = new StreamingContext(StreamingContextStates.File); bf.Serialize(str, phuong); str.Close(); // Giải tuần tự hóa và hiển thị Phuong. str = File.OpenRead("phuong.bin"); bf = new BinaryFormatter(); phuong = (Employee)bf.Deserialize(str); str.Close(); Console.WriteLine(phuong); Console.ReadLine(); } } 2. 2. Hi n th c ki u kh -sao-chép (cloneable type)ệ ự ể ả Hi n th c ki u kh -sao-chép (cloneable type)ệ ự ể ả Bạn cần tạo một kiểu tùy biến cung cấp một cơ chế đơn giản để lập trình viên tạo bản sao cho các thể hiện của kiểu. Hiện thực giao diện System.ICloneable . Khi gán một kiểu giá trị sang một kiểu giá trị khác là bạn đã tạo một bản sao của giá trị đó. Không có mối liên hệ nào giữa hai giá trị—một thay đổi trên giá trị này sẽ không ảnh hưởng đến giá trị kia. Tuy nhiên, khi gán một kiểu tham chiếu sang một kiểu tham chiếu khác (ngoại trừ chuỗi—được bộ thực thi xử lý đặc biệt), bạn không tạo một bản sao mới của kiểu tham chiếu. Thay vào đó, cả hai kiểu tham chiếu đều chỉ đến cùng một đối tượng, và những thay đổi trên giá trị của đối tượng đều được phản ánh trong cả hai tham chiếu. Để tạo một bản sao thật của một kiểu tham chiếu, bạn phải “nhái” lại đối tượng mà nó chỉ đến. 615 Chương 16: Các giao diện và mẫu thông dụng Giao diện ICloneable nhận dạng một kiểu là khả-sao-chép và khai báo phương thức Clone là một cơ chế mà thông qua đó, bạn có thể thu lấy bản sao của một đối tượng. Phương thức Clone không nhận đối số nào và trả về một System.Object , bất chấp kiểu đang hiện thực là gì. Điều này nghĩa là một khi đã sao một đối tượng, bạn phải ép bản sao về đúng kiểu. Cách hiện thực phương thức Clone cho một kiểu tùy biến tùy thuộc vào các thành viên dữ liệu được khai báo bên trong kiểu. Nếu kiểu tùy biến chỉ chứa các thành viên dữ liệu kiểu giá trị ( int , byte ) và System.String , bạn có thể hiện thực phương thức Clone bằng cách tạo một đối tượng mới và thiết lập các thành viên dữ liệu của nó có giá trị giống như đối tượng hiện tại. Lớp Object (tất cả các kiểu đều dẫn xuất từ đây) chứa phương thức MemberwiseClone dùng để tự động hóa quá trình này. Ví dụ dưới đây trình bày một lớp đơn giản có tên là Employee , chỉ chứa các thành viên chuỗi. Do đó, phương thức Clone dựa vào phương thức thừa kế MemberwiseClone để tạo bản sao. using System; public class Employee : ICloneable { public string Name; public string Title; // Phương thức khởi dựng đơn giản. public Employee(string name, string title) { Name = name; Title = title; } // Tạo một bản sao bằng phương thức Object.MemberwiseClone // vì lớp Employee chỉ chứa các tham chiếu chuỗi. public object Clone() { return MemberwiseClone(); } } Nếu kiểu tùy biến của bạn có chứa các thành viên dữ liệu kiểu tham chiếu, bạn phải quyết định xem phương thức Clone của bạn sẽ thực hiện một bản sao cạn (shallow copy) hay một bản sao sâu (deep copy). Bản sao cạn nghĩa là bất kỳ thành viên dữ liệu kiểu tham chiếu nào trong bản sao đều sẽ chỉ đến đối tượng giống như thành viên dữ liệu kiểu tham chiếu tương ứng trong đối tượng gốc. Bản sao sâu nghĩa là bạn phải sao toàn bộ đồ thị đối tượng (object graph) để các thành viên dữ liệu kiểu tham chiếu của bản sao chỉ đến các bản sao (độc lập về mặt vật lý) của các đối tượng được tham chiếu bởi đối tượng gốc. 616 Chương 16: Các giao diện và mẫu thông dụng Dễ dàng hiện thực một bản sao cạn—sử dụng phương thức MemberwiseClone vừa được mô tả. Tuy nhiên, một bản sao sâu thường là cái mà lập trình viên mong đợi khi lần đầu tiên sao một đối tượng—nhưng hiếm khi là cái họ lấy. Điều này đặc biệt đúng đối với các lớp tập hợp trong không gian tên System.Collections , tất cả đều hiện thực bản sao cạn trong các phương thức Clone của chúng. Mặc dù sẽ có ích nếu các tập hợp này hiện thực bản sao sâu, có hai lý do chính để các kiểu (đặc biệt là các lớp tập hợp) không hiện thực bản sao sâu: • Việc tạo bản sao của một đồ thị đối tượng lớn sẽ tốn nhiều bộ nhớ và thời gian xử lý. • Các tập hợp thông thường có thể chứa các đồ thị đối tượng sâu và rộng, bao gồm bất kỳ kiểu đối tượng nào. Việc tạo một hiện thực bản sao sâu để phục vụ nhiều thứ như thế là không khả thi vì một số đối tượng trong tập hợp có thể không phải là khả-sao-chép, và một số khác có thể chứa các tham chiếu vòng, khiến quá trình sao chép trở thành một vòng lặp vô tận. Đối với các tập hợp kiểu mạnh, trong đó bản chất của các phần tử được hiểu và được kiểm soát thì một bản sao sâu có thể là một tính năng rất hữu ích. Ví dụ, System.Xml.XmlNode hiện thực một bản sao sâu trong phương thức Clone , điều này cho phép bạn tạo đúng bản sao của toàn bộ hệ thống phân cấp đối tượng XML chỉ với một lệnh đơn. Nếu cần sao một đối tượng không hiện thực ICloneable nhưng lại là khả-tuần-tự- hóa, bạn có thể tuần tự hóa rồi giải tuần tự hóa đối tượng đó để có được cùng kết quả như khi sao chép. Tuy nhiên, quá trình tuần tự hóa có thể không tuần tự hóa tất cả các thành viên dữ liệu (như đã được thảo luận trong mục 16.1). Cũng vậy, nếu tạo một kiểu khả-tuần-tự-hóa tùy biến, bạn có thể sử dụng quá trình tuần tự hóa vừa được mô tả để thực hiện một bản sao sâu bên trong phương thức ICloneable.Clone . Để sao một đối tượng khả-tuần-tự-hóa, bạn hãy sử dụng lớp System.Runtime.Serialization.Formatters.Binary.BinaryFormatter để tuần tự hóa đối tượng này thành một đối tượng System.IO.MemoryStream , và rồi giải tuần tự hóa đối tượng này từ System.IO.MemoryStream . Lớp Team dưới đây hiện thực phương thức Clone thực hiện một bản sao sâu. Lớp Team chứa một tập hợp các đối tượng Employe mô tả một nhóm người. Khi bạn gọi phương thức Clone của một đối tượng Team , phương thức này sẽ tạo bản sao của mỗi đối tượng Employee và thêm nó vào đối tượng Team được sao. Lớp Team cung cấp một phương thức khởi dựng private để đơn giản hóa mã lệnh trong phương thức Clone (sử dụng phương thức khởi dựng là cách thông thường để đơn giản hóa quá trình sao chép). using System; using System.Collections; public class Team : ICloneable { public ArrayList TeamMembers = new ArrayList(); public Team() { 617 Chương 16: Các giao diện và mẫu thông dụng } // Phương thức khởi dựng private — được phương thức Clone gọi // để tạo một đối tượng Team mới và đổ vào ArrayList của nó // bản sao của các đối tượng Employee từ một ArrayList có trước. private Team(ArrayList members) { foreach (Employee e in members) { TeamMembers.Add(e.Clone()); } } // Thêm một đối tượng Employee vào Team. public void AddMember(Employee member) { TeamMembers.Add(member); } public object Clone() { // Tạo một bản sao sâu của Team bằng cách gọi phương thức // khởi dựng Team và truyền cho nó ArrayList chứa // các thành viên của Team. return new Team(this.TeamMembers); // Lệnh này sẽ tạo một bản sao cạn của Team: // return MemberwiseClone(); } } 3. 3. Hi n th c ki u kh -so-sánh (comparable type)ệ ự ể ả Hi n th c ki u kh -so-sánh (comparable type)ệ ự ể ả Bạn cần một cơ chế dùng để so sánh các kiểu tùy biến, cho phép bạn dễ dàng sắp xếp tập hợp chứa các thể hiện của kiểu này. Để cung cấp một cơ chế so sánh chuẩn cho một kiểu, hiện thực giao diện System.IComparable . Để hỗ trợ nhiều dạng so sánh, tạo riêng từng kiểu trợ giúp ( helper ) và các kiểu này hiện thực giao diện System.Collections.IComparer . 618 Chương 16: Các giao diện và mẫu thông dụng Nếu muốn sắp xếp kiểu của bạn chỉ theo một thứ tự nào đó (như ID tăng dần, hay tên theo thứ tự alphabet), bạn nên hiện thực giao diện IComparable . Giao diện này định nghĩa phương thức CompareTo như sau: int CompareTo(object obj); Đối tượng ( obj ) được truyền cho phương thức phải cùng kiểu với đối tượng đang gọi, nếu không CompareTo sẽ ném ngoại lệ System.ArgumentException . Giá trị do CompareTo trả về được tính như sau: • Nếu đối tượng hiện tại nhỏ hơn obj , trả về một số âm (chẳng hạn, -1). • Nếu đối tượng hiện tại có cùng giá trị như obj , trả về zero. • Nếu đối tượng hiện tại lớn hơn obj , trả về một số dương (chẳng hạn, 1). Phép so sánh này thực hiện điều gì là tùy thuộc vào kiểu đã hiện thực giao diện IComparable . Ví dụ, nếu muốn sắp xếp dựa theo tên, bạn cần thực hiện phép so sánh chuỗi ( String ). Tuy nhiên, nếu muốn sắp xếp dựa theo ngày sinh, bạn cần thực hiện phép so sánh ngày ( System.DateTime ). Để hỗ trợ nhiều dạng sắp xếp cho một kiểu cụ thể, bạn phải hiện thực riêng rẽ từng kiểu trợ giúp và các kiểu này hiện thực giao diện IComparer . Giao diện này định nghĩa phương thức Compare như sau: int Compare(object x, object y); Kiểu trợ giúp phải đóng gói logic cần thiết để so sánh hai đối tượng và trả về một giá trị dựa trên logic như sau: • Nếu x nhỏ hơn y , trả về một số âm (chẳng hạn, -1). • Nếu x có cùng giá trị như y , trả về zero. • Nếu x lớn hơn y , trả về một số dương (chẳng hạn, 1). Lớp Newspaper dưới đây hiện thực cả giao diện IComparable và IComparer . Phương thức Newspaper.CompareTo thực hiện phép so sánh không phân biệt chữ hoa-thường hai đối tượng Newspaper dựa trên trường name của chúng. Một lớp private lồng bên trong có tên là AscendingCirculationComparer hiện thực IComparer và so sánh hai đối tượng Newspaper dựa trên trường circulation của chúng. Đối tượng AscendingCirculationComparer được thu lấy bằng thuộc tính tĩnh Newspaper.CirculationSorter . using System; using System.Collections; public class Newspaper : IComparable { private string name; private int circulation; 619 Chương 16: Các giao diện và mẫu thông dụng private class AscendingCirculationComparer : IComparer { int IComparer.Compare(object x, object y) { // Xử lý các tham chiếu null. // Null được coi như nhỏ hơn bất cứ giá trị nào khác. if (x == null && y == null) return 0; else if (x == null) return -1; else if (y == null) return 1; // Trường hợp x và y tham chiếu đến cùng một đối tượng. if (x == y) return 0; // Bảo đảm x và y đều là các thể hiện của Newspaper. Newspaper newspaperX = x as Newspaper; if (newspaperX == null) { throw new ArgumentException("Invalid object type", "x"); } Newspaper newspaperY = y as Newspaper; if (newspaperY == null) { throw new ArgumentException("Invalid object type", "y"); } // So sánh circulation. IComparer quy định rằng: // trả về một số âm nếu x < y // trả về zero nếu x = y // trả về một số dương nếu x > y // Dễ dàng hiện thực logic này bằng phép tính số nguyên. return newspaperX.circulation - newspaperY.circulation; } } public Newspaper(string name, int circulation) { 620 Chương 16: Các giao diện và mẫu thông dụng this.name = name; this.circulation = circulation; } // Khai báo một thuộc tính chỉ-đọc, trả về một thể hiện của // AscendingCirculationComparer. public static IComparer CirculationSorter{ get { return new AscendingCirculationComparer(); } } public override string ToString() { return string.Format("{0}: Circulation = {1}", name, circulation); } // Phương thức CompareTo so sánh hai đối tượng Newspaper dựa trên // phép so sánh trường name (không phân biệt chữ hoa-thường). public int CompareTo(object obj) { // Một đối tượng luôn được coi như lớn hơn null. if (obj == null) return 1; // Trường hợp đối tượng kia là một tham chiếu đến đối tượng này. if (obj == this) return 0; // Ép đối tượng kia về Newspaper. Newspaper other = obj as Newspaper; // Nếu "other" là null, nó không phải là một thể hiện của // Newspaper. Trong trường hợp này, CompareTo phải ném // ngoại lệ System.ArgumentException. if (other == null) { throw new ArgumentException("Invalid object type", "obj"); } else { . cùng tiến trình, nhưng không cần nếu dữ liệu sẽ được ghi ra file. Trong quá trình giải tuần tự hóa, formatter sẽ gọi phương thức khởi dựng việc giải tuần tự hóa, lại truyền cho nó các tham chiếu. (object graph) để các thành viên dữ liệu kiểu tham chiếu của bản sao chỉ đến các bản sao (độc lập về mặt vật lý) của các đối tượng được tham chiếu bởi đối tượng gốc. 616 Chương 16: Các giao diện. thực phương thức Clone bằng cách tạo một đối tượng mới và thiết lập các thành viên dữ liệu của nó có giá trị giống như đối tượng hiện tại. Lớp Object (tất cả các kiểu đều dẫn xuất từ đây)