8.4 Explicit Interface Implementation

In the implementation shown so far, the implementing class (in this case, Document) creates a member method with the same signature and return type as the method detailed in the interface. It is not necessary to explicitly state that this is an implementation of an interface; this is understood by the compiler implicitly.

What happens, however, if the class implements two interfaces, each of which has a method with the same signature? Example 8-5 creates two interfaces: IStorable and ITalk. The latter implements a Read( )method that reads a book aloud. Unfortunately, this conflicts with the Read( ) method in IStorable.

Because both IStorable and ITalk have a Read( ) method, the implementing Document class must use explicit implementation for at least one of the methods. With explicit implementation, the implementing class (Document) explicitly identifies the interface for the method:

void ITalk.Read( )

This resolves the conflict, but it creates a series of interesting side effects.

First, there is no need to use explicit implementation with the other method of Talk( ):

public void Talk( )    

Because there is no conflict, this can be declared as usual.

More importantly, the explicit implementation method cannot have an access modifier:

void ITalk.Read( )

This method is implicitly public.

In fact, a method declared through explicit implementation cannot be declared with the abstract, virtual, override, or new modifiers.

Most important, you cannot access the explicitly implemented method through the object itself. When you write:

theDoc.Read( );

the compiler assumes you mean the implicitly implemented interface for IStorable. The only way to access an explicitly implemented interface is through a cast to an interface:

ITalk itDoc = theDoc as ITalk; if (itDoc != null) {     itDoc.Read( ); }

Explicit implementation is demonstrated in Example 8-5.

Example 8-5. Explicit implementation
using System; interface IStorable {     void Read( );     void Write( ); } interface ITalk {     void Talk( );     void Read( ); } // Modify Document to implement IStorable and ITalk public class Document : IStorable, ITalk {     // the document constructor     public Document(string s)      {          Console.WriteLine("Creating document with: {0}", s);              }          // Make read virtual     public virtual void Read( )     {          Console.WriteLine("Implementing IStorable.Read");             }     public void Write( )     {          Console.WriteLine("Implementing IStorable.Write");                      }     void ITalk.Read( )     {         Console.WriteLine("Implementing ITalk.Read");     }     public void Talk( )         {         Console.WriteLine("Implementing ITalk.Talk");     } } public class Tester {       static void Main( )     {         // create a document object         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( );     } } Output: Creating document with: Test Document Implementing IStorable.Read Implementing ITalk.Read Implementing IStorable.Read Implementing ITalk.Talk

8.4.1 Selectively Exposing Interface Methods

A class designer can take advantage of the fact that when an interface is implemented through explicit implementation, the interface is not visible to clients of the implementing class except through casting.

Suppose the semantics of your Document object dictate that it implement the IStorable interface, but you do not want the Read( ) and Write( ) methods to be part of the public interface of your Document. You can use explicit implementation to ensure that they are not available except through casting. This allows you to preserve the semantics of your Document class while still having it implement IStorable. If your client wants an object that implements the IStorable interface, it can make an explicit cast, but when using your document as a Document, the semantics will not include Read( ) and Write( ).

In fact, you can select which methods to make visible through explicit implementation, so that you can expose some implementing methods as part of Document but not others. In Example 8-5, the Document object exposes the Talk( ) method as a method of Document, but the ITalk.Read( ) method can be obtained only through a cast. Even if IStorable did not have a Read( ) method, you might choose to make Read( ) explicitly implemented so that you do not expose Read( ) as a method of Document.

Note that because explicit interface implementation prevents the use of the virtual keyword, a derived class would be forced to reimplement the method. Thus, if Note derived from Document, it would be forced to reimplement ITalk.Read( ) because the Document implementation of ITalk.Read( ) could not be virtual.

8.4.2 Member Hiding

It is possible for an interface member to become hidden. For example, suppose you have an interface IBase that has a property P:

interface IBase {    int P { get; set; } }

Suppose you derive from that interface a new interface, IDerived, that hides the property P with a new method P( ):

interface IDerived : IBase  {    new int P( ); }

Setting aside whether this is a good idea, you have now hidden the property P in the base interface. An implementation of this derived interface will require at least one explicit interface member. You can use explicit implementation for either the base property or the derived method, or you can use explicit implementation for both. Thus, any of the following three versions would be legal:

class myClass : IDerived {    // explicit implementation for the base property    int IBase.P { get {...} }        // implicit implementation of the derived method    public int P( ) {...} } class myClass : IDerived {    // implicit implementation for the base property    public int P { get {...} }        // explicit implementation of the derived method    int IDerived.P( ) {...} } class myClass : IDerived {    // explicit implementation for the base property    int IBase.P { get {...} }        // explicit implementation of the derived method    int IDerived.P( ) {...} }

8.4.3 Accessing Sealed Classes and Value Types

Generally, it is preferable to access the methods of an interface through an interface cast. The exception is with value types (e.g., structs) or with sealed classes. In that case, it is preferable to invoke the interface method through the object.

When you implement an interface in a struct, you are implementing it in a value type. When you cast to an interface reference, there is an implicit boxing of the object. Unfortunately, when you use that interface to modify the object, it is the boxed object, not the original value object, that is modified. Further, if you change the value type, the boxed type will remain unchanged. Example 8-6 creates a struct that implements IStorable and illustrates the impact of implicit boxing when you cast the struct to an interface reference.

Example 8-6. References on value types
using System; // declare a simple interface interface IStorable {    void Read( );    int Status { get;set;} } // Implement through a struct 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( )    {       // create a myStruct object       myStruct theStruct = new myStruct( );       theStruct.Status = -1;  // initialize       Console.WriteLine(          "theStruct.Status: {0}", theStruct.Status);       // Change the value       theStruct.Status = 2;       Console.WriteLine("Changed object.");       Console.WriteLine(          "theStruct.Status: {0}", theStruct.Status);                // cast to an IStorable       // implicit box to a reference type       IStorable isTemp = (IStorable) theStruct;       // set the value through the interface reference       isTemp.Status = 4;       Console.WriteLine("Changed interface.");       Console.WriteLine("theStruct.Status: {0}, isTemp: {1}",           theStruct.Status, isTemp.Status);       // Change the value again       theStruct.Status = 6;       Console.WriteLine("Changed object.");       Console.WriteLine("theStruct.Status: {0}, isTemp: {1}",           theStruct.Status, isTemp.Status);    } } Output: theStruct.Status: -1 Changed object. theStruct.Status: 2 Changed interface. theStruct.Status: 2, isTemp: 4 Changed object. theStruct.Status: 6, isTemp: 4

In Example 8-6, the IStorable interface has a method (Read) and a property (Status).

This interface is implemented by the struct named myStruct:

public struct myStruct : IStorable

The interesting code is in Tester. Start by creating an instance of the structure and initializing its property to -1. The status value is then printed:

myStruct theStruct = new myStruct( ); theStruct.status = -1;  // initialize Console.WriteLine(    "theStruct.Status: {0}", theStruct.status);

The output from this shows that the status was set properly:

theStruct.Status: -1

Next access the property to change the status, again through the value object itself:

// Change the value theStruct.status = 2; Console.WriteLine("Changed object."); Console.WriteLine(    "theStruct.Status: {0}", theStruct.status);

The output shows the change:

Changed object. theStruct.Status: 2

No surprises so far. At this point, create a reference to the IStorable interface. This causes an implicit boxing of the value object theStruct. Then use that interface to change the status value to 4:

// cast to an IStorable // implicit box to a reference type IStorable isTemp = (IStorable) theStruct; // set the value through the interface reference isTemp.status = 4; Console.WriteLine("Changed interface."); Console.WriteLine("theStruct.Status: {0}, isTemp: {1}",      theStruct.status, isTemp.status);

Here the output can be a bit surprising:

Changed interface. theStruct.Status: 2, isTemp: 4

Aha! The object to which the interface reference points has been changed to a status value of 4, but the struct value object is unchanged. Even more interesting, when you access the method through the object itself:

// Change the value again theStruct.status = 6; Console.WriteLine("Changed object."); Console.WriteLine("theStruct.Status: {0}, isTemp: {1}",      theStruct.status, isTemp.status);

the output reveals that the value object has been changed, but the boxed reference value for the interface reference has not:

Changed object. theStruct.Status: 6, isTemp: 4

A quick look at the MSIL code (Example 8-7) reveals what is going on under the hood:

Example 8-7. MSIL code resulting from Example 8-6
  .method private hidebysig static void  Main( ) cil managed   {     .entrypoint     // Code size       187 (0xbb)     .maxstack  4     .locals init ([0] valuetype myStruct theStruct,              [1] class IStorable isTemp)     IL_0000:  ldloca.s   theStruct     IL_0002:  initobj    myStruct     IL_0008:  ldloca.s   theStruct     IL_000a:  ldc.i4.m1     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:  box        [mscorlib]System.Int32     IL_0021:  call       void [mscorlib]System.Console::WriteLine(string,                                                                   object)     IL_0026:  ldloca.s   theStruct     IL_0028:  ldc.i4.2     IL_0029:  call       instance void myStruct::set_Status(int32)     IL_002e:  ldstr      "Changed object."     IL_0033:  call       void [mscorlib]System.Console::WriteLine(string)     IL_0038:  ldstr      "theStruct.Status: {0}"     IL_003d:  ldloca.s   theStruct     IL_003f:  call       instance int32 myStruct::get_Status( )     IL_0044:  box        [mscorlib]System.Int32     IL_0049:  call       void [mscorlib]System.Console::WriteLine(string,                                                                   object)     IL_004e:  ldloc.0     IL_004f:  box        myStruct     IL_0054:  stloc.1     IL_0055:  ldloc.1     IL_0056:  ldc.i4.     IL_0057:  callvirt   instance void IStorable::set_Status(int32)     IL_005c:  ldstr      "Changed interface."     IL_0061:  call       void [mscorlib]System.Console::WriteLine(string)     IL_0066:  ldstr      "theStruct.Status: {0}, isTemp: {1}"     IL_006b:  ldloca.s   theStruct     IL_006d:  call       instance int32 myStruct::get_Status( )     IL_0072:  box        [mscorlib]System.Int32     IL_0077:  ldloc.1     IL_0078:  callvirt   instance int32 IStorable::get_Status( )     IL_007d:  box        [mscorlib]System.Int32     IL_0082:  call       void [mscorlib]System.Console::WriteLine(string,                                                                   object,                                                                   object)     IL_0087:  ldloca.s   theStruct     IL_0089:  ldc.i4.6     IL_008a:  call       instance void myStruct::set_Status(int32)     IL_008f:  ldstr      "Changed object."     IL_0094:  call       void [mscorlib]System.Console::WriteLine(string)     IL_0099:  ldstr      "theStruct.Status: {0}, isTemp: {1}"     IL_009e:  ldloca.s   theStruct     IL_00a0:  call       instance int32 myStruct::get_Status( )     IL_00a5:  box        [mscorlib]System.Int32     IL_00aa:  ldloc.1     IL_00ab:  callvirt   instance int32 IStorable::get_Status( )     IL_00b0:  box        [mscorlib]System.Int32     IL_00b5:  call       void [mscorlib]System.Console::WriteLine(string,                                                                   object,                                                                   object)     IL_00ba:  ret   } // end of method Tester::Main

On line IL:000b, set_Status( ) was called on the value object. We see the second call on line IL_0017. Notice that the calls to WriteLine( ) cause boxing of the integer value status so that the GetString( ) method can be called.

The key line is IL_004f (highlighted) where the struct itself is boxed. It is that boxing that creates a reference type for the interface reference. Notice on line IL_0057 that this time IStorable::set_Status is called rather than myStruct::set_Status.

The design guideline is if you are implementing an interface with a value type, be sure to access the interface members through the object rather than through an interface reference.



Programming C#
C# Programming: From Problem Analysis to Program Design
ISBN: 1423901460
EAN: 2147483647
Year: 2003
Pages: 182
Authors: Barbara Doyle

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net