Now that you know what .NET is all about, let’s talk about programming for the .NET environment. This chapter presents the common programming model that .NET provides, the core languages and features that .NET supports, and language integration—how you can take advantage of object-oriented features even across different languages that target the CLR.
Without the .NET Framework, programmers must choose from a wealth of APIs or libraries that support system services. For example, if you want to write GUI applications on Windows, you have a slew of options from which to choose, including the Win32 API, MFC, ATL, VB, and so on. Once you’ve chosen the library, you have to learn how to use the structures, classes, functions, interfaces, and so forth that the library provides. Unfortunately, this knowledge doesn’t transfer directly into a different environment. For instance, there’s a big difference between the code to manage IO in MFC and the code to manage IO in VB.
One of the goals of the .NET Framework is to bring commonality to application development by providing a framework of common classes to developers who are using compilers that generate IL. This set of classes, known as the Base Class Library (BCL), is extremely helpful: if you know how to take advantage of IO functionality in .NET using your favorite language, you can easily port that code to another language. This is possible because the namespaces, classes, methods, and so forth are equally accessible in all languages. For example, you can output a line of text to the console the same way across all .NET languages by using the WriteLine( ) method of the Console object, as we have seen elsewhere in this book. This consistent framework requires less development training and enables higher programmer productivity.
Since a full discussion of the entire set of classes in the .NET BCL is beyond the scope of this book (see O’Reilly’s In a Nutshell .NET series), we talk about the System.Object class and present the major namespaces in the .NET Framework, opening the doors for you to step into this world.
Every type in .NET is an object, meaning that it must derive directly or indirectly from the Object class. If you don’t specify a base class when you define a class, the compiler will inject this requirement into the IL code. The Object class supports a commonality that all .NET classes inherit and, thus, automatically provide to their consumers. The Object class exposes the public methods listed in Table 3-1, which you can invoke on any given .NET object at runtime.
Methods |
Description |
Compares two objects and determines whether they are equivalent (having the same content). | |
Compares two object references and determines whether they are referring to the same object in memory. | |
Gets the object’s hash code. Hash codes are used as an added mechanism for determining object uniqueness at runtime. For instance, if you want your objects to be used as keys in a hashtable, you must override this function and provide a unique hash value for each instance of your class. | |
Obtains the object’s type at runtime. Once you have obtained the object’s type, you can obtain everything about that type using the Reflection API, as explained in Chapter 2. | |
Gets a string representation of the object. Often used for debugging purposes, this method spits out the fully qualified class name by default. |
Examine the following program, which illustrates the use of all these methods:
using System; namespace Cpm { class CPModel { public static void Main( ) { CPModel c = new CPModel( ); // Test for self equivalence. Console.WriteLine("Equivalence: " + c.Equals(c) ); // Get the hash code from this object. Console.WriteLine("Object hash: " + c.GetHashCode( ) ); // Use the type to obtain method information. Console.WriteLine("Object method: " + c.GetType( ).GetMethods( )[1] ); // Convert the object to a string. Console.WriteLine("String representation: " + c.ToString( ) ); } } }
If you compile and run this C# program, you get the following output:
Equivalence: True
Object hash: 2
Object method: Boolean Equals(System.Object)
Object dump: Cpm.CPModel
The boldface line displays the second method of the
CPModel class.
If you look back at the program’s code,
you’ll see that we use the GetType( ) method to get
the type, and then we use the GetMethods( ) method to retrieve the
array of methods supported by this type. From this array, we pull off
the second method, which happens to be Equals( )
,
a method that’s implemented by
System.Object
.
As you can see, the System.Object class provides a mechanism for runtime type identification, equivalence, and inspection for all .NET objects.
Table 3-2 is a short list of important namespaces and classes in the .NET Framework that provide support for almost any application that you will develop. These are the namespaces that you’ll find yourself using again and again the more you develop .NET applications. For more information, consult MSDN Online or your SDK documentation, as a detailed discussion of these namespaces and classes is beyond the scope of this book.
Namespace |
Description |
Includes basic classes almost every program will use. Some simple classes that belong in this namespace are Object, Char, String, Array, and Exception. This namespace also includes more advanced classes such as GC and AppDomain. | |
Provides a set of classes to support synchronous and asynchronous IO manipulation for data streams. Also provides classes that allow you to manipulate the file system, such as creating, managing, and deleting files and directories. Some of these classes are FileStream, MemoryStream, Path, and Directory. | |
Includes a set of classes that allow you to manage collections of objects. Some of these classes are ArrayList, DictionaryBase, Hashtable, Queue, and Stack. | |
Includes a set of classes that support multithreaded programming. Some of these classes are Thread, ThreadPool, Mutex, and AutoResetEvent. | |
Includes a set of classes that support dynamic binding and type inspection. Some of these classes are Assembly, Module, and MethodInfo. | |
Includes a set of classes and child namespaces that provide security support. The interesting child namespaces include Cryptography, Permissions, Policy, and Principal. | |
Includes a set of classes and child namespaces that provide support for network programming. Some of these classes are IPAddress, Dns, and HttpWebRequest. | |
Contains classes for ADO.NET. See Chapter 5. | |
Contains classes for XML web services. See Chapter 6. | |
Contains classes for ASP.NET web pages. See Chapter 7. | |
Contains classes for Windows user interface applications. See Chapter 8. |
Keep in mind that if you know how to use any of the classes in these namespaces, you can write the code to take advantage of them in any language that targets the CLR, because the class and method names remain consistent across all .NET languages.
Since one of .NET’s goals is to support a common paradigm for application programming, it must specify and utilize programming concepts consistently. In this section, we will examine four core Microsoft .NET languages, including Managed C++, VB.NET, C#, and J#, and several core programming concepts that all .NET languages support, including:
Mitigates name collisions.
Specifies the methods and properties that must be implemented by objects that expose the interface.
In object-oriented languages, allows a class to combine all its data and behavior.
Allows a class to inherit from a parent class so that it can reuse rich functionality that the parent class has implemented, thus reducing development effort and programming errors.
Permits developers to specify or implement behaviors in a base class that can be overridden by a derived class. This is a very powerful feature because it allows developers to select the correct behavior based on the referenced runtime object.
Allows us to write easier-to-understand code because it allows us to capture all errors in a common, understandable pattern—totally opposite to that of nine levels of nested conditional blocks.
Although this is not a complete list of concepts that .NET supports, it includes all the major .NET concepts that we want to cover in this section. We will show you examples of all these features in Managed C++, VB.NET, C#, and J#. These concepts are nothing new: we’re merely demonstrating how they’re represented in all core Microsoft .NET languages.
Before we start, you should understand first what our examples will
accomplish. First, we will create a namespace, called Lang, that
encapsulates an interface, ISteering. Then we will create two
classes: Vehicle, which is an abstract base class that implements
ISteering, and Car, which is a derivative of Vehicle. We will support
an entry point that instantiates and uses Car within a
try
block. We will unveil other details as we work
through the examples.
Managed C++ is Microsoft’s implementation of the C++ programming language with some newly added keywords and features to support .NET programming. This allows you to use C++ to develop managed objects, which are objects that run in the CLR. Using Managed C++, you can obtain the performance[1] that is inherent in C++ programs, and at the same time, you can also take advantage of CLR features.[2]
Now let’s look at an example that includes all the
concepts we want to examine. As you can see in the following code
listing, we start off creating a new namespace,
Lang
, which envelops everything except
main( )
. With the exception of the first line and
special keywords, the code listing conforms perfectly to the C++
standard:
#using <mscorlib.dll>
using namespace System;
namespace Lang
{
Next, we specify an interface, called ISteering. If you are a C++
programmer, you will immediately notice that there are two new
keywords in the following code listing,
_ _gc
and
_ _interface
. The new
keyword _ _interface
allows you to declare an
interface, which is basically equivalent to an abstract base class in
C++. In other words, the two method prototypes are specified, but not
implemented here. The class that implements this interface provides
the implementation for these methods:
_ _gc _ _interface ISteering
{
void TurnLeft( );
void TurnRight( );
};
If you are a COM
programmer, you know that in COM you have to manage the lifetimes of
your objects and components yourself. Even worse, you also have to
rely on your clients to negotiate and interoperate correctly with
your COM components; otherwise, extant references will never be
reclaimed. Managed C++ removes this problem by adding a new keyword,
_ _gc
that tells the CLR to garbage-collect the
references to your interface when they are no longer in use. Aside
from these two keywords, the previous code listing requires no other
explanation for programmers who have experience with C-like
languages.
Now that we have an interface, let’s implement it.
The following code listing is a Managed C++ class (as indicated by
the _ _gc
) that implements our ISteering
interface. One thing to notice is that this class is an abstract base
class because the ApplyBrakes( ) method is a pure virtual function,
as indicated by the =0
syntax. Vehicle
doesn’t provide the implementation for this method,
but its derived class must supply the implementation:
_ _gc class Vehicle : public ISteering { public: void TurnLeft( ) { Console::WriteLine("Vehicle turns left."); } void TurnRight( ) { Console::WriteLine("Vehicle turns right."); } virtual void ApplyBrakes( ) = 0; };
Since Vehicle is an abstract base class and can’t be
instantiated, we need to provide a Vehicle derivative, which we will
call Car. As you can see in the following listing, everything about
the class is C++, with the exception of the keyword _
_gc
. Note that the ApplyBrakes( ) function first dumps a
text message to the console and then immediately creates and throws
an exception, notifying an exception handler that there has been a
brake failure.
_ _gc class Car : public Vehicle { public: void ApplyBrakes( ) { Console::WriteLine("Car trying to stop."); throw new Exception ("Brake failure!"); } }; } // This brace ends the Lang namespace.
What is special here is that the Exception class is a part of the .NET Framework, specifically belonging to the System namespace. This is great because this class works exactly the same way in all languages and there’s no longer a need to invent your own exception hierarchy.
Two important types that derive from Exception are SystemException (framework and Windows errors) and ApplicationException (your application’s errors). If you create your own exception classes, they should derive from ApplicationException. .NET conventions also suggest throwing ApplicationException objects for application-specific errors.
Now that we have a concrete class, we can write the main( ) function
to test our Car class. Notice that we have added a
try
block that
encapsulates the bulk of our code so that we can handle any
exceptions in the
catch
block. Looking
carefully at the following code listing, you’ll see
that we’ve instantiated a new Car on the managed
heap, but we’ve actually referred to this Car
instance using a Vehicle pointer. Next, we tell the vehicle to
TurnLeft( )—there’s no surprise here because
we’ve implemented this method in Vehicle. However,
in the following statement, we tell the Vehicle that
we’re applying the brakes, but ApplyBrakes( ) is not
implemented in Vehicle. Since this is a virtual method, the correct
vptr
and
vtbl
[3]
will be used, resulting in a call to
Car::ApplyBrakes( )
. Of
course Car::ApplyBrakes( )
will throw an
exception, putting us into the catch block. Inside the catch block,
we convert the caught exception into a string and dump it out to the
console.
We can do this because Exception is a class in the .NET Framework and all classes in the framework must derive from System.Object, which implements a rudimentary ToString( ) function to convert any object into a string:
void main( ) { try { Lang::Vehicle *pV = 0; // Namespace qualifier pV = new Lang::Car( ); // pV refers to a car pV->TurnLeft( ); // Interface usage pV->ApplyBrakes( ); // Polymorphism in action } catch(Exception *pe) { Console::WriteLine(pe->ToString( )); } }
Notice that you don’t have to deallocate your objects on the managed heap when you’ve finished using them, because the garbage collector will do that for you in .NET.
Although this is a simple example, we have used Managed C++ to illustrate all major object-oriented programming concepts, including namespaces, interfaces, encapsulation, inheritance, polymorphism, and exception handling. Next, we demonstrate that you can translate this code into any other .NET language because they all support these concepts. Specifically, we’ll show you this same example in VB.NET, C#, J#, and IL, just to prove that these concepts can be represented the same way in all languages that targets the CLR.
Microsoft has revamped VB and added full features for object-oriented programming. The new VB language, Visual Basic .NET (or VB.NET), allows you to do all that you can do with VB, albeit much more easily. If you are a VB programmer with knowledge of other object-oriented languages, such as C++ or Smalltalk, then you will love the new syntax that comes along with VB.NET. If you are a VB programmer without knowledge of other object-oriented languages, you will be surprised by the new VB.NET syntax at first, but you will realize that the new syntax simplifies your life as a programmer.[4]
In addition to the VB-style Rapid Application Development (RAD) support, VB.NET is a modernized language that gives you full access to the .NET Framework. The VB.NET compiler generates metadata and IL code, making the language an equal citizen to that of C# or Managed C++. Unlike VB versions prior to VB6, there will be no interpreter in VB.NET, so there should be no violent arguments about performance drawbacks of VB versus another language.
Perhaps the most potent feature is that now you can write interfaces and classes that look very similar to those written in other .NET languages. The new syntax allows you to inherit from base classes, implement interfaces, override virtual functions, create an abstract base class, and so forth. In addition, it also supports exception handling exactly as does C# and Managed C++, making error handling much easier. Finally, VB.NET ships with a command-line compiler, vbc.exe, introduced in Chapter 2.
Let’s see how to translate the previous Managed C++ program into VB.NET so that you can see the striking conceptual resemblance. First, we’ll start by defining a namespace called Lang, shown here in bold:
Imports System
Namespace Lang
Next, we specify the ISteering interface, which is easy to do in
VB.NET since the syntax is very straightforward, especially when you
compare it with Managed C++. In the following code listing,
you’ll notice that instead of using opening and
closing braces as in Managed C++, you start the interface definition
by using the appropriate VB.NET keyword,
Interface
, and end it
by prefixing the associated keyword with the word
End
. This is just normal VB-style syntax and
shouldn’t surprise any VB programmer:
Interface ISteering Sub TurnLeft( ) Sub TurnRight( ) End Interface
With our interface specified, we can now implement it. Since our
Vehicle class is an abstract base class, we must add the
MustInherit
keyword
when we define it, explicitly telling the VB.NET compiler that this
class cannot be instantiated. In VB.NET, the
Class
keyword allows
you to define a class, and the
Implements
keyword
allows you implement an interface. Another thing that you should be
aware of is that ApplyBrakes( ) is not implemented in this class, and
we have appropriately signaled this to the VB.NET compiler by using
the
MustOverride
keyword:
MustInherit Class Vehicle Implements ISteering Public Sub TurnLeft( ) Implements ISteering.TurnLeft Console.WriteLine("Vehicle turns left.") End Sub Public Sub TurnRight( ) Implements ISteering.TurnRight Console.WriteLine("Vehicle turn right.") End Sub Public MustOverride Sub ApplyBrakes( ) End Class
As far as language differences go, you must explicitly describe the
access (i.e., public
, private
,
and so forth) for each method separately. This is different from C++
because all members take on the previously defined access type.
Now we are ready to translate the concrete Car class. In VB.NET, you
can derive from a base class by using the
Inherits
keyword, as
shown in the following code. Since we have said that ApplyBrakes( )
must be overridden, we provide its implementation here. Again, notice
that we’re throwing an exception:
Class Car
Inherits Vehicle
Public Overrides Sub ApplyBrakes( )
Console.WriteLine("Car trying to stop.")
throw new Exception("Brake failure!")
End Sub
End Class
End Namespace
Now that we have all the pieces in place, let’s define a module with an entry point, Main( ), that the CLR will execute. In Main( ), you’ll notice that we’re handling exceptions exactly as we did in the Managed C++ example. You should also note that this code demonstrates the use of polymorphism because we first create a Vehicle reference that refers to a Car object at runtime. We tell the Vehicle to ApplyBrakes( ), but since the Vehicle happens to be referring to a Car, the object that is stopping is the target Car object:
Public Module Driver Sub Main( ) Try Dim v As Lang.Vehicle ' namespace qualifier v = New Lang.Car ' v refers to a car v.TurnLeft( ) ' interface usage v.ApplyBrakes( ) ' polymorphism in action Catch e As Exception Console.WriteLine(e.ToString( )) End Try End Sub End Module
This simple program demonstrates that we can take advantage of .NET object-oriented features using VB.NET. Having seen this example, you should see that VB.NET is very object oriented, with features that map directly to those of Managed C++ and other .NET languages.
As you’ve just seen, VB.NET is a breeze compared to Managed C++, but VB.NET is not the only simple language in .NET—C# is also amazingly simple. Developed from the ground up, C# supports all the object-oriented features in .NET. It maps so closely to the Java and C++ languages that if you have experience with either of these languages, you can pick up C# and be productive with it immediately.
Microsoft has developed many tools using C#; in fact, most of the components in Visual Studio .NET and the .NET class libraries were developed using C#. Microsoft is using C# extensively, and we think that C# is here to stay.[5]
Having said that, let’s translate our previous program into C# and illustrate all the features we want to see. Again, we start by defining a namespace. As you can see, the syntax for C# maps really closely to that of Managed C++:
using System;
namespace Lang
{
Following is the IStreering interface specification in C#. Since C#
was developed from scratch, we don’t need to add any
funny keywords like _ _gc
and _
_interface
, as we did in the Managed C++ version of this
program:
interface ISteering
{
void TurnLeft( );
void TurnRight( );
}
Having defined our interface, we can now implement it in the abstract
Vehicle class. Unlike Managed C++ but similar to VB.NET, C# requires
that you explicitly notify the C# compiler that the Vehicle class is
an abstract base class by using the
abstract
keyword. Since
ApplyBrakes( ) is an abstract method—meaning that this class
doesn’t supply its implementation—you must
make the class abstract, otherwise the C# compiler will barf at you.
Put another way, you must explicitly signal to the C# compiler the
features you want, including abstract
,
public
,
private
, and so forth,
each time you define a
class,
method,
property, and so on:
abstract class Vehicle : ISteering { public void TurnLeft( ) { Console.WriteLine("Vehicle turns left."); } public void TurnRight( ) { Console.WriteLine("Vehicle turn right."); } public abstract void ApplyBrakes( ); }
Here’s our Car class that derives from Vehicle and overrides the ApplyBrakes( ) method declared in Vehicle. Note that we are explicitly telling the C# compiler that we are indeed overriding a method previously specified in the inheritance chain. You must add the override modifier, or ApplyBrakes( ) will hide the one in the parent class. Otherwise, we are also throwing the same exception as before:
class Car : Vehicle
{
public override void ApplyBrakes( )
{
Console.WriteLine("Car trying to stop.");
throw new Exception("Brake failure!");
}
}
} // This brace ends the Lang namespace.
Finally, here’s a class that encapsulates an entry point for the CLR to invoke. If you look at this code carefully, you’ll see that it maps directly to the code in both Managed C++ and VB.NET:
class Drive { public static void Main( ) { try { Lang.Vehicle v = null; // Namespace qualifier v = new Lang.Car( ); // v refers to a car v.TurnLeft( ); // Interface usage v.ApplyBrakes( ); // Polymorphism in action } catch(Exception e) { Console.WriteLine(e.ToString( )); } } }
There are two other interesting things to note about C#. First, unlike C++ but similar to Java, C# doesn’t use header files.[6]
Second, the C# compiler generates XML documentation for you if you use XML comments in your code. To take advantage of this feature, start your XML comments with three slashes, as in the following examples:
/// <summary>Vehicle Class</summary> /// <remarks> /// This class is an abstract class that must be /// overridden by derived classes. /// </remarks> abstract class Vehicle : ISteering { /// <summary>Add juice to the vehicle.</summary> /// <param name="gallons"> /// Number of gallons added. /// </param> /// <return>Whether the tank is full.</return> public bool FillUp(int gallons) { return true; } }
These are simple examples using the predefined tags that the C#
compiler understands. You can also use your own XML tags in XML
comments, as
long as your resulting XML is well formed. Given that you have a
source code file with XML comments, you can automatically generate an
XML-formatted reference document by using the C#
compiler’s /doc
: option, as
follows:
csc /doc:doc.xml mylangdoc.cs
Although we didn’t specify the types of our parameters in the XML comments shown previously, the C# compiler will detect the correct types and add the fully qualified types into the generated XML document. For example, the following generated XML listing corresponds to the XML comments for the FillUp( ) method. Notice that the C# compiler added System.Int32 into the generated XML document:
<member name="M:Lang.Vehicle.FillUp(System.Int32)"> <summary>Add juice to the vehicle.</summary> <param name="gallons"> Number of gallons added. </param> <return>Whether the tank is full.</return> </member>
Now that you have the generated XML document, you can write your own XSL document to translate the XML into any visual representation you prefer.
Shipped with .NET Framework 1.1 (and thus with Visual Studio .NET 2003), J# is a Java language that targets the CLR. For completeness, here’s the same program in J#, demonstrating that J# also supports the same object-oriented features that we’ve been illustrating. We simply took the preceding C# program and made a few minor changes, resulting in the J# program that we are about to examine.
Let’s first look at the namespace declaration.
Instead of using the keyword namespace, Java uses the keyword
package
, which is conceptually equivalent to the
namespace concept we’ve been observing, since the
purpose of a package is to prevent name conflicts:
package Lang;
import System.Console;
The interface specification for ISteering
in J#
looks exactly equivalent to the one written in C#:
interface ISteering { void TurnLeft( ); void TurnRight( ); }
For the Vehicle class, there are two changes, which are shown in
bold. First, the keyword implements
is used to
declare that a class implements one or more interfaces. Second, since
Java requires thrown exceptions to be explicitly declared within the
method signature, we’ve added this declaration in
the ApplyBrakes( ) method:
abstract class Vehicle implements ISteering { public void TurnLeft( ) { Console.WriteLine("Vehicle turns left."); } public void TurnRight( ) { Console.WriteLine("Vehicle turn right."); } public abstract void ApplyBrakes( ) throws Exception; }
There are also two changes for the Car class, which are shown in
bold. The extends
keyword is used to declare that
a class derives from (or extends) another class. The declaration for
ApplyBrakes( )
must match it’s
parents signature, so we’ve explicitly indicated
that an exception may be thrown from this method, as shown in bold:
// extends - used to derive from a base class. class Car extends Vehicle { public void ApplyBrakes( ) throws Exception { Console.WriteLine("Car trying to stop."); throw new Exception("Brake failure!"); } }
Finally, we’ve made one minor change in the Drive
class: we simply changed Main( )
to main(
)
, as required by J#:
class Drive
{
public static void main( )
{
try
{
Lang.Vehicle v = null; // Namespace qualifer
v = new Lang.Car( ); // v refers to a car
v.TurnLeft( ); // Interface usage
v.ApplyBrakes( ); // Polymorphism in action
}
catch(Exception e)
{
Console.WriteLine(e.ToString( ));
}
}
}
Like C#, J# supports all the object-oriented concepts we’ve been studying. Also, J# and C# are syntactically very similar.
Since all languages compile to IL, let’s examine the IL code for the program that we’ve been studying. As explained in Chapter 2, IL is a set of stack-based instructions that supports an exhaustive list of popular object-oriented features, including the ones that we’ve already examined in this chapter. It is an intermediary step, gluing .NET applications to the CLR.
Let’s start by looking at the namespace declaration.
Notice the
.namespace
IL
declaration allows us to create our Lang namespace. Similar to C#, IL
uses opening and closing braces:
.namespace Lang
{
Now for the IStreering interface. In IL, any type that is to be
managed by the CLR must be declared using
the
.class
IL declaration. Since the CLR must manage
the references to an interface, you must use the
.class
IL declaration to specify an interface in
IL, as shown in the following code listing:
.class interface private abstract auto ansi ISteering { .method public hidebysig newslot virtual abstract instance void TurnLeft( ) cil managed { } // End of method ISteering::TurnLeft .method public hidebysig newslot virtual abstract instance void TurnRight( ) cil managed { } // End of method ISteering::TurnRight } // End of class ISteering
In addition, you must insert two special IL attributes:
Other attributes shown in this definition that aren’t necessarily needed to specify an interface in IL include the following:
private
Because we haven’t provided the visibility of our
interface definition in C#, the generated IL code shown here adds the
private
IL attribute to this interface definition.
This means that this particular interface is visible only within the
current assembly and no other external assembly can see it.
auto
Tells the CLR to perform automatic layout of this type at runtime.
ansi
Tells the CLR to use ANSI string buffers to marshal data across managed and unmanaged boundaries.
Now you know how to specify an interface in IL. Before we proceed
further, let’s briefly look at the attributes in the
.method
declarations—at least the attributes
that we haven’t examined, including:
Having specified the ISteering interface in IL,
let’s implement it in our Vehicle class. As you can
see in the following code fragment, there’s no
surprise. We extend the
System.Object
class (indicated by the
extends
keyword) and
implement Lang.ISteering
(as indicated by the
implements
keyword):
.class private abstract auto ansi beforefieldinit Vehicle extends [mscorlib]System.Object implements Lang.ISteering { .method public hidebysig newslot final virtual instance void TurnLeft( ) cil managed { // IL code omitted for clarity } // End of method Vehicle::TurnLeft .method public hidebysig newslot final virtual instance void TurnRight( ) cil managed { // IL code omitted for clarity } // End of method Vehicle::TurnRight .method public hidebysig newslot virtual abstract instance void ApplyBrakes( ) cil managed { } // End of method Vehicle::ApplyBrakes // .ctor omitted for clarity } // End of class Vehicle
Notice also that this class is an abstract class and that the
ApplyBrakes( ) method is an abstract method, similar to what
we’ve seen in the previous examples. Another thing
to note is the final
IL attribute in the
.method
declarations for both TurnLeft( ) and
TurnRight( ). This IL attribute specifies that these methods can no
longer be overridden by subclasses of Vehicle. Having seen all these
attributes, you should realize that everything in IL is explicitly
declared so that all components of the CLR can take advantage of this
information to manage your types at runtime.
Now let’s look at the Car class that derives from
the Vehicle class. You’ll notice that in the
ApplyBrakes( ) method implementation, the newobj
instance
IL instruction creates a new instance of
the Exception class. Next, the throw
IL
instruction immediately raises the exception object just created:
.class private auto ansi beforefieldinit Car extends Lang.Vehicle { .method public hidebysig virtual instance void ApplyBrakes( ) cil managed { // IL code omitted for clarity newobj instance void [mscorlib]System.Exception::.ctor(class System.String) throw } // End of method Car::ApplyBrakes // .ctor omitted for clarity } // End of class Car } // End of namespace Lang
Finally, let’s look at our Main( )
function, which is part of the Drive class. We’ve
removed most of the IL code—which you’ve
already learned—from this function to make the following code
easier to read, but we’ve kept the important
elements that must be examined. First, the
.locals
directive identifies all the local variables for the Main( )
function. Second, you can see that IL also supports exception
handling through the .try
instruction. In both the
.try
and catch
blocks, notice
that there is a leave.s
instruction that forces
execution to jump to the IL instruction on line
IL_0024
, thus leaving both the
.try
and
catch
blocks:
.class private auto ansi beforefieldinit Drive extends [mscorlib]System.Object { .method public hidebysig static void Main( ) cil managed { .entrypoint // Code size 37 (0x25) .maxstack 1 .locals (class Lang.Vehicle V_0, class [mscorlib]System.Exception V_1) .try { // IL code omitted for clarity leave.s IL_0024 } // End .try catch [mscorlib]System.Exception { // IL code omitted for clarity leave.s IL_0024 } // End handler IL_0024: ret } // End of method Drive::Main // .ctor omitted for clarity } // End of class Drive
As you can see, all the major concepts that we’ve examined apply intrinsically to IL. Since you’ve seen Managed C++, VB.NET, C#, J#, and IL code that support these features, we won’t attempt to further convince you that all these features work in other languages that target the CLR.
In the previous section, we saw that you can take advantage of .NET object-oriented concepts in any .NET language. In this section, we show that you can take advantage of language integration—the ability to derive a class from a base that is specified in a totally different language; to catch exceptions thrown by code written in a different language; or to take advantage of polymorphism across different languages, and so forth.
Before we discuss the examples in this section, let’s first understand what we want to accomplish (see Figure 3-1). We will first use Managed C++ to develop a Vehicle class that is an abstract base class. The Vehicle class exposes three polymorphic methods, including TurnLeft( ), TurnRight( ), and ApplyBrakes( ). We will then use VB.NET to develop a Car class that derives from Vehicle and overrides these three virtual methods. In addition, we will use C# to develop the Plane class that derives from Vehicle and overrides these three virtual methods.
In the upcoming code example, we can tell a Vehicle to TurnLeft( ) or TurnRight( ), but what turns left or right depends on the target object, whether a Car or a Plane. Unlike the examples in the previous section, the examples here illustrate that we can inherit classes and call virtual functions from ones that are defined in another language. In addition, we will demonstrate in our test program that exception handling works across different languages.
Let’s use Managed C++ to develop the Vehicle class, which is an abstract base class because ApplyBrakes( ) is a pure virtual function. Vehicle implements the ISteering interface to support turning left and turning right. Since the ApplyBrakes( ) function is a pure virtual function, any concrete derivative of Vehicle must implement this method:
#using <mscorlib.dll> using namespace System; public _ _gc _ _interface ISteering { void TurnLeft( ); void TurnRight( ); }; public _ _gc class Vehicle : public ISteering { public: virtual void TurnLeft( ) { Console::WriteLine("Vehicle turns left."); } virtual void TurnRight( ) { Console::WriteLine("Vehicle turns right."); } virtual void ApplyBrakes( ) = 0; };
Given this abstract base class, we can create a DLL that hosts this
definition. The first command here shows how we use the Managed C++
compiler to compile (as indicated by the /c
option) the vehicle.cpp file, which contains the
previous code. The second command shows how we use the C++ linker to
create a DLL with metadata and IL code:
cl /CLR /c vehicle.cpp link -dll /out:vehicle.dll vehicle.obj
Given just a few lines of Managed C++ code, we can build a DLL that
can be used by another component. Note that there is no need to
provide code for IUnknown, DllGetClassObject( )
,
DllCanUnloadNow( )
, DllRegisterServer(
)
, DllUnregisterServer( )
, and so forth.
In the old days, you had to provide code for these functions and
interfaces for legacy COM DLLs.
Given this abstract Vehicle class, the Car class can derive from it and provide the implementation for the three virtual methods defined by Vehicle. In the following code, note that we’ve overridden and provided the implementation for TurnLeft( ), TurnRight( ), and ApplyBrakes( ). The ApplyBrakes( ) method is special in that it throws an exception, which will be caught by code written in J#, as we’ll see later:
Imports System Public Class Car Inherits Vehicle Overrides Public Sub TurnLeft( ) Console.WriteLine("Car turns left.") End Sub Overrides Public Sub TurnRight( ) Console.WriteLine("Car turns right.") End Sub Overrides Public Sub ApplyBrakes( ) Console.WriteLine("Car trying to stop.") throw new Exception("Brake failure!") End Sub End Class
With this code, we can build a DLL using the command-line VB.NET compiler, as follows:
vbc /r:vehicle.dll /t:library /out:car.dll car.vb
Since we want the VB.NET compiler to generate a DLL, we must signal
this by using the /t:library
option. Also, since
Car derives from Vehicle, the VB.NET compiler must resolve the
references to Vehicle. We can tell the VB.NET compiler the location
of external references using the /r
: option. It is
important to note that you don’t need to have the
source code for the vehicle DLL to reuse its code because all type
information can be obtained from any .NET assembly. In addition, you
should note that from this example, we have proven that you can
derive a VB.NET class from a Managed C++ class.
Now let’s use C# to develop the Plane class, which derives from the Vehicle class written in Managed C++. Similar to the Car class, the Plane class implements the three virtual functions from the Vehicle class. Unlike the Car class, though, the ApplyBrakes( ) method of this class doesn’t throw an exception:
using System; public class Plane : Vehicle { override public void TurnLeft( ) { Console.WriteLine("Plane turns left."); } override public void TurnRight( ) { Console.WriteLine("Plane turns right."); } override public void ApplyBrakes( ) { Console.WriteLine("Air brakes being used."); } }
You can build a DLL from this code using the following command:
csc /r:vehicle.dll /t:library /out:plane.dll plane.cs
Notice that we have used the /r
: option to tell
the C# compiler that Vehicle is defined in
vehicle.dll.
Having developed vehicle.dll, car.dll, and plane.dll, we are now ready to demonstrate that polymorphism and exception handling work across different languages. Written in J#, the next code listing contains a main( ) method with a Vehicle reference and an exception handler.
Inside the
try
block, we first
instantiate a Plane class and refer to this instance using the local
Vehicle reference. Instead of telling the Plane
to
TurnLeft( )
or ApplyBrakes( )
,
we tell the Vehicle
to do so. Similarly, we
instantiate a Car
and refer to this instance using
the local Vehicle reference. Again, instead of telling the Car to
TurnLeft( )
or ApplyBrakes( )
,
we tell the Vehicle to do so. In both cases, we tell the Vehicle
either to TurnLeft( )
or ApplyBrakes(
)
, but the actual vehicle that employs TurnLeft(
)
or ApplyBrakes( )
is the Plane
instance in the first case and the Car instance in the second case;
that’s polymorphism, and it works across languages.
You should note that the second call to ApplyBrakes(
)
would cause an exception because we threw an exception
from Car’s ApplyBrakes( )
.
Although Car’s ApplyBrakes( )
was
written using VB.NET, we could still catch the exception that
it’s throwing in J#, proving that exception handling
works across languages:
class TestDrive
{
public static void main( )
{
Vehicle v = null; // Vehicle reference
try
{
Plane p = new Plane( );
v = p;
v.TurnLeft( );
v.ApplyBrakes( );
Car c = new Car( );
v = c;
v.TurnLeft( );
v.ApplyBrakes( );
// Exception
}
catch(System.Exception e)
{
System.Console.WriteLine(e.ToString( ));
}
}
}
If you want to test out these features, you can create an EXE using the following command:
vjc /r:vehicle.dll;car.dll;plane.dll /t:exe /out:drive.exe drive.jsl
Since we have used the Vehicle, Car, and Plane classes in this code,
we must include references to vehicle.dll,
car.dll, and plane.dll. And
since we are building an EXE, we need to signal this to the
J# compiler using the /t:exe
option. Once you have
built this EXE and executed it, you get the following output:
Plane turns left. Air brakes being used. Car turns left. Car trying to stop. System.Exception: Brake failure! at Car.ApplyBrakes( ) at TestDrive.main( )
As expected, the plane first turns left and then uses its air brakes. Then the car turns left, tries to stop, but can’t, so it throws an exception that is caught in the main( ) method.
In this simple example, we have shown that you can now take advantage of inheritance, polymorphism, and exception handling across different languages that target the CLR.
We started this chapter by telling you that .NET provides a common programming model, which reduces the learning curve and increases productivity. Once you’ve learned how to do something using the classes in the .NET Framework, this knowledge will transfer to any .NET language. We then illustrated that we could write the same type of code, supporting major .NET features, in any given language that targets the CLR. Finally, we proved to you that .NET indeed supports language integration, which was never possible using Microsoft platforms and tools, prior to .NET.
[1] You can easily
mix managed and unmanaged code in C++ programs. The unmanaged code
will perform better. See this chapter’s example
code, which you can download from http://www.oreilly.com/catalog/dotnetfrmess3/
.
[2] However,
if you look carefully at the features and new keywords (_
_abstract
, _ _box
, _
_delegate
, _ _gc
, _
_nogc
, _ _pin
, etc.) that have been
added to Microsoft C++, we doubt that you’ll want to
use Managed C++ to write new code for the CLR, especially when you
have C#.
[3] Many C++ compilers use vtbls
(a
vtbl
is a table of function pointers) and
vptrs
(a vptr
is a pointer to
the vtbl
) to support dynamic binding or
polymorphism.
[4] To learn more about VB.NET, see O’Reilly’s VB.NET Language in a Nutshell, Second Edition, by Steven Roman, PhD., Ron Petrusha, and Paul Lomax, or Programming Visual Basic .NET, Second Edition, by Jesse Liberty.
[5] To learn more about C#, check out O’Reilly’s C# Essentials, Second Edition, by Ben Albahari, Peter Drayton, and Brad Merrill; the forthcoming C# in a Nutshell, Second Edition, by Peter Drayton, Ben Albahari, and Ted Neward; and Programming C#, Third Edition, by Jesse Liberty.
[6] If you’ve never used C++, a header file is optional and usually contains class and type declarations. The implementation for these classes is usually stored in source files.