• Class Interfaces
• Interface Inheritance
• Considerations for Visual C++ Programmers
• Considerations for Visual Basic 6 Programmers
To conclude our examination of developing COM components that are exposed to .NET, we’re going to focus on implementing .NET interfaces in unmanaged code. A .NET interface is any interface defined in metadata that’s not marked with the ComImportAttribute
pseudo-custom attribute.
Just as a .NET class that implements a COM interface is sometimes said to be a “COM-compatible” class, a COM class that implements a .NET interface could be called a “.NET-compatible” class. This scenario is much rarer than the reverse, however, because programmers writing new code that implements .NET interfaces are likely writing managed code. But having a COM class implement a .NET interface can be a minor but important tweak that can greatly improve the use of an existing COM object from managed code. Because .NET is not a binary standard like COM but a type standard, COM objects implementing .NET interfaces can achieve a degree of type compatibility with .NET objects.
This process begins by exporting a type library containing an interface definition if one doesn’t already exist, and ends with importing a type library for your COM component that references the exported type library. Implementing a .NET interface is really no different than implementing a COM interface, but there are some surprises that you may encounter, as you’ll see in this chapter.
Unlike the reverse direction (covered in Chapter 14, “Implementing COM Interfaces for Binary Compatibility”) in which a COM-Callable Wrapper (CCW) implements several COM interfaces, a Runtime-Callable Wrapper (RCW) doesn’t implement any .NET interfaces by default. This is because none of a Runtime-Callable Wrapper’s base classes (System.__ComObject
, System.MarshalByRefObject
, and System.Object
) implement any interfaces. Therefore, it’s up to the COM object to implement any .NET interfaces it wishes to expose.
Class interfaces exposed for .NET classes require special care. Although they can be used by COM clients, they cannot be implemented in a way that is meaningful for the .NET world because class interfaces do not exist from the .NET perspective. For example, suppose a Visual Basic 6 application attempts to implement the _Object
class interface exposed for System.Object
as follows:
Implements mscorlib.Object
Private Function Object_Equals(ByVal obj As Variant) As Boolean
...
End Function
Private Function Object_GetHashCode() As Long
...
End Function
Private Function Object_GetType() As mscorlib.Type
...
End Function
Private Property Get Object_ToString() As String
...
End Property
(Visual Basic 6 hides the underscore for the _Object
interface, making it look like we’re implementing something called Object
.) Although a separate COM component could consume such an object and use its _Object
implementation, chances are that the author of the previous code wanted to expose the class to .NET and have its methods override the default implementation of the System.Object
methods in its Runtime-Callable Wrapper. Things don’t work that way, however. Thanks to the IDL custom attribute on the _Object
interface definition linking it to the System.Object
type, attempting to import a type library for the previous code results in an error because it appears that the class is trying to implement a class rather than an interface:
TlbImp error: System.TypeLoadException - Could not load type Project1.Class1Class from assembly Project1, Version=1.0.0.0 because it attempts to implement a class as an interface.
You cannot implement a .NET class interface in COM in a meaningful way for .NET clients. Although COM clients could communicate amongst themselves using a .NET class interface definition, you should never do this.
You cannot override methods nor can you control the class type of a COM object’s Runtime-Callable Wrapper by implementing a class interface. Note that this differs from the class interfaces generated by Visual Basic 6 for any coclass because these are real interfaces that can always be treated as such. Fortunately, users can only run into this confusion for auto-dual class interfaces, which are sufficiently rare. There should be no temptation to implement auto-dispatch class interfaces because their definitions contain no members.
When a .NET class type is used as a parameter of a .NET member (or field of a struct), the parameter that’s exported to a type library is always replaced with the corresponding coclass’s default interface. When a .NET class implements a real interface and suppresses its class interface with the recommended ClassInterface(ClassInterfaceType.None)
setting, the result can be confusing when the class type is used as a parameter rather than the interface type. Consider the following C# code:
[ClassInterface(ClassInterfaceType.None)] public class MyClass : IRealInterface{ ...}public interface IRealInterface{ ...}public interface IDemo{ void GoodMethod(IRealInterface x); void BadMethod(MyClass x);}
The IDemo
interface appears as follows in an exported type library:
interface IDemo : IDispatch { [id(0x60020000)] HRESULT GoodMethod([in] IRealInterface* x); [id(0x60020001)] HRESULT BadMethod([in] IRealInterface* x);};
Regardless of whether the IRealInterface
interface or MyClass
class is used as the .NET parameter, the exported parameter looks the same! However, a COM object can implement IRealInterface
and be passed to GoodMethod
, whereas no COM object can ever be passed to BadMethod
. That’s because an instance of MyClass
is needed at run time, despite the misleading type library description of BadMethod
.
Therefore, sometimes COM objects can’t even usefully implement real interfaces in order to be used with certain .NET APIs! This is why using class types for parameters when defining public .NET methods should be avoided wherever possible.
Often developers are tempted to implement _Object
, because the ability to override its members’ default implementation can be very useful. Although this isn’t possible, there are reasonable alternatives for plugging in new functionality related to the members of System.Object
. The following list contains the public and protected virtual (Overridable
in VB .NET) instance methods of System.Object
and what a COM object can do to customize related functionality:
• Equals
—Can implement IComparer
instead to use in some scenarios, described in the “Example: Implementing IHashCodeProvider
and IComparer
to Use a COM Object as a Hashtable Key” section.
• Finalize
—A COM object is released during its RCW’s finalization, so its destructor (or Class_Terminate
method in Visual Basic 6) is run at this time if it’s ready to be destroyed. Any COM objects that release limited resources upon destruction should also implement IDisposable
, covered in the “Example: Implementing IDisposable
to Clean Up Resources” section.
• GetHashCode
—Can implement IHashCodeProvider
instead to use in some scenarios, described in the “Example: Implementing IHashCodeProvider
and IComparer
to Use a COM Object as a Hashtable Key” section.
• ToString
—Can implement IFormattable
instead, shown in the “Example: Implementing IFormattable
to Customize
ToString” section.
Notice that none of these alternatives are specific to COM objects. .NET objects can and do use these additional interfaces.
Recall that interface hierarchies are “sliced” when exposed to COM. Listing 17.1 demonstrates how three related interfaces in the System.Collections
namespace are transformed by the type library exporter when defined in mscorlib.tlb
.
Listing 17.1. An Interface Hierarchy Is Exported to COM as Unrelated Interfaces Containing Only their Direct Members
This slicing of interfaces is significant when writing a COM object implementing a .NET interface that derives from other .NET interfaces. That’s because a COM object can implement a derived interface like IDictionary
without bothering to implement its base ICollection
and IEnumerable
interfaces! A COM object implementing only IDictionary
and its ten members defined in mscorlib.tlb
compiles without errors because compilers consuming the type library are not aware of any relationship IDictionary
has with other interfaces. The type library importer even successfully imports an Interop Assembly containing such a class; the metadata description indicates that it implements IDictionary
.
But what does this mean for .NET programs that attempt to use such an object that only partially implements the interface? .NET languages enable calling base interface members without even a cast. Suppose you have the following C# code that uses the IDictionary
interface:
public void PrintDictionaryProperties(IDictionary d)
{
// Call property defined directly on IDictionary
Console.WriteLine(d.IsReadOnly);
// Call property on base ICollection
Console.WriteLine(d.Count);
}
If a COM object implementing IDictionary
but not ICollection
were passed to this method, the first call to IDictionary.IsReadOnly
would succeed, but the second call to ICollection.Count
would throw the following exception:
System.InvalidCastException: QueryInterface for interface System.Collections.ICollection failed.
at System.Collections.ICollection.get_Count()
at Chapter17.PrintDictionaryProperties(IDictionary d)
at Chapter17.Main()
It just goes to show you that what you see isn’t always what you get when dealing with COM objects. Fortunately the exception message is clear enough to determine what happened.
Whenever implementing a .NET interface, be sure to check its .NET definition for any base interfaces and implement them too. Passing an object with an incomplete interface definition to .NET objects is likely to behave incorrectly.
Implementing a .NET interface in an unmanaged C++ project is just like implementing any other interface defined in an external type library. There are a few things to be careful with, however. Here are the steps for implementing a .NET interface in an ATL COM project in Visual C++ 6:
1. Right-click on your class in the ClassView
window and select Implement Interface...
, as shown in Figure 17.1. If you haven’t compiled your project yet, a dialog will appear warning that your project doesn’t have a type library. You can click OK
to get past that because we want to implement an interface in an external type library anyway.
Figure 17.1. Implementing a COM interface in Visual C++ 6 using the ATL Wizard.
4. On the Implement Interface
dialog that appears, click the Add Typelib...
button. This brings up the dialog shown in Figure 17.2, listing all the type libraries registered on your computer. Select one and click OK
. As in the Visual Basic 6 References
dialog and the Visual Studio .NET Add Reference
dialog, you can click the Browse...
button to select a type library that isn’t registered. In the figure, the user selects Common Language Runtime Library
, which is the type library exported from the mscorlib
assembly.
Figure 17.2. Selecting a type library containing the desired interface to implement in Visual C++ 6.
7. After selecting a type library, we’re now back to the Implement Interface
dialog. Select the interface you wish to implement and press OK
. This is shown in Figure 17.3.
Figure 17.3. Selecting the COM interface you wish to implement in Visual C++ 6.
These steps are sufficient for implementing an interface, but if you plan to use the object from .NET clients, you should also list the interface as being implemented by your coclass in the project’s IDL file (as recommended in Chapter 15, “Creating and Deploying Useful Primary Interop Assemblies”). The wizard does not do this for you. This involves two extra steps:
1. Add an importlib
statement inside your library
block listing the type library containing the interface definition, for example: importlib("mscorlib.tlb")
.
2. List the interface inside the coclass statement, for example:
[
uuid(1700FAA2-790D-4D27-AD0E-89AAB1EC39F2)
]
coclass FileWriter
{
[default] interface IFileWriter;
interface IDisposable;
};
If you’re implementing an interface defined in mscorlib.tlb
, the ATL wizard generates a line like the following in your header file:
#import "path\mscorlib.tlb" raw_interfaces_only, raw_native_types, no_namespace, named_guids
However, the class interface for System.Reflection.Module
is exported as a _Module
interface, and this causes compilation errors in a Visual C++ 6 ATL project because it already defines a CComModule
instance with the name _Module
. To fix this, you could remove the no_namespace
directive or add the #import
statement’s rename
directive to rename the _Module
identifier from the type library. For example:
#import "path\mscorlib.tlb" raw_interfaces_only, raw_native_types, no_namespace, named_guids, rename("_Module", "_ReflectionModule")
ATL projects in Visual C++ .NET no longer use _Module
, so this error does not occur for them.
IDisposable
to Clean Up ResourcesNow that you’ve seen how to implement a .NET interface, let’s see how to update an existing COM component with an additional implemented interface. Listings 17.2 and 17.3 contain a C++ header file and implementation for the following simple FileWriter
coclass that implements the IFileWriter
interface, shown here in IDL:
[
object,
uuid(379FF39A-2404-4FAE-B928-B53FADF9255B),
dual,
pointer_default(unique)
]
interface IFileWriter : IDispatch
{
[id(1)] HRESULT WriteLine([in] BSTR message);
};
[
uuid(1700FAA2-790D-4D27-AD0E-89AAB1EC39F2)
]
coclass FileWriter
{
[default] interface IFileWriter;
};
The CFileWriter
C++ class contains the implementation, which opens a file upon construction and closes it upon destruction. Therefore, this is a good candidate to be updated to implement the .NET IDisposable
interface, as discussed in Chapter 16, “COM Design Guidelines for Components Used by .NET Clients.”
To create such a project in Visual C++ 6, do the following:
1. Create a new ATL COM AppWizard
project, and on the two dialogs that follow, click Finish
, then OK
.
2. Right-click on the topmost node in the ClassView
window and select New ATL Object...
.
3. Select Simple Object
from the Objects
category, then click the Next
button.
4. Type “FileWriter” in the Short Name
text box, then click the Attributes
tab, check Support ISupportErrorInfo
, and click OK
.
5. Right-click on IFileWriter
in the ClassView
window and select Add method...
.
6. Type “WriteLine” in the Method Name
text box and type “[in] BSTR message” in the Parameters
text box, then click OK
. This creates an IDL file with the class and interface previously shown (but with different GUIDs) and a header file similar to Listing 17.2.
7. Fill in the implementation as in Listing 17.3 and update the header file with the contents of Listing 17.2.
Listing 17.2. FileWriter.h
. The Initial Header File for the CFileWriter
Class, Originally Generated by the ATL COM AppWizard
Listing 17.3. FileWriter.cpp
. Initial Implementation of the CFileWriter
Class that Keeps a File Open During the Course of the Object’s Lifetime
Notice that the class implements ISupportErrorInfo
, so the ATL wizard-generated implementation of InterfaceSupportsErrorInfo
appears in Lines 12–24. This listing doesn’t use IErrorInfo
because it doesn’t return any failure HRESULT
s, but we’ll be making use of it when we update the code in Listing 17.5.
Lines 29–33 contain the class’s constructor, which opens a file for reading and appending using the fopen
C runtime library function. The destructor in Lines 38–41 uses the fclose
C runtime library function to close the file. The only other method is WriteLine
, in Lines 46–51, which prints the passed-in message to the file using fwprintf
, followed by a newline character.
This simple COM component’s strategy of opening and closing the file has no problems when COM objects release the component as soon as they are finished (as they usually do). When called from .NET clients, however, the file can remain open a long time longer than it should because of the time difference between being finished and garbage collection.
Listings 17.4 and 17.5 update the two files from Listings 17.2 and 17.3 to make the class implement the .NET IDisposable
interface, using the steps outlined previously, at the beginning of the “Considerations for Visual C++ Programmers” section.
Listing 17.4. FileWriter.h
. Updated C++ Header File for the CFileWriter
Class That Implements IDisposable
The differences between Listings 17.4 and 17.2 appear in bold. All of the additional lines were added by the ATL wizard when choosing to implement IDisposable
. The one line that was tweaked is Line 8, to add the rename
directive for the _Module
type to avoid conflicts with ATL’s _Module
definition.
Listing 17.5. FileWriter.cpp
. Updated Implementation of the CFileWriter
Class That Implements IDisposable
Again, the differences between Listings 17.5 and 17.3 appear in bold. None of the changes in this listing were done by the ATL wizard, but had to be done manually instead. Line 5 includes CorError.h
, the header file defining .NET HRESULT
s (with FACILITY_URT
), which ships with the .NET Framework SDK. Line 18 adds the IID of IDisposable
to the list of interfaces for which InterfaceSupportsErrorInfo
returns S_OK
. Although this listing’s implementation of IDisposable
doesn’t use IErrorInfo
, it’s important to remember to update your implementation of InterfaceSupportsErrorInfo
whenever you implement a new interface. Otherwise, any IErrorInfo
information you set in the members belonging to the interface gets ignored.
The constructor is the same as in Listing 17.3, but the destructor in Lines 40–45 has a new check for NULL
before calling fclose
. This is necessary because the file might have already been closed by a call to Dispose
(which also sets the filePointer
variable to NULL
). If Dispose
has already been called, then the destructor simply exits without doing any work. This is analogous to a .NET Dispose
implementation calling GC.SuppressFinalize
to suppress any work done by a finalizer during garbage collection. Because the class’s destructor is always called, the check must be done to suppress its own behavior.
The implementation of WriteLine
(Lines 50–91) is now significantly longer than before, but the logic is simple. If the file has already been closed (meaning filePointer
is NULL
), a failure HRESULT
is returned, otherwise the message is printed to the file. This extra check is a necessity (in managed or unmanaged code) when the encapsulated resource can be disposed while the object instance is still alive.
Before returning the failure HRESULT
, Lines 59–81 set the IErrorInfo
information so .NET clients see a descriptive exception message when trying to call WriteLine
after Dispose
has been called. Objects that implement IDisposable
are supposed to throw a System.ObjectDisposedException
, but it’s not possible for a COM object to cause that exception type to be thrown because it doesn’t have a distinct HRESULT
value. Instead, it returns the HRESULT
corresponding to ObjectDisposedException
’s base InvalidOperationException
class and sets the message to explain that it’s related to the object being disposed prematurely.
When implementing a .NET interface, try to return HRESULT
values that correspond to the exceptions .NET clients would expect to be thrown by .NET components implementing the same interface. This isn’t always possible, however, because only exception types that are defined in the mscorlib
assembly and have distinct HRESULT
values can be thrown by COM clients. When you can’t return an HRESULT
that corresponds exactly to the desired exception, you can do two things to make up for it:
Return the HRESULT
value corresponding to the exception’s most derived base class that defines a distinct value. This is the same value you’d get by calling Marshal.GetHRForException
from the System.Runtime.InteropServices
namespace. If this ends up being a generic HRESULT
like E_FAIL
that results in a COMException
when thrown, you might as well define and return your own custom HRESULT
value (and document it). See Appendix C, “HRESULT
to .NET Exception Transformations,” to see which HRESULT
s correspond to which .NET exception types.
Use IErrorInfo
to give the exception a message that describes exactly what the problem is. This is often just as critical as returning the appropriate HRESULT
value.
Even when you can return the desired HRESULT
that corresponds to a .NET exception, COM components have a disadvantage because they can’t use a .NET exception’s default message (which would be seen when a .NET client throws the exception without setting a message). If a COM method returns a .NET HRESULT
without setting a message, it will always be “Exception from HRESULT
...,” so setting a message is always critical.
Finally, IDisposable
’s Dispose
method appears on Lines 96–106. In this method, the file is closed if it hasn’t been already. As the contract for Dispose
requires, calling it multiple times has the same effect as calling it once.
A .NET client can now use the FileWriter
object and take advantage of its IDisposable
implementation to close the file in a timely and familiar fashion. For example, a C# client can use the using
construct as follows:
using ((IDisposable)FileWriter f = new FileWriter())
{
foreach(int i in new int[]{1,2,3,4,5,6,7,8,9,10})
((FileWriter)f).WriteLine(i.ToString());
}
COM objects implementing .NET interfaces aren’t quite as natural to use in managed code as .NET objects implementing .NET interfaces simply because a coclass interface must be explicitly cast to any interface other than the coclass’s default interface. To avoid the cast, you could use the actual class type instead (with the Class
suffix):
using (FileWriterClass f = new FileWriterClass())
{
foreach(int i in new int[]{1,2,3,4,5,6,7,8,9,10})
f.WriteLine(i.ToString());
}
Implementing a .NET interface in a Visual Basic 6 project is pretty easy; just reference the appropriate type library, as shown in Chapter 8, “The Essentials for Using .NET Components from COM,” then type Implements
InterfaceName
at the top of the class module file.
There’s one unfortunate limitation to implementing interfaces in Visual Basic 6—the compiler doesn’t allow implementing an interface containing a member whose name contains an underscore. Because of that, any interface with overloaded members cannot be implemented by a Visual Basic 6 class. Figure 17.4 demonstrates what happens when a Visual Basic 6 user attempts to implement System.Reflection.ICustomAttributeProvider
from the mscorlib
assembly. This interface has two GetCustomAttribute
methods, so cannot be implemented. As shown, the Visual Basic 6 IDE silently omits the interface from the drop-down list that usually lists all implemented interfaces.
Figure 17.4. Attempting to implement a .NET interface with overloaded methods in Visual Basic 6.
When attempting to compile such a project, the error message shown in Figure 17.5 is displayed.
Figure 17.5. The error message when attempting to implement a .NET interface with overloaded methods in Visual Basic 6.
There is no good workaround for this problem. If the interface were derived directly from IUnknown
rather than IDispatch
(so late binding via name is out of the question), you could safely modify the exported method names to something without underscores, using OLEVIEW.EXE
to save the IDL representation of mscorlib.tlb
and using MIDL.EXE
to create a new type library from the altered IDL file. Or, you could create a new type library that contains an updated definition of ICustomAttributeProvider
with the new names. But this is not a good idea when the interface in question derives from IDispatch
, as ICustomAttributeProvider
does, because such modified names would be unrecognized by the object’s IDispatch
implementation if COM clients attempted to use late binding.
IFormattable
to Customize ToString
As mentioned in the “Class Interfaces” section at the beginning of the chapter, it’s impossible for a COM object to override Object.ToString
in its Runtime-Callable Wrapper. The functionality for this handy method, however, can be controlled by implementing the .NET System.IFormattable
interface.
IFormattable
has a single ToString
method that returns a string and has two parameters—a string specifying a format and an IFormatProvider
instance for customizing the output with locale-specific information. Listing 17.6 contains a Visual Basic 6 class that implements IFormattable
in order to control its ToString
output.
Listing 17.6. FormattableClass.cls
. A Visual Basic 6 Class That Implements IFormattable
to Control its ToString
Formatting
Line 2 contains the critical line that makes the class implement IFormattable
, defined in mscorlib.tlb
, which must be referenced by the Visual Basic 6 project. The definition of IFormattable.ToString
appears in Lines 13–66. Notice that it’s defined as a property get accessor rather than a method. That’s because of the type library exporter’s transformation of any ToString
methods into read-only properties, as explained in Chapter 9, “An In-Depth Look at Exported Type Libraries.” This doesn’t affect the .NET view of the method, so you can just implement the property the same way you’d implement the method.
By default, calling ToString
on a COM object returns the fully-qualified type name of the Runtime-Callable Wrapper, which could be MyProject.FormattableClass
in this case or System.__ComObject
if the class has no metadata available or associated with an instance. The goal of this IFormattable
implementation is to enable a variety of different formats to be displayed, including displaying the class’s CLSID or even the internal state of the object’s private array member.
The IFormatProvider
parameter is ignored in this simple implementation, making it culture-neutral. The format
string parameter can be one of the following values: “G”, “GN”, “GD”, “GB”, “GP”, “P”, “S”, an empty string or a null string. The documentation for IFormattable
states that all implementations must support at least the “G” formatting code (which stands for “General”). Every implementation should also be prepared to handle a null string to accept the default formatting. The default formatting doesn’t have to match the “G” formatting, although it’s a good idea in order to prevent confusion.
Line 20 chooses the “G” formatting if a null string or empty string is passed. Visual Basic 6 strings cannot be null, so if a .NET client passes null (Nothing
in VB .NET), the VB6 object simply sees an empty string. Line 22 begins a Select Case
statement to return the correct string for each valid formatting specifier. The “G” formatting returns “COM Class: ProgID {CLSID}”, and “GN”, “GD”, “GB”, and “GP” return the CLSID with formatting that matches System.Guid.ToString
’s “N”, “D”, “B”, and “P” formatting, respectively. The CLSID needs to be hardcoded after compiling once and checking the project’s type library, because Visual Basic 6 doesn’t provide a way to control CLSIDs.
The “P” formatting returns the class’s ProgID, and “S” returns “ClassName with Array { contents of array } “. If an unsupported formatting specifier was given, Lines 62–64 cause a FormatException
to be thrown, as specified by the IFormattable
contract. A FormatException
can be thrown simply by raising an error with the COR_E_FORMAT HRESULT
value.
A Visual Basic .NET client can use an instance of FormattableClass
as follows:
Dim c As FormattableClass = new FormattableClass()
Console.WriteLine(c)
Console.WriteLine(c.ToString("GN", Nothing))
Console.WriteLine(c.ToString("GD", Nothing))
Console.WriteLine(c.ToString("GB", Nothing))
Console.WriteLine(c.ToString("GP", Nothing))
Console.WriteLine(c.ToString("P", Nothing))
Console.WriteLine(c.ToString("S", Nothing))
Running this would produce the following output:
COM Class: MyProject.FormattableClass {C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4}
C2FBA9E3B52347C5AFB6F6180EEB6CD4
C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4
{C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4}
(C2FBA9E3-B523-47C5-AFB6-F6180EEB6CD4)
MyProject.FormattableClass
FormattableClass with Array { 0 1 2 3 4 5 6 7 8 9 }
IHashCodeProvider
and IComparer
to Use a COM Object as a Hashtable KeyAn object’s GetHashCode
and Equals
methods are important for objects serving as keys in a hashtable. The default implementation provided by System.Object
is acceptable if two keys should be compared based on their object references, but developers often want key objects to be compared based on some sort of value that’s independent of object references.
For example, imagine that you have instances of a chess board that you want to use as keys in a hashtable. You want to consider two chess boards as equal if they “look” exactly the same—both have the exact same pieces in the exact same places. To get this behavior, a ChessBoard
class would normally need to override its Equals
method to choose a behavior other than reference equality and override its GetHashCode
method to return the same hash code for “equal” instances.
Although COM objects can’t override the System.Object
implementation of Equals
or GetHashCode
, they can implement the .NET IComparer
interface (which has a single Equals
method) and the .NET IHashCodeProvider
interface (which has a single GetHashCode
method) to plug in the same sort of functionality used by certain .NET types. For example, an IComparer
instance can be passed to System.Array.BinarySearch
, System.Array.Sort
, a System.Collections.Hashtable
constructor, or a System.Collections.SortedList
constructor, and these types will use its Equals
method rather than the usual Object.Equals
method to test equality. Similarly, an IHashCodeProvider
instance can be passed to a System.Collections.Hashtable
constructor, and it will use its GetHashCode
method rather than the usual Object.GetHashCode
method to retrieve the hash code for any key.
These interfaces exist in order to provide hash codes or equality comparisons on behalf of a separate object that doesn’t appropriately override its GetHashCode
and Equals
methods. Therefore, you could author a new .NET class implementing IHashCodeProvider
and IComparer
that can be used in conjunction with an existing COM object. However, a COM object could easily implement these two interfaces on behalf of itself. Listing 17.7 does this for a Visual Basic 6 COM class that represents a chess board.
Listing 17.7. ChessBoard.cls
. A Visual Basic 6 Class That Implements IHashCodeProvider
and IComparer
So the ChessBoard
Type Can Be Used as Keys in a Hashtable
Lines 1 and 2 make the class implement IHashCodeProvider
and IComparer
, referenced from mscorlib.tlb
. Line 4 contains the class’s raw representation of the chess board—an 8×8 array of bytes. Each element represents a square on the chess board, and each value represents the contents of the square, such as 0 for empty, 1 for white king, 2 for white queen, and so on. Lines 6–8 contain a public read-only property for accessing the contents of each square. The ellipses in Line 10 represent additional ChessBoard
functionality that’s not important for this listing.
The implementation of IComparer.Compare
in Lines 12–27 returns true if the value of every element of the array belonging to x
matches every element of the array belonging to y
, or false if a single element doesn’t match. The passed-in x
and y
parameters are assumed to be instances of ChessBoard
. The implementation of IHashCodeProvider.GetHashCode
in Lines 29–41 does a simple mathematical operation to ensure that a given chess board arrangement always returns the same hash code value, which is the only requirement of a hash code. Coming up with one that’s unique for every key gives the best performance, but is not necessary. In this example, the same hash code can correspond to multiple board states, but it still enables appropriate hashing behavior whereas Object.GetHashCode
would not. (For example, any two boards that differ only by the value of element (0,0) return the same hash code in our implementation. That’s why GetHashCode
isn’t used by the Equals
method.)
A C# client can use ChessBoard
instances in a System.Collections.Hashtable
instance it constructs as follows:
ChessBoard b = new ChessBoard();
Hashtable t = new Hashtable((IHashCodeProvider)b, (IComparer)b);
or:
ChessBoardClass b = new ChessBoardClass();
Hashtable t = new Hashtable(b, b);
The first parameter is used for its IHashCodeProvider
implementation, and the second parameter (which could have been a completely different object) is used for its IComparer
implementation.
You should now know everything there is to know about writing COM classes that implement .NET interfaces. The four main points are:
• Implementing regular .NET interfaces is no different than implementing a COM interface once you reference an exported type library.
• Do not attempt to implement a .NET class interface.
• Visual Basic 6 classes can’t implement .NET interfaces with overloaded methods because of the renaming that includes an underscore.
• Try to return failure HRESULT
s that are as close as possible to the .NET exception expected, and always return additional error information that includes a message.
Many interfaces in the .NET Framework are COM-invisible because of their assemblies being marked with ComVisible(false)
. There’s no way for a COM object to implement such an interface without resorting to custom marshaling, discussed in Chapter 20, “Custom Marshaling.”
When using the type library importer to create an Interop Assembly for a COM class that implements a .NET interface, two things must be true:
• The exported type library describing COM’s view of the .NET interface must be registered so the importer can resolve the dependency.
• The assembly containing the original .NET interface must be in a location that can be loaded (such as the GAC or the directory in which you’re running TLBIMP.EXE
). The importer must load this assembly to get the .NET type of the interface being implemented. Failure to have it in a loadable location results in a System.IO.FileNotFoundException
.