Giáo trình Ngôn ngữ lập trình C# (Phần 2)

pdf 214 trang ngocly 100
Bạn đang xem 20 trang mẫu của tài liệu "Giáo trình Ngôn ngữ lập trình C# (Phần 2)", để tải tài liệu gốc về máy bạn click vào nút DOWNLOAD ở trên

Tài liệu đính kèm:

  • pdfgiao_trinh_ngon_ngu_lap_trinh_c_phan_2.pdf

Nội dung text: Giáo trình Ngôn ngữ lập trình C# (Phần 2)

  1. Ngôn Ngữ Lập Trình C# Chương 8 THỰC THI GIAO DIỆN Thực thi giao diện Thực thi nhiều giao diện Mở rộng giao diện Kết hợp các giao diện Truy cập phương thức giao diện Gán đối tượng cho một giao diện Toán tử is Toán tử as Giao diện đối lập với trừu tượng Thực thi phủ quyết giao diện Thực thi giao diện tường minh Lựa chọn thể hiện phương thức giao diện Ẩ n thành viên Câu hỏi & bài tập Giao diện là ràng buộc, giao ước đảm bảo cho các lớp hay các cấu trúc sẽ thực hiện một điều gì đó. Khi một lớp thực thi một giao diện, thì lớp này báo cho các thành phần client biết rằng lớp này có hỗ trợ các phương thức, thuộc tính, sự kiện và các chỉ mục khai báo trong giao diện. Một giao diện đưa ra một sự thay thế cho các lớp trừu tượng để tạo ra các sự ràng buộc giữa những lớp và các thành phần client của nó. Những ràng buộc này được khai báo bằng cách sử dụng từ khóa interface, từ khóa này khai báo một kiểu dữ liệu tham chiếu để đóng gói các ràng buộc. Một giao diện thì giống như một lớp chỉ chứa các phương thức trừu tượng. Một lớp trừu tượng được dùng làm lớp cơ sở cho một họ các lớp dẫn xuất từ nó. Trong khi giao diện là sự trộn lẫn với các cây kế thừa khác. 176 Thực Thi Giao Diện
  2. Ngôn Ngữ Lập Trình C# Khi một lớp thực thi một giao diện, lớp này phải thực thi tất cả các phương thức của giao diện. Đây là một bắt buộc mà các lớp phải thực hiện. Trong chương này chúng ta sẽ thảo luận cách tạo, thực thi và sử dụng các giao diện. Ngoài ra chúng ta cũng sẽ bàn tới cách thực thi nhiều giao diện cùng với cách kết hợp và mở rộng giao diện. Và cuối cùng là các minh họa dùng để kiểm tra khi một lớp thực thi một giao diện. Thực thi một giao diện Cú pháp để định nghĩa một giao diện như sau: [thuộc tính] [bổ sung truy cập] interface [: danh sách cơ sở] { } Phần thuộc tính chúng ta sẽ đề cập sau. Thành phần bổ sung truy cập bao gồm: public, private, protected, internal, và protected internal đã được nói đến trong Chương 4, ý nghĩa tương tự như các bổ sung truy cập của lớp. Theo sau từ khóa interface là tên của giao diện. Thông thường tên của giao diện được bắt đầu với từ I hoa (điều này không bắt buộc nhưng việc đặt tên như vậy rất rõ ràng và dễ hiểu, tránh nhầm lẫn với các thành phần khác). Ví dụ một số giao diện có tên như sau: IStorable, ICloneable, Danh sách cơ sở là danh sách các giao diện mà giao diện này mở rộng, phần này sẽ được trình bày trong phần thực thi nhiều giao diện của chương. Phần thân của giao diện chính là phần thực thi giao diện sẽ được trình bày bên dưới. Giả sử chúng ta muốn tạo một giao diện nhằm mô tả những phương thức và thuộc tính của một lớp cần thiết để lưu trữ và truy cập từ một cơ sở dữ liệu hay các thành phần lưu trữ dữ liệu khác như là một tập tin. Chúng ta quyết định gọi giao diện này là IStorage. Trong giao diện này chúng ta xác nhận hai phương thức: Read() và Write(), khai báo này sẽ được xuất hiện trong phần thân của giao diện như sau: interface IStorable { void Read(); void Write(object); } Mục đích của một giao diện là để định nghĩa những khả năng mà chúng ta muốn có trong một lớp. Ví dụ, chúng ta có thể tạo một lớp tên là Document, lớp này lưu trữ các dữ liệu trong cơ sở dữ liệu, do đó chúng ta quyết định lớp này này thực thi giao diện IStorable. Để làm được điều này, chúng ta sử dụng cú pháp giống như việc tạo một lớp mới Document được thừa kế từ IStorable bằng dùng dấu hai chấm (:) và theo sau là tên giao diện: 177 Thực Thi Giao Diện
  3. Ngôn Ngữ Lập Trình C# public class Document : IStorable { public void Read() { } public void Write() { } } Bây giờ trách nhiệm của chúng ta, với vai trò là người xây dựng lớp Document phải cung cấp một thực thi có ý nghĩa thực sự cho những phương thức của giao diện IStorable. Chúng ta phải thực thi tất cả các phương thức của giao diện, nếu không trình biên dịch sẽ báo một lỗi. Sau đây là đoạn chương trình minh họa việc xây dựng lớp Document thực thi giao diện IStorable. Ví dụ 8.1: Sử dụng một giao diện. using System; // khai báo giao diện interface IStorable { // giao diện không khai báo bổ sung truy cập // phương thức là public và không thực thi void Read(); void Write(object obj); int Status { get; set; } } // tạo một lớp thực thi giao diện IStorable public class Document : IStorable { public Document( string s) { Console.WriteLine(“Creating document with: {0}”, s); 178 Thực Thi Giao Diện
  4. Ngôn Ngữ Lập Trình C# } // thực thi phương thức Read() public void Read() { Console.WriteLine(“Implement the Read Method for IStorable”); } // thực thi phương thức Write public void Write( object o) { Console.WriteLine(“Impleting the Write Method for IStorable”); } // thực thi thuộc tính public int Status { get { return status; } set { status = value; } } // lưu trữ giá trị thuộc tính private int status = 0; } public class Tester { static void Main() { // truy cập phương thức trong đối tượng Document Document doc = new Document(“Test Document”); doc.Status = -1; doc.Read(); Console.WriteLine(“Document Status: {0}”, doc.Status); // gán cho một giao diện và sử dụng giao diện IStorable isDoc = (IStorable) doc; isDoc.Status = 0; 179 Thực Thi Giao Diện
  5. Ngôn Ngữ Lập Trình C# isDoc.Read(); Console.WriteLine(“IStorable Status: {0}”, isDoc.Status); } }  Kết quả: Creating document with: Test Document Implementing the Read Method for IStorable Document Status: -1 Implementing the Read Method for IStorable IStorable Status: 0 Ví dụ 8.1 định nghĩa một giao diện IStorable với hai phương thức Read(), Write() và một thuộc tính tên là Status có kiểu là số nguyên Lưu ý rằng trong phần khai báo thuộc tính không có phần thực thi cho get() và set() mà chỉ đơn giản là khai báo có hành vi là get() và set(): int Status { get; set;} Ngoài ra phần định nghĩa các phương thức của giao diện không có phần bổ sung truy cập (ví dụ như: public, protected, internal, private). Việc cung cấp các bổ sung truy cập sẽ tạo ra một lỗi. Những phương thức của giao diện được ngầm định là public vì giao diện là những ràng buộc được sử dụng bởi những lớp khác. Chúng ta không thể tạo một thể hiện của giao diện, thay vào đó chúng ta sẽ tạo thể hiện của lớp có thực thi giao diện. Một lớp thực thi giao diện phải đáp ứng đầy đủ và chính xác các ràng buộc đã khai báo trong giao diện. Lớp Document phải cung cấp cả hai phương thức Read() và Write() cùng với thuộc tính Status. Tuy nhiên cách thực hiện những yêu cầu này hoàn toàn phụ thuộc vào lớp Document. Mặc dù IStorage chỉ ra rằng lớp Document phải có một thuộc tính là Status nhưng nó không biết hay cũng không quan tâm đến việc lớp Document lưu trữ trạng thái thật sự của các biến thành viên, hay việc tìm kiếm trong cơ sở dữ liệu. Những chi tiết này phụ thuộc vào phần thực thi của lớp. Thực thi nhiều giao diện Trong ngôn ngữ C# cho phép chúng ta thực thi nhiều hơn một giao diện. Ví dụ, nếu lớp Document có thể được lưu trữ và dữ liệu cũng được nén. Chúng ta có thể chọn thực thi cả hai giao diện IStorable và ICompressible. Như vậy chúng ta phải thay đổi phần khai báo trong danh sách cơ sở để chỉ ra rằng cả hai giao diện điều được thực thi, sử dụng dấu phẩy (,) để phân cách giữa hai giao diện: public class Document : IStorable, ICompressible 180 Thực Thi Giao Diện
  6. Ngôn Ngữ Lập Trình C# Do đó Document cũng phải thực thi những phương thức được xác nhận trong giao diện ICompressible: public void Compress() { Console.WriteLine(“Implementing the Compress Method”); } public void Decompress() { Console.WriteLine(“Implementing the Decompress Method”); } Bổ sung thêm phần khai báo giao diện ICompressible và định nghĩa các phương thức của giao diện bên trong lớp Document. Sau khi tạo thể hiện lớp Document và gọi các phương thức từ giao diện ta có kết quả tương tự như sau: Creating document with: Test Document Implementing the Read Method for IStorable Implementing Compress Mở rộng giao diện C# cung cấp chức năng cho chúng ta mở rộng một giao diện đã có bằng cách thêm các phương thức và các thành viên hay bổ sung cách làm việc cho các thành viên. Ví dụ, chúng ta có thể mở rộng giao diện ICompressible với một giao diện mới là ILoggedCompressible. Giao diện mới này mở rộng giao diện cũ bằng cách thêm phương thức ghi log các dữ liệu đã lưu: interface ILoggedCompressible : ICompressible { void LogSavedBytes(); } Các lớp khác có thể thực thi tự do giao diện ICompressible hay ILoggedCompressible tùy thuộc vào mục đích có cần thêm chức năng hay không. Nếu một lớp thực thi giao diện ILoggedCompressible, thì lớp này phải thực thi tất cả các phương thức của cả hai giao diện ICompressible và giao diện ILoggedCompressible. Những đối tượng của lớp thực thi giao diện ILoggedCompressible có thể được gán cho cả hai giao diện ILoggedCompressible và ICompressible. Kết hợp các giao diện Một cách tương tự, chúng ta có thể tạo giao diện mới bằng cách kết hợp các giao diện cũ và ta có thể thêm các phương thức hay các thuộc tính cho giao diện mới. Ví dụ, chúng ta quyết định tạo một giao diện IStorableCompressible. Giao diện mới này sẽ kết hợp những 181 Thực Thi Giao Diện
  7. Ngôn Ngữ Lập Trình C# phương thức của cả hai giao diện và cũng thêm vào một phương thức mới để lưu trữ kích thước nguyên thuỷ của các dữ liệu trước khi nén: interface IStorableCompressible : IStoreable, ILoggedCompressible { void LogOriginalSize();  } Ví dụ 8.2: Minh họa việc mở rộng và kết hợp các giao diện. using System; interface IStorable { void Read(); void Write(object obj); int Status { get; set;} } // giao diện mới interface ICompressible { void Compress(); void Decompress(); } // mở rộng giao diện interface ILoggedCompressible : ICompressible { void LogSavedBytes(); } // kết hợp giao diện interface IStorableCompressible : IStorable, ILoggedCompressible { void LogOriginalSize(); } interface IEncryptable { void Encrypt(); void Decrypt(); } public class Document : IStorableCompressible, IEncryptable { 182 Thực Thi Giao Diện
  8. Ngôn Ngữ Lập Trình C# // bộ khởi tạo lớp Document lấy một tham số public Document( string s) { Console.WriteLine(“Creating document with: {0}”, s); } // thực thi giao diện IStorable public void Read() { Console.WriteLine(“Implementing the Read Method for IStorable”); } public void Write( object o) { Console.WriteLine(“Implementing the Write Method for IStorable”); } public int Status { get { return status; } set { status = value; } } // thực thi ICompressible public void Compress() { Console.WriteLine(“Implementing Compress”); } public void Decompress() { Console.WriteLine(“Implementing Decompress”); } // thực thi giao diện ILoggedCompressible public void LogSavedBytes() { Console.WriteLine(“Implementing LogSavedBytes”); 183 Thực Thi Giao Diện
  9. Ngôn Ngữ Lập Trình C# } // thực thi giao diện IStorableCompressible public void LogOriginalSize() { Console.WriteLine(“Implementing LogOriginalSize”); } // thực thi giao diện public void Encrypt() { Console.WriteLine(“Implementing Encrypt”); } public void Decrypt() { Console.WriteLine(“Implementing Decrypt”); } // biến thành viên lưu dữ liệu cho thuộc tính private int status = 0; } public class Tester { public static void Main() { // tạo đối tượng document Document doc = new Document(“Test Document”); // gán đối tượng cho giao diện IStorable isDoc = doc as IStorable; if ( isDoc != null) { isDoc.Read(); } else { Console.WriteLine(“IStorable not supported”); } ICompressible icDoc = doc as ICompressible; if ( icDoc != null ) { icDoc.Compress(); 184 Thực Thi Giao Diện
  10. Ngôn Ngữ Lập Trình C# } else { Console.WriteLine(“Compressible not supported”); } ILoggedCompressible ilcDoc = doc as ILoggedCompressible; if ( ilcDoc != null ) { ilcDoc.LogSavedBytes(); ilcDoc.Compress(); // ilcDoc.Read(); // không thể gọi được } else { Console.WriteLine(“LoggedCompressible not supported”); } IStorableCompressible isc = doc as IStorableCompressible; if ( isc != null ) { isc.LogOriginalSize(); // IStorableCompressible isc.LogSavedBytes(); // ILoggedCompressible isc.Compress(); // ICompress isc.Read(); // IStorable } else { Console.WriteLine(“StorableCompressible not supported”); } IEncryptable ie = doc as IEncryptable; if ( ie != null ) { ie.Encrypt(); } else { Console.WriteLine(“Encryptable not supported”); } } 185 Thực Thi Giao Diện
  11. Ngôn Ngữ Lập Trình C# }  Kết quả: Creating document with: Test Document Implementing the Read Method for IStorable Implementing Compress Implementing LogSavedBytes Implementing Compress Implementing LogOriginalSize Implementing LogSaveBytes Implementing Compress Implementing the Read Method for IStorable Implementing Encrypt Ví dụ 8.2 bắt đầu bằng việc thực thi giao diện IStorable và giao diện ICompressible. Sau đó là phần mở rộng đến giao diện ILoggedCompressible rồi sau đó kết hợp cả hai vào giao diện IStorableCompressible. Và giao diện cuối cùng trong ví dụ là IEncrypt. Chương trình Tester tạo đối tượng Document mới và sau đó gán lần lượt vào các giao diện khác nhau. Khi một đối tượng được gán cho giao diện ILoggedCompressible, chúng ta có thể dùng giao diện này để gọi các phương thức của giao diện ICompressible bởi vì ILogged- Compressible mở rộng và thừa kế các phương thức từ giao diện cơ sở: ILoggedCompressible ilcDoc = doc as ILoggedCompressible; if ( ilcDoc != null ) { ilcDoc.LogSavedBytes(); ilcDoc.Compress(); // ilcDoc.Read(); // không thể gọi được } Tuy nhiên, ở đây chúng ta không thể gọi phương thức Read() bởi vì phương thức này của giao diện IStorable, không liên quan đến giao diện này. Nếu chúng ta thêm lệnh này vào thì chương trình sẽ biên dịch lỗi. Nếu chúng ta gán vào giao diện IStorableCompressible, do giao diện này kết hợp hai giao diện IStorable và giao diện ICompressible, chúng ta có thể gọi tất cả những phương thức của IStorableCompressible, ICompressible, và IStorable: IStorableCompressible isc = doc as IStorableCompressible; if ( isc != null ) { isc.LogOriginalSize(); // IStorableCompressible 186 Thực Thi Giao Diện
  12. Ngôn Ngữ Lập Trình C# isc.LogSaveBytes(); // ILoggedCompressible isc.Compress(); // ICompress isc.Read(); // IStorable } Truy cập phương thức giao diện Chúng ta có thể truy cập những thành viên của giao diện IStorable như thể là các thành viên của lớp Document: Document doc = new Document(“Test Document”); doc.status = -1; doc.Read(); hay là ta có thể tạo thể hiện của giao diện bằng cách gán đối tượng Document cho một kiểu dữ liệu giao diện, và sau đó sử dụng giao diện này để truy cập các phương thức: IStorable isDoc = (IStorable) doc; isDoc.status = 0; isDoc.Read(); Ghi chú: Cũng như đã nói trước đây, chúng ta không thể tạo thể hiện của giao diện một cách trực tiếp.Do đó chúng ta không thể thực hiện như sau: IStorable isDoc = new IStorable(); Tuy nhiên chúng ta có thể tạo thể hiện của lớp thực thi như sau: Document doc = new Document(“Test Document”); Sau đó chúng ta có thể tạo thể hiện của giao diện bằng cách gán đối tượng thực thi đến kiểu dữ liệu giao diện, trong trường hợp này là IStorable: IStorable isDoc = (IStorable) doc; Chúng ta có thể kết hợp những bước trên như sau: IStorable isDoc = (IStorable) new Document(“Test Document”); Nói chung, cách thiết kế tốt nhất là quyết định truy cập những phương thức của giao diện thông qua tham chiếu của giao diện. Do vậy cách tốt nhất là sử dụng isDoc.Read(), hơn là sử dụng doc.Read() trong ví dụ trước. Truy cập thông qua giao diện cho phép chúng ta đối xử giao diện một cách đa hình. Nói cách khác, chúng ta tạo hai hay nhiều hơn những lớp thực thi giao diện, và sau đó bằng cách truy cập lớp này chỉ thông qua giao diện. Gán đối tượng cho một giao diện Trong nhiều trường hợp, chúng ta không biết trước một đối tượng có hỗ trợ một giao diện đưa ra. Ví dụ, giả sử chúng ta có một tập hợp những đối tượng Document, một vài đối tượng đã được lưu trữ và số còn lại thì chưa. Và giả sử chúng ta đã thêm giao diện giao diện thứ hai, ICompressible cho những đối tượng để nén dữ liệu và truyền qua mail nhanh chóng: interface ICompressible { 187 Thực Thi Giao Diện
  13. Ngôn Ngữ Lập Trình C# void Compress(); void Decompress(); } Nếu đưa ra một kiểu Document, và ta cũng không biết là lớp này có hỗ trợ giao diện IStorable hay ICompressible hoặc cả hai. Ta có thể có đoạn chương trình sau: Document doc = new Document(“Test Document”); IStorable isDoc = (IStorable) doc; isDoc.Read(); ICompressible icDoc = (ICompressible) doc; icDoc.Compress(); Nếu Document chỉ thực thi giao diện IStorable: public class Document : IStorable phép gán cho ICompressible vẫn được biên dịch bởi vì ICompressible là một giao diện hợp lệ. Tuy nhiên, do phép gán không hợp lệ nên khi chương trình chạy thì sẽ tạo ra một ngoại lệ (exception): A exception of type System.InvalidCastException was thrown. Phần ngoại lệ sẽ được trình bày trong Chương 11. Toán tử is Chúng ta muốn kiểm tra một đối tượng xem nó có hỗ trợ giao diện, để sau đó thực hiện các phương thức tương ứng. Trong ngôn ngữ C# có hai cách để thực hiện điều này. Phương pháp đầu tiên là sử dụng toán tử is. Cú pháp của toán tử is là: is Toán tử is trả về giá trị true nếu biểu thức thường là kiểu tham chiếu có thể được gán an toàn đến kiểu dữ liệu cần kiểm tra mà không phát sinh ra bất cứ ngoại lệ nào. Ví dụ 8.3 minh họa việc sử dụng toán tử is để kiểm tra Document có thực thi giao diện IStorable hay ICompressible.  Ví dụ 8.3: Sử dụng toán tử is. using System; interface IStorable { void Read(); void Write(object obj); int Status { get; set; } } // giao diện mới 188 Thực Thi Giao Diện
  14. Ngôn Ngữ Lập Trình C# interface ICompressible { void Compress(); void Decompress(); } // Document thực thi IStorable public class Document : IStorable { public Document( string s) { Console.WriteLine(“Creating document with: {0}”, s); } // IStorable public void Read() { Console.WriteLine(“Implementing the Read Method for IStorable”); } // IStorable.WriteLine() public void Write( object o) { Console.WriteLine(“Implementing the Write Method for IStorable”); } // IStorable.Status public int Status { get { return status; } set { status = value; } } // bien thanh vien luu gia tri cua thuoc tinh Status private int status = 0; } public class Tester 189 Thực Thi Giao Diện
  15. Ngôn Ngữ Lập Trình C# { static void Main() { Document doc = new Document(“Test Document”); // chỉ gán khi an toàn if ( doc is IStorable ) { IStorable isDoc = (IStorable) doc; isDoc.Read(); } // việc kiểm tra này sẽ sai if ( doc is ICompressible ) { ICompressible icDoc = (ICompressible) doc; icDoc.Compress(); } } } Trong ví dụ 8.3, hàm Main() lúc này sẽ thực hiện việc gán với interface khi được kiểm tra hợp lệ. Việc kiểm tra này được thực hiện bởi câu lệnh if: if ( doc is IStorable ) Biểu thức điều kiện sẽ trả về giá trị true và phép gán sẽ được thực hiện khi đối tượng có thực thi giao diện bên phải của toán tử is. Tuy nhiên, việc sử dụng toán tử is đưa ra một việc không có hiệu quả. Để hiểu được điều này, chúng ta xem đoạn chương trình được biên dịch ra mã IL. Ở đây sẽ có một ngoại lệ nhỏ, các dòng bên dưới là sử dụng hệ thập lục phân: IL_0023: isinst ICompressible IL_0028: brfalse.s IL_0039 IL_002a: ldloc.0 IL_002b: castclass ICompressible IL_0030: stloc.2 IL_0031: ldloc.2 IL_0032: callvirt instance void ICompressible::Compress() IL_0037: br.s IL_0043 IL_0039: ldstr “Compressible not supported” Điều quan trọng xảy ra là khi phép kiểm tra ICompressible ở dòng 23. Từ khóa isinst là mã MSIL tương ứng với toán tử is. Nếu việc kiểm tra đối tượng (doc) đúng kiểu của kiểu bên 190 Thực Thi Giao Diện
  16. Ngôn Ngữ Lập Trình C# phải. Thì chương trình sẽ chuyển đến dòng lệnh 2b để thực hiện tiếp và castclass được gọi. Điều không may là castcall cũng kiểm tra kiểu của đối tượng. Do đó việc kiểm tra sẽ được thực hiện hai lần. Giải pháp hiệu quả hơn là việc sử dụng toán tử as. Toán tử as Toán tử as kết hợp toán tử is và phép gán bằng cách đầu tiên kiểm tra hợp lệ phép gán (kiểm tra toán tử is trả về true) rồi sau đó phép gán được thực hiện. Nếu phép gán không hợp lệ (khi phép gán trả ề giá trị false), thì toán tử as trả về giá trị null. Ghi chú: Từ khóa null thể hiện một tham chiếu không tham chiếu đến đâu cả (null reference). Đối tượng có giá trị null tức là không tham chiếu đến bất kỳ đối tượng nào. Sử dụng toán tử as để loại bỏ việc thực hiện các xử lý ngoại lệ. Đồng thời cũng né tránh việc thực hiện kiểm tra dư thừa hai lần. Do vậy, việc sử dụng tối ưu của phép gán cho giao diện là sử dụng as. Cú pháp sử dụng toán tử as như sau: as Đoạn chương trình sau thay thế việc sử dụng toán tử is bằng toán tử as và sau đó thực hiện việc kiểm tra xem giao diện được gán có null hay không: static void Main() { Document doc = new Document(“Test Document”); IStorable isDoc = doc as IStorable; if ( isDoc != null ) { isDoc.Read(); } else { Console.WriteLine(“IStorable not supported”); } ICompressible icDoc = doc as ICompressible; if ( icDoc != null) { icDoc.Compress(); } else { Console.WriteLine(“Compressible not supported”); } 191 Thực Thi Giao Diện
  17. Ngôn Ngữ Lập Trình C# } Ta có thể so sánh đoạn mã IL sau với đoạn mã IL sử dụng toán tử is trước sẽ thấy đoạn mã sau có nhiều hiệu quả hơn: IL_0023: isinst ICompressible IL_0028: stloc.2 IL_0029: ldloc.2 IL_002a: brfalse.s IL_0034 IL_002c: ldloc.2 IL_002d: callvirt instance void ICompressible::Compress() Ghi chú: Nếu mục đích của chúng ta là kiểm tra một đối tượng có hỗ trợ một giao diện và sau đó là thực hiện việc gán cho một giao diện, thì cách tốt nhất là sử dụng toán tử as là hiệu quả nhất. Tuy nhiên, nếu chúng ta chỉ muốn kiểm tra kiểu dữ liệu và không thực hiện phép gán ngay lúc đó. Có lẽ chúng ta chỉ muốn thực hiện việc kiểm tra nhưng không thực hiện việc gán, đơn giản là chúng ta muốn thêm nó vào danh sách nếu chúng thực sự là một giao diện. Trong trường hợp này, sử dụng toán tử is là cách lựa chọn tốt nhất. Giao diện đối lập với lớp trừu tượng Giao diện rất giống như các lớp trừu tượng. Thật vậy, chúng ta có thể thay thế khai báo của IStorable trở thành một lớp trừu tượng: abstract class Storable { abstract public void Read(); abstract public void Write(); } Bây giờ lớp Document có thể thừa kế từ lớp trừu tượng IStorable, và cũng không có gì khác nhiều so với việc sử dụng giao diện. Tuy nhiên, giả sử chúng ta mua một lớp List từ một hãng thứ ba và chúng ta muốn kết hợp với lớp có sẵn như Storable. Trong ngôn ngữ C++ chúng ta có thể tạo ra một lớp StorableList kế thừa từ List và cả Storable. Nhưng trong ngôn ngữ C# chúng ta không thể làm được, chúng ta không thể kế thừa từ lớp trừu tượng Storable và từ lớp List bởi vì trong C# không cho phép thực hiện đa kế thừa từ những lớp. Tuy nhiên, ngôn ngữ C# cho phép chúng ta thực thi bất cứ những giao diện nào và dẫn xuất từ một lớp cơ sở. Do đó, bằng cách làm cho Storable là một giao diện, chúng ta có thể kế thừa từ lớp List và cũng từ IStorable. Ta có thể tạo lớp StorableList như sau: public class StorableList : List, IStorable { // phương thức List 192 Thực Thi Giao Diện
  18. Ngôn Ngữ Lập Trình C# public void Read() { } public void Write( object o) { } // } Thực thi phủ quyết giao diện Khi thực thi một lớp chúng ta có thể tự do đánh dấu bất kỳ hay tất cả các phương thức thực thi giao diện như là một phương thức ảo. Ví dụ, lớp Document thực thi giao diện IStorable và có thể đánh dấu các phương thức Read() và Write() như là phương thức ảo. Lớp Document có thể đọc và viết nội dung của nó vào một kiểu dữ liệu File. Những người phát triển sau có thể dẫn xuất một kiểu dữ liệu mới từ lớp Document, có thể là lớp Note hay lớp EmailMessage, và những người này mong muốn lớp Note đọc và viết vào cơ sở dữ liệu hơn là vào một tập tin. Ví dụ 8.4 mở rộng từ ví dụ 8.3 và minh họa việc phủ quyết một thực thi giao diện. Phương thức Read() được đánh dấu như phương thức ảo và thực thi bởi Document.Read() và cuối cùng là được phủ quyết trong kiểu dữ liệu Note được dẫn xuất từ Document. Ví dụ 8.4: Phủ quyết thực thi giao diện. using System; interface IStorable { void Read(); void Write(); } // lớp Document đơn giản thực thi giao diện IStorable public class Document : IStorable { // bộ khởi dựng public Document( string s) { Console.WriteLine(“Creating document with: {0}”, s); } // đánh dấu phương thức Read ảo public virtual void Read() { Console.WriteLine(“Document Read Method for IStorable”); 193 Thực Thi Giao Diện
  19. Ngôn Ngữ Lập Trình C# } // không phải phương thức ảo public void Write() { Console.WriteLine(“Document Write Method for IStorable”); } } // lớp dẫn xuất từ Document public class Note : Document { public Note( string s) : base(s) { Console.WriteLine(“Creating note with: {0}”, s); } // phủ quyết phương thức Read() public override void Read() { Console.WriteLine(“Overriding the Read Method for Note!”); } // thực thi một phương thức Write riêng của lớp public void Write() { Console.WriteLine(“Implementing the Write method for Note!”); } } public class Tester { static void Main() { // tạo một đối tượng Document Document theNote = new Note(“Test Note”); IStorable isNote = theNote as IStorable; if ( isNote != null) { isNote.Read(); isNote.Write(); } Console.WriteLine(“\n”); 194 Thực Thi Giao Diện
  20. Ngôn Ngữ Lập Trình C# // trực tiếp gọi phương thức theNote.Read(); theNote.Write(); Console.WriteLine(“\n”); // tạo đối tượng Note Note note2 = new Note(“Second Test”); IStorable isNote2 = note2 as IStorable; if ( isNote != null ) { isNote2.Read(); isNote2.Write(); } Console.WriteLine(“\n”); // trực tiếp gọi phương thức note2.Read(); note2.Write(); } }  Kết quả: Creating document with: Test Note Creating note with: Test Note Overriding the Read method for Note! Document Write Method for IStorable Overriding the Read method for Note! Document Write Method for IStorable Creating document with: Second Test Creating note with: Second Test Overriding the Read method for Note! Document Write Method for IStorable Overriding the Read method for Note! Implementing the Write method for Note! Trong ví dụ trên, lớp Document thực thi một giao diện đơn giản là IStorable: interface IStorable 195 Thực Thi Giao Diện
  21. Ngôn Ngữ Lập Trình C# { void Read(); void Write(); } Người thiết kế của lớp Document thực thi phương thức Read() là phương thức ảo nhưng không tạo phương thức Write() tương tự như vậy: public virtual void Read() Trong ứng dụng thế giới thực, chúng ta cũng đánh dấu cả hai phương thức này là phương thức ảo. Tuy nhiên trong ví dụ này chúng ta minh họa việc người phát triển có thể tùy ý chọn các phương thức ảo của giao diện mà lớp thực thi. Một lớp mới Note dẫn xuất từ Document: public class Note : Document Việc phủ quyết phương thức Read() trong lớp Note là không cần thiết, nhưng ở đây ta tự do làm điều này: public override void Read() Trong lớp Tester, phương thức Read() và Write() được gọi theo bốn cách sau: . Thông qua lớp cơ sở tham chiếu đến đối tượng của lớp dẫn xuất . Thông qua một giao diện tạo từ lớp cơ sở tham chiếu đến đối tượng dẫn xuất . Thông qua một đối tượng dẫn xuất . Thông qua giao diện tạo từ đối tượng dẫn xuất Thực hiện cách gọi thứ nhất, một tham chiếu Document được tạo ra, và địa chỉ của một đối tượng mới là lớp dẫn xuất Note được tạo trên heap và gán trở lại cho đối tượng Document: Document theNote = new Note(“Test Note”); Môt tham chiếu giao diện được tạo ra và toán tử as được sử dụng để gán Document cho tham chiếu giao diện IStorable: IStorable isNote = theNote as IStorable; Sau đó gọi phương thức Read() và Write() thông qua giao diện. Kết xuất của phương thức Read() được thực hiện một cách đa hình nhưng phương thức Write() thì không, do đó ta có kết xuất sau: Overriding the Read method for Note! Document Write Method for IStorable Phương thức Read() và Write() cũng được gọi trực tiếp từ bản thân đối tượng: theNote.Read(); theNote.Write(); và một lần nữa chúng ta thấy việc thực thi đa hình làm việc: Overriding the Read method for Note! Document Write Method for IStorable 196 Thực Thi Giao Diện
  22. Ngôn Ngữ Lập Trình C# Trong trường hợp này, phương thức Read() của lớp Note được gọi, và phương thức Write() của lớp Document được gọi. Để chứng tỏ rằng kết quả này của phương thức phủ quyết, chúng ta tiếp tục tạo đối tượng Note thứ hai và lúc này ta gán cho một tham chiếu Note. Điều này được sử dụng để minh họa cho những trường hợp cuối cùng (gọi thông qua đối tượng dẫn xuất và gọi thông qua giao diện được tạo từ đối tượng dẫn xuất): Note note2 = new Note(“Second Test”); Một lần nữa, khi chúng ta gán cho một tham chiếu, phương thức phủ quyết Read() được gọi. Tuy nhiên, khi những phương thức được gọi trực tiếp từ đối tượng Note: note2.Read(); note2.Write(); kết quả cho ta thấy rằng cách phương thức của Note được gọi chứ không phải của một phương thức Document: Overriding the Read method for Note! Implementing the Write method dor Note! Thực thi giao diện tường minh Trong việc thực thi giao diện cho tới giờ, những lớp thực thi (trong trường hợp này là Document) tạo ra các phương thức thành viên cùng ký hiệu và kiểu trả về như là phương thức được mô tả trong giao diên. Chúng ta không cần thiết khai báo tường minh rằng đây là một thực thi của một giao diện, việc này được hiểu ngầm bởi trình biên dịch. Tuy nhiên, có vấn đề xảy ra khi một lớp thực thi hai giao diện và cả hai giao diện này có các phương thức cùng một ký hiệu. Ví dụ 8.5 tạo ra hai giao diện: IStorable và ITalk. Sau đó thực thi phương thức Read() trong giao diện ITalk để đọc ra tiếng nội dung của một cuốn sách. Không may là phương thức này sẽ tranh chấp với phương thức Read() của IStorable mà Document phải thực thi. Bởi vì cả hai phương thức IStorable và ITalk có cùng phương thức Read(),việc thực thi lớp Document phải sử dụng thực thi tường minh cho mỗi phương thức. Với việc thực thi tường minh, lớp thực thi Document sẽ khai báo tường minh cho mỗi phương thức: void ITalk.Read(); Điều này sẽ giải quyết việc tranh chấp, nhưng nó sẽ tạo ra hàng loạt các hiệu ứng thú vị. Đầu tiên, không cần thiết sử dụng thực thi tường minh với những phương thức khác của Talk: public void Talk(); vì không có sự tranh chấp cho nên ta khai báo như thông thường. Điều quan trọng là các phương thức thực thi tường minh không có bổ sung truy cập: void ITalk.Read(); Phương thức này được hiểu ngầm là public. 197 Thực Thi Giao Diện
  23. Ngôn Ngữ Lập Trình C# Thật vậy, một phương thức được khai báo tường minh thì sẽ không được khai báo với các từ khóa bổ sung truy cập: abstract, virtual, override, và new. Một địều quan trọng khác là chúng ta không thể truy cập phương thức thực thi tường minh thông qua chính đối tượng. Khi chúng ta viết: theDoc.Read(); Trình biên dịch chỉ hiểu rằng chúng ta thực thi phương thức giao diện ngầm định cho IStorable. Chỉ một cách duy nhất truy cập các phương thức thực thi tường minh là thông qua việc gán cho giao diện để thực thi: ITalk itDoc = theDoc as ITalk; if ( itDoc != null ) { itDoc.Read(); } Sử dụng thực thi tường minh được áp dụng trong ví dụ 8.5 Ví dụ 8.5: Thực thi tường minh. using System; interface IStorable { void Read(); void Write(); } interface ITalk { void Talk(); void Read(); } // lớp Document thực thi hai giao diện public class Document : IStorable, ITalk { // bộ khởi dựng public Document( string s) { Console.WriteLine(“Creating document with: {0}”,s); } // tạo phương thức ảo public virtual void Read() { 198 Thực Thi Giao Diện
  24. Ngôn Ngữ Lập Trình C# Console.WriteLine(“Implementing IStorable.Read”); } // thực thi bình thường public void Write() { Console.WriteLine(“Implementing IStorable.Write”); } // thực thi tường minh void ITalk.Read() { Console.WriteLine(“Implementing ITalk.Read”); } public void Talk() { Console.WriteLine(“Implementing ITalk.Talk”); } } public class Tester { static void Main() { // tạo đối tượng Document Document theDoc = new Document(“Test Document”); IStorable isDoc = theDoc as IStorable; if ( isDoc != null ) { isDoc.Read(); } ITalk itDoc = theDoc as ITalk; if ( itDoc != null ) { itDoc.Read(); } theDoc.Read(); theDoc.Talk(); } } 199 Thực Thi Giao Diện
  25. Ngôn Ngữ Lập Trình C#  Kết quả: Creating document with: Test Document Implementing IStorable.Read Implementing ITalk.Read Implementing IStorable.Read Implementing ITalk.Talk Lựa chọn việc thể hiện phương thức giao diện Những người thiết kế lớp có thể thu được lợi khi một giao diện được thực thi thông qua thực thi tường minh và không cho phép các thành phần client của lớp truy cập trừ phi sử dụng thông qua việc gán cho giao diện. Giả sử nghĩa của đối tượng Document chỉ ra rằng nó thực thi giao diện IStorable, nhưng không muốn phương thức Read() và Write() là phần giao diện public của lớp Document. Chúng ta có thể sử dụng thực thi tường minh để chắc chắn chỉ có thể truy cập thông qua việc gán cho giao diện. Điều này cho phép chúng ta lưu trữ ngữ nghĩa của lớp Document trong khi vẫn có thể thực thi được giao diện IStorable. Nếu thành phần client muốn đối tượng thực thi giao diện IStorable, nó có thể thực hiện gán tường minh cho giao diện để gọi các phương thức thực thi giao diện. Nhưng khi sử dụng đối tượng Document thì nghĩa là không có phương thức Read() và Write(). Thật vậy, chúng ta có thể lựa chọn thể hiện những phương thức thông qua thực thi tường minh, do đó chúng ta có thể trưng bày một vài phương thức thực thi như là một phần của lớp Document và một số phương thức khác thì không. Trong ví dụ 8.5, đối tượng Document trưng bày phương thức Talk() như là phương thức của lớp Document, nhưng phương thức Talk.Read() chỉ được thể hiện thông qua gán cho giao diện. Thậm chí nếu IStorable không có phương thức Read(), chúng ta cũng có thể chọn thực thi tường minh phương thức Read() để phương thức không được thể hiện ra bên ngoài như các phương thức của Document. Chúng ta lưu ý rằng vì thực thi giao diện tường minh ngăn ngừa việc sử dụng từ khóa virtual, một lớp dẫn xuất có thể được hỗ trợ để thực thi lại phương thức. Do đó, nếu Note dẫn xuất từ Document, nó có thể được thực thi lại phương thức Talk.Read() bởi vì lớp Document thực thi phương thức Talk.Read() không phải ảo. Ẩ n thành viên Ngôn ngữ C# cho phép ẩn các thành viên của giao diện. Ví dụ, chúng ta có một giao diện IBase với một thuộc tính P: interface IBase 200 Thực Thi Giao Diện
  26. Ngôn Ngữ Lập Trình C# { int P { get; set;} } và sau đó chúng ta dẫn xuất từ giao diện này ra một giao diện khác, IDerived, giao diện mới này làm ẩn thuộc tính P với một phương thức mới P(): interface IDerived : IBase { new int P(); } Việc cài đặt này là một ý tưởng tốt, bây giờ chúng ta có thể ẩn thuộc tính P trong lớp cơ sở. Một thực thi của giao diện dẫn xuất này đòi hỏi tối thiểu một thành viên giao diện tường minh. Chúng ta có thể sử dụng thực thi tường minh cho thuộc tính của lớp cơ sở hoặc của phương thức dẫn xuất, hoặc chúng ta có thể sử dụng thực thi tường minh cho cả hai. Do đó, ba phiên bản được viết sau đều hợp lệ: class myClass : IDerived { // thực thi tường minh cho thuộc tính cơ sở int IBase.p { get{ }} // thực thi ngầm định phương thức dẫn xuất public int P() { } } class myClass : IDerived { // thực thi ngầm định cho thuộc tính cơ sở public int P { get{ }} // thực thi tường minh phương thức dẫn xuất int IDerived.P() { } } class myClass : IDerived { // thực thi tường minh cho thuộc tính cơ sở int IBase.P { get{ }} // thực thi tường minh phương thức dẫn xuất int IDerived.P(){ } } Truy cập lớp không cho dẫn xuất và kiểu giá trị 201 Thực Thi Giao Diện
  27. Ngôn Ngữ Lập Trình C# Nói chung, việc truy cập những phương thức của một giao diện thông qua việc gán cho giao diện thì thường được thích hơn. Ngoại trừ đối với kiểu giá trị (như cấu trúc) hoặc với các lớp không cho dẫn xuất (sealed class). Trong trường hợp này, cách ưu chuộng hơn là gọi phương thức giao diện thông qua đối tượng. Khi chúng ta thực thi một giao diện trong một cấu trúc, là chúng ta đang thực thi nó trong một kiểu dữ liệu giá trị. Khi chúng ta gán cho môt tham chiếu giao diện, có một boxing ngầm định của đối tượng. Chẳng may khi chúng ta sử dụng giao diện để bổ sung đối tượng, nó là một đối tượng đã boxing, không phải là đối tượng nguyên thủy cần được bổ sung. Xa hơn nữa, nếu chúng ta thay đổi kiểu dữ liệu giá trị, thì kiểu dữ liệu được boxing vẫn không thay đổi. Ví dụ 8.6 tạo ra một cấu trúc và thực thi một giao diện IStorable và minh họa việc boxing ngầm định khi gán một cấu trúc cho một tham chiếu giao diện. Ví dụ 8.6: Tham chiếu đến kiểu dữ liệu giá trị. using System; // khai báo một giao diện đơn interface IStorable { void Read(); int Status { get; set;} } // thực thi thông qua cấu trúc public struct myStruct : IStorable { public void Read() { Console.WriteLine(“Implementing IStorable.Read”); } public int Status { get { return status; } set { status = value; } } 202 Thực Thi Giao Diện
  28. Ngôn Ngữ Lập Trình C# // biến thành viên lưu giá trị thuộc tính Status private int status; } public class Tester { static void Main() { // tạo một đối tượng myStruct myStruct theStruct = new myStruct(); theStruct.Status = -1; // khởi tạo Console.WriteLine(“theStruct.Status: {0}”, theStruct.Status); // thay đổi giá trị theStruct.Status = 2; Console.WriteLine(“Changed object”); Console.WriteLine(“theStruct.Status: {0}”, theStruct.Status); // gán cho giao diện // boxing ngầm định IStorable isTemp = (IStorable) theStruct; // thiết lập giá trị thông qua tham chiếu giao diện isTemp.Status = 4; Console.WriteLine(“Changed interface”); Console.WriteLine(“theStruct.Status: {0}, isTemp: {1}”, theStruct.Status, isTemp.Status); // thay đổi giá trị một lần nữa theStruct.Status = 6; Console.WriteLine(“Changed object.”); Console.WriteLine(“theStruct.Status: {0}, isTemp: {1}”, theStruct.Status, isTemp.Status); } }  Kết quả: theStruct.Status: -1 Changed object. theStruct.Status: 2 Changed interface theStruct.Status: 2, isTemp: 4 Changed object 203 Thực Thi Giao Diện
  29. Ngôn Ngữ Lập Trình C# theStruct.Status: 6, isTemp: 4 Trong ví dụ 8.6, giao diện IStorable có một phương thức Read() và môt thuộc tính là Status. Giao diện này được thực thi bởi một cấu trúc tên là myStruct: public struct myStruct : IStorable Đoạn mã nguồn thú vị bên trong Tester. Chúng ta bắt đầu bằng việc tạo một thể hiện của cấu trúc và khởi tạo thuộc tính là –1, sau đó giá trị của status được in ra:0 myStruct theStruct = new myStruct(); theStruct.Status = -1; // khởi tạo Console.WriteLine(“theStruct.Status: {0}”, theStruct.status); Kết quả là giá trị của status được thiết lập: theStruct.Status = -1; Kế tiếp chúng ta truy cập thuộc tính để thay đổi status, một lần nữa thông qua đối tượng giá trị: // thay đổi giá trị theStruct.Status = 2; Console.WriteLine(“Changed object”); Console.WriteLine(“theStruct.Status: {0}”, theStruct.Status); kết quả chỉ ra sự thay đổi: Changed object theStruct.Status: 2 Tại điểm này, chúng ta tạo ra một tham chiếu đến giao diện IStorable, một đối tượng giá trị theStruct được boxing ngầm và gán lại cho tham chiếu giao diện. Sau đó chúng ta dùng giao diện để thay đổi giá trị của status bằng 4: // gán cho một giao diện // boxing ngầm định IStorable isTemp = (IStorable) theStruct; // thiết lập giá trị thông qua tham chiếu giao diện isTemp.Status = 4; Console.WriteLine(“Changed interface”); Console.WriteLine(“theStruct.Status: {0}, isTemp: {1}”, theStruct.Status, isTemp.Status); như chúng ta đã thấy kết quả thực hiện có một điểm khác biệt: Changed interface theStruct.Status: 2, isTemp: 4 Điều xảy ra là: đối tượng được giao diện tham chiếu đến thay đổi giá trị status bằng 4, nhưng đối tượng giá trị cấu trúc không thay đổi.Thậm chí có nhiều thú vị hơn khi chúng ta truy cập phương thức thông qua bản thân đối tượng: 204 Thực Thi Giao Diện
  30. Ngôn Ngữ Lập Trình C# // than đổi giá trị lần nữa theStruct.Status = 6; Console.WriteLine(“Changed object”); Console.WriteLine(“theStruct.Status: {0}, isTemp: {1}”, theStruct.Status, isTemp.Status); kết quả đối tượng giá trị thay đổi nhưng đối tượng được boxing và được giao diện tham chịếu không thay đổi: Changed object theStruct.Status: 6, isTemp: 4 Ta thử xem đoạn mã IL để hiểu tham về cách thực hiện trên:  Ví dụ 8.7: MSIL phát sinh từ ví dụ 8.6. method private hidebysig static void Main() il managed { .entrypoint // Code size 206 (0xce) .maxstack 4 .local ([0] value class myStruct theStruct, [1] class IStorable isTemp, [2] int32 V_2) IL_0000: ldloca.s theStruct IL_0002: iniobj myStruct IL_0008: ldloca.s theStruct IL_000a: ldc.i4.ml IL_000b: call instance void myStruct::set_status(int32) IL_0010: ldstr “theStruct.Status: {0}” IL_0015: ldloca.s theStruct IL_0017: call instance int32 myStruct::get_status() IL_001c: stloc.2 IL_001d: ldloca.s V_2 IL_001f: box [mscorlib]System.Int32 IL_0024: call void [mscorlib] System.Console::WriteLine (class System.String, class System.Object) IL_0029: ldloca.s theStruct IL_002b: ldc.i4.2 IL_002c: call instance void myStruct::set_status(int32) 205 Thực Thi Giao Diện
  31. Ngôn Ngữ Lập Trình C# IL_0031: ldstr “Changed object” IL_0036: call void [mscorlib]System.Console::WriteLine (class System.String) IL_003b: ldstr “theStruct.Status: {0}” IL_0040: ldloca.s theStruct IL_0042: call instance int32 myStruct::get_status() IL_0047: stloc.2 IL_0048: ldloca.s V_2 IL_004a: box [mscorlib]System.Int32 IL_004f: call void [mscorlib]System.Console::WriteLine (class System.String, class System.Object) IL_0054: ldloca.s theStruct IL_0056: box myStruct IL_005b: stloc.1 IL_005c: ldloc.1 IL_005d: ldc.i4.4 IL_005e: callvirt instance void IStorable::set_status(int32) IL_0063: ldstr “Changed interface” IL_0068: call void [mscorlib]System.Console::WriteLine (class System.String) IL_006d: ldstr “theStruct.Status: {0}, isTemp: {1}” IL_0072: ldloca.s theStruct IL_0074: call instance int32 mySystem::get_status() IL_0079: stloc.2 IL_007a: ldloca.s V_2 IL_007c: box [mscorlib]System.Int32 IL_0081: ldloc.1 IL_0082: callvirt instance int32 IStorable::get_status() IL_0087: stloc.2 IL_0088: ldloca.s V_2 IL_008a: box [mscorlib]System.Int32 IL_008f: call void [mscorlib]System.Console::WriteLine (class System.String, class System.Object, class System.Object) IL_0094: ldloca.s theStruct IL_0096: ldc.i4.6 IL_0097: call instance void myStruct::set_status(int32) IL_009c: ldstr “Changed object” IL_00a1: call void [mscorlib]System.Console::WriteLine 206 Thực Thi Giao Diện
  32. Ngôn Ngữ Lập Trình C# (class System.String) IL_00a6: ldstr “theStruct.Status: {0}, isTemp: {1}” IL_00ab: ldloca.s theStruct IL_00ad: call instance int32 myStruct::get_status() IL_00b2: stloc.2 IL_00b3: ldloca.s V_2 IL_00b5: box [mscorlib]System.Int32 IL_00ba: ldloc.1 IL_00bb: callvirt instance int32 IStorable::get_status() IL_00c0: stloc.2 IL_00c1: ldloca.s V_2 IL_00c3: box [mscorlib]System.Int32 IL_00c8: call void [mscorlib]System.Console::WriteLine (class System.String, class System.Object, class System.Object) IL_00cd: ret } // end fo method Tester::Main Trong dòng lệnh IL_00b, giá trị của status được thiết lập thông qua việc gọi đối tượng giá trị. Tiếp theo chúng ta thấy lệnh gọi thứ hai ở dòng IL_0017. Lưu ý rằng việc gọi WriteLine() dẫn đến việc boxing một giá trị nguyên để phương thức GetString của lớp object được gọi. Điều muốn nhấn mạnh là ở dòng lệnh IL_0056 khi một cấu trúc myStruct đã được boxing. Việc boxing này tạo ra một kiểu dữ lịêu tham chiếu cho tham chiếu giao diện. Và điều quan trọng là ở dòng IL_005e lúc này IStorable::set_status được gọi chứ không phải là myStruct::setStatus. Điều quan trọng muốn trình bày ở đây là khi chúng ta thực thi một giao diện với một kiểu giá trị, phải chắc chắn rằng truy cập các thành viên của giao diện thông qua đối tượng hơn là thông qua một tham chiếu giao diện. Câu hỏi và trả lời Câu hỏi 1: So sánh giữa lớp và giao diện? Trả lời 1: Giao diện khác với lớp ở một số điểm sau: giao diện không cung cấp bất cứ sự thực thi mã nguồn nào cả. Điều này sẽ được thực hiện tại các lớp thực thi giao diện. Một giao diện đưa ra chỉ để nói rằng có cung cấp một số sự xác nhận hướng dẫn cho những điều gì đó xảy ra và không đi vào chi tiết. Một điều khác nữa là tất cả các thành viên của giao diện được giả sử là public ngầm định. Nếu chúng ta cố thay đổi thuộc tính truy cập của thành viên trong giao diện thì sẽ nhận được lỗi. Giao diện chỉ chứa những phương thức, thuộc tính, sự kiện, chỉ mục. Và không chứa dữ liệu thành viên, bộ khởi dựng, và bộ hủy. Chúng cũng không chứa bất cứ thành viên static nào cả. 207 Thực Thi Giao Diện
  33. Ngôn Ngữ Lập Trình C# Câu hỏi 2: Sự khác nhau giữa giao diện và lớp trừu tượng? Trả lời 2: Sự khác nhau cơ bản là sự kế thừa. Một lớp có thể kế thừa nhiều giao diện cùng một lúc, nhưng không thể kế thừa nhiều hơn một lớp trừu tượng. Câu hỏi 3: Các lớp thực thi giao diện sẽ phải làm gì? Trả lời 3: Các lớp thực thi giao diện phải cung cấp các phần thực thi chi tiết cho các phương thức, thuộc tính, chỉ mục, sự kiện được khai báo trong giao diện. Câu hỏi 4: Có bao nhiêu cách gọi một phương thức được khai báo trong giao diện? Trả lời 4: Có 4 cách gọi phương thức được khai báo trong giao diện: . Thông qua lớp cơ sở tham chiếu đến đối tượng của lớp dẫn xuất . Thông qua một giao diện tạo từ lớp cơ sở tham chiếu đến đối tượng dẫn xuất . Thông qua một đối tượng dẫn xuất . Thông qua giao diện tạo từ đối tượng dẫn xuất Câu hỏi 5: Các thành viên của giao diện có thể có những thuộc tính truy cập nào? Trả lời 5: Mặc định các thành viên của giao diện là public. Vì mục tiêu của giao diện là xây dựng cho các lớp khác sử dụng. Nếu chúng ta thay đổi thuộc tính này như là internal, protected hay private thì sẽ gây ra lỗi. Câu hỏi 6: Chúng ta có thể tạo thể hiện của giao diện một cách trực tiếp được không? Trả lời 6: Không thể tạo thể hiện của giao diện trực tiếp bằng khai báo new được. Chúng ta chỉ có thể tạo thể hiện giao diện thông qua một phép gán với đối tượng thực thi giao diện. Câu hỏi thêm Câu hỏi 1: Toán tử is được dùng làm gì trong giao diện? Câu hỏi 2: Toán tử as có lợi hơn toán tử is về mặt nào khi được sử dụng liện quan đến giao diện? Câu hỏi 3: Giao diện là kiểu dữ liệu tham chiếu hay kiểu giá trị? Câu hỏi 4: Khi thực thi giao diện với cấu trúc. Thì truy cập các thành viên của giao diện thông qua đối tượng hay thông qua tham chiếu giao diện là tốt nhất? Câu hỏi 5: Số giao diện có thể được kế thừa cho một lớp? Câu hỏi 6: Việc thực thi giao diện tường minh là thực thi như thế nào? Trong trường hợp nào thì cần thực hiện tường minh? Bài tập Bài tập 1: Hãy viết một giao diện khai báo một thuộc tính ID chứa chuỗi giá trị. Viết một lớp Employee thực thi giao diện đó. Bài tập 2: Đọan mã nguồn sau đây có lỗi hãy sử lỗi và hãy cho biết tại sao có lỗi này. Sau khi sửa lỗi hãy viết một lớp Circle thực thi giao diện này? public interface IDimensions { 208 Thực Thi Giao Diện
  34. Ngôn Ngữ Lập Trình C# long width; long height; double Area(); double Circumference(); int Side(); } Bài tập 3: Chương trình sau đây có lỗi hãy sử lỗi, biên dịch và chạy lại chương trình? Giải thích tại sao chương trình có lỗi. using System; interface IPoint { // Property signatures: int x { get; set; } int y { get; set; } } class MyPoint : IPoint { // Fields: private int myX; private int myY; // Constructor: public MyPoint(int x, int y) { myX = x; myY = y; } // Property implementation: public int x 209 Thực Thi Giao Diện
  35. Ngôn Ngữ Lập Trình C# { get { return myX; } set { myX = value; } } public int y { get { return myY; } set { myY = value; } } } class MainClass { private static void PrintPoint(IPoint p) { Console.WriteLine("x={0}, y={1}", p.x, p.y); } public static void Main() { MyPoint p = new MyPoint(2,3); Console.Write("My Point: "); PrintPoint(p); IPoint p2 = new IPoint(); PrintPoint(p2); } } 210 Thực Thi Giao Diện
  36. Ngôn Ngữ Lập Trình C# Bài tập 4: Xây dựng một giao diện IDisplay có khai báo thuộc tính Name kiểu chuỗi. Hãy viết hai lớp Dog và Cat thực thi giao diện IDisplay, cho biết thuộc tính Name là tên của đối tượng. 211 Thực Thi Giao Diện
  37. Ngôn Ngữ Lập Trình C# Chương 9 MẢNG, CHỈ MỤC, VÀ TẬP HỢP Mảng Khai báo mảng Giá trị mặc định Truy cập các thành phần trong mảng Khởi tạo thành phần trong mảng Sử dụng từ khóa params Câu lệnh lặp foreach Mảng đa chiều Mảng đa chiều cùng kích thước Mảng đa chiều kích thước khác nhau Chuyển đổi mảng System.Array Bộ chỉ mục Bộ chỉ mục và phép gán Sử dụng kiểu chỉ số khác Giao diện tập hợp Câu hỏi & bài tập Môi trường .NET cung cấp rất đa dạng số lượng các lớp về tập hợp, bao gồm: Array, ArrayList, Queue, Stack, BitArray, NameValueCollection, và StringCollection. Trong số đó tập hợp đơn giản nhất là Array, đây là kiểu dữ liệu tập hợp mà ngôn ngữ C# hỗ trợ xây dựng sẵn. Chương này chúng ta sẽ tìm hiểu cách làm việc với mảng một chiều, mảng đa chiều, và mảng các mảng (jagged array). Chúng ta cũng được giới thiệu phần chỉ mục indexer, đây là cách thiết lập để làm cho việc truy cập những thuộc tính giống nhau trở nên đơn giản hơn, một lớp được chỉ mục thì giống như một mảng. 212 Mảng, Chỉ Mục, và Tập Hợp
  38. Ngôn Ngữ Lập Trình C# .NET cũng cung cấp nhiều các giao diện, như IEnumerable và ICollection. Những phần thực thi của các giao diện này cung cấp các tiêu chuẩn để tương tác với các tập hợp. Trong chương này chúng ta sẽ được cách sử dụng hiệu quả của các giao diện. Cũng thông qua chương này chúng ta sẽ được giới thiệu cách sử dụng chung của các tập hợp trong .NET, bao gồm: ArrayList, Dictionary, Hashtable, Queue, và Stack. Mảng Mảng là một tập hợp có thứ tự của những đối tượng, tất cả các đối tượng này cùng một kiểu. Mảng trong ngôn ngữ C# có một vài sự khác biệt so với mảng trong ngôn ngữ C++ và một số ngôn ngữ khác, bởi vì chúng là những đối tượng. Điều này sẽ cung cấp cho mảng sử dụng các phương thức và những thuộc tính. Ngôn ngữ C# cung cấp cú pháp chuẩn cho việc khai báo những đối tượng Array. Tuy nhiên, cái thật sự được tạo ra là đối tượng của kiểu System.Array. Mảng trong ngôn ngữ C# kết hợp cú pháp khai báo mảng theo kiểu ngôn ngữ C và kết hợp với định nghĩa lớp do đó thể hiện của mảng có thể truy cập những phương thức và thuộc tính của System.Array. Một số các thuộc tính và phương thức của lớp System.Array Thành viên Mô tả BinarySearch() Phương thức tĩnh public tìm kiếm một mảng một chiều đã sắp thứ tự. Clear() Phương thức tĩnh public thiết lập các thành phần của mảng về 0 hay null. Copy() Phương thức tĩnh public đã nạp chồng thực hiện sao chép một vùng của mảng vào mảng khác. CreateInstance() Phương thức tĩnh public đã nạp chồng tạo một thể hiện mới cho mảng IndexOf() Phương thức tĩnh public trả về chỉ mục của thể hiện đầu tiên chứa giá trị trong mảng một chiều LastIndexOf() Phương thức tĩnh public trả về chỉ mục của thể hiện cuối cùng của giá trị trong mảng một chiều Reverse() Phương thức tĩnh public đảo thứ tự của các thành phần trong mảng một chiều Sort() Phương thức tĩnh public sắp xếp giá trị trong mảng một chiều. IsFixedSize Thuộc tính public giá trị bool thể hiện mảng có kích thước cố định hay không. IsReadOnly Thuộc tính public giá trị bool thể hiện mảng chỉ đọc hay không 213 Mảng, Chỉ Mục, và Tập Hợp
  39. Ngôn Ngữ Lập Trình C# IsSynchronized Thuộc tính public giá trị bool thể hiện mảng có hỗ trợ thread-safe Length Thuộc tính public chiều dài của mảng Rank Thuộc tính public chứa số chiều của mảng SyncRoot Thuộc tính public chứa đối tượng dùng để đồng bộ truy cập trong mảng GetEnumerator() Phương thức public trả về IEnumerator GetLength() Phương thức public trả về kích thước của một chiều cố định trong mảng GetLowerBound() Phương thức public trả về cận dưới của chiều xác định trong mảng GetUpperBound() Phương thức public trả về cận trên của chiều xác định trong mảng Initialize() Khởi tạo tất cả giá trị trong mảng kiểu giá trị bằng cách gọi bộ khởi dụng mặc định của từng giá trị. SetValue() Phương thức public thiết lập giá trị cho một thành phần xác định trong mảng. Bảng 9.1: Các phương thức và thuộc tính của System.Array. Khai báo mảng Chúng ta có thể khai báo một mảng trong C# với cú pháp theo sau: [] Ví dụ ta có khai báo như sau: int[] myIntArray; Cặp dấu ngoặc vuông ([]) báo cho trình biên dịch biết rằng chúng ta đang khai báo một mảng. Kiểu dữ liệu là kiểu của các thành phần chứa bên trong mảng. Trong ví dụ bên trên. myIntArray được khai báo là mảng số nguyên. Chúng ta tạo thể hiện của mảng bằng cách sử dụng từ khóa new như sau: myIntArray = new int[6]; Khai báo này sẽ thiết lập bên trong bộ nhớ một mảng chứa sáu số nguyên. Ghi chú: dành cho lập trình viên Visual Basic, thành phần đầu tiên luôn bắt đầu 0, không có cách nào thiết lập cận trên và cận dưới của mảng, và chúng ta cũng không thể thiết lập lại kích thước của mảng. Điều quan trọng để phân biệt giữa bản thân mảng (tập hợp các thành phần) và các thành phần trong mảng. Đối tượng myIntArray là một mảng, thành phần là năm số nguyên được lưu giữ. Mảng trong ngôn ngữ C# là kiểu dữ liệu tham chiếu, được tạo ra trên heap. Do đó myIntArray được cấp trên heap. Những thành phần của mảng được cấp phát dựa trên các kiểu dữ liệu của chúng. Số nguyên là kiểu dữ liệu giá trị, và do đó những thành phần của 214 Mảng, Chỉ Mục, và Tập Hợp
  40. Ngôn Ngữ Lập Trình C# myIntArray là kiểu dữ liệu giá trị, không phải số nguyên được boxing. Một mảng của kiểu dữ liệu tham chiếu sẽ không chứa gì cả nhưng tham chiếu đến những thành phần được tạo ra trên heap. Giá trị mặc định Khi chúng ta tạo một mảng có kiểu dữ liệu giá trị, mỗi thành phần sẽ chứa giá trị mặc định của kiểu dữ liệu (xem bảng 4.2, kiểu dữ liệu và các giá trị mặc định). Với khai báo: myIntArray = new int[5]; sẽ tạo ra một mảng năm số nguyên, và mỗi thành phần được thiết lập giá trị mặc định là 0, đây cũng là giá trị mặc định của số nguyên. Không giống với mảng kiểu dữ liệu giá trị, những kiểu tham chiếu trong một mảng không được khởi tạo giá trị mặc định. Thay vào đó, chúng sẽ được khởi tạo giá trị null. Nếu chúng ta cố truy cập đến một thành phần trong mảng kiểu dữ liệu tham chiếu trước khi chúng được khởi tạo giá trị xác định, chúng ta sẽ tạo ra một ngoại lệ. Giả sử chúng ta tạo ra một lớp Button. Chúng ta khai báo một mảng các đối tượng Button với cú pháp sau: Button[] myButtonArray; và chúng ta tạo thể hiện của mảng như sau: myButtonArray = new Button[3]; Ghi chú: chúng ta có thể viết ngắn gọn như sau: Button muButtonArray = new Button[3]; Không giống với ví dụ mảng số nguyên trước, câu lệnh này không tao ra một mảng với những tham chiếu đến ba đối tượng Button. Thay vào đó việc này sẽ tạo ra một mảng myButtonArray với ba tham chiếu null. Để sử dụng mảng này, đầu tiên chúng ta phải tạo và gán đối tượng Button cho từng thành phần tham chiếu trong mảng. Chúng ta có thể tạo đối tượng trong vòng lặp và sau đó gán từng đối tượng vào trong mảng. Truy cập các thành phần trong mảng Để truy cập vào thành phần trong mảng ta có thể sử dụng toán tử chỉ mục ([]). Mảng dùng cơ sở 0, do đó chỉ mục của thành phần đầu tiên trong mảng luôn luôn là 0. Như ví dụ trước thành phần đầu tiên là myArray[0]. Như đã trình bày ở phần trước, mảng là đối tượng, và do đó nó có những thuộc tính. Một trong những thuộc tính hay sử dụng là Length, thuộc tính này sẽ báo cho biết số đối tượng trong một mảng. Một mảng có thể được đánh chỉ mục từ 0 đến Length –1. Do đó nếu có năm thành phần trong mảng thì các chỉ mục là: 0, 1, 2, 3, 4. Ví dụ 9.1 minh họa việc sử dụng các khái niệm về mảng từ đầu chương tới giờ. Trong ví dụ một lớp tên là Tester tạo ra một mảng kiểu Employee và một mảng số nguyên. Tạo các đối tượng Employee sau đó in hai mảng ra màn hình. 215 Mảng, Chỉ Mục, và Tập Hợp
  41. Ngôn Ngữ Lập Trình C#  Ví dụ 9.1: làm việc với một mảng. namespace Programming_CSharp { using System; // tạo một lớp đơn giản để lưu trữ trong mảng public class Employee { // bộ khởi tạo lấy một tham số public Employee( int empID ) { this.empID = empID; } public override string ToString() { return empID.ToString(); } // biến thành viên private private int empID; private int size; } public class Tester { static void Main() { int[] intArray; Employee[] empArray; intArray = new int[5]; empArray = new Employee[3]; // tạo đối tượng đưa vào mảng for( int i = 0; i < empArray.Length; i++) { empArray[i] = new Employee(i+5); } // xuất mảng nguyên for( int i = 0; i < intArray.Length; i++) { Console.Write(intArray[i].ToString()+”\t”); 216 Mảng, Chỉ Mục, và Tập Hợp
  42. Ngôn Ngữ Lập Trình C# } // xuất mảng Employee for( int i = 0; i < empArray.Length; i++) { Console.WriteLine(empArray[i].ToString()+”\t”); } } } }  Kết quả: 0 0 0 0 0 5 6 7 Ví dụ bắt đầu với việc định nghĩa một lớp Employee, lớp này thực thi một bộ khởi dựng lấy một tham số nguyên. Phương thức ToString() được kế thừa từ lớp Object được phủ quyết để in ra giá trị empID của đối tượng Employee. Các kiểu tạo ra là khai báo rồi mới tạo thể hiện của hai mảng. Mảng số nguyên được tự động thiết lập giá trị 0 mặc định cho từng số nguyên trong mảng. Nội dung của mảng Employee được tạo bằng các lệnh trong vòng lặp. Cuối cùng, nội dung của cả hai mảng được xuất ra màn hình console để đảm bảo kết quả như mong muốn; năm giá trị đầu của mảng nguyên, ba số sau cùng là của mảng Employee. Khởi tạo thành phần của mảng Chúng ta có thể khởi tạo nội dung của một mảng ngay lúc tạo thể hiện của mảng bằng cách đặt những giá trị bên trong dấu ngoặc ({}). C# cung cấp hai cú pháp để khởi tạo các thành phần của mảng, một cú pháp dài và một cú pháp ngắn: int[] myIntArray = new int[5] { 2, 4, 6, 8, 10}; int[] myIntArray = { 2, 4, 6, 8, 10}; Không có sự khác biệt giữa hai cú pháp trên, và hầu hết các chương trình đều sử dụng cú pháp ngắn hơn do sự tự nhiên và lười đánh nhiều lệnh của người lập trình. Sử dụng từ khóa params Chúng ta có thể tạo một phương thức rồi sau đó hiển thị các số nguyên ra màn hình console bằng cách truyền vào một mảng các số nguyên và sử dụng vòng lặp foreach để duyệt qua từng thành phần trong mảng. Từ khóa params cho phép chúng ta truyền một số biến của tham số mà không cần thiết phải tạo một mảng. Trong ví dụ kế tiếp, chúng ta sẽ tạo một phương thức tên DisplayVals(), phương thức này sẽ lấy một số các biến của tham số nguyên: public void DisplayVals( params int[] intVals) 217 Mảng, Chỉ Mục, và Tập Hợp
  43. Ngôn Ngữ Lập Trình C# Phương thức có thể xem mảng này như thể một mảng được tạo ra tường minh và được truyền vào tham số. Sau đó chúng ta có thể tự do lặp lần lượt qua các thành phần trong mảng giống như thực hiện với bất cứ mảng nguyên nào khác: foreach (int i in intVals) { Console.WriteLine(“DisplayVals: {0}”, i); } Tuy nhiên, phương thức gọi không cần thiết phải tạo tường minh một mảng, nó chỉ đơn giản truyền vào các số nguyên, và trình biên dịch sẽ kết hợp những tham số này vào trong một mảng cho phương thức DisplayVals, ta có thể gọi phương thức như sau: t.DisplayVals(5,6,7,8); và chúng ta có thể tự do tạo một mảng để truyền vào phương thức nếu muốn: int [] explicitArray = new int[5] {1,2,3,4,5}; t.DisplayArray(explicitArray); Ví dụ 9.3 cung cấp tất cả mã nguồn để minh họa sử dụng cú pháp params. Ví dụ 9.3: minh họa sử dụng params. namespace Programming_CSharp { using System; public class Tester { static void Main() { Tester t = new Tester(); t.DisplayVals(5,6,7,8); int[] explicitArray = new int[5] {1,2,3,4,5}; t.DisplayVals(explicitArray); } public void DisplayVals( params int[] intVals) { foreach (int i in intVals) { Console.WriteLine(“DisplayVals {0}”, i); } } } } 218 Mảng, Chỉ Mục, và Tập Hợp
  44. Ngôn Ngữ Lập Trình C#  Kết quả: DisplayVals 5 DisplayVals 6 DisplayVals 7 DisplayVals 8 DisplayVals 1 DisplayVals 2 DisplayVals 3 DisplayVals 4 DisplayVals 5 Câu lệnh lặp foreach Câu lệnh lặp foreach khá mới với những người đã học ngôn ngữ C, từ khóa này được sử dụng trong ngôn ngữ Visual Basic. Câu lệnh foreach cho phép chúng ta lặp qua tất cả các mục trong một mảng hay trong một tập hợp. Cú pháp sử dụng lệnh lặp foreach như sau: foreach ( in ) { // thực hiện thông qua tương ứng với // từng mục trong mảng hay tập hợp } Do vậy, chúng ta có thể cải tiến ví dụ 9.1 trước bằng cách thay việc sử dụng vòng lặp for bằng vòng lặp foreach để truy cập đến từng thành phần trong mảng. Ví dụ 9.2: Sử dụng foreach. namespace Programming_CSharp { using System; // tạo một lớp đơn giản để lưu trữ trong mảng public class Employee { // bộ khởi tạo lấy một tham số public Employee( int empID ) { this.empID = empID; } 219 Mảng, Chỉ Mục, và Tập Hợp
  45. Ngôn Ngữ Lập Trình C# public override string ToString() { return empID.ToString(); } // biến thành viên private private int empID; private int size; } public class Tester { static void Main() { int[] intArray; Employee[] empArray; intArray = new int[5]; empArray = new Employee[3]; // tạo đối tượng đưa vào mảng for( int i = 0; i < empArray.Length; i++) { empArray[i] = new Employee(i+10); } // xuất mảng nguyên foreach (int i in intArray) { Console.Write(i.ToString()+”\t”); } // xuất mảng Employee foreach ( Employee e in empArray) { Console.WriteLine(e.ToString()+”\t”); } } } } Kết quả của ví dụ 9.2 cũng tương tự như ví dụ 9.1. Tuy nhiên, với việc sử dụng vòng lặp for ta phải xác định kích thước của mảng, sử dụng biến đếm tạm thời để truy cập đến từng thành phần trong mảng: 220 Mảng, Chỉ Mục, và Tập Hợp
  46. Ngôn Ngữ Lập Trình C# for (int i = 0 ; i [,] Ví dụ để khai báo một mảng hai chiều có tên là myRectangularArray để chứa hai dòng và ba cột các số nguyên, chúng ta có thể viết như sau: int [ , ] myRectangularArray; 221 Mảng, Chỉ Mục, và Tập Hợp
  47. Ngôn Ngữ Lập Trình C# Ví dụ tiếp sau đây minh họa việc khai báo, tạo thể hiện, khởi tạo và in nội dung ra màn hình của một mảng hai chiều. Trong ví dụ này, vòng lặp for được sử dụng để khởi tạo các thành phần trong mảng. Ví dụ 9.4: Mảng hai chiều. namespace Programming_CSharp { using System; public class Tester { static void Main() { // khai báo số dòng và số cột của mảng const int rows = 4; const int columns = 3; // khai báo mảng 4x3 số nguyên int [,] rectangularArray = new int[rows, columns]; // khởi tạo các thành phần trong mảng for(int i = 0; i < rows; i++) { for(int j = 0; j < columns; j++) { rectangularArray[i,j] = i+j; } } // xuất nội dung ra màn hình for(int i = 0; i < rows; i++) { for(int j = 0; j < columns; j++) { Console.WriteLine(“rectangularArray[{0},{1}] = {2}”, i, j, rectangularArray[i, j]); } } } } } 222 Mảng, Chỉ Mục, và Tập Hợp
  48. Ngôn Ngữ Lập Trình C#  Kết quả: rectangularArray[0,0] = 0 rectangularArray[0,1] = 1 rectangularArray[0,2] = 2 rectangularArray[1,0] = 1 rectangularArray[1,1] = 2 rectangularArray[1,2] = 3 rectangularArray[2,0] = 2 rectangularArray[2,1] = 3 rectangularArray[2,2] = 4 rectangularArray[3,0] = 3 rectangularArray[3,1] = 4 rectangularArray[3,2] = 5 Trong ví dụ này, chúng ta khai báo hai giá trị: const int rows = 4; const int columns = 3; hai giá trị này được sử dụng để khai báo số chiều của mảng: int [,] rectangularArray = new int[rows, columns]; Lưu ý trong cú pháp này, dấu ngoặc vuông trong int[,] chỉ ra rằng đang khai báo một kiểu dữ liệu là mảng số nguyên, và dấu phẩy (,) chỉ ra rằng đây là mảng hai chiều (hai dấu phẩy khai báo mảng ba chiều, và nhiều hơn nữa). Việc tạo thể hiện thực sự của mảng ở lệnh new int [rows,columns] để thiết lập kích thước của mỗi chiều. Ở đây khai báo và tạo thể hiện được kết hợp với nhau. Chương trình khởi tạo tất cả các giá trị các thành phần trong mảng thông qua hai vòng lặp for. Lặp thông qua mỗi cột của mỗi dòng. Do đó, thành phần đầu tiên được khởi tạo là rectangularArray[0,0], tiếp theo bởi rectangularArray[0,1] và đến rectangularArray[0,2]. Một khi điều này thực hiện xong thì chương trình sẽ chuyển qua thực hiện tiếp ở dòng tiếp tục: rectangularArray[1,0], rectangularArray[1,1], rectangularArray[1,2]. Cho đến khi tất cả các cột trong tất cả các dòng đã được duyệt qua tức là tất cả các thành phần trong mảng đã được khởi tạo. Như chúng ta đã biết, chúng ta có thể khởi tạo mảng một chiều bằng cách sử dụng danh sách các giá trị bên trong dấu ngoặc ({}). Chúng ta cũng có thể làm tương tự với mảng hai chiều. Trong ví dụ 9.5 khai báo mảng hai chiều rectangularArray, và khởi tạo các thành phần của nó thông qua các danh sách các giá trị trong ngoặc, sau đó in ra nội dung của nội dung. Ví dụ 9.5: Khởi tạo mảng đa chiều. 223 Mảng, Chỉ Mục, và Tập Hợp
  49. Ngôn Ngữ Lập Trình C# namespace Programming_CSharp { using System; public class Tester { static void Main() { // khai báo biến lưu số dòng số cột mảng const int rows = 4; const int columns = 3; // khai báo và định nghĩa mảng 4x3 int[,] rectangularArray = { {0,1,2}, {3,4,5}, {6,7,8},{9,10,11} }; // xụất nội dung của mảng for( int i = 0; i < rows; i++) { for(int j = 0; j < columns; j++) { Console.WriteLine(“rectangularArray[{0},{1}] = {2}”, i, j, rectangularArray[i,j]); } } } } }  Kết quả: rectangularArray[0,0] = 0 rectangularArray[0,1] = 1 rectangularArray[0,2] = 2 rectangularArray[1,0] = 3 rectangularArray[1,1] = 4 rectangularArray[1,2] = 5 rectangularArray[2,0] = 6 rectangularArray[2,1] = 7 rectangularArray[2,2] = 8 rectangularArray[3,0] = 9 224 Mảng, Chỉ Mục, và Tập Hợp
  50. Ngôn Ngữ Lập Trình C# rectangularArray[3,1] = 10 rectangularArray[3,2] = 11 Ví dụ trên cũng tương tự như ví dụ 9.4, nhưng trong ví dụ này chúng ta thực hiện việc khởi tạo trực tiếp khi tạo các thể hiện: int[,] rectangularArray = { {0,1,2}, {3,4,5}, {6,7,8},{9,10,11} }; Giá trị được gán thông qua bốn danh sách trong ngoặc móc, mỗi trong số đó là có ba thành phần, bao hàm một mảng 4x3. Nếu chúng ta viết như sau: int[,] rectangularArray = { {0,1,2,3}, {4,5,6,7}, {8,9,10,11} }; thì sẽ tạo ra một mảng 3x4. Mảng đa chiều có kích khác nhau Cũng như giới thiệu trước kích thước của các chiều có thể không bằng nhau, điều này khác với mảng đa chiều cùng kích thước. Nếu hình dạng của mảng đa chiều cùng kích thước có dạng hình chữ nhật thì hình dạng của mảng này không phải hình chữ nhật vì các chiều của chúng không điều nhau. Khi chúng ta tạo một mảng đa chiều kích thước khác nhau thì chúng ta khai báo số dòng trong mảng trước. Sau đó với mỗi dòng sẽ giữ một mảng, có kích thước bất kỳ. Những mảng này được khai báo riêng. Sau đó chúng ta khởi tạo giá trị các thành phần trong những mảng bên trong. Trong mảng này, mỗi chiều là một mảng một chiều. Để khai báo mảng đa chiều có kích thước khác nhau ta sử dụng cú pháp sau, khi đó số ngoặc chỉ ra số chiều của mảng: [] [] Ví dụ, chúng ta có thể khai báo mảng số nguyên hai chiều khác kích thước tên myJagged- Array như sau: int [] [] myJaggedArray; Chúng ta có thể truy cập thành phần thứ năm của mảng thứ ba bằng cú pháp: myJagged- Array[2][4]. Ví dụ 9.6 tạo ra mảng khác kích thước tên myJaggedArray, khởi tạo các thành phần, rồi sau đó in ra màn hình. Để tiết kiệm thời gian, chúng ta sử dụng mảng các số nguyên để các thành phần của nó được tự động gán giá trị mặc định. Và ta chỉ cần gán một số giá trị cần thiết. 225 Mảng, Chỉ Mục, và Tập Hợp
  51. Ngôn Ngữ Lập Trình C#  Ví dụ 9.6: Mảng khác chiều. namespace Programming_CSharp { using System; public class Tester { static void Main() { const int rows = 4; // khai báo mảng tối đa bốn dòng int[][] jaggedArray = new int[rows][]; // dòng đầu tiên có 5 phần tử jaggedArray[0] = new int[5]; // dòng thứ hai có 2 phần tử jaggedArray[1] = new int[2]; // dòng thứ ba có 3 phần tử jaggedArray[2] = new int[3]; // dòng cuối cùng có 5 phần tử jaggedArray[3] = new int[5]; // khởi tạo một vài giá trị cho các thành phần của mảng jaggedArray[0][3] = 15; jaggedArray[1][1] = 12; jaggedArray[2][1] = 9; jaggedArray[2][2] = 99; jaggedArray[3][0] = 10; jaggedArray[3][1] = 11; jaggedArray[3][2] = 12; jaggedArray[3][3] = 13; jaggedArray[3][4] = 14; for(int i = 0; i < 5; i++) { Console.WriteLine(“jaggedArray[0][{0}] = {1}”, i, jaggedArray[0][i]); } for(int i = 0; i < 2; i++) { Console.WriteLine(“jaggedArray[1][{0}] = {1}”, 226 Mảng, Chỉ Mục, và Tập Hợp
  52. Ngôn Ngữ Lập Trình C# i, jaggedArray[1][i]); } for(int i = 0; i < 3; i++) { Console.WriteLine(“jaggedArray[2][{0}] = {1}”, i, jaggedArray[2][i]); } for(int i = 0; i < 5; i++) { Console.WriteLine(“jaggedArray[3][{0}] = {1}”, i, jaggedArray[3][i]); } } } }  Kết quả: jaggedArray[0][0] = 0 jaggedArray[0][1] = 0 jaggedArray[0][2] = 0 jaggedArray[0][3] = 15 jaggedArray[0][4] = 0 jaggedArray[1][0] = 0 jaggedArray[1][1] = 12 jaggedArray[2][0] = 0 jaggedArray[2][1] = 9 jaggedArray[2][2] = 99 jaggedArray[3][0] = 10 jaggedArray[3][1] = 11 jaggedArray[3][2] = 12 jaggedArray[3][3] = 13 jaggedArray[3][4] = 14 Trong ví dụ này, mảng được tạo với bốn dòng: int[][] jaggedArray = new int[rows][]; Chú ý rằng chiều thứ hai không xác định. Do sau đó chúng ta có thể khai báo mỗi dòng có kích thước khác nhau. Bốn lệnh sau tạo cho mỗi dòng một mảng một chiều có kích thước khác nhau: 227 Mảng, Chỉ Mục, và Tập Hợp
  53. Ngôn Ngữ Lập Trình C# // dòng đầu tiên có 5 phần tử jaggedArray[0] = new int[5]; // dòng thứ hai có 2 phần tử jaggedArray[1] = new int[2]; // dòng thứ ba có 3 phần tử jaggedArray[2] = new int[3]; // dòng cuối cùng có 5 phần tử jaggedArray[3] = new int[5]; Sau khi tạo các dòng cho mảng xong, ta thực hiện việc đưa các giá trị vào các thành phần của mảng. Và cuối cùng là xuất nội dung của mảng ra màn hình. Ghi chú: Khi chúng ta truy cập các thành phần của mảng kích thước bằng nhau, chúng ta đặt tất cả các chỉ mục của các chiều vào trong cùng dấu ngặc vuông: rectangularArray[i,j] Tuy nhiên với mảng có kích thước khác nhau ta phải để từng chỉ mục của từng chiều trong đấu ngoặc vuông riêng: jaggedArray[i][j] Chuyển đổi mảng Những mảng có thể chuyển đổi với nhau nếu những chiều của chúng bằng nhau và nếu các kiểu của các thành phần có thể chuyển đổi được. Chuyển đổi tường minh giữa các mảng xảy ra nếu các thành phần của những mảng có thể chuyển đổi tường minh. Và ngược lại, chuyển đổi ngầm định của mảng xảy ra nếu các thành phần của những mảng có thể chuyển đổi ngầm định. Nếu một mảng chứa những tham chiếu đến những đối tượng tham chiếu, một chuyển đổi có thể được tới một mảng của những đối tượng cơ sở. Ví dụ 9.7 minh họa việc chuyển đổi một mảng kiểu Button đến một mảng những đối tượng. Ví dụ 9.7: Chuyển đổi giữa những mảng. namespace Programming_CSharp { using System; // tạo lớp để lưu trữ trong mảng public class Employee { public Employee( int empID) { this.empID = empID; } 228 Mảng, Chỉ Mục, và Tập Hợp
  54. Ngôn Ngữ Lập Trình C# public override string ToString() { return empID.ToString(); } // biến thành viên private int empID; private int size; } public class Tester { // phương thức này lấy một mảng các object // chúng ta truyền vào mảng các đối tượng Employee // và sau đó là mảng các string, có sự chuyển đổi ngầm // vì cả hai điều dẫn xuất từ lớp object public static void PrintArray(object[] theArray) { Console.WriteLine(“Contents of the Array: {0}”, theArray.ToString()); // in ra từng thành phần trong mảng foreach (object obj in theArray) { // trình biên dịch sẽ gọi obj.ToString() Console.WriteLine(“Value: {0}”, obj); } } static void Main() { // tạo mảng các đối tượng Employee Employee[] myEmployeeArray = new Employee[3]; // khởi tạo các đối tượng của mảng for (int i = 0; i < 3; i++) { myEmployeeArray[i] = new Employee(i+5); } // hiểu thị giá trị của mảng PrintArray( myEmployeeArray ); // tạo mảng gồm hai chuỗi string[] array ={ “hello”, “world”}; // xuất ra nội dung của chuỗi 229 Mảng, Chỉ Mục, và Tập Hợp
  55. Ngôn Ngữ Lập Trình C# PrintArray( array ); } } }  Kết quả: Contents of the Array Programming_CSharp.Employee[] Value: 5 Value: 6 Value: 7 Contents of the Array Programming_CSharp.String[] Value: hello Value: world Ví dụ 9.7 bắt đầu bằng việc tạo một lớp đơn giản Employee như các ví dụ trước. Lớp Tester bây giờ được thêm một phương thức tĩnh PrintArray() để xuất nội dung của mảng, phương thức này có khai báo một tham số là mảng một chiều các đối tượng object: public static void PrintMyArray( object[] theArray) object là lớp cơ sở ngầm định cho tất cả các đối tượng trong môi trường .NET, nên nó được khai báo ngầm định cho cả hai lớp string và Employee. Phương thức PrintArray thực hiện hai hành động. Đầu tiên, là gọi phương thức ToString() của mảng: Console.WriteLine(“Contents of the Array {0}”, theArray.ToString()); Tên của kiểu dữ liệu mảng được in ra: Contents of the Array Programming_CSharp.Employee[] Contents of the Array System.String[] Sau đó phương thức PrintArray thực hiện tiếp việc gọi phương thức ToString() trong mỗi thành phần trong mảng nhận được. Do ToString() là phương thức ảo của lớp cơ sở object, và chúng ta đã thực hiện phủ quyết trong lớp Employee. Nên phương thức ToString() của lớp Employee được gọi. Việc gọi ToString() có thể không cần thiết, nhưng nếu gọi thì cũng không có hại gì và nó giúp cho ta đối xử với các đối tượng một cách đa hình. System.Array Lớp mảng Array chứa một số các phương thức hữu ích cho phép mở rộng những khả năng của mảng và làm cho mảng mạnh hơn những mảng trong ngôn ngữ khác (xem bảng 9.1). Hai phương thức tĩnh hữu dụng của lớp Array là Sort() và Reverse(). Có một cách hỗ trợ đầy đủ cho những kiểu dữ liệu nguyên thủy như là kiểu. Đưa mảng làm việc với những kiểu khác 230 Mảng, Chỉ Mục, và Tập Hợp
  56. Ngôn Ngữ Lập Trình C# như Button có một số khó khăn hơn. Ví dụ 9.8 minh họa việc sử dụng hai phương thức để thao tác đối tượng chuỗi. Ví dụ 9.8: Sử dụng Array.Sort() và Array.Reverse(). namespace Programming_CSharp { using System; public class Tester { public static void PrintArray(object[] theArray) { foreach( object obj in theArray) { Console.WriteLine(“Value: {0}”, obj); } Console.WriteLine(“\n”); } static void Main() { string[] myArray = { “Who”, “is”,”Kitty”,”Mun” }; PrintArray( myArray ); Array.Reverse( myArray ); PrintArray( myArray ); string[] myOtherArray = { “Chung”, “toi”, “la”, “nhung”,”nguoi”, ”lap”,”trinh”, “may”, “tinh” }; PrintArray( myOtherArray ); Array.Sort( myOtherArray ); PrintArray( myOtherArray ); } } } 231 Mảng, Chỉ Mục, và Tập Hợp
  57. Ngôn Ngữ Lập Trình C#  Kết quả: Value: Who Value: is Value: Kitty Value: Mun Value: Mun Value: Kitty Value: is Value: Who Value: Chung Value: toi Value: la Value: nhung Value: nguoi Value: lap Value: trinh Value: may Value: tinh Value: Chung Value: la Value: lap Value: may Value: nguoi Value: nhung Value: tinh Value: toi Value: trinh Ví dụ bắt đầu bằng việc tạo mảng myArray, mảng các chuỗi với các từ sau: “Who”, “is”, “Kitty”, ”Mun” mảng này được in ra, sau đó được truyền vào cho hàm Array.Reverse(), kết quả chúng ta thấy là kết quả của chuỗi như sau: Value: Mun Value: Kitty Value: is 232 Mảng, Chỉ Mục, và Tập Hợp
  58. Ngôn Ngữ Lập Trình C# Value: Who Tương tự như vậy, ví dụ cũng tạo ra mảng thứ hai, myOtherArray, chứa những từ sau: “Chung”, “toi”, “la”, “nhung”,”nguoi”, ”lap”,”trinh”, “máy”, “tính” Sau khi gọi phương thức Array.Sort() thì các thành phần của mảng được sắp xếp lại theo thứ tự alphabe: Value: Chung Value: la Value: lap Value: may Value: nguoi Value: nhung Value: tinh Value: toi Value: trinh Bộ chỉ mục Đôi khi chúng ta chúng ta mong muốn truy cập một tập hợp bên trong một lớp như thể bản thân lớp là một mảng. Ví dụ, giả sử chúng ta tạo một điều khiển kiểu ListBox tên là myListBox chứa một danh sách các chuỗi lưu trữ trong một mảng một chiều, một biến thành viên private myStrings. Một List Box chứa các thuộc tính thành viên và những phương thức và thêm vào đó mảng chứa các chuỗi của nó. Tuy nhiên, có thể thuận tiện hơn nếu có thể truy cập mảng ListBox với chỉ mục như thể ListBox là một mảng thật sự. Ví dụ, ta có thể truy cập đối tượng ListBox được tạo ra như sau: string theFirstString = myListBox[0]; string theLastString = myListBox[myListBox.Length - 1]; Bộ chỉ mục là một cơ chế cho phép các thành phần client truy cập một tập hợp chứa bên trong một lớp bằng cách sử dụng cú pháp giống như truy cập mảng ([]). Chỉ mục là một loại thuộc tính đặc biệt và bao gồm các phương thức get() và set() để xác nhận những hành vi của chúng. Chúng ta có thể khai báo thuộc tính chỉ mục bên trong của lớp bằng cách sử dụng cú pháp như sau: this [ ] { get; set; } Kiểu trả về được quyết định bởi kiểu của đối tượng được trả về bởi bộ chỉ mục, trong khi đó kiểu của đối mục được xác định bởi kiểu của đối mục dùng để làm chỉ số vào trong tập hợp chứa đối tượng đích. Mặc dù kiểu của chỉ mục thường dùng là các kiểu nguyên, chúng ta 233 Mảng, Chỉ Mục, và Tập Hợp
  59. Ngôn Ngữ Lập Trình C# cũng có thể dùng chỉ mục cho tập hợp bằng các kiểu dữ liệu khác, như kiểu chuỗi. Chúng ta cũng có thể cung cấp bộ chỉ mục với nhiều tham số để tạo ra mảng đa chiều. Từ khóa this tham chiếu đến đối tượng nơi mà chỉ mục xuất hiện. Như một thuộc tính bình thường, chúng ta cũng có thể định nghĩa phương thức get() và set() để xác định đối tượng nào trong mảng được yêu cầu truy cập hay thiết lập. Ví dụ 9.9 khai báo một điều khiển ListBox, tên là ListBoxTest, đối tượng này chứa một mảng đơn giản (myStrings) và một chỉ mục để truy cập nội dung của mảng. Ghi chú: Đối với lập trình C++, bộ chỉ mục đưa ra giống như việc nạp chồng toán tử chỉ mục ([]) trong ngôn ngữ C++. Toán tử chỉ mục không được nạp chồng trong ngôn ngữ C#, và được thay thế bởi bộ chỉ mục. Ví dụ 9.9: Sử dụng bộ chỉ mục. namespace Programming_CSharp { using System; // tạo lớp ListBox public class ListBoxTest { // khởi tạo ListBox với một chuỗi public ListBoxTest( params string[] initialStrings) { // cấp phát không gian cho chuỗi strings = new String[256]; // copy chuỗi truyền từ tham số foreach ( string s in initialStrings) { strings[ctr++] = s; } } // thêm một chuỗi public void Add(string theString) { if (ctr >= strings.Length) { // xử lý khi chỉ mục sai } else strings[ctr++] = theString; 234 Mảng, Chỉ Mục, và Tập Hợp
  60. Ngôn Ngữ Lập Trình C# } // thực hiện bộ truy cập public string this[int index] { get { if ( index = strings.Length) { // xử lý chỉ mục sai } return strings[index]; } set { if ( index >= ctr) { // xử lý lỗi chỉ mục không tồn tại } else strings[index] = value; } } // lấy số lượng chuỗi được lưu giữ public int GetNumEntries() { return ctr; } // các biến thành vịên lưu giữ mảng cho bộ chỉ mục private string[] strings; private int ctr = 0; } // lớp thực thi public class Tester { static void Main() { // tạo một đối tượng ListBox mới và khởi tạo ListBoxTest lbt = new ListBoxTest(“Hello”,”World”); 235 Mảng, Chỉ Mục, và Tập Hợp
  61. Ngôn Ngữ Lập Trình C# // thêm một số chuỗi vào lbt lbt.Add(“Who”); lbt.Add(“is”); lbt.Add(“Ngoc”); lbt.Add(“Mun”); // dùng bộ chỉ mục string strTest = “Universe”; lbt[1] = strTest; // truy cập và xuất tất cả các chuỗi for(int i = 0; i < lbt.GetNumEntries(); i++) { Console.WriteLine(“lbt[{0}]: {1}”, i, lbt[i]); } } } }  Kết quả: lbt[0]: Hello lbt[1]: Universe lbt[2]: Who lbt[3]: is lbt[4]: Ngoc lbt[5]: Mun Trong chương trình trên, đối tượng ListBox lưu giữ một mảng các chuỗi myStrings và một biến thành viên ctr đếm số chuỗi được chứa trong mảng myStrings. Chúng ta khởi tạo một mảng tối đa 256 chuỗi như sau: myStrings = new String[256]; Phần còn lại của bộ khởi dựng là thêm các chuỗi được truyền vào tham số, và đơn giản dùng lệnh lặp foreach để lấy từng thành phần trong mảng tham số đưa vào myStrings Ghi chú: Nếu chúng ta không biết số lượng bao nhiêu tham số được truyền vào phương thức, chúng ta sử dụng từ khóa params như đã mô tả trong phần trước của chương. Phương thức Add() của ListBoxTest không làm gì khác hơn là thêm một chuỗi mới vào bên trong mảng myStrings. Tuy nhiên phương thức quan trọng của ListBoxTest là bộ chỉ mục. Một bộ chỉ mục thì không có tên nên ta dùng từ khóa this: public string this [int index] 236 Mảng, Chỉ Mục, và Tập Hợp
  62. Ngôn Ngữ Lập Trình C# Cú pháp của bộ chỉ mục cũng tương tự như những thuộc tính. Chúng có thể có phương thức get() hay set() hay cả hai phương thức. Phương thức get() được thực thi đầu tiên bằng cách kiểm tra giá trị biên của chỉ mục và giả sử chỉ mục đòi hỏi hợp lệ, thì phương thức trả về giá trị đòi hỏi: get { if (index = myStrings.Length) { // xử lý chỉ mục sai } return myStrings[index]; } Đối với phương thức set() thì đầu tiên nó sẽ kiểm trả xem chỉ mục của đối tượng cần lấy có vượt quá số lượng của các đối tượng trong mảng hay không. Nếu giá trị chỉ mục hợp lệ tức là tồn tại một đối tượng có chỉ mục tương đương, phương thức sẽ bắt đầu thiết lập lại giá trị của đối tượng này. Từ khóa value được sử dụng để tham chiếu đến tham số đưa vào trong phép gán của thuộc tính: set { if ( index >= ctr) { // chỉ mục không tồn tại } } Do vậy, nếu chúng ta viết: myStrings[10] = “Hello C#”; trình biên dịch sẽ gọi phương thức set() của bộ chỉ mục trên đối tượng và truyền vào một chuỗi “Hello C#” như là một tham số ngầm định tên là value. Bộ chỉ mục và phép gán Trong ví dụ 9.9, chúng ta không thể gán đến một chỉ mục nếu nó không có giá trị. Do đó, nếu chúng ta viết: lbt[10] = “ah!”; Chúng ta có thể viết điều kiện ràng buộc bên trong phương thức set(), lưu ý rằng chỉ mục mà chúng ta truyền vào là 10 lớn hơn bộ đếm số đối tượng hiện thời là 6. Dĩ nhiên, chúng ta có thể sử dụng phương thức set() cho phép gán, đơn giản là phải xử lý những chỉ mục mà ta nhận được. Để làm được điều này, chúng ta phải thay đổi phương thức set() để kiểm tra giá trị Length của bộ đệm hơn là giá trị hiện thời của bộ đếm số đối tượng. 237 Mảng, Chỉ Mục, và Tập Hợp
  63. Ngôn Ngữ Lập Trình C# Nếu một giá trị được nhập vào cho chỉ mục chưa có giá trị, chúng ta có thể cập nhật bộ đếm như sau: set { if ( index >= strings.Length) { // chỉ mục vượt quá số tối đa của mảng } else { strings[index] = value; if ( ctr < index+1) ctr = index+1; } } Điều này có thể cho phép chúng ta tạo một mảng phân mảng các giá trị, khi đó ta có thể gán cho đối tượng có chỉ mục thứ 10 mà không cần phải có phép gán với đối tượng trước có chỉ mục là 9. Điều này hoàn toàn thực hiện tốt vì ban đầu chúng ta đã cấp phát mảng 256 các phần tử. Do đó chỉ cần truy cập đến đối tượng có chỉ mục từ 0 đến 255 là hợp lệ. Khi đó ta có thể viết: lbt[10] = “ah!”; Kết quả thực hiện tương tự như sau: lbt[0]: Hello lbt[1]: Universe lbt[2]: Who lbt[3]: is lbt[4]: Ngoc lbt[5]: Mun lbt[6]: lbt[7]: lbt[8]: lbt[9]: lbt[10]: “ah!” Sử dụng kiểu chỉ số khác Ngôn ngữ C# không đòi hỏi chúng ta phải sử dụng giá trị nguyên làm chỉ mục trong một tập hợp. Khi chúng ta tạo một lớp có chứa một tập hợp và tạo một bộ chỉ mục, bộ chỉ mục 238 Mảng, Chỉ Mục, và Tập Hợp
  64. Ngôn Ngữ Lập Trình C# này có thể sử dụng kiểu chuỗi làm chỉ mục hay những kiểu dữ liệu khác ngoài kiểu số nguyên thường dùng. Trong trường hợp lớp ListBox trên, chúng ta muốn dùng giá trị chuỗi làm chỉ mục cho mảng string. Ví dụ 9.10 sau sử dụng chuỗi làm chỉ mục cho lớp ListBox. Bộ chỉ mục gọi phương thức findString() để lấy một giá trị trả về là một số nguyên dựa trên chuỗi được cung cấp. Lưu ý rằng ở đây bộ chỉ mục được nạp chồng và bộ chỉ mục từ ví dụ 9.9 trước vẫn còn tồn tại. Ví dụ 9.10: Nạp chồng chỉ mục. namespace Programming_CSharp { using System; // tạo lớp List Box public class ListBoxTest { // khởi tạo với những chuỗi public ListBoxTest(params string[] initialStrings) { // cấp phát chuỗi strings = new String[256]; // copy các chuỗi truyền vào foreach( string s in initialStrings) { strings[ctr++] = s; } } // thêm một chuỗi vào cuối danh sách public void Add( string theString) { strings[ctr] = theString; ctr++; } // bộ chỉ mục public string this [ int index ] { get { if (index = strings.Length) 239 Mảng, Chỉ Mục, và Tập Hợp
  65. Ngôn Ngữ Lập Trình C# { // chỉ mục không hợp lệ } return strings[index]; } set { strings[index] = value; } } private int findString( string searchString) { for(int i = 0; i < strings.Length; i++) { if ( strings[i].StartsWith(searchString)) { return i; } } return -1; } // bộ chỉ mục dùng chuỗi public string this [string index] { get { if (index.Length == 0) { // xử lý khi chuỗi rỗng } return this[findString(index)]; } set { strings[findString(index)] = value; } } // lấy số chuỗi trong mảng 240 Mảng, Chỉ Mục, và Tập Hợp
  66. Ngôn Ngữ Lập Trình C# public int GetNumEntries() { return ctr; } // biến thành viên lưu giữ mảng các chuỗi private string[] strings; // biến thành viên lưu giữa số chuỗi trong mảng private int ctr = 0; } public class Tester { static void Main() { // tạo đối tượng List Box và sau đó khởi tạo ListBoxTest lbt = new ListBoxTest(“Hello”,”World”); // thêm các chuỗi vào lbt.Add(“Who”); lbt.Add(“is”); lbt.Add(“Ngoc”); lbt.Add(“Mun”); // truy cập bộ chỉ mục string str = “Universe”; lbt[1] = str; lbt[“Hell”] = “Hi”; //lbt[“xyzt”] = “error!”; // lấy tất cả các chuỗi ra for(int i = 0; i < lbt.GetNumEntries();i++) { Console.WriteLine(“lbt[{0}] = {1}”, i, lbt[i]); } } } }  Kết quả: lbt[0]: Hi lbt[1]: Universe lbt[2]: Who 241 Mảng, Chỉ Mục, và Tập Hợp
  67. Ngôn Ngữ Lập Trình C# lbt[3]: is lbt[4]: Ngoc lbt[5]: Mun Ví dụ 9.10 thì tương tự như ví dụ 9.9 ngoại trừ việc thêm vào một chỉ mục được nạp chồng lấy tham số chỉ mục là chuỗi và phương thức findString() tạo ra để lấy chỉ mục nguyên từ chuỗi. Phương thức findString() đơn giản là lặp mảng strings cho đến khi nào tìm được chuỗi có ký tự đầu tiên trùng với ký tự đầu tiên của chụổi tham số. Nếu tìm thấy thì trả về chỉ mục của chuỗi, trường hợp ngược lại trả về -1. Như chúng ta thấy trong hàm Main(), lệnh truy cập chỉ mục thứ hai dùng chuỗi làm tham số chỉ mục, như đã làm với số nguyên trước: lbt[“hell”] = “Hi”; Khi đó nạp chồng chỉ mục sẽ được gọi, sau khi kiểm tra chuỗi hợp lệ tức là không rỗng, chuỗi này sẽ được truyền vào cho phương thức findString(), kết quả findString() trả về là một chỉ mục nguyên, số nguyên này sẽ được sử dụng làm chỉ mục: return this[ findString(index)]; Ví dụ 9.10 trên tồn tại lỗi khi một chuỗi truyền vào không phù hợp với bất cứ chuỗi nào trong mảng, khi đó giá trị trả về là –1. Sau đó giá trị này được dùng làm chỉ mục vào chuỗi mảng strings. Điều này sẽ tạo ra một ngoại lệ (System.NullReferenceException). Trường hợp này xảy ra khi chúng ta bỏ đấu comment của lệnh: lbt[“xyzt”] = ”error!”; Các trường hợp phát sinh lỗi này cần phải được loại bỏ, đây có thể là bài tập cho chúng ta làm thêm và việc này hết sức cần thiết. Giao diện tập hợp Môi trường .NET cung cấp những giao diện chuẩn cho việc liệt kê, so sánh, và tạo các tập hợp. Một số các giao diện trong số đó được liệt kê trong bảng 9.2 sau: Giao diện Mục đích IEnumerable Liệt kê thông qua một tập hợp bằng cách sử dụng foreach. ICollection Thực thi bởi tất cả các tập hợp để cung cấp phương thức CopyTo() cũng như các thuộc tính Count, ISReadOnly, ISSynchronized, và SyncRoot. IComparer So sánh giữa hai đối tượng lưu giữ trong tập hợp để sắp xếp các đối tượng trong tập hợp. IList Sử dụng bởi những tập hợp mảng được chỉ mục 242 Mảng, Chỉ Mục, và Tập Hợp
  68. Ngôn Ngữ Lập Trình C# IDictionary Dùng trong các tập hợp dựa trên khóa và giá trị như Hashtable và SortedList. IDictionaryEnumerator Cho phép liệt kê dùng câu lệnh foreach qua tập hợp hỗ trợ IDictionary. Bảng 9.2: Giao diện cho tập hợp. Giao diện IEnumerable Chúng ta có thể hỗ trợ cú pháp foreach trong lớp ListBoxTest bằng việc thực thi giao diện IEnumerator. Giao diện này chỉ có một phương thức duy nhất là GetEnumerator(), công việc của phương thức là trả về một sự thực thi đặc biệt của IEnumerator. Do vậy, ngữ nghĩa của lớp Enumerable là nó có thể cung cấp một Enumerator: public IEnumerator GetEnumerator() { return (IEnumerator) new ListBoxEnumerator(this); } Enumerator phải thực thi những phương thức và thuộc tính IEnumerator. Chúng có thể được thực thi trực tiếp trong lớp chứa (trong trường hợp này là lớp ListBoxTest) hay bởi một lớp phân biệt khác. Cách tiếp cận thứ hai thường được sử dụng nhiều hơn, do chúng được đóng gói trong lớp Enumerator hơn là việc phân vào trong các lớp chứa. Do lớp Enumerator được xác định cho lớp chứa, vì theo như trên thì lớp ListBoxEnumerator phải biết nhiều về lớp ListBoxTest. Nên chúng ta phải tạo cho nó một sự thực thi riêng chứa bên trong lớp ListBoxTest. Lưu ý rằng phương thức GetEnumerator truyền đối tượng List- BoxTest hiện thời (this) cho enumerator. Điều này cho phép enumerator có thể liệt kê được các thành phần trong tập hợp của đối tượng ListBoxTest. Ở đây lớp thực thi Enumerator là ListBoxEnumerator, đây là một lớp private được định nghĩa bên trong lớp ListBoxTest. Lớp này thực thi đơn giản bao gồm một thuộc tính public, và hai phương thức public là MoveNext(), và Reset(). Đối tượng ListBoxTest được truyền vào như một đối mục của bộ khởi tạo. Ở đây nó được gán cho biến thành viên myLBT. Trong hàm khởi tạo này cũng thực hiện thiết lập giá trị biến thành viên index là -1, chỉ ra rằng chưa bắt đầu thực hiện việc enumerator đối tượng: public ListBoxEnumerator(ListBoxTest lbt) { this.lbt = lbt; index = -1; } Phương thức MoveNext() gia tăng index và sau đó kiểm tra để đảm bảo rằng việc thực hiện không vượt quá số phần tử trong tập hợp của đối tượng: public bool MoveNext() 243 Mảng, Chỉ Mục, và Tập Hợp
  69. Ngôn Ngữ Lập Trình C# { index++; if (index >= lbt.strings.Length) return false; else return true; } Phương thức IEnumerator.Reset() không làm gì cả nhưng thiết lập lại giá trị của index là -1. Thuộc tính Current trả về đối tượng chuỗi hiện hành. Đó là tất cả những việc cần làm cho lớp ListBoxTest thực thi một giao diện IEnumerator. Câu lệnh foreach sẽ được gọi để đem về một enumerator, và sử dụng nó để liệt kê lần lượt qua các thành phần trong mảng. Sau đây là toàn bộ chương trình minh họa cho việc thực thi trên. Ví dụ 9.11: Tạo lớp ListBox hỗ trợ enumerator. namespace Programming_CSharp { using System; using System.Collections; // tạo một control đơn giản public class ListBoxTest: IEnumerable { // lớp thực thi riêng ListBoxEnumerator private class ListBoxEnumerator : IEnumerator { public ListBoxEnumerator(ListBoxTest lbt) { this.lbt = lbt; index = -1; } // gia tăng index và đảm bảo giá trị này hợp lệ public bool MoveNext() { index++; if (index >= lbt.strings.Length) return false; else return true; } 244 Mảng, Chỉ Mục, và Tập Hợp
  70. Ngôn Ngữ Lập Trình C# public void Reset() { index = -1; } public object Current { get { return( lbt[index]); } } private ListBoxTest lbt; private int index; } // trả về Enumerator public IEnumerator GetEnumerator() { return (IEnumerator) new ListBoxEnumerator(this); } // khởi tạo listbox với chuỗi public ListBoxTest (params string[] initStr) { strings = new String[10]; // copy từ mảng chuỗi tham số foreach (string s in initStr) { strings[ctr++] = s; } } public void Add(string theString) { strings[ctr] = theString; ctr++; } // cho phép truy cập giống như mảng public string this[int index] { get 245 Mảng, Chỉ Mục, và Tập Hợp
  71. Ngôn Ngữ Lập Trình C# { if ( index = strings.Length) { // xử lý index sai } return strings[index]; } set { strings[index] = value; } } // số chuỗi nắm giữ public int GetNumEntries() { return ctr; } private string[] strings; private int ctr = 0; } public class Tester { static void Main() { ListBoxTest lbt = new ListBoxTest(“Hello”, “World”); lbt.Add(“What”); lbt.Add(“Is”); lbt.Add(“The”); lbt.Add(“C”); lbt.Add(“Sharp”); string subst = “Universe”; lbt[1] = subst; // truy cập tất cả các chuỗi int count =1; foreach (string s in lbt) { Console.WriteLine(“Value {0}: {1}”,count, s); count++; 246 Mảng, Chỉ Mục, và Tập Hợp
  72. Ngôn Ngữ Lập Trình C# } } } }  Kết quả: Value 1: Hello Value 2: Universe Value 3: What Value 4: Is Value 5: The Value 6: C Value 7: Sharp Value 8: Value 9: Value 10: Chương trình thực hiện bằng cách tạo ra một đối tượng ListBoxTest mới và truyền hai chuỗi vào cho bộ khởi dựng. Khi một đối tượng được tạo ra thì mảng của chuỗi được định nghĩa có kích thước 10 chuỗi. Năm chuỗi sau được đưa vào bằng phương thức Add(). Và chuỗi thứ hai sau đó được cập nhật lại giá trị mới. Sự thay đổi lớn nhất của chương trình trong phiên bản này là câu lệnh foreach được gọi để truy cập từng chuỗi trong ListBox. Vòng lặp foreach tự động sử dụng giao diện IEnumerator bằng cách gọi phương thức GetEnumerator(). Một đối tượng ListBoxEnumerator được tạo ra và giá trị index = -1 được thiết lập trong bộ khởi tạo. Vòng lặp foreach sau đó gọi phương thức MoveNext(), khi đó index sẽ được gia tăng đến 0 và trả về true. Khi đó foreach sử dụng thuộc tính Current để nhận lại chuỗi hiện hành. Thuộc tính Current gọi chỉ mục của ListBox và nhận lại chuỗi được lưu trữ tại vị trí 0. Chuỗi này được gán cho biến s được định nghĩa trong vòng lặp, và chuỗi này được hiển thị ra màn hình console. Vòng lặp tiếp tục thực hiện tuần tự từngt bước: MoveNext(), Current(), hiển thị chuỗi cho đến khi tất cả các chuỗi trong list box được hiển thị. Trong minh họa này chúng ta khai báo mảng chuỗi có 10 phần tử, nên trong kết quả ta thấy chuỗi ở vị trí 8, 9, 10 không có nội dung. Giao diện ICollection Một giao diện quan trọng khác cho những mảng và cho tất cả những tập hợp được cung cấp bởi .NET Framework là ICollection. ICollection cung cấp bốn thuộc tính: Count, IsReadOnly, IsSynchronized, và SyncRoot. Ngoài ra ICollection cũng cung cấp một phương 247 Mảng, Chỉ Mục, và Tập Hợp
  73. Ngôn Ngữ Lập Trình C# thức CopyTo(). Thuộc tính thường được sử dụng là Count, thuộc tính này trả về số thành phần trong tập hợp: for(int i = 0; i < myIntArray.Count; i++) { // } Ở đây chúng ta sử dụng thuộc tính Count của myIntArray để xác định số đối tượng có thể được sử dụng trong mảng. Giao diện IComparer Giao diện IComparer cung cấp phương thức Compare(), để so sánh hai phần tử trong một tập hợp có thứ tự. Phương thức Compare() thường được thực thi bằng cách gọi phương thức CompareTo() của một trong những đối tượng. CompareTo() là phương thức có trong tất cả đối tượng thực thi IComparable. Nếu chúng ta muốn tạo ra những lớp có thể được sắp xếp bên trong một tập hợp thì chúng ta cần thiết phải thực thi IComparable. .NET Framework cung cấp một lớp Comparer thực thi IComparable và cung cấp một số thực thi cần thiết. Phần danh sách mảng sau sẽ đi vào chi tiết việc thực thi IComparable. Danh sách mảng Một vấn đề hạn chế của kiểu dữ liệu mảng là kích thước cố định. Nếu chúng ta không biết trước số lượng đối tượng trong một mảng sẽ được lưu giữ, thì sẽ khó khăn vì có thể chúng ta khai báo kích thước của mảng quá nhỏ (vượt quá kích thước lưu trữ của mảng) hoặc là kích thước quá lớn (dẫn đến lãng phí bộ nhớ). Chương trình của chúng ta có thể hỏi người dùng về kích thước, hoặc thu những input từ trong một web site.Tuy nhiên việc xác định số lượng của đối tượng trong những session có tính chất tương tác động là không thích hợp. Việc sử dụng mảng có kích thước cố định là không thích hợp cũng như là chúng ta không thể đoán trước được kích thước của mảng cần thiết. Lớp ArrayList là một kiểu dữ liệu mảng mà kích thước của nó được gia tăng một cách động theo yêu cầu. ArrayList cung cấp một số phương thức và những thuộc tính cho những thao tác liên quan đến mảng. Một vài phương thức và thuộc tính quan trọng của ArrayList được liệt kê trong bảng 9.3 như sau: Phương thức- thuộc tính Mục đích Adapter() Phương thức static tạo một wrapper ArrayList cho đối tượng IList FixedSize() Phương thức static nạp chồng trả về sanh sách đối tượng như là một wrapper. Danh sách có kích thước cố định, các thành phần của nó có thể được sửa chữa nhưng không thể thêm hay xóa. 248 Mảng, Chỉ Mục, và Tập Hợp
  74. Ngôn Ngữ Lập Trình C# ReadOnly() Phương thức static nạp chồng trả về danh sách lớp như là một wrapper, chỉ cho phép đọc. Repeat() Phương thức static trả về một ArrayList mà những thành phần của nó được sao chép với giá trị xác định. Synchronized() Phương thức static trả về danh sách wrapper được thread- safe Capacity Thuộc tính để get hay set số thành phần trong ArrayList. Count Thuộc tính nhận số thành phần hiện thời trong mảng IsFixedSize Thuộc tính kiểm tra xem kích thước của ArrayList có cố định hay không IsReadOnly Thuộc tính kiểm tra xem ArrayList có thuộc tính chỉ đọc hay không. IsSynchronized Thuộc tính kiểm tra xem ArrayList có thread-safe hay không Item() Thiết lập hay truy cập thành phần trong mảng tại vị trí xác định. Đây là bộ chỉ mục cho lớp ArrayList. SyncRoot Thuộc tính trả về đối tượng có thể được sử dụng để đồng bộ truy cập đến ArrayList Add() Phương thức public để thêm một đối tượng vào ArrayList AddRange() Phương thức public để thêm nhiều thành phần của một ICollection vào cuối của ArrayList BinarySearch() Phương thức nạp chồng public sử dụng tìm kiếm nhị phận để định vị một thành phần xác định trong ArrayList được sắp xếp. Clear() Xóa tất cả các thành phần từ ArrayList Clone() Tạo một bản copy Contains() Kiểm tra một thành phần xem có chứa trong mảng hay không CopyTo() Phương thức public nạp chồng để sao chép một ArrayList đến một mảng một chiều. GetEnumerator() Phương thức public nạp chồng trả về một enumerator dùng để lặp qua mảng GetRange() Sao chép một dãy các thành phần đến một ArrayList mới IndexOf() Phương thức public nạp chồng trả về chỉ mục vị trí đầu tiên xuất hiện giá trị Insert() Chèn một thành phần vào trong ArrayList InsertRange(0 Chèn một dãy tập hợp vào trong ArrayList 249 Mảng, Chỉ Mục, và Tập Hợp
  75. Ngôn Ngữ Lập Trình C# LastIndexOf() Phương thức public nạp chồng trả về chỉ mục trị trí cuối cùng xuất hiện giá trị. Remove() Xóa sự xuất hiện đầu tiên của một đối tượng xác định. RemoveAt() Xóa một thành phần ở vị trí xác định. RemoveRange() Xóa một dãy các thành phần. Reverse() Đảo thứ tự các thành phần trong mảng. SetRange() Sao chép những thành phần của tập hợp qua dãy những thành phần trong ArrayList. Sort() Sắp xếp ArrayList. ToArray() Sao chép những thành phần của ArrayList đến một mảng mới. TrimToSize() Thiết lập kích thước thật sự chứa các thành phần trong ArrayList Bảng 9.3: Các phương thức và thuộc tính của ArrayList Khi tạo đối tượng ArrayList, không cần thiết phải định nghĩa số đối tượng mà nó sẽ chứa. Chúng ta thêm vào ArrayList bằng cách dùng phương thức Add(), và danh sách sẽ quan lý những đối tượng bên trong mà nó lưu giữ. Ví dụ 9.12 sau minh họa sử dụng ArrayList. Ví dụ 9.12: Sử dụng ArrayList. namespace Programming_CSharp { using System; using System.Collections; // một lớp đơn giản để lưu trữ trong mảng public class Employee { public Employee(int empID) { this.empID = empID; } public override string ToString() { return empID.ToString(); } public int EmpID { get { 250 Mảng, Chỉ Mục, và Tập Hợp