Tuesday, August 28, 2007

Highlander2 Beta: Generics in Delphi for .NET

CodeGear's Delphi Product Manager Nick Hodges has once again released me from an NDA - now for the beta testing of what has been dubbed Highlander2 - the version of RAD Studio 2007 (renamed from BDS 2007) that includes an updated Delphi for .NET personality, compatible with .NET 2.0.

New stuff

There are many new features in this version, including:

  • Delphi for .NET now targets .NET 2.0, including full support for Generics (consume and create generic types)
  • ASP.NET 2.0 support, CSS templates, etc
  • ECO for VCL for .NET
  • Vista support in VCL for .NET
  • Blackfish, a managed .NET SQL database, supporting writing stored procedures and triggers in any .NET language. [I'm told there is also a Blackfish version for Java]. This is previously known as NDataStore, IIRC.
  • Updated and improved help
  • Bugfixes for Delphi Win32 and C++Builder

The Delphi Win32 personality receives a number of bug fixes (which will also be included in a free Update 3, I hear), but AFAIK, no significant new features per se. Of course, the IDE enhancements and fixes also benefit Win32 programming.

Since I'm a low-level kind of guy and a self-declared language freak, I'll mostly concentrate on the new language features in Delphi for .NET - particularly Generics.

Generics

Unless you have been living under a rock for the last few years, you'll know that Generics is the ability of programming with types as parameters at compile-time (and, in the case of .NET, at JIT-time). C++ has had templates since 1990 - which is a less constrained (literally), but more complex and potentially more powerful mechanism.

The Delphi for .NET Generics model maps directly to the CLR generics architecture that C# also uses. It is written in the stars (i.e. Delphi roadmap) that generics will also become available for Delphi Win32 in the future - it remains to be seen if this will be a superset or a subset of the current .NET implementation.

Enough talk already, lets look at some generics code in Delphi syntax.

Generic classes

Probably the most common use of generics is to define a class that has generic type parameters. These parameters are provided when a consumer uses the class - in effect instanciating a new concrete class just by declaring it. You can name type parameters anything you like.

type
TGenericClass<T> = class
Field: T;
procedure Method(A: T);
function Foo: T;
property Bar: T read Foo write Method;
end;

function TGenericClass<T>.Foo: T;
begin
Result := Field;
end;

procedure TGenericClass<T>.Method(A: T);
begin
Writeln(TObject(A).ToString);
end;

As you can see in the code above, you (or more often, Code Completion) should repeat both the class name and the generic type parameter list in the implementation of each method. This is to be able to have same-named non-generic and generic classes of different number of type parameters.


To use a generic class, supply the type parameters, and treat the result class like any other class.

type
TIntegerClass = TGenericClass<integer>;

procedure Test;
var
A: TIntegerClass;
B: TGenericClass<integer>;
C: TGenericClass<string>;
begin
A := TIntegerClass.Create;
B := TIntegerClass.Create;
C := TGenericClass<string>.Create;
A.Method(42);
end;

Note that you can use generic types to define new types and inline in variable declaration and in code. Also, different type declarations that map to the same generic type instanciation are assignment compatible.


Generic methods


You can also declare one or more generic type parameters on a method signature. The compiler will generate implementations for each unique type the method is invoked with.

type
// Generic methods - of non-generic class
TFoo = class
procedure GenericMethod<T>(A: T);
function GenericFunction<T>: T;
end;

function TFoo.GenericFunction<T>: T;
begin
Result := Default(T)
end;

procedure TFoo.GenericMethod<T>(A: T);
begin
Writeln(TObject(A).ToString);
end;

When calling a generic method you can either explicitly tell the compiler what type to use for the generic type parameter, or you can have the compiler deduce the type from the actual argument types.

var
Foo: TFoo;
begin
Foo := TFoo.Create;
Foo.GenericMethod<integer>(42);
Foo.GenericMethod(13);
end;

Generic methods can be declared both in generic types or in plain non-generic types.

type
// Generic method - of generic class
TGenericClass<T, U>= class
procedure GenericMethod<V>(A: T; B: U; C:V);
end;

procedure TGenericClass<T, U>.GenericMethod<V>(A: T; B: U; C: V);
begin
end;

Although, one might think that having support for generic properties as well would be a natural extension (after all a property just maps to getter and setter methods, after all), but indeed this is not possible. I don't think C# supports this, either, btw.

type
// Generic property - not supported
TBar = class
procedure SetGenericProperty<T>(const A: T);
function GetGenericProperty<T>: T;
// NOTE: Is this not allowed - Would be cool?!
property GenericProperty<T>: T read GetGenericProperty<T> write SetGenericProperty<T>;
end;

Generic interfaces


Interfaces can now also be declared with generic type parameters. This allows generic classes implement a whole class of interfaces (for all the possible type parameters).

type  
IGenericInterface<T> = interface
procedure Handle(A: T);
end;

Using generic interfaces instead of plain interfaces can improve performance, because you don't have to sort to late-bound checks and casts (or boxing) to and from TObject.


The .NET 2.0 specific System.Collections.Generic namespace provides a number of generic interfaces that the generic collection classes use and implement, such as IComparer<T> and IEnumerable<T>.


Generic records


Just as you can have generic classes, you can have generic records. They work exactly the same, the difference is that records implement value types that are stored inline and not via a reference.

type
TGenericRecord<T> = record
Field: T;
procedure DoSomething(A: T);
function GetSomething: T;
property SomeThing: T read GetSomething write DoSomething;
end;

procedure TGenericRecord<T>.DoSomething(A: T);
begin
Writeln(TObject(A).ToString);
end;

function TGenericRecord<T>.GetSomething: T;
begin
Result := Default(T)
end;

This makes it possible to write low- or zero-cost (combined with inlining) encapsulations of generic field wrappers, for instance.


Again, generic methods on records (normal or generic) is supported.

type
TAnyRecord = record
procedure DoSomething<T>(A: T);
end;

procedure TAnyRecord.DoSomething<T>(A: T);
begin
Writeln(TObject(A).ToString);
end;

Generic event types


Method pointers or event types can also include generic type parameters now.

type
// Generic event type
TOnGetValue<T> = procedure (Sender: TObject; var Value: T) of object;

You can use this event type in any normal class, by providing a concrete type parameter, or you can use it inside a generic classs forwarding the class' type parameter.

type
// Using a generic event type
TBar<T> = class
private
FOnGetValue: TGetValueEvent<T>;
public
property OnGetValue: TGetValueEvent<T> read FOnGetValue write FOnGetValue;
end;

Constraints


None of the examples so far has had any constraints on their generic type parameters. This means that you can supply any type arguments for these classes (i.e. integer, IInterface, TObject, TMyObject, boolean, etc) - so it gives you maximum flexibility. The downside is that you cannot do much with the values of generic type parameters - other than storing them, passing them in parameters and comparing or assigning the default value.


Unlike C++ templates, .NET Generics is an explicitly constrained system where you cannot perform any operations on generic parameters and values unless there are one or more "rules" (or constraints) on what the type parameter. In C++ the constraints are implicit; if you try to instanciate an illegal combination of template type parameters, you typically get long and cryptic error messages.


In Delphi.Net the constraints are specified by using a colon following by one or more constraints (much like you use Name: Type pattern for normal parameters, you use a TypeName: Constraints syntax for generic type parameters).

type  
// Generic type constraints
TConstrainedGeneric<
T; // No constraints
U: class;
V: constructor;
W: record;
X: TFoo;
Y: class, IMyInterface;
Z: IGenericInterface<T>> = class
end;

As you can see above, there are several types of constraints and most of them are self-explanatory and can be combined in the expected ways (record is incompatible with class and constructor, constructor implies class, and so on). You can constrain a type parameter to a specific class type or descendants (as TFoo above), but not a specific record type.


In .NET both classes and records can implement interfaces, so they are the most generic [sic] type of constraints and often used to call specific methods inherited from the interface.


Default values


When writing generic code, you'll often find the need to compare or assign the default value of the type parameter (nil for classes, all zero bits for records), but until now there has been no syntax to do this. In Win32 you could use FillChar, but this is not type safe (and thus not supported in .NET) and is not a function you can use in expressions.


There is now a new intrinsic function called Default that takes a generic type parameter and returns an the default value (nil or zero-filled record).

procedure TFoo<T>.GetValue(Sender: TObject; var Value: T); 
begin
Writeln('Before:', TObject(Value).ToString);
Value := Default(T);
Writeln('After:', TObject(Value).ToString);
end;

Default(T) can be used on all type parameters, even if they have no constraints.


Creating instances of type parameters


With one or more constraints specified on the type parameters, you can start to do more interesting things with the type values. For instance, a type that has a constructor constraint can be used to create new instances of the type.

type  
// Creating an instance of a type parameter
TCreateGeneric<V: constructor> = class
private
FV: V;
public
procedure Test;
end;

procedure TCreateGeneric<V>.Test;
begin
FV := V.Create;
end;

Without the constructor constraint on the V type parameter, this code does not compile;



[DCC Error] TestGenerics1.dpr(134): E2018 Record, object or class type required


Conclusion


There is much more to the RAD Studio 2007 version than I have room for here - take a look at some other beta bloggers, Nick's blog and information at CodeGear's site.


There is no doubt that this is the best and most functional version of Delphi/BDS/RAD Studio ever. It includes updated versions of the Delphi Win32 and C++Builder personalities, and brings the Delphi for .NET personality a whole generation step forward.


If you are still on the fence with an older Win32-only version of Delphi (D7 and earlier), upgrading now is a no-brainer. Consider getting Software Assurance as well. If you are a .NET programmer, this is a must-have release, of course. Getting no-fuss access to .NET 2.0 including powerful support for consuming and creating generic types is finally here.


 If you are only interested in Win32 programming and you already have Delphi 2007 with no SA, you may want to consider waiting until the next version that (according to the roadmap) should bring generics to Win32. Or better yet, upgrade now, get SA and start playing with and learning generics - it can quickly become addictive :).



Copyright © 2004-2007 by Hallvard Vassbotn