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 does create 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(  );
}

// Simplify Document to implement only IStorable
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

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 Talk.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 Talk.Read( ) because the Document implementation of Talk.Read( )could not be virtual.

Member Hiding

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

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

and you derive from that interface a new interface, IDerived, which 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(  ) {...}
}

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. You 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 you 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, you create a reference to the IStorable interface. This causes an implicit boxing of the value object theStruct. You 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 not the boxed reference value for the interface reference:

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(  ) il managed
{
  .entrypoint
  // Code size     206 (0xce)
  .maxstack  4
  .locals ([0] value class myStruct theStruct,
         [1] class IStorable isTemp,
         [2] int32 V_2)
  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:  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)
  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 myStruct::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
(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 of method Tester::Main

On line IL:000b, the 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_0056 (highlighted) where the struct itself is boxed. It is that boxing that creates a reference type for the interface reference. Notice on line IL_005e that this time IStorable::set_status is called rather than myStruct::setStatus.

The design guideline is this: 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.147.126.211