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
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.
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( ) {...} }
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.m1IL_000b: call instance void myStruct::set_status(int32)
IL_0010: ldstr "theStruct.Status: {0}" IL_0015: ldloca.s theStructIL_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 theStructIL_0056: box myStruct
IL_005b: stloc.1 IL_005c: ldloc.1 IL_005d: ldc.i4.4IL_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.
3.147.126.211