GIÁO TRÌNH

Giáo trình ngôn ngữ lập trình C#

Science and Technology

Truy cập phương thức giao diện

Tác giả: Khuyet Danh

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();

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 { 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 những bài sau.

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à:

<biểu thức> is <kiểu dữ liệu>

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ụ sau 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.

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 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 { 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ụ trên, 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_0028: brfalse.s
IL_0023: isinst
IL_002a: ldloc.0
IL_002b: castclass
IL_0030: stloc.2
IL_0031: ldloc.2
IL_0032: callvirt
IL_0037: br.s
IL_0039 ldstr

Đ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 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.

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:

<biểu thức> as <kiểu dữ liệu>

Đ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"); } }

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
IL_0028: stloc.2
IL_0029: ldloc.2
IL_002a: brfalse.s
IL_002c: ldloc.2
IL_002d: callvirt
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... ... 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ụ sau mở rộng từ ví dụ trên 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.

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");

}

// 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");

// 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 { 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

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.

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ụ sau

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()

{

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();

}

}

-----------------------------------------------------------------------------

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 { 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ị

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ụ sau 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.

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; } } 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

theStruct.Status: 6, isTemp: 4

-----------------------------------------------------------------------------

Trong ví dụ trên, 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:

// thay đổ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:

MSIL phát sinh từ ví dụ trên.

-----------------------------------------------------------------------------

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
IL_0029: ldloca.s theStruct
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
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
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_0087: stloc.2
IL_0088: ldloca.s V_2
IL_008a: box [mscorlib]System.Int32
IL_008f: call void [mscorlib]System.Console::WriteLine
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_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

So sánh giữa lớp và giao diện?

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ả.

Sự khác nhau giữa giao diện và lớp trừu tượng?

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ác lớp thực thi giao diện sẽ phải làm gì?

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ó bao nhiêu cách gọi một phương thức được khai báo trong giao diện?

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ác thành viên của giao diện có thể có những thuộc tính truy cập nào?

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.

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?

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

Toán tử is được dùng làm gì trong giao diện?

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?

Giao diện là kiểu dữ liệu tham chiếu hay kiểu giá trị?

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?

Số giao diện có thể được kế thừa cho một lớp?

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

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 đó.

Đọ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 { long width; long height; double Area(); double Circumference(); int Side(); }

-----------------------------------------------------------------------------

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 { get { retuen myX; } set { myX=value; } } public int y { get { retuen 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); } }

------------------------------------------------------------------

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.