Section 8.4. Explicit Interface Implementation


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 important, the explicit implementation method can't have an access modifier:

void ITalk.Read( )

This method is implicitly public.

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

Most important, you can't 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; itDoc.Read();

Explicit implementation is demonstrated in Example 8-5.

Example 8-5. Explicit implementation
#region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ExplicitImplementation {    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;          isDoc.Read( );          ITalk itDoc = theDoc;          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 don't 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 aren't available except through casting. This allows you to preserve the public API of your Document class while still having it implement IStorable. If your client wants an object that implements the IStorable interface, it can make a cast, but when using your document as a Document, the API 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 didn't have a Read() method, you might choose to make Read( ) explicitly implemented so that you don't 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() couldn't 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 of the struct from inside the method, the boxed type will remain unchanged (this is considered quite funny when it is in someone else's code). 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; #region Using directives using System; using System.Collections.Generic; using System.Text; #endregion namespace ReferencesOnValueTypes { // 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's 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       194 (0xc2)   .maxstack  3   .locals init ([0] valuetype ReferencesOnValueTypes.myStruct theStruct,            [1] class ReferencesOnValueTypes.IStorable isTemp)   IL_0000:  ldloca.s   theStruct   IL_0002:  initobj    ReferencesOnValueTypes.myStruct   IL_0008:  ldloca.s   theStruct   IL_000a:  ldc.i4.m1   IL_000b:  call       instance void ReferencesOnValueTypes.myStruct::                        set_Status(int32)   IL_0010:  ldstr      "theStruct.Status: {0}"   IL_0015:  ldloca.s   theStruct   IL_0017:  call       instance int32 ReferencesOnValueTypes.myStruct::                        get_Status( )   IL_001c:  box        [mscorlib]System.Int32   IL_0021:  call       void [mscorlib]System.Console::WriteLine(string,                                                                 object)   IL_0026:  nop   IL_0027:  ldloca.s   theStruct   IL_0029:  ldc.i4.2   IL_002a:  call       instance void ReferencesOnValueTypes.myStruct::                        set_Status(int32)   IL_002f:  ldstr      "Changed object."   IL_0034:  call       void [mscorlib]System.Console::WriteLine(string)   IL_0039:  nop   IL_003a:  ldstr      "theStruct.Status: {0}"   IL_003f:  ldloca.s   theStruct   IL_0041:  call       instance int32 ReferencesOnValueTypes.myStruct::                        get_Status( )   IL_0046:  box        [mscorlib]System.Int32   IL_004b:  call       void [mscorlib]System.Console::WriteLine(string,                                                                 object)   IL_0050:  nop   IL_0051:  ldloc.0   IL_0052:  box        ReferencesOnValueTypes.myStruct   IL_0057:  stloc.1   IL_0058:  ldloc.1   IL_0059:  ldc.i4.4   IL_005a:  callvirt   instance void ReferencesOnValueTypes.IStorable::                        set_Status(int32)   IL_005f:  ldstr      "Changed interface."   IL_0064:  call       void [mscorlib]System.Console::WriteLine(string)   IL_0069:  nop   IL_006a:  ldstr      "theStruct.Status: {0}, isTemp: {1}"   IL_006f:  ldloca.s   theStruct   IL_0071:  call       instance int32 ReferencesOnValueTypes.myStruct::                        get_Status( )   IL_0076:  box        [mscorlib]System.Int32   IL_007b:  ldloc.1   IL_007c:  callvirt   instance int32 ReferencesOnValueTypes.IStorable::                        get_Status( )   IL_0081:  box        [mscorlib]System.Int32   IL_0086:  call       void [mscorlib]System.Console::WriteLine(string,                                                                 object,                                                                 object)   IL_008b:  nop   IL_008c:  ldloca.s   theStruct   IL_008e:  ldc.i4.6   IL_008f:  call       instance void ReferencesOnValueTypes.myStruct::                        set_Status(int32)   IL_0094:  ldstr      "Changed object."   IL_0099:  call       void [mscorlib]System.Console::WriteLine(string)   IL_009e:  nop   IL_009f:  ldstr      "theStruct.Status: {0}, isTemp: {1}"   IL_00a4:  ldloca.s   theStruct   IL_00a6:  call       instance int32 ReferencesOnValueTypes.myStruct::                        get_Status( )   IL_00ab:  box        [mscorlib]System.Int32   IL_00b0:  ldloc.1   IL_00b1:  callvirt   instance int32 ReferencesOnValueTypes.IStorable::                        get_Status( )   IL_00b6:  box        [mscorlib]System.Int32   IL_00bb:  call       void [mscorlib]System.Console::WriteLine(string,                                                                 object,                                                                 object)   IL_00c0:  nop   IL_00c1:  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_001c (highlighted) where the struct itself is boxed. It is that boxing that creates a reference type for the interface reference. Notice on line IL_005a 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) Building. NET Applications with C#
Programming C#: Building .NET Applications with C#
ISBN: 0596006993
EAN: 2147483647
Year: 2003
Pages: 180
Authors: Jesse Liberty

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