[ Team LiB ] |
8.4 Explicit Interface ImplementationIn 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 implementationusing 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 MethodsA 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 HidingIt 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 TypesGenerally, 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 typesusing 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. |
[ Team LiB ] |