Platform Changes


Above and beyond the changes to the type system in Visual Basic .NET, there were also a number of changes required by the underlying structure of the .NET runtime environment itself.

Deterministic Finalization and Garbage Collection

The .NET platform was built from the ground up to be a garbage-collected system. That is to say, the .NET runtime environment tracks all references to objects allocated on the heap. Periodically, it checks whether some objects on the heap no longer have any references to them. If there are any such objects, they are garbage collected ”that is, freed. This scheme for freeing heap allocations is different than the scheme employed by COM. COM employed a reference counting mechanism to track references to objects. When a reference to an object was copied , the copier would call the AddRef method on the object to let it know that there was another reference to it. When a piece of code was done with a reference to an object, it called the Release method on the object. When the number of references to an object reached zero, the object would free itself.

The advantage of the COM scheme is that all objects are deterministically finalized. That is, an object is freed at the very moment that the last reference to it is released. On .NET, in comparison, an object will not be freed until sometime after the last reference is released ”that is, the next time the garbage collector runs. Deterministic finalization is advantageous because an object may be holding on to references to other objects itself, some of which may be scarce (such as a database connection). In that case, as soon as you free an object, you want the resources it holds to be freed as well. Even if the garbage collector runs every few milliseconds , that may not be fast enough to prevent resources from being used up.

The advantage of the .NET scheme is that it is very difficult to get the COM scheme right. Many thousands of hours have been spent in the software industry as a whole tracking down AddRef / Release problems. It is very easy when programming against a COM object to forget to do an AddRef when you copy a reference, resulting in a crash if the object is finalized too early. It is also very easy to forget to Release a reference once you are done with it, resulting in memory leaks and objects hanging around forever and never releasing their resources.

Another advantage of the .NET scheme is that it easily solves the problem of circular references ”in other words, a situation where object A has a reference to object B and B has a reference to A . Once a circular reference has been created, in COM the developer has to explicitly use some other mechanism to break the cycle and finalize the object. However, because the .NET runtime environment knows about all references between objects, the garbage collector can determine when a set of objects that contain circular references are no longer referenced outside the cycle and finalize all the objects at once.

By and large, all of the problems with the COM scheme mentioned earlier have been hidden from Visual Basic developers by the compiler except for circular references. In most cases, the compiler would correctly insert AddRef or Release calls into the compiled code to manage the references. Thus, the fact that .NET employs garbage collection does not buy Visual Basic as much as it buys programmers who write in other .NET languages, although there are still benefits such as circular reference finalization.

Unfortunately, there is no easy way to have deterministic finalization and garbage collection coexist within the same type system. A full discussion of this issue is beyond the scope of this appendix, but the core problem can be boiled down to a single question: Is the type Object deterministically finalized or garbage collected? If it is garbage collected, then casting an instance of a deterministically finalized object to Object would have to cause the instance to lose its deterministic finalization property (once it was typed as Object , AddRef / Release would not be called). Given that there are many situations (such as collections) where instances need to be cast to Object , this is not a workable solution. If Object is deterministically finalized, then effectively all types must be deterministically finalized, which means that garbage collection goes out the window. One could work around the question of Object by splitting the type system into two completely separate camps, garbage-collected types and deterministically finalized types, but then there would have to be two different root Object types that would cause every type that takes Object (such as collections) to have two versions: one for deterministically finalized types and one for garbage-collected types.

The end result is that there is no solid replacement for deterministic finalization in Visual Basic .NET. An interface, IDisposable , has been added to the .NET Framework that a class should implement if it holds on to precious resources that should be disposed of as soon as possible, but the onus is on the developer to correctly call the Dispose method when he or she is done with the instance. C# added another piece of syntax, using , to help formalize the call to Dispose , but we did not feel that using was exactly the right solution for Visual Basic developers. We expect to address this situation in a better way in a future release.

Let and Set Assignment

Visual Basic has historically distinguished between value assignment ( Let ) and reference assignment ( Set ). While this is a useful distinction in many situations, one of the reasons it was truly necessary in Visual Basic had to do with parameterless default properties. Objects in previous versions of Visual Basic can expose a default property that has no parameters. In that case, the default property is considered the "value" property of the object (indeed, in most cases the parameterless default property is named Value ). Because of this, there have to be two forms of assignment that indicate whether you are assigning the "value" of an object or assigning the actual reference to an object (see Listing A.9).

Listing A.9 module1.bas (VB6)
 Sub Main()     Dim x As Integer     Dim f1 As Field     Dim f2 As Field     Let x = f1        ' Assigns the value of the field     f2 = f1           ' Assigns the value field f1 to the                       ' value of f2     Set f2 = f1       ' Assigns the reference to the field End Sub 

COM distinguished between the Let and Set style of assignments, allowing properties to be defined with both kinds of accessors (see Listing A.10).

Listing A.10 class1.cls (VB6)
 Property Set foo(Value As Variant) End Property Property Let foo(Value As Variant) End Property 

The problem is that .NET does not distinguish between the Let and Set types of assignment, so properties can define only one kind of assignment accessor. The reason has to do with the fact that most programming languages do not make a distinction between types of assignment and, to be honest, the majority won out. It would have been possible for Visual Basic to define both types of accessors but expose only one to other languages, but this was very problematic . If you had an Object property that could take values and references, which one should be exposed? No matter which one was chosen , it would be wrong in many cases. The reverse problem would occur with properties defined in other languages.

After much deliberation and consideration of alternatives, we decided the simplest way was to drop the distinction between the Let and Set forms of assignment. This meant that properties could define only one kind of assignment accessor. It also meant that parameterless default properties had to be removed from the language. Although parameterless default properties were useful, they could also be obscure and confusing, so this was not considered a huge loss.

Late Binding

Late binding is the mechanism by which resolution of a method call can be deferred until runtime. Essentially , when making a call on a variable typed as Object , the compiler will emit a call to a helper with all the relevant information about the method call. At runtime, the helper will resolve which method (if any) to call and then make the call itself.

The first challenge in implementing late binding was adapting to the change from COM to .NET. In COM, late binding was handled by a component called OLE Automation through an interface called IDispatch . IDispatch relied on the information specified in a type library that described a class and was either compiled into or accompanied a COM component. In contrast, .NET late binding is done through a component called reflection. Whereas IDispatch and type libraries were not easily accessible to previous versions of Visual Basic and were considered more of an internal implementation detail, reflection is a full-fledged part of the .NET Framework. Reflection allows inspection of the type information that is part of every .NET executable (i.e., the equivalent of a type library compiled into the executable). It also has some mechanisms for doing late binding, but they are extremely simple. They really only allow you to inspect the methods that an object has and then invoke one of them at runtime.

Because we needed to express the full set of Visual Basic binding semantics, we had to write special helpers that sit on top of reflection to do that binding. When a late-bound helper is invoked, it first goes through much the same process as the compiler does at compile time to determine which method to call. The runtime binder considers inheritance and name hiding and does overload resolution. It understands Visual Basic's conversion rules and knows which calls are legal and which are not. It is a relatively complex piece of code.

Another challenge had to do with reference parameters. IDispatch was built using variants, which, as noted earlier, have the ability to store pointers in them. This meant that when a late-bound call was made, Visual Basic could pass ByRef variants to IDispatch for each argument. Then, if the actual parameter of the method being called was declared ByRef , the pointer could be passed in and the method would work as expected. Because .NET does not have a real equivalent to ByRef variants, we had to employ the same copy-in/copy-out mechanism that we used for ByRef Object parameters, with an additional twist.

When making a call to an early-bound method, it is possible for the compiler to know which parameters are ByRef Object s and which are not. Thus, it can figure out which arguments, if any, need to be passed using copy-in/copy-out semantics. When making a late-bound call, however, it's impossible to know until runtime which parameters are ByRef Object s. Thus, there is no way to know whether to generate code to do the copy-out. The ugly, but unavoidable solution is to pass an array of Boolean values corresponding to the argument list to the late-binding helper. Once the late-binding helper has determined which method it is going to call, it sets each element to True that corresponds to a ByRef parameter. The compiler generates code to check the element for each argument that could do a copy-back (i.e., ignoring literals, read-only fields, and so on) and then does a copy-back if the element has been set to True .

The change in the locus of identity from interfaces in COM to classes in .NET affects late binding, too. In COM, you would late-bind to the particular interface that you happened to have in your hand when you made the call. However, because .NET sees only instances of classes, it is never possible to have an instance of an interface in hand. This means that it is not possible to late-bind to interfaces, only classes. If a class Foo implements an interface Bar using a private member, there is no way to late-bind to that interface member. This is an inescapable result of the design of the .NET type system.

On Error and Structured Exception Handling

The On Error style of error handling has always been built on top of exception-handling mechanisms provided by the system. With Visual Basic .NET, we decided to expose this underlying structured exception-handling mechanism directly to programmers. Structured exception handling has two advantages over On Error -style error handling. First, its structured nature encourages more discriminating and precise use of exception handling, which can in some cases result in more optimized assembly output. Second, structured exception handling gives a much-finer-grained control over which exceptions are handled and when. This is not to say that On Error -style error handling does not have advantages over structured exception handling. For example, there is no equivalent to Resume or Resume Next in structured exception handling.

Implementing On Error -style exception handling on top of structured exception handling is a relatively straightforward task (except for Resume and Resume Next , which we'll discuss in a moment). Any method that contains an On Error statement is wrapped in a Try statement around all the code in the method. Then, a Catch handler is added that catches all exceptions. Each On Error statement sets a value in a local variable that indicates which On Error statement is currently active. When an exception occurs, the Catch handler switches on that local variable to determine where to go to. Consider Listing A.11 as an example.

Listing A.11 test.vb (VB.NET)
 Module Test     Sub Main()         Dim i As Integer         On Error Goto Foo         i = 20 Foo:         On Error Goto Bar         i = 30 Bar:         On Error Goto 0         i = 40     End Sub End Module 

It is equivalent to Listing A.12.

Listing A.12 test.vb (VB.NET)
 Module Test     Sub Main()         Try             Dim i As Integer             Dim CurrentHandler As Integer             CurrentHandler = 1   ' On Error Goto Foo             i = 20 Foo:             CurrentHandler = 2   ' On Error Goto Bar             i = 30 Bar:             CurrentHandler = 0   ' On Error Goto 0             i = 40         Catch e As Exception Where CurrentHandler > 0             Select Case CurrentHandler                 Case 1                     Goto Foo                 Case 2                     Goto Bar             End Select             Throw   ' In case something goes wrong.         End Try     End Sub End Module 

Resume and Resume Next are slightly more complex cases. When compiling a method that contains a Resume or Resume Next statement, the compiler adds a local that tracks which statement is being processed . Code is inserted after each statement to increment the local. When an exception occurs and a Resume or Resume Next statement is executed (either through an On Error statement or on its own), the local is loaded (and incremented with Resume Next ) and then a Select statement is executed to jump to the statement indicated by the local. No example is provided here because of space considerations, but the process works very much along the lines of the Listing A.11/Listing A.12 example.

It's important to note that this can be done only by the compiler and not by the .NET runtime environment ”even if the .NET runtime environment had a resume mechanism, it could work only based on IL instructions, whereas the Resume statement works based on statements. There's no way for the .NET runtime environment to know where the "next" statement or "current" statement begins and ends.

Events and Delegates

Events in COM and .NET are implemented very differently, even though they have virtually the same functionality. In COM, a class that sources events exposes one or more event interfaces. An event interface contains callback methods to handle each event that the class exposes. Another class that wishes to handle the events sourced by the class would then implement the event interface with the various event handlers that it wished to use. The class that was handling the events would then provide this event interface to the class that sourced the events (called a connection point ), and the sourcing class would invoke the appropriate methods when the event was raised.

In .NET, events are built around delegates, which are essentially managed function pointers. A delegate contains the address of a method and a particular instance of the containing class if the method is an instance method. A delegate can invoke the method to which it points, given a list of arguments. Delegates are multicast, which is to say that a single delegate can point to more than one method. When the delegate is invoked, all the methods to which the delegate points are called in order.

The semantics of the AddressOf expression were extended slightly in Visual Basic .NET to make working with delegates easier. The AddressOf operator can be applied to any method and produces, essentially, an unnamed delegate type representing that method's signature. The .NET type system does not actually support unnamed delegate types, so this is merely a compiler trick ”the result of an AddressOf expression must ultimately be converted to a named delegate type. The compiler then inserts an instantiation for the correct delegate type at the point of conversion. If the target delegate type is not specified (by converting the AddressOf expression to Object ) or is ambiguous (as is possible in overload resolution), an error will result.

An event, then, is made up of three things in .NET: a delegate type that defines the signature of the event, a method to add a new handler for the event, and a method that removes an existing handler for the event. When a class wishes to handle an event raised by a class, it first creates a new delegate of the type of the event on the method that will handle the event. It then passes this delegate to the add handler method of the event. The add handler method combines the delegate being passed in with all the other delegates with which it has been called to handle the event. When the class that sources the event raises the event, it simply invokes the delegate with the appropriate parameters, which calls each handler in turn . If a class wishes to stop handling the event, it creates another delegate on the handler that it wants to stop having called and calls the remove handler method with the delegate. The remove handler method then removes the particular delegate from the delegate list that it is maintaining for the event.

Visual Basic .NET significantly simplifies the process of defining and handling events. Although we give you access to most of the low-level definition of events, most of the time you don't need to bother with the inner guts. For example, you can declare an event with just a signature, and the compiler will define the event delegate for you under the covers. Also, if you declare a field with the WithEvents modifier, you can then declare methods in the class with the Handles clause, and the compiler will automatically call the add and remove event methods for you. Consider Listing A.13.

Listing A.13 class.vb (VB.NET)
 Class Raiser     Public Event E()     Public Sub Raise()         RaiseEvent E()     End Sub End Class Class Handler     Public WithEvents R As Raiser     Public Sub New()         R = New Raiser()     End Sub     Public Sub DoRaise()         R.Raise()     End Sub     Public Sub HandleE() Handles R.E     End Sub End Class 

It is equivalent to Listing A.14.

Listing A.14 class1.vb (VB.NET)
 Class Raiser     Public Delegate Sub EEventHandler()     Public Event E As EEventHandler     Public Sub Raise()         RaiseEvent E()     End Sub End Class Class Handler     Private RLocal As Raiser     Public Property R As Raiser         Get             Return RLocal         End Get         Set (ByVal Value As Raiser)             If Not RLocal Is Nothing Then                 R.remove_E(new EEventHandler( _                                AddressOf HandleE))             End If             RLocal = Value             If Not RLocal Is Nothing Then                 R.add_E(new EEventHandler( _                                AddressOf HandleE))             End If         End Set     End Property     Public Sub DoRaise()         R.Raise()     End Sub     Public Sub HandleE()     End Sub End Class 

There are some details, such as the add and remove methods of the event, that Visual Basic .NET does not allow you to define for yourself. This may be changed in future versions of the compiler, as there are some advanced scenarios where this ability may be desirable.



Programming in the .NET Environment
Programming in the .NET Environment
ISBN: 0201770180
EAN: 2147483647
Year: 2002
Pages: 146

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