The first release of the dccil compiler included new features required by the CLR, and more have been added in subsequent updates.
Namespaces play an important role in the .NET Framework. They allow the class hierarchy to be extended by multiple third parties without fear of conflicting symbol names. Windows and COM use a 16-byte GUID to uniquely identify components, and this magic number must be recorded in the system registry. On the .NET platform, the concept of namespaces—plus metadata and the hard-and-fast rules about locating assemblies—makes GUIDs obsolete.
Ironically, the idea of a Delphi unit is similar to the CLR's namespaces. It's not too far a leap, if you think of a unit as a container of symbols, and a namespace as a container of units. In Delphi for .NET, the namespace to which a unit belongs is declared in the unit clause:
The dots indicate the containment of one namespace within another, and ultimately of the unit within the namespace. The dots separate the declaration into components, and each component—up to but not including the rightmost one—is a namespace. The entire declaration taken as a whole, dots and all, is the unit name. The dots simply serve as separators; no new symbols are introduced by the declaration. In this example, NamespaceA.NamespaceB is the namespace, and NamespaceA.NamespaceB.UnitA is the name of the unit. NamespaceA.NamespaceB.UnitA.pas would be the name of the source file, and the compiler would produce an output file called NamespaceA.NamespaceB.UnitA.dcuil.
The program statement (and eventually the package and library statements) optionally declares the default namespace for the entire project. Otherwise, the project is called a generic project, and the default namespace is that specified by the –ns compiler option. If no default project namespace is specified with compiler options, then behavior reverts to not using namespaces, like in Delphi 7 (and prior releases).
The unit clause does not have to declare membership in any explicit namespace. It might look like a traditional Delphi statement:
A unit that does not declare membership in a namespace is called a generic unit. Generic units automatically become members of the project namespace. Note, however, that this does not affect the source filename.
At the time of this writing, namespace support is very limited; this section describes how it should work in the future, rather than how it works now.
In the project file, you can specify a namespaces clause to list a set of namespaces for the compiler to search when it is trying to resolve references to generic units. The namespaces clause must appear immediately after the program (or package or library) statement and before any other clause or block type. The namespaces are separated by commas, and the list is terminated with a semicolon. For example:
program NamespaceA.MyProgram namespaces Foo.Bar, Foo.Frob, Foo.Nitz;
This example adds the namespaces Foo.Bar, Foo.Frob, and Foo.Nitz to the generic unit search space.
This discussion leads up to showing you how the compiler searches for generic units when you build your program. When you use a unit and fully qualify its name with the full namespace declaration, there is no problem:
The compiler knows the name of the dcuil file (or the .pas file) in this case. But suppose you only said the following:
This is called a generic unit reference, and the compiler must have a way to find its dcuil file.
The compiler searches namespaces in the following order:
The current unit namespace (if any)
The default project namespace (if any)
The namespaces listed in the project's namespaces clause (if any)
The namespaces specified by compiler options
For the first item, if the current unit specifies a namespace, then subsequent generic unit references in the current unit's uses clause are looked for first in the current unit's namespace. Consider this example:
unit Foo.Frob.Gizmos; uses doodads;
The first search location for the unit doodads would be in the namespace Foo.Frob. So, the compiler would try to open Foo.Frob.Doodads.dcuil. Failing this, the compiler would move on and prefix the unit name doodads with the default project namespace, and so on down the list.
The same symbol name can appear in different namespaces. When such ambiguity occurs, you must refer to the symbol by its full namespace and unit name. If you have a symbol named Hoozitz in unit Foo.Frob.Gizmos, you can refer to the symbol with either
Hoozitz; // if the name is unambiguous Foo.Frob.Gizmos.Hoozitz;
but not with
Gizmos.Hoozitz; // error! Frob.Gizmos.Hoozitz; // error!
Unit and namespace names can become quite long and unwieldy. You can create an alias for the fully qualified name with the as keyword in the uses clause:
uses Foo.Frob.DepartmentOfRedundancyDepartment.UIToys as ToyUnit;
Unit aliases introduce new identifiers, so their names cannot conflict with any other identifiers in the same unit (aliases are local to their unit). Even if you declare an alias, you can still use the original, longer name to refer to the unit.
The case of a namespace declaration is preserved and emitted into assembly metadata as is. However, as far as Delphi is concerned, two namespaces that differ only in case are equivalent.
The cross-language integration of the CTS and CLR brings up some interesting situations for compiler developers. For example, what if the name of an identifier in an assembly is the same as one of your language keywords? Consider the Delphi language keyword type. Type is also the name of a CLR class. Because type is a language keyword, it cannot be used as the name of an identifier. You can avoid this problem two ways in Delphi for .NET (these techniques were not implemented in Delphi 7 and previous versions).
First, you can use the fully qualified name of the identifier:
var T: System.Type;
The second, shorter way is to use the new ampersand operator (&) to prefix the identifier. The following has the same effect as the previous example:
var T: &Type;
In this statement the ampersand tells the compiler to look for a symbol with the name Type and to not consider it as a keyword. The compiler will look for the Type symbol in the available units, finding it in System (the same mechanism works regardless of the unit defining the symbol).
Two more concepts specified by the Common Language Infrastructure (CLI) have been added to the Delphi for .NET compiler: the class attribute sealed and the method attribute final. Putting the sealed attribute on a class effectively ends the class's ability to be used as a base class. Here is a sample code snippet:
type TDeriv1 = class (TBase) procedure A; override; end sealed;
A class cannot derive from a class that has been sealed. Similarly, a virtual method marked with the final attribute cannot be overridden in any descendant class, as in the following sample code.
type TDeriv1 = class (TBase) procedure A; override; final; end; TDeriv2 = class (TDeriv1) procedure A; override; // error: "cannot override a final method" end;
Borland added the sealed and final keywords to map an existing feature of .NET, but why did Microsoft introduce these attributes? The final and sealed attributes give users of your code important insights into how you intend your classes to be used. Moreover, these attributes give the compiler hints that allow it to generate more efficient Common Intermediate Language (CIL).
Delphi's notion of visibility—public, protected, and private—is a bit different from that of the CLI. In languages like C++ and Java, when you specify a visibility of private or protected on a class member, that class member is only visible to descendants of the class in which it is defined. As you saw in Chapter 2, however, Delphi enforces the idea of private and protected only for classes in different units, because everything is visible within a single unit. To be CTS compliant, the language required new visibility specifiers:
class private A member declared with class private visibility follows the C++ and Java rules. That is, class private members can be accessed only in methods or properties of the declaring class. Procedures and functions declared at the unit level and methods of other classes do not have access.
class protected Similarly, class protected members are visible only within the declaring class, and to descendants of the declaring class. Other classes in the same unit have access only if they inherit from this class.
See the ProtectedPrivate example in the LanguageTest folder of the chapter's source code for a trivial test case.
Delphi has long supported class methods—methods you can apply to a class as a whole and also as a specific instance, even if the methods' code cannot refer to the current object (the Self parameter of a class methods references the current class, not the current object). Delphi for .NET extends this idea by adding the class static specifier, class properties, class static fields, and class constructors:
Class Static Methods Like Delphi 7 class methods, class static members can be called without an object instance, and no Self parameter refers to an object. Unlike in Delphi 7, however, you cannot refer to the class itself. For example, calling the ClassName method will fail. Also unlike in Delphi 7, you cannot use the virtual keyword with class static methods.
Class Static Properties Like class methods, class static properties can be accessed without an object instance. The access methods or backing fields for class static properties must be declared class static themselves. Class static properties cannot be published, nor can they have stored or default value definitions.
Class Static Fields A class static field can be accessed without an object instance. Class static fields and properties are typically used as design tools; they allow you to declare variables and constants within the meaningful context of a class declaration.
Class Constructor A class constructor is a private constructor (it must be declared with class private visibility) that runs prior to the first use of the declaring class. The CLR offers no guarantee of when this will happen, except to say it will happen before the first use of the class. In CLR terms, this can get a bit tricky, because code is not considered "used" unless (and until) it is executed. A class can declare only one class constructor. Descendants can declare their own class constructors, but only one can be declared in any class.
You can't call a class constructor from source code; it is called automatically as a way to initialize class static fields and properties. Even the inherited keyword is prohibited, because the compiler takes care of this for you.
The following example class declaration illustrates the syntax for these new specifiers:
TMyClass = class class private // can only be accessed within TMyClass // Class constructor must have class private visibility class constructor Create; class protected // can be accessed in TMyClass and in descendants // Class static accessors for class static property P1, below class static function getP1 : Integer; class static procedure setP1(val : Integer); public // fx can be called without an object instance class static function fx(p : Integer) : Integer; // Class static property P1 must have class static accessors class static property P1 : Integer read getP1 write setP1; end;
Nested types are similar to class fields, in that they can be accessed through a class reference; an object instance is not needed. Declared within the scope of a class, nested types give you a way to use the enclosing class as a kind of namespace for the type.
Delphi has always had the ability to set an event listener—a function that is called when an event is fired. The CLR supports the use of multiple event listeners so that more than one function can respond when an event is fired. These are called multicast events. Delphi for .NET introduces two new property access methods, add and remove, to support multicast events. The add and remove methods can be used only on properties that are events.
To support multicast events, you must have a way to store all the functions that register themselves as listeners. As stated in Chapter 24, multicast events are implemented using the CLR MulticastDelegate class. And, as discussed there, the compiler hides a lot of complexity behind the scenes. The add and remove keywords handle the storage and removal of event listeners, but the containment mechanism is an implementation detail you aren't expected to deal with. The compiler automatically generates add and remove methods for you, and these methods implement storage of event listeners in an efficient way.
In the final release of Delphi for .NET, the add and remove methods should work hand in hand with an overloaded version of the standard functions Include and Exclude. In your source code, when you'd want to register a method as an event listener, you call Include. To remove a method, call Exclude. For example:
Include(EventProp, eventHandler); Exclude(EventProp, eventHandler);
Behind the scenes, Include and Exclude will call the methods assigned to the add and remove access functions, respectively. At the time of this writing, this technology wasn't working, so the book examples don't use it.
To support legacy code, the Delphi assignment operator (:=) still works as a way to assign a single event handler. The compiler generates code to go back and replace the last event handler (and only that event handler) that was set with the assignment operator. The assignment operator works separately and independently from the add/remove (or Include/Exclude) mechanism. In other words, the use of the assignment operator does not affect the list of event handlers that have been added to the MulticastDelegate.
As an example, you can refer to the XmlDemo program. The following code snippet (the working code at the time of this writing) creates a button at run time and installs two event handlers for its Click event:
MyButton := Button.Create; MyButton.Location := Point.Create ( Width div 2 - MyButton.Width div 2, 2); MyButton.Text := 'Load'; MyButton.add_Click (OnButtonClick); MyButton.add_click (OnButtonClick2); Controls.Add (MyButton);
Recall from Chapter 24 that one of the requirements of the CLI is an extensible metadata system. All .NET language compilers are required to emit metadata for the types defined within an assembly. The extensible part of extensible metadata means that programmers can define their own attributes and apply them to just about anything: assemblies, classes, methods, and more. The compiler emits these into the assembly's metadata. At run time, you can query for the attributes that were applied to an entity (assembly, class, method, and so on) using the methods of the CLR class System.Type.
Custom attributes are reference types derived from the CLR class System.Attribute. Declaring a custom attribute class is just like declaring any other class (this code snippet is extracted from the trivial NetAttributes project part of the LanguageTest folder):
type TMyCustomAttribute = class(TCustomAttribute) private FAttr : Integer; public constructor Create(val: Integer); property customAttribute : Integer read FAttr write FAttr; end; ... constructor TMyCustomAttribute.Create(val: Integer) begin inherited Create; customAttribute := val; end;
The syntax for applying the custom attribute is similar to that of C#:
type [TMyCustomAttribute(17)] TFoo = class public function P1(X : Integer) : Integer; end;
The custom attribute is applied to the construct immediately following it. In the example, it is applied to the class TFoo. No doubt you noticed that the custom attribute syntax is nearly identical to that of Delphi's GUID syntax. Here we have a problem: GUIDs are applied to interfaces; they must immediately follow the interface declaration. Custom attributes, on the other hand, must immediately precede the declaration to which they apply. How can the compiler determine whether the thing in the square brackets is a traditional Delphi-style GUID (which should be applied to the preceding interface declaration) or a .NET-style custom attribute (which should be applied to the first member of the interface)?
There is no way to tell, so you have to punt—make a special case for custom attributes and interfaces. If you apply a GUID to an interface, it must immediately follow the declaration of the interface, and it must follow the established Delphi syntax:
type interface IMyInterface ['(12345678-1234-1234-1234-1234567890ab)']
CLR's GuidAttribute custom attribute is used to apply GUIDs; it is part of the System.Runtime.InteropServices namespace. If you use this custom attribute to apply a GUID, then you must follow the CLR standard and put the attribute declaration before the interface.
Class helpers are an intriguing new language feature added to Delphi for .NET. The main reason for supporting class helpers is the way Borland maps .NET core classes with its own RTL classes, as covered later in the section "Class Helpers for the RTL." Here I will focus on this feature from a language perspective.
A class helper gives you a way to extend a class without using derivation, by adding new methods (but not new data). The odd fact, compared to inheritance, is that you can create objects of the original class, which is extended maintaining the same name. This means you can plug-in methods to an existing object of an existing class. A simple example will help clarify the idea.
Suppose you have a class (probably one you haven't written yourself—otherwise you could have extended it right away) like this:
type TMyObject = class private Value: Integer; Text: string; public procedure Increase; end;
Now you can add a Show method to objects of this class by writing a class helper to extend it:
type TMyObjectHelper = class helper for TMyObject public procedure Show; end; procedure TMyObjectHelper.Show; begin WriteLn (Text + ' ' + IntToStr (Value) + ' -- ' + Self.ClassType.ClassName + ' -- ' + ToString); end;
Notice that Self in the class helper method is the object of the class that the helper is for. You can use it like this:
Obj := TMyObject.Create; ... Obj.Show;
You'll end up seeing the name of the TMyObject class in the output. If you inherit from the class, however, the class helper will also be usable on the derived class (so you end up adding a method to an entire hierarchy), and everything will work properly. For your experiments, refer to the ClassHelperDemo example in the LanguageTest folder.