The Delphi Language, Part 1

The book Delphi for .NET Developer's Guide, by Xavier Pacheco, (Sams, ISBN: 0-672-32443-1, 2004) includes this chapter by co-author and Delphi expert Steve Teixeira. This part of the chapter covers types of comments, parentheses in calls, default value parameters, Delphi language's structured style of variable declaration, differences between C# constants and Delphi constants,  operators, variants, arrays and records and how Delphi's strongly-typed nature enables it to perform a sanity check of your code.

Contributed by
Rating: 4 stars4 stars4 stars4 stars4 stars / 19
July 07, 2004
Rate this Article:
MEH MEH++


SEARCH ASP FREE
TOOLS YOU CAN USE

advertisement

delphiThis chapter covers Delphi's underlying language, Object Pascal. First, you'll receive an introduction to the basics of the Delphi language, such as language rules and constructs. Then, you'll learn about some of the more advanced aspects of the Delphi language, such as classes and exception handling. This chapter assumes that you have some experience with other high-level computer languages. Therefore, it does not teach the concepts associated with programming languages, but rather on how to express those concepts in Delphi. By the time you're finished with this chapter, you'll understand how programming concepts such as variables, types, operators, loops, cases, exceptions, and objects work in Delphi and how many of these elements relate to the underlying .NET Framework. To provide additional practical grounding, we will draw comparisons where appropriate with Delphi's more widely used .NET cousins, C# and Visual Basic .NET.

It's All about .NET

Delphi 8 produces applications that run completely within the context of the Microsoft .NET Framework. Therefore, the capabilities and features of Delphi 8's compiler must be subject to the capabilities and features of the underlying .NET Framework. This notion might be vaguely disconcerting for those coming into the .NET from the native code world. A native code compiler can essentially do anything it wants—its capabilities limited only by the desires of the compiler vendor. In .NET development, literally everything one might do—even something as trivial as adding two integers together—boils down to the compiler generating code that manipulates features and types of the .NET Framework.

Rather than producing native code, a .NET compiler such as Delphi 8 produces code in a format called Microsoft Intermediate Language, or MSIL, which is the lowest-level representation of application instructions.

Note - Chapter 2 discusses the just-in-time (JIT) compilation nature of the .NET Framework. Now might be a good time to review that information.

Comments

The Delphi language supports three types of comments: curly brace comments, parenthesis/asterisk comments, and double backslash comments. Examples of each type of comment follow:

{ Comment using curly braces }
(* Comment using paren and asterisk *)
// double backslash comment

The first two types of comments are virtually identical in behavior. The compiler considers the comment to be everything between the open-comment and close-comment delimiters. For double backslash comments, everything following the double backslash until the end of the line is considered a comment.


Note - You cannot nest comments of the same type. Although it is legal syntax to nest comments of different types inside one another, we don't recommend the practice. Here are some examples:

{ (* This is legal *) }
(* { This is legal } *)
(* (* This is illegal *) *)
{ { This is illegal } }

Another handy technique for disabling a body of code, particularly when different kinds of comments are used in the code, is to use the $IFDEF compiler directive. For example, the following code uses $IFDEF to comment out a block of code:

{$IFDEF DONTCOMPILEME}
// imagine a bunch of code here
{$ENDIF}

Because the DONTCOMPILEME identifier is not defined, the code inside the $IFDEF is effectively commented out.


This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Procedures and Functions

Because procedures and functions are fairly universal topics as far as programming languages are concerned, we won't go into a great deal of detail here. We just want to fill you in on a few unique or little-known features in this area.


Note - Functions with no return value (a void return in C# parlance) are called procedures, while functions returning some value are referred to as functions. Often, the term routine will be used to describe both procedures and functions, and the term method describes a routine within a class.


Parentheses in Calls

One of the lesser-known features of the Delphi language is that parentheses are optional when calling a procedure or function that takes no parameters. Therefore, the following syntax examples are both valid:

Form1.Show; Form1.Show();

Granted, this feature isn't one of those things that sends chills up and down your spine, but it's particularly nice for those who split their time between Delphi and a language like C#, where parentheses are required. If you're not able to spend 100% of your time in Delphi, this feature means that you don't have to remember to use different function-calling syntax for different languages.

Overloading

The Delphi language supports the concept of function overloading, that is, the ability to have multiple procedures or functions of the same name with different parameter lists. All overloaded methods are required to be declared with the overload directive, as shown here:

procedure Hello(I: Integer); overload;
procedure Hello(S: string); overload;
procedure Hello(D: Double); overload;

Note that the rules for overloading methods of a class are slightly different and are explained in the section "Method Overloading."

Default Value Parameters

Also supported in the Delphi language are default value parameters—that is, the ability to provide a default value for a function or procedure parameter and not have to pass that parameter when calling the routine. In order to declare a procedure or function that contains default value parameters, follow the parameter type with an equal sign and the default value, as shown in the following example:

procedure HasDefVal(S: string; I: Integer = 0);

The HasDefVal() procedure can be called in one of two ways. First, you can specify both parameters:

HasDefVal('hello', 26);

Second, you can specify only parameter S and use the default value for I:

HasDefVal('hello'); // default value used for I

You must follow several rules when using default value parameters:

  • Parameters having default values must appear at the end of the parameter list. Parameters without default values cannot follow parameters with default values in a procedure or function's parameter list.

  • Default value parameters can be of an ordinal, string, floating point, pointer, or set type. Class, interface, dynamic array, procedural, and class references are also supported, but only when the default value is made equal to nil.

  • Default value parameters must be passed by value or as const. They cannot be reference ( var, out) or untyped parameters.

One of the biggest benefits of default value parameters is in adding functionality to existing functions and procedures without sacrificing backward compatibility. For example, suppose that you publish a unit containing a revolutionary function called AddInts() that adds two numbers:

function AddInts(I1, I2: Integer): Integer;
begin
  Result := I1 + I2;
end;

In order to keep up with the competition, you feel you must update this function so that it has the capability for adding three numbers. However, you're loathe to do so because adding a parameter will cause existing code that calls this function to not compile. Thanks to default parameters, you can enhance the functionality of AddInts() without compromising compatibility. Here's an example:

function AddInts(I1, I2: Integer; I3: Integer = 0);
begin
  Result := I1 + I2 + I3;
end;

Tip - Generally, you should look to overloaded routines when choosing between the addition of default values or overloaded routines as a means to add new features without breaking backward compatibility. Overloads are executed a bit more efficiently and are more compatible with other .NET languages because default value parameters are not supported in C# or managed C++.


Variables

You might be used to declaring variables off the cuff: "I need another integer, so I'll just declare one right here in the middle of this block of code." This is a perfectly reasonable notion if you're coming from another language such as C# or Visual Basic .NET. If that has been your practice, you're going to have to retrain yourself a little in order to use variables in the Delphi language. Delphi requires you to declare all variables up front in their own section before you begin a procedure, function, or program. Perhaps you used to write free-wheeling code like this:

public void foo()
{
  int x = 1;
  x++;
  int y = 2;
  float f;
 
//... etc ...
}

In Delphi, any such code must be tidied up and structured a bit more to look like this:

procedure Foo;
var
  x, y: Integer;
  f: Double; 
begin
 
x := 1;
 
inc(x);
  y := 2;
 
//... etc ...
end;


Case Sensitivity and Capitalization - The Delphi language—like Visual Basic .NET, but unlike C#—is not a case sensitive language. Upper- and lowercase is used for clarity's sake, so use your best judgment, as the style used in this book indicates. If the identifier name is several words mashed together, remember to camel-cap for clarity by capitalizing the first letter of each word in the identifier. For example, the following name is unclear and difficult to read:

procedure thisprocedurenamemakesnosense;

This code is quite readable, however:

procedure ThisProcedureNameIsMoreClear;


You might be wondering what all this structure business is and why it's beneficial. You'll find, however, that the Delphi language's structured style of variable declaration lends itself to code that's more readable, maintainable, and less buggy than other languages that rely on convention rather than rule to enforce sanity.

Notice how Delphi enables you to group more than one variable (or formal parameter, for that matter) of the same type together on the same line with the following syntax:

VarName1, VarName2: SomeType;

Using this feature tends to make code much more compact and readable compared to a language such as C#, where every variable or parameter must have the type spelled out.

Remember that when you're declaring a variable in Delphi, the variable name precedes the type, and there's a colon between the variables and types. For local variables, the variable initialization is always separate from the variable declaration.

Delphi permits initialization of global variables inside a var block. Here are some examples demonstrating the syntax for doing so:

var
  i: Integer = 10;
  S: string = 'Hello world';
  D: Double = 3.141579;

Note - Preinitialization of variables is only allowed for global variables, not variables that are local to a procedure or function.

Zero Initialization - The CLR sees to it that all variables are automatically zero-initialized. When your application starts or a function is entered, all integer types will hold 0, floating-point types will hold 0.0, objects will be nil, strings will be empty, and so forth. Therefore, it isn't necessary to zero-initialize variables in your source code.

Unlike Win32 versions of Delphi, this is true for variables local to functions as well as global variables.


This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Constants, Operators

 

Constants in Delphi are defined in a const clause, which behaves similarly to the C#'s const keyword. Here's an example of three constant declarations in C#:

public const float ADecimalNumber = 3.14;
public const int i = 10;
public const String ErrorString = "Danger, Danger, Danger!";

The major difference between C# constants and Delphi language constants is that Delphi, like Visual Basic .NET, doesn't require you to declare the constant's type along with the value in the declaration. The Delphi compiler automatically allocates the appropriate type for the constant based on its value, or, in the case of scalar constants such as Integer, the compiler keeps track of the values as it works, and space never is allocated. Here's an example:

const
  ADecimalNumber = 3.14;
  i = 10;
  ErrorString = 'Danger, Danger, Danger!';

Optionally, you can also specify a constant's type in the declaration. This provides you with full control over how the compiler treats your constants:

const
  ADecimalNumber: Double = 3.14;
  I: Integer = 10;
  ErrorString: string = 'Danger, Danger, Danger!';

The Type Safety of Typed Constant - Typed constants have one distinct advantage over the untyped variety: Untyped constants' lack of strict compile-time typing prevents them from supporting object method calls. For example, the following code is legal:

const
 
I: Integer = 19710704;
  S: string;
begin
  S := I.ToString;

Whereas the following code will not compile:

const
 
I = 19710704;
 
S: string;
begin
 
S := I.ToString;


The Delphi language permits the usage of compile-time functions in const and var declarations. These routines include Ord(), Chr(), Trunc(), Round(), High(), Low(), Abs(), Pred(), Succ(), Length(), Odd(), Round(), Trunc(), and SizeOf(). For example, all of the following code is valid:

type
  A = array[1..2] of Integer;

const
  w: Word = SizeOf(Byte);

var
 
i: Integer = 8;
  j: SmallInt = Ord('a');
  L: Longint = Trunc(3.14159);
  x: ShortInt = Round(2.71828);
  B1: Byte = High(A);
  B2: Byte = Low(A);
  C: Char = Chr(46);


Caution - For backward compatibility, the compiler provides a switch that enables typed constants to be assignable, just like variables. This switch is accessible via the Compiler page of the Project, Options dialog box, or using the $WRITEABLECONST (or $J) compiler directive. However, this behavior should be officially deemed icky, and it should be avoided by leaving this functionality at its default disabled state.

Note - Like C# and Visual Basic .NET, the Delphi language doesn't have a preprocessor as does the C language. There's no concept of a macro in Delphi and, therefore, no Delphi equivalent for C's #define for constant declaration. Although you can use Delphi's $define compiler directive for conditional compiles similar to C's #define, you cannot use it to define constants. Use const in Delphi where you would use #define to declare a constant in C.
Operators

Operators are the symbols in your code that enable you to manipulate all types of data. For example, there are operators for adding, subtracting, multiplying, and dividing numeric data. There are also operators for addressing a particular element of an array. This section explains some of the Delphi operators and their correlation with C# and CLR counterparts.

Assignment Operators

Unless you have some experience with Pascal, Delphi's assignment operator might be one of the toughest things to get used to. To assign a value to a variable, use the := operator as you would use the = operator in C# or Visual Basic .NET. Delphi programmers often call this the gets or assignment operator, and the expression

Number1 := 5;

is read either "Number1 gets the value 5," or "Number1 is assigned the value 5."

Comparison Operators

If you've already programmed in Visual Basic .NET, you should be very comfortable with Delphi's comparison operators because they're virtually identical. These operators are fairly standard throughout programming languages, so they're covered only briefly in this section.

The Delphi language uses the = operator to perform logical comparisons between two expressions or values. Delphi's = operator is analogous to the C# == operator, so a C# expression written as

if (x == y)

is written like this in Delphi:

if x = y

Note - Remember that in the Delphi language, the := operator is used to assign a value to a variable, and the = operator compares the values of two operands.


Delphi's "not equal to" operator is <>, and its purpose is identical to C#'s != operator. To determine whether two expressions are not equal, use this code:

if x <> y then DoSomething Logical Operators

Delphi uses the words and and or as logical "and" and "or" operators, whereas C# uses the && and || symbols, respectively, for these operators. The most common use of the and and or operators is as part of an if statement or loop, as demonstrated in the following two examples:

if (Condition 1) and (Condition 2) then
DoSomething;

while (Condition 1) or (Condition 2) do
DoSomething;

Delphi's logical "not" operator is not, which is used to invert a Boolean expression. It's analogous to the C#'s ! operator. It's also often used as a part of if statements, as shown here:

if not (condition) then (do something); // if condition is false then...

Table 5.1 provides an easy reference of how Delphi operators map to corresponding C# and Visual Basic .NET operators.

Table 5.1 Assignment, Comparison, and Logical Operators

Operator

Delphi

C#

Visual Basic .NET

Assignment

:=

=

=

Comparison

=

==

= or Is*

Not equal to

<>

!=

<>

Less than

<

<

<

Greater than

>

>

>

Less than or equal to

<=

<=

<=

Greater than or equal to

>=

>=

>=

Logical and

and

&&

And

Logical or

or

||

Or

Logical not

not

!

Not

Logical xor

xor

^

Xor


Arithmetic Operators

You should already be familiar with most Delphi language arithmetic operators because they're generally similar to those used in most common languages. Table 5.2 illustrates all the Delphi arithmetic operators and their C# and Visual Basic .NET counterparts.

Table 5.2 Arithmetic Operators

Operator

Delphi

C#

Visual Basic .NET

Addition

+

+

+

Subtraction

-

-

-

Multiplication

*

*

*

Floating-point division

/

/

/

Integer division

div

/

Modulus

mod

%

Mod

Exponent power

None

None

^


You might notice that Delphi and Visual Basic .NET provide different division operators for floating-point and integer math, although this isn't the case for C#. The div operator automatically truncates any remainder when you're dividing two integer expressions.


Note - Remember to use the correct division operator for the types of expressions with which you're working. The Delphi compiler gives you an error if you try to divide two floating-point numbers with the integer div operator or assign to an integer the result of two integers divided with the floating-point / operator, as the following code illustrates:

var
 
i: Integer;
  d: Double;
begin
  i := 4 / 3; // This line will cause a compiler error
  d := 3.4 div 2.3; // This line also will cause an error
end;

If you must divide two integers using the / operator, you can assign the result to an integer if you convert the expression to an integer using the Trunc() or Round() functions.


Bitwise Operators

Bitwise operators enable you to modify individual bits of a given integral variable. Common bitwise operators enable you to shift the bits to the left or right or to perform bitwise "and," "not," "or," and "exclusive or" (xor) operations with two numbers. The Shift-left and Shift-right operators are shl and shr, respectively, and they're much like the C# << and >> operators. The remainder of Delphi's bitwise operators is easy enough to remember: and, not, or, and xor. Table 5.3 lists the bitwise operators.

Table 5.3 Bitwise Operators

Operator

Delphi

C#

Visual Basic .NET

And

and

&

And

Not

not

~

Not

Or

or

|

Or

Xor

xor

^

Xor

Shift-left

shl

<<

None

Shift-right

shr

>>

None


Increment and Decrement Procedures

Increment and decrement operators provide a convenient means to add to or subtract from a given integral variable. Delphi doesn't really provide honest-to-gosh increment and decrement operators similar to the C# ++ and -- operators, but Delphi does provide procedures called Inc() and Dec() intended for this very purpose.

You can call Inc() or Dec() with one or two parameters. For example, the following two lines of code increment and decrement variable, respectively, by 1:

Inc(variable); Dec(variable);

The following two lines increment or decrement variable by 3:

Inc(variable, 3); Dec(variable, 3);

Table 5.4 compares the increment and decrement operators of different languages.

Table 5.4 Increment and Decrement Operators

Operator

Delphi

C#

Visual Basic .NET

Increment

Inc()

++

None

Decrement

Dec()

--

None


Note that the C# ++ and –- operators return a value, whereas Inc() and Dec() do not. Delphi's Succ() and Pred() functions do return a value; however, these do not modify the parameter.

Do-and-assign Operators

Not present in the Delphi language are handy do-and-assign operators like those found in C#. These operators, such as += and *=, perform an arithmetic operation (in this case, an add and a multiply) before making the assignment. In Delphi, this type of operation must be performed using two separate operators. Therefore, this code in C#:

x += 5;

becomes this in Delphi:present in the Delphi language are handy do-and-assign

x := x + 5;

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Delphi Language Types

One of the Delphi language's greatest features is that it's strongly typed, making it an excellent fit for the .NET platform. This means that actual variables passed to procedures and functions must be of the same type as the formal parameters identified in the procedure or function definition—very little is done for you in terms of implicit type conversion. Delphi's strongly-typed nature enables it to perform a sanity check of your code—to ensure that you aren't trying to put a square peg in a round hole. After all, the easiest bug fixes are the ones the compiler tells you to perform!

Objects, Objects Everywhere!

One of the most important notions regarding fundamental types in the .NET version of the Delphi compiler is that they are all value types capable of implicit conversion to classes. This conversation between value and object and back again is known as boxing and unboxing in .NET parlance. Integers, strings, floating points, and all of the rest are not implemented as primitive concepts in the compiler, as in Win32, but instead map to value types provided either by the .NET Framework or by Borland's RTL or VCL. Unlike Win32 Delphi, these value types can have their own procedures and functions in addition to those available in corresponding classes created by the implicit boxing and unboxing of these types. This allows for a kind of syntax that might be foreign for those coming from native compilers (although perhaps familiar for those experienced with languages such as Java or SmallTalk):

var
  S: string;
  I: Integer;
begin
  I := 42;
  S := I.ToString; // can call a method on an Integer!
end;

Note - The string type is used heavily in development. This is not to say that other types are not, but there are some special considerations when using strings. Chapter 11 addresses strings specifically; therefore, you will not see detailed coverage of them here.


A Comparison of Types

Delphi surfaces most of the primitive types available in the CLR. Table 5.5 compares and contrasts the base types of the Delphi language with those of C# and the CLR. This table also indicates whether each type is compliant with the CLS.

Table 5.5 A Delphi-to-C#-to-CLR Type Comparison

Variable range

Delphi

C#

CLR

CLS Compliant?

8-bit signed integer

ShortInt

sbyte

System.SByte

No

8-bit unsigned integer

Byte

byte

System.Byte

Yes

16-bit signed integer

SmallInt

short

System.Int16

Yes

16-bit unsigned integer

Word

ushort

System.UInt16

No

32-bit signed integer

Integer

int

System.Int32

Yes

32-bit unsigned integer

Cardinal

uint

System.UInt32

No

64-bit signed integer

Int64

long

System.Int64

Yes

64-bit unsigned integer

UInt64

ulong

System.UInt64

No

single precision float

Single

float

System.Single

Yes

double precision float

Double

double

System.Double

Yes

fixed-point decimal

None

decimal

System.Decimal

Yes

Delphi fixed-point decimal

Currency

None

None

No

date/time

TDateTime*

None

System.DateTime

Yes

variant

Variant, OleVariant

None

None

No

1-byte character

AnsiChar

None

None

No

2-byte character

Char, WideChar

char

System.Char

Yes

Fixed-length byte string

ShortString

None

None

No

Dynamic 1-byte string

AnsiString

None

None

No

Dynamic 2-byte string

string, WideString

string

System.String

Yes

Boolean

Boolean

bool

System.Boolean

None


*TDateTime is a record that wraps a System.DateTime with methods and operator overloads that makes it behave very much like System.DateTime , but with additional behavior to be compatible with Win32 Delphi's TDateTime type. 

Characters

Delphi provides three character types:

  • WideChar—This CLS-compatible character is two bytes in size and represents a Unicode character.

  • Char—This is an alias for WideChar. In Win32 versions of Delphi, Char was an AnsiChar.

  • AnsiChar—An old school one-byte ANSI character.

Never assume the size of a Char (or any other type, for that matter), in your code. Instead, you should use the SizeOf() function where appropriate.


Note - The SizeOf() standard procedure returns the size, in bytes, of a type or variable.


This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Variant Types

The Variant class provides an implementation of a type capable of serving as a kind of container for many other types. This means that a variant can change the kind of data to which it refers at runtime. For example, the following code will compile and run properly:

var
  V: Variant;
  I: IInterface;
begin
  V := 'Delphi is Great!'; // Variant holds a string
  V := 1;                  // Variant now holds an Integer
  V := 123.34;       // Variant now holds a floating point
  V := True;         // Variant now holds a boolean
  V := I;            // Variant now holds an interface
end;

Variants support a wide variety of types, including complex types such as arrays and interfaces. The following code from the Borland.Vcl.Variants unit shows the various supported types, which are identified by values known as TVarTypes:

type
  TVarType = Integer;

const            // Decimal | Other names for this type
  varEmpty = $0000;    // = 0 | Unassigned, Nil
  varNull = $0001;     // = 1 | Null, System.DBNull
  varSmallInt = $0002; // = 2 | I2, System.Int16
  varInteger = $0003;  // = 3 | I4, System.Int32
  varSingle = $0004;   // = 4 | R4, System.Single
  varDouble = $0005;   // = 5 | R8, System.Double
  varCurrency = $0006; // = 6 | Borland.Delphi.System.Currency
  varDate = $0007; // = 7 | Borland.Delphi.System.TDateTime
  varString = $0008;   // = 8  | WideString, System.String
  varError = $000A;    // = 10 | Exception, System.Exception
  varBoolean = $000B;  // = 11 | Bool, System.Boolean
  varObject = $000C;   // = 12 | TObject, System.Object
  varDecimal = $000E;  // = 14 | System.Decimal
  varShortInt = $0010; // = 16 | I1, System.SByte
  varByte = $0011;     // = 17 | U1, System.Byte
  varWord = $0012;     // = 18 | U2, System.UInt16
  varLongWord = $0013; // = 19 | U4, System.UInt32
  varInt64 = $0014;    // = 20 | I8, System.Int64
  varUInt64 = $0015;   // = 21 | U8, System.UInt64
  varChar = $0016;     // = 22 | WideChar, System.Char
  varDateTime = $0017; // = 23 | System.DateTime;

  varFirst = varEmpty;
  varLast = varDateTime;

  varArray = $2000;   // = 8192 | System.Array, Dynamic   Arrays
  varTypeMask = $0FFF; // = 4095
  varUndefined = -1;

As a function of value assignment, variants are capable of coercing themselves into other types, as needed. For example:

var
  V: Variant; 
  I: Integer;
begin
  V := '1'; // V hold string '1'
  I := V;   // Implicitly converts to Integer, I is now 1
end;

Typecasting Variants

You can explicitly typecast expressions to type Variant. For example, the expression

Variant(X);

results in a Variant type whose type code corresponds to the result of the expression X, which must be an integer, floating point, currency, string, character, date/time, class, or Boolean type.

You can also typecast a variant to that of a simple data type. For example, given the assignment

V := 1.6;

where V is a variable of type Variant, the following expressions will have the following results shown:

S := string(V);  // S will contain the string '1.6';
I := Integer(V); // I is rounded to the nearest Integer value, in this case: 2.
B := Boolean(V); // B contains True, since V is not equal to 0
D := Double(V);  // D contains the value 1.6

These results are dictated by certain type-conversion rules applicable to Variant types. These rules are defined in detail in the Delphi Language Guide.

By the way, in the preceding example, it isn't necessary to typecast the variant to another data type to make the assignment. The following code would work just as well:

V := 1.6;
S := V;
I := V;
B := V;
D := V;

When multiple variants are used in an expression, there can be much more behind-the-scenes code logic attached to these implicit type coercions. In such cases, if you're sure of the type a variant contains, you're better off explicitly typecasting it to that type in order to speed up the operation.

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Variants in Expressions

You can use variants in expressions with the following operators: +, =, *, /, div, mod, shl, shr, and, or, xor, not, :=, <>, <, >, <=, and >=. The standard Inc(), Dec(), Trunc(), and Round() functions are also valid with variant expressions.

When using variants in expressions, Delphi knows how to perform the operations based on the types contained within a variant. For example, if two variants, V1 and V2, contain integers, the expression V1 + V2 results in the addition of the two integers. However, if V1 and V2 contain strings, the result is a concatenation of the two strings. What happens if V1 and V2 contain two different data types? Delphi uses certain promotion rules in order to perform the operation. For example, if V1 contains the string '4.5' and V2 contains a floating-point number, V1 will be converted to a floating point and then added to V2. The following code illustrates this:

var
  V1, V2, V3: Variant;
begin
  V1 := '100'; // A string type
  V2 := '50';  // A string type
  V3 := 200;   // An Integer type
  V1 := V1 + V2 + V3;
end;

Based on what we just mentioned about promotion rules, it would seem at first glance that the preceding code would result in the value 350 as an integer. However, if you take a closer look, you'll see that this is not the case. Because the order of precedence is from left to right, the first equation executed is V1 + V2. Because these two variants refer to strings, a string concatenation is performed, resulting in the string '10050'. That result is then added to the integer value held by the variant V3. Because V3 is an integer, the result '10050' is converted to an integer and added to V3, thus providing an end result of 10250.

Delphi promotes the variants to the numerically largest type in the equation in order to successfully carry out the calculation. However, when an operation is attempted on two variants of which Delphi cannot make any sense, an EVariantTypeCast exception is raised. The following code illustrates this:

var
  V1, V2: Variant;
begin
  V1 := 77;
  V2 := 'hello';
  V1 := V1 / V2; // Raises an exception.
end;

As stated earlier, it's sometimes a good idea to explicitly typecast a variant to a specific data type if you know what that type is and if it's used in an expression. Consider the following line of code:

V4 := V1 * V2 / V3;

Before a result can be generated for this equation, each operation is handled by a runtime function that goes through several gyrations to determine the compatibility of the types the variants represent. Then the conversions are made to the appropriate data types. This results in a large amount of overhead and code size. A better solution is obviously not to use variants. However, when necessary, you can also explicitly typecast the variants so the operations are resolved at compile time:

V4 := Integer(V1) * Double(V2) / Integer(V3);

Keep in mind that this assumes you know the data types to which the variants can be successfully converted.

Empty and Null

Two special values for variants merit a brief discussion. The first is Unassigned, which means that the variant has not yet been assigned a value. This is the initial value of the variant as it comes into scope. The other is Null, which is different from Unassigned in that it actually represents the value Null as opposed to a lack of value. This distinction between no value and a Null value is especially important when applied to the field values of a database table. You will learn about database programming in Part IV, "Database Development with ADO.NET."

Another difference is that attempting to perform operations with a variant containing an Unassigned value will result in the value being converted to 0 for numeric operations or an empty string for string operations. The same isn't true of variants containing a Null value, however. When a variant involved in an equation contains a Null value, operating on the variant with that value might result in an EVariantTypeCast exception being raised.

If you want to assign or compare a variant to one of these two special values, the System.Vcl.Variants unit defines two variants, appropriated called Unassigned and Null, which can be used for assignment and comparison.


Controlling Variant Behavior - You can control whether an exception is raised when performing an arithmetic operation with a Null variant by setting the Variant.NullStrictOperations property to True. The other Variant flags controlling Null behavior and string and Boolean conversions include

NullEqualityRule: TNullCompareRule;
NullMagnitudeRule: TNullCompareRule;
NullAsStringValue: string;
NullStrictConvert: Boolean;
BooleanToStringRule: TBooleanToStringRule;
BooleanTrueAsOrdinalValue: Integer;


User-Defined Types

Integers, strings, and floating-point numbers often are not enough to adequately represent variables in the real-world problems that programmers must try to solve. In cases like these, you must create your own types to better represent variables in the current problem. In Delphi, these user-defined types usually come in the form of records or classes; you declare these types using the Type keyword.

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Arrays

The Delphi language enables you to create arrays of any type of variable. For example, a variable declared as an array of eight integers reads like this:

var A:
  Array[0..7] of Integer;

This statement is equivalent to the following C# declaration:

int A[8];

It's also equivalent to this Visual Basic .NET statement:

Dim A(8) as Integer

Delphi arrays have a special property that differentiates them from other languages: They don't have to be based at a certain number. You can therefore declare a three-element array that starts at 28, as in the following example:

var
  A: Array[28..30] of Integer;

Because Delphi arrays aren't guaranteed to begin at 0 or 1, you must use some care when iterating over array elements in a for loop. The compiler provides built-in functions called High() and Low(), which return the lower and upper bounds of an array variable or type, respectively. Your code will be less error prone and easier to maintain if you use these functions to control your for loop, as shown here:

var
  A: array[28..30] of Integer;
 
i: Integer;
begin
  for i := Low(A) to High(A) do // don't hard-code for loop!
  A[i] := i;
end;

To specify multiple dimensions, use a comma-delimited list of bounds:

var
  // Two-dimensional array of Integer:
 
A: array[1..2, 1..2] of Integer;

To access a multidimensional array, use commas to separate each dimension within one set of brackets:

I := A[1, 2];

Dynamic Arrays

Dynamic arrays are dynamically allocated arrays in which the dimensions aren't known at compile time. To declare a dynamic array, just declare an array without including the dimensions, like this:

var
  // dynamic array of string:
  SA: array of string;

Before you can use a dynamic array, you must use the SetLength() procedure to allocate memory for the array:

begin
  // allocate room for 33 elements:
  SetLength(SA, 33);

Once memory has been allocated, you can access the elements of the dynamic array just like a normal array:

SA[0] := 'Pooh likes hunny';
OtherString := SA[0];

Note - Dynamic arrays are always zero-based.

Dynamic arrays are lifetime managed by the .NET runtime, so there's no need (and, in fact, no way) to free them when you're through using them because they will be garbage collected at some point after leaving scope. However, there might come a time when you want to request that the .NET runtime remove the dynamic array from memory before it leaves scope (if it uses a lot of memory, for example). To do this, you need only assign the dynamic array to nil:

SA := nil; // requests release of SA

Note that assigning to nil does not release SA; it only releases a reference to it, as there could be more than one variable referencing the array indicated by SA. When the last reference to SA is released, the .NET garbage collector will release the memory occupied by the array during the next garbage collection.

Dynamic arrays are manipulated using reference semantics rather than value semantics like a normal array. Here's a quick test: What is the value of A1[0] at the end of the following code fragment?

var
  A1, A2: array of Integer;
begin
  SetLength(A1, 4);
  A2 := A1;
  A1[0] := 1;
  A2[0] := 26;

The correct answer is 26 because the assignment A2 := A1 doesn't create a new array but instead provides A2 with a reference to the same array as A1. Therefore, any modifications to A2 will also affect A1. If you want instead to make a complete copy of A1 in A2, use the Copy() standard procedure:

A2 := Copy(A1);

After this line of code is executed, A2 and A1 will be two separate arrays initially containing the same data. Changes to one will not affect the other. You can optionally specify the starting element and number of elements to be copied as parameters to Copy(), as shown here:

// copy 2 elements, starting at element one:
A2 := Copy(A1, 1, 2);

Dynamic arrays can also be multidimensional. To specify multiple dimensions, add an additional array of to the declaration for each dimension:

var
  // two-dimensional dynamic array of Integer:
  IA: array of array of Integer;

To allocate memory for a multidimensional dynamic array, pass the sizes of the other dimensions as additional parameters to SetLength():

begin
  // IA will be a 5 x 5 array of Integer
  SetLength(IA, 5, 5);

You access multidimensional dynamic arrays the same way you do normal multidimensional arrays; each element is separated by a comma with a single set of brackets:

IA[0,3] := 28;

C-style sytanx for multidimensional array access is also supported:

IA[0][3] := 28;

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Records and Sets

A user-defined structure is referred to as a record in the Delphi language, and it's the equivalent of C#'s struct or Visual Basic .NET's Type. As an example, here's a record definition in Delphi as well as equivalent definitions in C# and Visual Basic .NET:

{ Delphi }
Type
  MyRec = record
    i: Integer;
    d: Double;
  end; 
/* C# */ 
public struct MyRec 

  int i; 
 
double d; 
}

'Visual Basic
Type MyRec
  i As Integer
  d As Double
End Type

When working with a record, you use the dot symbol to access its fields. Here's an example:

var
  N: MyRec;
begin
  N.i := 23;
  N.d := 3.4;
end;

Methods, operator overloads, and interfaced implementations are also supported for records. This capability was not supported on previous versions of the Delphi compiler. These topics are covered in more detail, along with class types, later in this chapter. However, the following code example shows the syntax for using these elements with records:

IBlah = interface
  procedure bar;
end;

Foo = record(IBlah) // record implements IBlah interface
  AField: Integer;
  procedure bar;
  class operator Add(a, b: Foo): Foo; // overload + operator
end;

Sets

Sets are a uniquely Delphi type that have no equivalent in C# or Visual Basic .NET. Sets provide an efficient and convenient means of representing a collection of ordinal, AnsiChar, or enumerated values. You can declare a new set type using the keywords set of followed by an ordinal type or subrange of possible set values. Here's an example:

type
  TCharSet = set of AnsiChar; // possible members: #0 - #255
  TEnum = (Monday, Tuesday, Wednesday, Thursday, Friday);
  TEnumSet = set of TEnum; // can contain any combination of TEnum members
  TSubrangeSet = set of 1..10; // possible members: 1 - 10

Note that a set can only contain up to 256 elements. Additionally, only ordinal types can follow the set of keywords. Therefore, the following declarations are illegal:

type
  TIntSet = set of Integer; // Invalid: too many elements
  TStrSet = set of string; // Invalid: not an ordinal type

Sets store their elements internally as individual bits, which makes them very efficient in terms of speed and memory usage.


Note - If you are porting Win32 code to .NET, bear in mind that chars are 2-bytes in the .NET world and 1-byte in Win32. This means that a set of Char declaration in .NET is demoted by the compiler to a set of AnsiChar, which can potentially change the meaning of code. The compiler will issue a warning to this effect, recommending that you explicitly use set of AnsiChar.


Using Sets

Use square brackets when constructing a literal set value from one or more elements. The following code demonstrates how to declare set type variables and assign them values:

type
  TCharSet = set of AnsiChar; // possible members: #0 - #255
  TEnum = (Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday);
  TEnumSet = set of TEnum; // can contain any combination of TEnum members

var
  CharSet: TCharSet;
  EnumSet: TEnumSet;
  SubrangeSet: set of 1..10; // possible members: 1 - 10
  AlphaSet: set of 'A'..'z'; // possible members: 'A' - 'z'

begin
  CharSet := ['A'..'J', 'a', 'm'];
  EnumSet := [Saturday, Sunday];
  SubrangeSet := [1, 2, 4..6];
  AlphaSet := []; // Empty; no elements
end;

Set Operators

The Delphi language provides several operators for use in manipulating sets. You can use these operators to determine set membership, union, difference, and intersection.

Membership

Use the in operator to determine whether a given element is contained in a particular set. For example, the following code would be used to determine whether the CharSet set mentioned earlier contains the letter 'S':

if 'S' in CharSet then
  // do something;

The following code determines whether EnumSet lacks the member Monday:

if not (Monday in EnumSet) then
  // do something;

Union and Difference

Use the + and - operators or the Include() and Exclude() procedures to add and remove elements to and from a set variable:

Include(CharSet, 'a');           // add 'a' to set
CharSet := CharSet + ['b'];      // add 'b' to set
Exclude(CharSet, 'x');           // remove 'z' from set
CharSet := CharSet - ['y', 'z']; // remove 'y' and 'z' from set

Tip - When possible, use Include() and Exclude() to add and remove a single element to and from a set rather than the + and operators, as the former is more efficient.


Intersection

Use the * operator to calculate the intersection of two sets. The result of the expression Set1 * Set2 is a set containing all the members that Set1 and Set2 have in common. For example, the following code could be used as an efficient means for determining whether a given set contains multiple elements:

if ['a', 'b', 'c'] * CharSet = ['a', 'b', 'c'] then
  // do something

Unsafe Code

Right about now, those with previous experience in Delphi for Win32 might be thinking, "this all sounds fine so far, but what happened to the pointers?" Although pointers are in the language, they are considered unsafe from a .NET standpoint because they allow for direct access to memory. Therefore, in order to employ pointers in your applications, you will need to let the compiler know that it should permit unsafe code. In order to write unsafe code, you must

  1. Include the {$UNSAFECODE ON} directive in the unit containing the unsafe code.

  2. Mark functions containing unsafe code with the unsafe keyword.

The following unsafe code will successfully compile in Delphi:

{$UNSAFECODE ON}

procedure RunningWithScissors; unsafe;
var
  A: array[0..31] of Char;
  P: PChar; // PChar is an unsafe type
begin
  A := 'safety first'; // fill character array
  P := @A[0];          // point to first element
  P[0] := 'S';         // change first element
  MessageBox.Show(A);  // show changed array
end;

Note the use of Delphi's @, or address of operator, to obtain the address of a bit of data.


Tip - Unsafe code is totally discouraged in .NET because an unsafe application will not pass muster with .NET's PEVerify tool and may therefore be subject to greater security restrictions. However, unsafe code can be an excellent way to bridge the gap between the Win32 world and the .NET world during a migration period. It might be difficult to port an entire large application from Win32 to .NET in one fell swoop; however, unsafe code allows you to port one portion at a time, keeping the old code as unsafe until it can be moved in its entirety to safe code.


You can assume that the code in this portion of the chapter must be compiled using the $UNSAFECODE and unsafe directives.

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Pointers

A pointer is a variable that contains a memory location. You already saw an example of a pointer in the PChar type earlier in this chapter. Delphi's generic pointer type is called, aptly, Pointer. A Pointer is sometimes called an untyped pointer because it contains only a memory address, and the compiler doesn't maintain any information on the data to which it points. That notion, however, goes against the grain of Delphi's type-safe nature, so pointers in your code will usually be typed pointers.

Note - In .NET, the System.IntPtr type is used to represent an opaque, untyped pointer.

Typed pointers are declared by using the ^ (or pointer) operator in the Type section of your program. Typed pointers help the compiler keep track of exactly what kind of type a particular pointer points to, thus enabling the compiler to keep track of what you're doing (and can do) with a pointer variable. Here are some typical declarations for pointers:

Type
  PInt = ^Integer; // PInt is now a pointer to an Integer
  Foo = record     // A record type
    GobbledyGook: string;
    Snarf: Double;
  end;
  PFoo = ^Foo;     // PFoo is a pointer to a foo type
var
  P: Pointer;      // Untyped pointer
  P2: PFoo;        // Instance of PFoo

Note - C/C++ programmers will notice the similarity between Delphi's ^ operator and C's * operator. Delphi's Pointer type corresponds to the C/C++ void * type.


Remember that a pointer variable only stores a memory address. Allocating space for whatever the pointer points to is your job as a programmer. Previous versions of Delphi had many functions that enabled a developer to allocate and deallocate memory; however, because direct memory allocation is so rare in .NET, it is typically accomplished only via the System.Runtime.InteropServices.Marshal class. The following code demonstrates how to use this class to create and free a block of memory, as a well as copy some array data in and out of the block.

{$UNSAFECODE ON}

type
  TArray = array[0..31] of Char;

procedure ArrayCopy; unsafe;
var
  A1: TArray;
  A2: array of char;
  P: IntPtr;
begin
  A1 := 'safety first'; // fill character array
  SetLength(A2, High(TArray) + 1);
  P := Marshal.AllocHGlobal(High(TArray) + 1);
  try
    Marshal.Copy(A1, 0, P, High(TArray) + 1); // copy A1 to temp
    Marshal.Copy(P, A2, 0, High(TArray) + 1); // copy temp to A2
    MessageBox.Show(A2); // show changed array
  finally
    Marshal.FreeHGlobal(P);
  end;
end;

If you want to access the data that a particular pointer points to, follow the pointer variable name with the ^ operator. This method is known as dereferencing the pointer. The following code illustrates working with pointers:

procedure PointerFun; unsafe;
var
  I: Integer;
  PI: ^Integer;
begin
  I := 42;
  PI := @I; // points to I
  PI^ := 24; // changes I
  MessageBox.Show(I.ToString);
end;

The Delphi compiler employs strict type checking on pointer types. For example, the variables a and b in the following example aren't type compatible:

var
  a: ^Integer;
  b: ^Integer;

By contrast, the variables a and b in the equivalent declaration in C are type compatible:

int *a;
int *b

The Delphi language creates a unique type for each pointer-to-type declaration, so you must create a named type if you want to assign values from a to b, as shown here:

type
  PtrInteger = ^Integer; // create named type
var
  a: PtrInteger;
 
b: PtrInteger; // now a and b are compatible


Note - When a pointer doesn't point to anything (its value is zero), its value is said to be nil, and it is often called a nil or null pointer.


Null-Terminated Strings

Earlier, this chapter mentions that Delphi has three different null-terminated string types: PChar, PAnsiChar, and PWideChar. As their names imply, each of these represents a null-terminated string of each of Delphi's three character types. In .NET, PChar is an alias for PWideChar, whereas PChar is an alias for PAnsiChar in Win32. In this chapter, we refer to each of these string types generically as PChar. The PChar type in Delphi exists mainly for backward compatibility with previous versions. A PChar is defined as a pointer to a string followed by a null (zero) value. Because it is a raw, unmanaged, unsafe pointer, memory for PChar types isn't automatically allocated and managed by .NET.

In the Win32 flavor of Delphi, PChars are assignment compatible with strings. However, in .NET, these types are no longer compatible, which makes their use far less common in the .NET world.

Variant Records

The Delphi language also supports variant records, which enable different pieces of data to overlay the same portion of memory in the record. Not to be confused with the Variant data type, variant records enable each overlapping data field to be accessed independently. If your background is C, you'll recognize variant records as being the same concept as a union within a C struct. The following code shows a variant record in which a Double, Integer, and Char all occupy the same memory space:

type
  TVariantRecord = record
    NullStrField: PChar;
    IntField: Integer;
    case Integer of
      0: (D: Double); 
      1: (I: Integer);
      2: (C: Char);
end;

Note - The rules of the Delphi language state that the variant portion of a record cannot be of any lifetime-managed type. This includes classes, interfaces, variants, dynamic arrays, and strings.


Here's the C equivalent of the preceding type declaration:

struct TUnionStruct
{
  char * StrField;
  int IntField;
  union u
  {
    double D;
    int i;
    char c;
  };
};

Because variant records deal with explicit memory layout, they also are considered unsafe types.


Note - Record memory layout can be controlled by the developer in .NET using the StructLayout and FieldOffset attributes.


This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

Classes and Objects

A class is a value type that can contain data, properties, methods, and operators. Delphi's object model is discussed in much greater detail later in the "Using Delphi Objects" section of this chapter, so this section covers just the basic syntax of Delphi classes. A class is defined as follows:

Type
  TChildObject = class(TParentObject)
  public
    SomeVar: Integer;
    procedure SomeProc;
  end;

This declaration is equivalent to the following C# declaration:

public class TChildObject: TParentObject
{
  public int SomeVar;
  public void SomeProc() {};
}

Methods are defined in the same way as normal procedures and functions (which are discussed in the section "Procedures and Functions"), with the addition of the classname and the dot symbol:

procedure TChildObject.SomeProc;
begin
  { procedure code goes here }
end;

Delphi's . symbol is similar in functionality to C# and Visual Basic .NET's . operator when referencing members.

Type Aliases

The Delphi language has the capability to create new names, or aliases, for types that are already defined. For example, if you want to create a new name for an Integer called MyReallyNiftyInteger, you could do so using the following code:

type
  MyReallyNiftyInteger = Integer;

The newly defined type alias is compatible in all ways with the type for which it's an alias, meaning, in this case, that you could use MyReallyNiftyInteger anywhere in which you could use Integer.

It's possible, however, to define strongly-typed aliases that are considered new, unique types by the compiler. To do this, use the type reserved word in the following manner:

type
  MyOtherNeatInteger = type Integer;

Using this syntax, the MyOtherNeatInteger type will be converted to an Integer when necessary for purposes of assignment, but MyOtherNeatInteger will not be compatible with Integer when used in var and out parameters. Therefore, the following code is syntactically correct:

var
  MONI: MyOtherNeatInteger;
  I: Integer;
begin
  I := 1;
  MONI := I;

On the other hand, the following code will not compile:

procedure Goon(var Value: Integer);
begin
  // some code
end;

var
  M: MyOtherNeatInteger;
begin
  M := 29;
  Goon(M); // Error: M is not var compatible with Integer

In addition to these compiler-enforced type compatibility issues, the compiler also generates runtime type information for strongly-typed aliases. This enables you to create unique property editors for simple types, as you'll learn in Chapter 8, "Mono—A Cross Platform .NET Project."

This chapter is from Delphi for .NET Developer's Guide, by Xavier Pacheco (Sams, 2004, ISBN: 0-672-32443-1). Check it out at your favorite bookstore today.

Buy this book now.

blog comments powered by Disqus
.NET ARTICLES

- .Net 4.5 Brings Changes
- Understanding Events in VB.NET
- Objects, Properties, Events and Methods in V...
- Install Visual Web Developer Express 2010
- Microsoft Gadgeteer an Open Source Alternati...
- Best DotNetNuke Modules
- Facebook Image Viewer in Visual Basic
- Murach`s ADO.NET 4 Database Programming with...
- 5 Must Have Visual Studio 2010 Extensions
- Dynamic Web Applications with ASP.NET Mono u...
- PDFSharp: HTML to PDF in ASP.NET 3.5 using V...
- Using the PDFSharp Library in ASP.NET 3.5 wi...
- Sending Email in ASP.NET 3.5 using VB.NET wi...
- ASP.NET 3.5 Role Based Security and User Aut...
- Creating ASP.NET Login Web Pages and Basic C...

ASP Web Hosting ASP.Net Web Hosting Windows Web Hosting
 
 
 

ASP Free Forums 
 RSS  Tutorials RSS
 RSS  Forums RSS
 RSS  All Feeds
Site Map 
Request Media Kit
Write For Us Get Paid 
Weekly Newsletter
 
Developer Updates  
Free Website Content 
Privacy Policy 
Support 


© 2003-2012 by Developer Shed. All rights reserved. DS Cluster 1 - Follow our Sitemap
Most Popular Topics
All ASP.Net Tutorials