| < Day Day Up > |
|
The Object Pascal implementation of Borland for the Microsoft Windows platform is available in the form of the Delphi tool, for the past several years, and has been very popular in the Windows developer community as a highly productive object-oriented RAD tool. The Delphi tool, along with its Object Pascal compiler, is ported by Borland to the Linux operating system as part of its Kylix development platform. Delphi’s view of the Object Pascal language with the associated object-oriented programming principles will be discussed in this section to provide the readers sufficient knowledge to enable building Delphi applications using the Kylix platform. It should be noted that while the current chapter aims at discussing the language syntax and semantics, Chapter 6 will use the Kylix Integrated Development Environment (IDE) to build Delphi applications. Therefore, by reading the current chapter, the readers will become familiar with the file extensions, some of the command-line options, and so on, as they are going to manually compile the programs. How the Kylix IDE handles the majority of these tasks will be noted in Chapter 6. It should also be noted that the contents presented about the Object Pascal or Delphi or Kylix are intended to provide an introduction to the reader and therefore are neither exhaustive nor complete. The readers are encouraged to refer to the product’s documentation or to check with Borland Software Corporation for more details on any of these topics.
A Delphi application contains a main program and one or more individual unit files. The main program is typically named <program name>.dpr. The extension ‘.dpr’ represents “Delphi Project file.” It is the main program that is going to integrate the individual Pascal unit files into a single application and finally create the executable file. Therefore, the Delphi program (or the project file) is usually very short, as the entire application logic is distributed across a number of individual Object Pascal programs called units. A single Delphi program represents a single Delphi application that integrates these different Object Pascal units. A Delphi program also integrates any resource files associated with the application. The typical structure of a Delphi program is given in Listing 5.1
Listing 5.1
program <program name>; uses <list of units used>; {<compiler directives>} var <variable declarations>; begin <program statements> end.
The words in bold are reserved words and should be used as they are. The first line, known as the program heading, specifies the name of the program and typically matches with the name of the program file (without the .dpr extension). For example, if this line specifies ‘program HelloWorld;’, then the program file could be named as HelloWorld.dpr. (It is important to keep in mind that the program name is case sensitive.) The uses clause lists all the units that are being linked within the application. Each item in the list represents a separate Object Pascal program unit (compiled or source). Again, similar to the program name, the unit names also must match the program names (without the extension). For any of the units in this list, it is acceptable to specify the file name including the full path, particularly if the unit file is not located in the directory search path identified by Delphi, or if multiple units with the same name exist in different directories, and we want to include one of them in the uses clause.
Comments make a program readable, particularly if used moderately. There are different ways Delphi programs understand comments. Anything surrounded by a pair of curly braces as in { this is a comment } is considered commented and is not compiled. The opening and closing curly braces may also be replaced by the combination of the opening parenthesis and asterisk (* and asterisk and closing parenthesis *), respectively. This means, anything between (* and *) is also considered as commented, as in (* this is also considered a comment *). The comments can span across several lines, as long as they are properly surrounded by the beginning and ending comment marks. A comment that contains a $ sign immediately after the opening comment mark { or (* is considered a compiler directive. Comments and compiler directives can be placed anywhere in the program. However, compiler directives convey special meaning to the compiler. For example, compiler directives may be used to direct the compiler to perform several tasks during the process of compiling the projects, such as telling the directory paths where to search for include files, or telling the directory paths where to write the output files or include debug information in the output, and so on. A few compiler directives are shown here for a better understanding of what they look like.
{$D+} {enables the compiler to generate debug information} {$D-} {disables the compiler to generate debug information} {$ObjExportAll On} {enables to export all the symbols} {$M+} {enables to generate runtime type information} {$SOSUFFIX '-1.4.0'} {adds the suffix 1.4.0 to the shared object file} {$APPTYPE CONSOLE} {creates the console application}
The first four compiler directives (specifying + or -, or On, or Off) are called switch type directives. By providing this type of compiler directives, we are either enabling or disabling a particular action for the compiler to perform. The last two compiler directives are of parameter type. In this type of directives, we provide one or more values to a particular compiler action through the parameters. In the fifth example, the $SOSUFFIX compiler directive adds the string passed to it to the end of the so file name before the file extension. The so files on Linux represent shared object files (similar to the Windows DLL files). These are library files and are loaded at runtime by the calling module. If the specified compiler directive is used in a program, say mylibrary, that is intended to create a shared object file called mylibrary.so, then the compiler would generate the so file with the name mylibrary-1.4.0.so. The sixth example indicates that the program is of console application type. There is a third type of compiler directives called conditional directives that can be used for conditional compilation, as shown here.
{$DEFINE LOGGING} {$IFDEF LOGGING} {Between the $IFDEF and $ENDIF we can write code to log messages which are useful during development process. Once the code is ready for release, the $DEFINE LOGGING compiler directive can be removed in the source, and this will disable the code between the $IFDEF and $ENDIF} {$ENDIF } {$UNDEF LOGGING}
In this example, the symbol LOGGING is recognized from the point where it is defined, until it is undefined or to the end of the module, whichever occurs first. This is helpful when writing code that should be compiled only when the particular symbol is defined, thus providing a single controlling point in the code to enable or disable compiling certain parts of the module.
There are several other compiler directives, and readers are encouraged to refer to the product documentation for additional details. The compiler directives can be used in three different ways. They can all be placed in a configuration file dcc.conf in the directory where the source files are present. There are other places also that the compiler looks for the file, but placing it in the same directory as the program source file is easy to do, and we can have one such file separately for every program if the program is kept in a different directory. The compiler directives can also be specified at the command line, which will take precedence if a tie occurs for the same directive. Finally, they can also be specified in the program source itself, and they take precedence over those used in the previous two methods.
Following the uses clause are declarations of global variables being used in the program. Program global variables should be declared before the execution block begins, using the var reserved word. The last section in the program is the execution block, which contains a group of executable statements. The execution block is enclosed between the begin and end reserved words. Notice the period . after the end, indicating the end of the program. A typical console application might include function calls to interact with the standard input and output, in addition to other statements, as will be noted from the examples discussed in this chapter. On the other hand, if the program represents a desktop GUI application or a Web application, the global Application variable encapsulates an application object accordingly, and its methods are called from within the program execution block, as will be noted from the discussion in Chapter 6.
Most of the work is performed in the individual program units, and the main program file only acts as an application integrator, as mentioned before. The typical structure of a unit file is provided in Listing 5.2 for a quick overview, followed by an explanation of the main sections of the unit. The subsequent sections will provide details on the different programming constructs and identifiers used in the units.
Listing 5.2
unit <unit name>; interface uses <list of units used>; implementation uses <list of units used>; initialization <unit initialization code> finalization <unit finalization code> end.
The unit name should match the file name containing the unit’s source code (without the extension). However, the unit files should be saved with the ‘.pas’ extension. Each unit is considered a module and is complete by itself. However, a unit might use identifiers and objects defined in other units. In that case, the unit’s uses clause has to specify other units’ names whose identifiers and objects are referenced in the unit. A unit typically contains four sections: the interface section, implementation section, initialization section and finalization section.
The interface section begins with the reserved word interface, and specifies all the identifiers—such as variables, constants, types, and forward declarations of procedures and functions that the unit wants to expose to other units—as public identifiers. That means any other unit that uses this unit can access identifiers of this unit only if they are declared in this section. The interface section can have its own uses clause to specify any other units required, which should appear immediately after the reserved word interface. The interface section is different from interface types supported by Delphi and mentioned earlier; they are discussed later in the chapter.
The implementation section begins with the reserved word implementation and provides implementation of the procedures and functions declared in the interface section. In addition, it may include procedures and functions that are not declared in the interface section, in which case they are only accessed by other procedures and functions within the unit and are not accessed by other units, as they are deemed to be private to the unit. The implementation section can define its own (private) identifiers that are used for the purpose of implementation of the procedures and functions. It also can have its own uses clause, and should be specified immediately after the reserved word implementation. This section could continue until the end of the unit or the beginning of the initialization section, whichever occurs first.
The initialization section is optional. If it is present, then it is used to perform unit-level initialization of identifiers, if required. It could continue to the end of the program or the beginning of the finalization section, whichever occurs first. The initialization section is used to allocate and initialize any unit-level data structures or resources, and the finalization section is used to release such resources. However, these sections should not be confused with the constructor and destructor methods of the classes, which are implemented at the class level. A unit might implement more than one class, as is done most of the time. The initialization section can be present without the finalization section, but the finalization section cannot be present without the initialization section. When a number of units are specified in the uses clause of a unit, then their initialization sections are executed in the order in which they are specified, and the finalization sections are executed in the reverse order.
The units specified in the uses clause should be present in the Delphi’s path of directory search or in the same directory as that of the unit that uses them. Otherwise, it is not permitted to specify the full path name of the used unit in the ‘uses’ clause of a unit, unlike the uses clause of the program. For every unit that is used in the current unit, either the compiled unit file that has the ‘.dcu’ extension or the source file with the ‘.pas’ extension should be present for the compilation to succeed.
Listing 5.3 presents a very simple Delphi program. If the listing is saved as Hello.pas, assuming that Kylix3 is installed in your system and its bin directory (e.g., /usr/local/kylix3/bin) is set in the PATH variable, the program can be compiled with the following command line. The executable file created is named as Hello and is in the Linux native executable format.
$ dcc Hello
This listing is available on the accompanying CD-ROM.
Listing 5.3: Hello.dpr
program Hello; {$APPTYPE CONSOLE} var PromptString: string; var YourName: string; begin PromptString := 'Please enter your name !!'; writeln(PromptString); readln(YourName); writeln('Hello ' + YourName + '! Welcome to Kylix Delphi'); end.
In this simple application, no separate units are used. However, the default ‘System’ unit is automatically used by the compiler, which provides a number of predefined system routines. The two functions ‘readln’ and ‘writeln’ appear to be part of the ‘System’ unit, but they are built into the compiler. In addition, Delphi comes with a number of units distributed across different packages (or libraries) to enable the development of desktop, database, and Web applications, and much more. Chapter 6 and some of the later chapters provide a thorough discussion on these packages.
Identifiers are used to define (or name) variables, constants, types, objects, literals, procedures, functions, and so on, and therefore the word ‘identifier’ is more generic in nature. The identifiers are case-insensitive and can have a significant length of up to 255 characters; characters beyond that length are not significant because Delphi does not recognize them. Delphi is a strongly typed language, similar to C++ and Java, which means that the variable types are distinct and should be used exactly as they are intended. Variables have to be declared before they are used. Variables are declared using the var reserved word. The general syntax and a few examples are shown here.
var <variable name>: <data type>; { general syntax in simplest form } var name: string; { declares a string variable } var fname, lname: string; { declares two string variables } var count: Integer; { declares an integer variable } var i: Shortint; { declares a short integer variable } var accept: Boolean; ch: Char; { declare different types at once }
Delphi supports several intrinsic data types such as Shortint (signed 8-bit integer), Smallint (signed 16-bit integer), Integer (or longint, which is a signed 32-bit integer), Byte (unsigned 8-bit integer), Word (unsigned 16-bit integer), Char (or AnsiChar, which is a 8-bit ANSI character), WideChar (16-bit UNICODE character), Boolean, Single (for single precision floating-point numbers), Double (for double-precision floating-point numbers), Currency (which is a fixed-point data type that minimizes rounding errors in currency-based computations), ShortString (of max 255 characters), AnsiString (which is a long string of up to 2GB size), and pointer data types. (The pointer data types are discussed in a separate subsection later in the chapter.) In addition, there are enumerated data types and subranges provided for supporting arrays and other structured data types. The Boolean constants True and False are used to set the values of a Boolean variable to true and false, respectively. Identifiers preceded with the reserved word const are considered to be constant and do not permit changing the value in the execution block.
The enumerated data types are declared using the var declaration enclosing the enumerated values in parentheses, or in two steps first declaring the enumerated type using the type identifier and then declaring a variable of that type using the var declaration, as shown below. In both cases, the different enumerated values are enclosed in a pair of parentheses.
var myfruit: (orange, apple, banana, pear, mango); {one way} type Fruit: (orange, apple, banana, pear, mango); var myfruit: Fruit; {second way}
As mentioned earlier, Delphi supports ShortString type strings with maximum size of 255 characters and the AnsiString type long strings which can be as large as 2GB in size. For short strings, the memory (of 256 bytes) is allocated statically, and for long string types, the memory is allocated dynamically as the string grows in size. The memory required for long string types is allocated on the heap, but completely managed automatically without additional programming requirements. There is a third type of string, known as WideString to support strings of Unicode characters. The string data type represents either the long string or the short string depending on the compiler directive {$H+} or {$H-}, respectively. By default, the {$H+} compiler directive is in effect, and the string type refers to the AnsiString type.
The PChar data type is a pointer to a null-terminated array of Char data type, and PWideChar data type is a pointer to a null-terminated array of WideChar data type. Arrays of Char and WideChar data types are created using syntax similar to the following.
type CharArray = array[0..100] of Char; type WCharArray = array[0..200] of WideChar;
These arrays are similar to the null-terminated strings used in the C/C++ language. The PChar and PWideChar pointer type variables can be instantiated with null-terminated strings of Char and WideChar, respectively, as shown below.
var CharPtr: PChar; var WCharPtr: PWideChar; begin CharPtr := 'This creates a null-terminated array of Chars'; WCharPtr := 'This creates a null-terminated array of Wide Chars'; end;
Alternatively, a PChar pointer can be assigned with the address of a null-terminated array of characters, as shown in the following example.
type CharArray = array[0..100] of Char; var MyArray: CharArray; var MyCharPtr: PChar; begin MyArray := 'This is a sample char array'; MyCharPtr := @MyArray; end;
From the code, it is clear that the address of the null-terminated array variable is assigned to the PChar type pointer variable using the @<array variable> notation. Delphi string manipulation is very efficient and flexible. The most commonly used string type is the long string type (and wide string type to store UNICODE type characters), and a rich set of system routines is available to perform a number of string operations, including concatenation of strings, retrieving substrings, converting to upper-and lowercases, and so on. Some of these commonly used routines are described in Table 5.1.
Function | Function Description |
---|---|
StringToWideChar | The function is used to convert long string data type to wide string (UNICODE) data type. The function arguments are a long string data type, a pointer to the wide string data buffer, and the size of the buffer. Because the buffer is nullterminated, only size-1 characters are copied. The function returns a pointer to the copied buffer. |
WideCharToString | Converts a null-terminated UNICODE character string to single or multi-byte string. The function takes one argument, a pointer to the wide string type, and returns a long string. |
AnsiCompareStr | This function is used to compare string str1 to string str2 based on current locale and returns an integer value greater than zero, less than zero, or equal to zero, indicating respectively whether str1 is greater than str2, str1 is less than str2 or both the strings are equal. The comparison is case sensitive. |
AnsiCompareText | This function is used very similar to except that it performs string comparison without case sensitivity. |
CompareStr (const | This function is used very similar to AnsiCompare Str to perform case-sensitive comparison, except that it performs string comparison based on the 8-bit ordinal value of each character and is not locale dependent. |
CompareText (const | This function is used very similar to AnsiCompareText to perform case-insensitive comparison, except that it performs string comparison based on the 8-bit ordinal value of each character and is not locale dependent. |
AnsiLowerCase | Converts a string to all lowercase (based on current locale) and returns the converted string. |
AnsiUpperCase | Converts an 8-bit string to all uppercase (based on current locale) and returns the converted string. |
LowerCase (const | Converts a string (based on the 7-bit ASCII character set) to all lowercase and returns the converted string. |
UpperCase (const | Converts a string (based on the 7-bit ASCII character set) to all uppercase and returns the converted string. |
Structured data types are more useful for handling complex data structures, and Delphi is very prominent for its support of a spectrum of structured data types. The structured types can handle multiple values within a single data type definition. Here are some examples of structured data types: A set data type represents a collection of data items of the same ordinal type usually used to define groups of elements or objects, and the members of a set are not indexed. An array is a collection of indexed elements or same ordinal type, which can be accessed through the index. Arrays can be static, where the number of elements of the array is fixed at the time of declaration, or dynamic, where the array can grow in size during run time. Arrays can also be multidimensional. The phrase same ordinal type means that all the members of the collection are of same data type, like all the members are Integers or ShortStrings, and so on. Examples of declaring sets and arrays are given here for the benefit of the readers.
type myColors = set of (red, orange, green, yellow); type cars = set of (Toyota, Honda, Ford, Chrysler, Mazda); var myCars: cars; type myInt = array[1..1000] of Integer; var intArray: myInt; var intArray2: array[1000..10000] of Integer;
The + operator performs the union of sets, the * operator performs the intersection and the - operator produces the difference of sets, as shown in the examples here.
type fruits = set of (orange, banana, mango, apple, pear, grape); var my_fruits, your_fruits: fruits; var our_fruits, diff_fruits1, diff_fruits2, common_fruits; begin my_fruits := [orange, mango, apple]; your_fruits := [mango, pear]; our_fruits := my_fruits + your_fruits; diff_fruits1 := my_fruits – your_fruits; diff_fruits2 := your_fruits – my_fruits; common_fruits := my_fruits * your_fruits; end;
In these examples, the our_fruits set includes members that belong to the my_fruits set or your_fruits set, which means the members are orange, mango, apple, and pear. Similarly, the result of other set computations can be computed. The in operator checks whether a particular member belongs to the set or not and accordingly returns a Boolean value (true or false), as shown in the example here.
if orange in our_fruits then writeln('Orange belongs to our_fruits set') else writeln('Orange does not belong to our_fruits set') end;
A static array is defined by declaring the array to contain a specific number of elements, as in array[1..10] of Integer, which declares an integer array of 10 elements. If the array is defined to start from the zero index, then it is called a zero-based array. A zero-based array used to store characters may be used as a null-terminated array as discussed before. Individual members of the array are accessed using the index, as shown in the example here.
type intArray = array[0..20] of Integer; var mia: intArray; begin mia[0] := 10; mia[1] := 20; mia[2] := 25; end;
It should be noted that variables of set and array types could be directly declared without using the type identifier. For example, the type and var are combined together as shown in the following example; the first example creates a one-dimensional array, and the second and third examples create two- and three-dimensional arrays. Elements of multidimensional arrays are accessed by specifying comma-separated index values for all the dimensions within the square brackets, such as tia[2,3] to specify the array element with the first index value 2, and the second index value 3. Any attempt to access an index value out of the defined range causes a compilation error, in the case of static arrays.
var mia: array[0..20] of Integer; var tia: array[1..10, 1..5] of Integer; var thia: array[1..5, 1..10, 1..20] of Integer;
Dynamic arrays are created without specifying the length at the time of declaration, and the length of the array should be set using the SetLength procedure, before assigning values to the members. Because the static arrays are created with predefined size, memory required for the static arrays is allocated at the time of declaration itself. On the other hand, in the case of dynamic arrays, the memory is not allocated until the SetLength procedure is called.
Another very popular structured data type supported by Delphi is the record type, which is similar to the C language struct type. As the name suggests, the record data type is useful in defining compound data types containing one or more individual members, or a structure that represents the file records, or the records read from a database table. The members of a record data type (or fields as they are called) can each be of different data types among themselves. For example, an employee record type may include the first name, last name, gender, date of employment, title, base pay, and so on. The general syntax and the employee record example are provided here for a better understanding of the concept. To simplify the example, the date of employment is represented as a string.
type <type name> = record <field1>: <data type>; <field2>: <data type>; . . . . . . . . . <fieldN>: <data type>; end; type Employee = record firstname: string; lastname: string; gender: (Male, Female); empdate: string; title: string; basePay: Integer; end;
Memory is allocated when variables (or instances) of record data type are declared. Individual fields of the record type variables are accessed by qualifying them with the variable name and the dot notation. The following example demonstrates how to declare records of the employee record type defined earlier.
var emp1, emp2: Employee; begin emp1.firstname := 'Satya'; emp1.lastname := 'Kolachina'; emp1.gender := Male; emp1.empdate := '01-01-2003'; emp1.title := 'Manager'; emp1.basepay := 90000; emp2.firstname := 'James'; emp2.lastname := 'Martin'; emp2.gender := Male; emp2.empdate := '10-01-2002'; emp2.title := 'Sr. Manager'; emp2.basepay := 95000; end;
When two records of the same record type are declared, as in the previous example, field values for one record can be assigned (or set) from those of the second record by a simple assignment of one record to the other, as shown here.
emp1 = emp2;
Variant data types are useful for declaring variables whose types are not known at program compile time. Delphi’s variant data types are very powerful and can hold any data type other than the structured types, pointers. The flexibility offered by the variant data types is available at the price of their higher memory requirement and more processing time needed by the operations involving them. When a variant is assigned to a variable of a normal data type, the value stored in the variant is automatically converted and assigned to the normal data type, if such conversion is appropriate. For example, if the variant stores an integer value in a string form (such as ‘20’) and the variant is assigned to an Integer type variable, then the value in the variant is automatically converted to Integer (such as 20) type and assigned to the Integer variable. Delphi provides a number of default variant data types as identified by the TVarType types. However, the developers are free to define their own custom variant data types, which should be derived from the TCustomVariantType class. There are a number of functions that operate on Variant type variables, and they are described in Table 5.2. However, to use these functions in Delphi programs, the Variants unit must be specified in the uses clause of the program/unit file where it is being referenced. Some of the functions may be defined in other units, which must be specified in the uses clause while using those functions.
Function | Function Description |
---|---|
VarArrayCreate (const | This function is used to create an array whose elements are variant types. The function takes two arguments. The first argument is an integer specifying the required bounds of the variant array, and the second argument indicates for what variant type should the array be created. For example, the function call VarArrayCreate([0,5], varVariant) will create a variant array with 6 elements (with index 0 through 5) to store variant types in all of them. The second argument is of TVarType, which could be one of several possible types; some of them include varInteger, varSmallint, varSingle, varDouble, varDate, varBoolean, varVariant, varString, and varArray. |
VarArrayFromStrings | This function is used to create a variant array of strings from the TStrings object, which is a Delphi CLX class that holds a list of strings. The function takes one argument, which is the TStrings object, and returns a variant array, whose elements are individual strings from the argument object. This function is defined in the Provider unit, which should be included to use this function. |
VarAsType (const | This function is used to convert a variant of one type to another variant of a different type. The function takes two arguments; the first argument is the variant to be converted, and the second argument is the target variant type to which the input variant should be converted. |
VarClear (vvar: Variant); | This procedure is used to clear a variant type variable. The procedure takes one argument, which is the variant that should be cleared. The result of clearing a variant is the same as assigning the Unassigned constant to it. After clearing the variant, it becomes empty and returns True when the VarIsEmpty or the VarIsClear function is called on this variant. The difference between these two functions is explained in the following topics. |
VarIsArray (vvar: | This function checks whether the input argument is a variant array, and if so, returns True; otherwise, it returns False. |
VarIsClear (vvar: | This function checks whether the input argument is an undefined or clear variant, and if so, returns True. A variant is undefined if any one of the following situations is valid: if it is assigned Unassigned constant or the VarClear function is executed on it, or the variant’s value may be an interface type that has been set to nil, or the variant is a custom variant type and returns True when its IsEmpty method is called. Unassigned variant (continued) is different from a null variant in that it is not assigned any value and hence cannot be used in expressions or variant conversions, whereas a null variant contains the Null value. |
VarIsEmpty (vvar: | This function should be used (only) to know if the variant data type is empty, which means unassigned. If the variant represents an interface pointer and the interface pointer is cleared, or if the variant is a custom variant data type, which is cleared, then this function should not be used to check if the variant is empty; rather, the VarIsClear function should be used. |
VarIsFloat (vvar: | This function is used to find out if the variant data type passed as argument represents a floating-point value such as Single, Double, or Currency and, if so, returns True. For all other variant data types, the function returns False. |
VarIsNull (vvar: | This function checks if the input argument is a null variant, and if so, returns True; otherwise, it returns False. A null variant is different from an unassigned variant in that, it contains the Null value and can be used in expressions, or converted into other variants. |
VarIsNumeric (vvar: | This function is used to check if the input argument represents a variant of numeric type such as a floating-point variant such as Single, Double or Currency, or a variant of any ordinal type such as Integer or Boolean, and if so, returns True. For all other variant data types, the function returns False. |
VarIsOrdinal (vvar: | This function to check if the input argument represents a variant of ordinal type such as an Integer or Boolean value, and if so, returns True. For all other variant data types, the function returns False. |
VarIsStr (vvar: | This function is used to check if the input argument represents a variant of string data type, and if so, returns True, otherwise returns False. |
VarIsType (vvar: | This function is used to check if a particular variant is of a particular variant data type. The function takes two arguments: the first argument is the variant variable on which the method has to perform the data type check, and the second argument is of TVarType type to be checked. For example, if it is necessary to check whether the variant is of varInteger type, then the function call could be something like VarIsType(vvar, varInteger). The function returns True if the variant is of the specified type; otherwise returns False. The function is overloaded such that an array of variant data types can be passed as the second parameter instead of one particular data type. This would enable the function to check the variable against all the data types specified in the array. |
VarToStr (vvar: | The function is used to convert the input variant data type to a string. |
VarType (vvar: | This function is used to retrieve the data type represented by the variant variable passed as the argument. The returned value is of TVarType type. |
A typical Delphi unit file contains implementation of a bunch of independent methods or class methods. Methods that return a value to the calling method are known as functions, and those that do not return a value are known as procedures, though for the most part they are similar. The general syntax of a procedure and a function are provided here for an overview to the reader.
procedure <procedure name> (argument list); <directives>; <local variable declarations> begin <statement1>; <statement2>; . . . . . . <statementN>; end;
The procedure keyword is required to tell the Delphi compiler that the construct following the keyword is a procedure. Following this keyword, the name of the procedure should be provided, which is just an identifier. The list of the arguments (or parameters) to the procedure, if any, should be provided in enclosed parentheses followed by the semicolon. The argument declaration syntax is similar to the notation arg1: <type1>; arg2: <type2>, arg3, arg4: <type3>; . . .. As seen from the notation, if more than one argument is of the same type, they all can be separated by commas and, at the end, the common type specifier should be mentioned. Arguments (or lists of arguments) are individually separated by semicolons. The first line of the procedure declaration is known as the procedure header or the procedure signature. Following the procedure header, all the local variable declarations should be provided. It should be noted that Delphi does not permit variable declarations within the execution block, unlike other programming languages such as C++ and Java, which permit the programmer to declare variables just before they are used. Following the local variable declarations is the procedure execution block, which begins with the reserved word begin and ends with the reserved word end followed by a semicolon. Let us now look at the syntax of a function declaration.
function <function name> (argument list); <return type>; <directives>; <local variable declarations> begin <statement1>; <statement2>; . . . . . . <statementN>; end;
As noticed from the declaration, the function declaration is very similar to the procedure declaration except that there is a return type specifier following the argument list in a function declaration. However, contrary to the expectation of some of the readers, there is no return statement in the function execution block. Instead, Delphi provides two ways to return a value to the calling procedure or function. The name of the function itself acts as a return value to the function, or a special implicitly defined Result variable should be assigned the return value of the function before the function exits. For example, if the function name is MyNewFunction then the assignment MyNewFunction := <value> will return the <value> to the calling method. Similarly, the assignment Result := <value> would also produce the same effect. Therefore it should be noted that, in a function block, one of these assignments should be made before the function terminates the execution. The Result variable can be used on the left or right side of statements or in expressions like any other variable. However, any such usage will affect the return value accordingly. On the other hand, if the function name is used on the right side of a statement, Delphi assumes that the function is being called recursively, and if it used on the left side of the statement, it is assumed as the return value.
In the procedure or function header, there is an optional <directives> part, which needs a bit of explanation. The directives are used to specify the calling conventions of the procedure or the function. The calling conventions indicate to the compiler how the arguments are passed to the method and whether to use the stack or the register while passing the arguments. There are five different calling conventions used, and they are register, pascal, cdecl, stdcall, and safecall. The register calling convention is the default if nothing is explicitly specified and uses the CPU register to pass the arguments to the method. Also, the leftmost argument is passed first and the rightmost argument is passed last. All other calling conventions use the stack to pass arguments. Among the others, the pascal calling convention passes the leftmost argument first and rightmost argument last, while the cdecl, stdcall, and safecall calling conventions pass the rightmost argument first and the leftmost argument last. In the case of the cdecl calling convention, the calling method removes the arguments from the stack after the control returns from the called method, and in the case of all the other calling conventions, the called method cleans up the stack (or register, in the case of the register calling convention). The register calling convention is the most efficient of all, as it uses the CPU registers and does not have to create the stack frame or does not use the stack itself. For all the published methods of classes, the register calling convention must be used. The cdecl and stdcall calling conventions are used while accessing methods from external libraries written languages such as C++. The most commonly used calling conventions for Kylix are register, cdecl, and stdcall, while the others have more relevance to the Windows operating system.
Apart from the different calling conventions, there are other directives that have special importance, as discussed here. The forward directive is used to provide the forward declaration of a procedure or function without an implementation. However, the actual implementation of the method should be provided somewhere in the unit after the forward declaration. A forward declaration tells the compiler that the method is implemented later, while only the method’s signature is provided at this time. When a method’s forward declaration is provided when actually implementing the method, the method’s header can omit the argument list if the method is not overloaded in the unit, because when methods are overloaded and the argument list is not specified in the method implementation, it results in an ambiguous situation to the compiler. Method overloading means that more than one method can have the same name as long as the argument list differs in the type or number of arguments or the return type differs. Overloaded procedures and functions should have the overload directive in their declaration header. It should be noted that the purpose of the forward declaration of a method is to make the declaration available before it is actually implemented (or defined). Methods declared in the interface section of a unit behave like forward declarations and hence the forward directive is not permitted for method declarations specified in the interface section. Here are some examples of function declarations using some of the special directives discussed so far.
// Draw a triangle using two sides and the angle between them procedure DrawTriangle(side1, side2: Double; angle: Double); overload; // Draw a triangle using three sides procedure DrawTriangle(side1, side2, side3: Double); overload; // function declaring cdecl calling convention function MyFunction(arg1, arg2: Integer, arg3: Double); cdecl;
The procedure and function data types are different from the procedures and functions. They represent pointers (or addresses) to the procedures and functions, respectively. A variable of a data type that is defined as of a specific procedure or function type can be assigned the name of a procedure or function, whichever is appropriate, and which satisfies the signature of the procedure or function type declaration. For example, consider the function declarations given here.
function MyStatus(MyName: string; MySalary: Integer): string; function YourStatus(YourName: string; YourSalary: Integer): string;
Now consider the following function variable type declaration and the accompanied assignments.
var status: function(name: string; salary: Integer): string; var stVar: string; status := MyStatus; stVar := status('Satya Sai Kolachina', 100000); . . . . . . status := YourStatus; stVar := status('Michael Johnson', 110000);
The variable status is declared with a function signature similar to the MyStatus function (or the YourStatus function) declared earlier. Therefore, the variable status can now be assigned to any function that has a similar signature, including the arguments and the return data type. This can be noticed from the subsequent two assignments to two different functions having the same signature. After the variable status is assigned the name of the appropriate function type, the variable behaves like the function itself and can be invoked by providing the necessary arguments and capturing the return value. This style of accessing a procedure or function indirectly will be of great help to call a method dynamically. The object event handling feature in the CLX component library is implemented using this concept. The procedure and function data types can also be passed as arguments to functions.
The class type structure forms the foundation of an object in an object-oriented language. Being a full supporter of object-oriented programming paradigm, Delphi’s support of the class type is very powerful, as might be expected. The class type declares the blueprint of an object’s design in terms of what the object contains, and how it interacts with the external world. The typical class type declaration is given here followed by a detailed discussion of the type.
type <class name> = class <(base class)> private {private members should be declared here} protected {protected members should be declared here} public {public members should be declared here} published {published members should be declared here} end;
The class type is declared with the name of the class following the reserved word type, following which the = sign and the reserved word class should be present. If a base class name is not provided then the class is automatically derived from the (implicitly predefined) TObject class; however, if a base class name is provided in enclosed parentheses, then the class inherits its characteristics from the specified base class. The TObject class is the ancestor of all the classes in the Delphi-CLX library. Although the characteristics of class type are discussed in this chapter, the CLX library is discussed in detail in Chapter 6. The class declaration should have the unit level scope, and hence should not be declared in a procedure or function. From the general syntax of the class type, it can be observed that there are specific sections within the class, which help us define the class members with specific levels of access. Because class forms the basis of an object, the data members of a class include other data types, such as ordinal types, strings and other objects, and the member functions that provide access to the data members. The three main sections of the class—public, private, and protected—control the access of the class’s data members to external objects or programs. Members that are declared as public are accessible to external objects; those that are declared as private are only accessible to other members of the class, and those that are declared as protected are accessible to the other members of the class, or other classes defined in the same module (or unit) as well as any class that descends from this class, without regard to whether the descending class is declared in the current module or another module. The published access specifier is related to the CLX component library and enables the component’s properties and methods to be accessible in the IDE during design time, as can be noticed from the discussion that follows in Chapter 6. When put in the order of visibility, the access modifiers (private, protected, public, and published) provide the least visibility to members declared as ‘private’ and the highest visibility to members declared as published. However, when a class descends from an ancestor class, the derived class can increase the visibility level of an inherited member from what it was in the ancestor class by redeclaring it in the appropriate section; but it cannot decrease the visibility level from what it was in the ancestor class.
While declaring the class type for member methods only the function/procedure heading is provided. The actual implementation of these methods is provided separately in the implementation section of the unit. Also, a descending class can declare new members in addition to the ones inherited from the ancestor class, or redeclare members that are inherited from the ancestor class, but cannot remove the inherited members from its declaration. The member methods of a class must have the register calling convention, which is the default calling convention in Delphi. The constructor method of a class should be named as Create, and can override the inherited method, or we can declare more than one constructor (overridden) with different signatures. Similarly, the destructor method should be named Destroy. There should only be one destructor method for a class. However, the descending class can redeclare its own destructor method, overriding the inherited method.
It is very common that one class can declare member variables involving other classes, and so it is necessary that the compiler know the other classes while compiling the current class. Therefore, forward declarations of classes should be provided without the complete definition. When a class type is declared with a name followed by a semicolon and without complete definition, as shown here, then it works as a forward declaration. However, the complete class definition should be provided later in the same type declaration section.
type <class1> = class; <class2> = class . . . . . . end; class1 = class . . . . . . end;
Inheritance is the phenomenon by which the descending class acquires the variables and methods of the ancestor class without any extra efforts. This means that the descendent class gets the characteristics of the ancestor class by default. However, it can hide the default behavior by redeclaring one or more of the inherited methods or variables. By doing so, the descendant class has the opportunity to hide the inherited behavior (and display its own behavior), or vice versa. The example shown in Listings 5.4 and 5.5 will make the concept clear. Listing 5.4 is the Cars.pas unit file, which contains a base class and two descendant class definitions. The Car class is defined as the base class, and MyCar and YourCar are defined as the descendant classes. The base class declares the setColor() procedure, which sets the color of the car to GoldenYellow by default, while the MyCar class redeclares the procedure to set the default color as PearlRed, thus hiding the base class procedure. The YourCar class does not redeclare the procedure and hence does not hide the base class implementation. The unit also contains the method getColorString(), which converts the color value to a string for the purpose of displaying to the user. Listing 5.5 is the main program file that uses the Cars.pas unit. In the program, two variables—baseCar1 and baseCar2—are created of type Car base class. At the time of declaring, both the variables are only references. In the execution block, an object of the base Car class is created and assigned to the first variable, and its setColor() method is invoked to set the default color. Then an object of the descendant MyCar class is created and assigned to the second variable. It is always permissible to assign an object of a descendant class to a reference variable of the ancestor class. By doing so, we can access the properties and methods of the descendant class through the base class reference (by properly typecasting) or access the base class properties and methods without typecasting. Listing 5.4 and 5.5 are available on the accompanying CD-ROM.
Listing 5.4: Cars.pas
{This unit defines a base class and two descendant classes} unit Cars; interface {The possible colors in the example are enumerated here} type CarColor = (GoldenYellow, PearlRed, ClassicBeige); {Function to convert a color to corresponding string value} function getColorString(color: CarColor): string; type {The base class} Car = class public color: CarColor; procedure setColor(newColor: CarColor = GoldenYellow); end; {The descendant class MyCar} MyCar = class(Car) public {hides the base class method} procedure setColor(newColor: CarColor = PearlRed); end; {The descendant class YourCar} YourCar = class(Car) public {does not hide the base class method} end; implementation {The function converts the color to color string; and it does not belong to any class} function getColorString(color: CarColor): string; var colorStr: string; begin case color of GoldenYellow: colorStr := 'Golden Yellow'; PearlRed: colorStr := 'Pearl Red'; ClassicBeige: colorStr := 'Classic Beige'; else colorStr := 'Unknown'; end; getColorString := colorStr; end; {The base class Car sets golden yellow color as default} procedure Car.setColor(newColor: CarColor = GoldenYellow); begin color := newColor; end; {The descendant class MyCar sets pearl red color as default} procedure MyCar.setColor(newColor: CarColor = PearlRed); begin color := newColor; end; {The descendant class YourCar does not hide base class method} end.
Listing 5.5: ClassInheritance.dpr
{The program demonstrates how an inherited method is hidden by the re-declared method} program ClassInheritance; {$APPTYPE CONSOLE} uses Cars; {define two variables of base class} var baseCar1, baseCar2: Car; {define two color variables and string variables} var baseColor, descColor: CarColor; begin {Create the base class and invoke its setColor method} baseCar1 := Car.Create; baseCar1.setColor(); baseColor:= baseCar1.color; {display the base car color value} writeln('Car has color ' + getColorString(baseColor)); {Create the first descendant class(MyCar) and invoke its setColor method; the descendant class's method gets invoked, since it hides the inherited method} baseCar2 := MyCar.Create; {the following statement will invoke base class's method} //baseCar2.setColor(); {the following statement will invoke descendant class's method} MyCar(baseCar2).setColor(); descColor:= MyCar(baseCar2).color; {display the MyCar color value} writeln('MyCar has color ' + getColorString(descColor)); {destroy the MyCar object} baseCar2.Destroy; {Create the second descendant class(YourCar) and invoke its setColor method; the base class's method gets invoked, since it is not hidden} baseCar2 := YourCar.Create; YourCar(baseCar2).setColor(); descColor:= YourCar(baseCar2).color; {display the YourCar color value} writeln('YourCar has color ' + getColorString(descColor)); end.
Notice from Listing 5.5 that we have the option of calling either the descendant class method or the base class method, as determined by whether we are typecasting the object reference variable with appropriate class name, as in MyCar(baseCar2).setColor(), where the construct of type <descendant class>(object reference variable) casts the object reference variable to point to the descendant class method. This is because the method is statically bound to the declared class type, and it is the declared class type of the object reference variable that determines which implementation of the method should be invoked, unless we tell the compiler the other way by typecasting.
However, if a method in the base class is declared as virtual (by specifying the virtual directive in the base class method declaration), then it can be overridden in the descendant class (by specifying the override directive in the descendant class method declaration), and therefore the actual runtime type of the class determines which method to be invoked. In this situation, there is no need to typecast the object reference variable because the runtime type of the object to which the reference variable is pointing to determines the method to be called. The unit and the program files in the previous example are modified and presented in Listings 5.6 and 5.7 to demonstrate the concept of overriding the virtual methods. These listings are available on the accompanying CD-ROM.
Listing 5.6: CarsVirtual.pas
{This unit defines a base class and its descendant class} unit CarsVirtual; interface {The possible colors in the example are enumerated here} type CarColor = (GoldenYellow, PearlRed, ClassicBeige); type {The base class} Car = class public color: CarColor; procedure setColor(newColor: CarColor); virtual; end; {The descendant class MyCar} MyCar = class(Car) public {hides the base class method} procedure setColor(newColor: CarColor); override; end; implementation {The base class Car sets golden yellow color as default} procedure Car.setColor(newColor: CarColor); begin writeln('Executing method from base class'); end; {The descendant class MyCar sets pearl red color as default} procedure MyCar.setColor(newColor: CarColor); begin writeln('Executing method from descendant class'); end; end.
Listing 5.7: ClassVirtualMethods.dpr
{The program demonstrates how an inherited method overridden in the descendant class} program ClassVirtualMethods; {$APPTYPE CONSOLE} uses CarsVirtual; var newCar: Car; begin {Create the base class and invoke its setColor method} newCar := Car.Create; newCar.setColor(GoldenYellow); newCar.Destroy; {Create the first descendant class(MyCar) and invoke its setColor method; the descendant class's method gets invoked, since it hides the inherited method} newCar := MyCar.Create; {the following statement will invoke descendant class's method without typecasting} newCar.setColor(PearlRed); end.
A virtual method may also be declared as abstract, which implies that the class in which it is declared abstract does not implement the method. Classes containing at least one abstract method are considered abstract classes, and we cannot instantiate objects from abstract classes. This means that the abstract methods should be implemented in one of the descendant classes in order to enable creation of objects. Similar to standalone procedures and functions, the methods of a class can also be overloaded using the overload directive. By now, the difference between the overload and override directives should be clear to the reader, with the help of the foregoing discussion.
The constructor methods create and instantiate objects from the class definitions. Because the TObject class is the ancestor of all the Delphi classes, it also provides the default constructor method, Create(). However, every class can declare its own constructor method, using declarations similar to the ones shown here.
constructor Create; constructor Create; override;
The constructor creates an object of the class (in which it is implemented) on the heap and returns a reference to the object, although it looks like a procedure. Therefore, the return value of the constructor should be assigned to a variable declared as of the object’s reference or as of reference to one of its ancestor classes. However, the return value should not be specified in the constructor; it is implied. When the constructor is executed, all the ordinal fields are set to zero, pointer and class-type fields are all assigned to nil, and string fields are all made empty before executing any code written in the constructor method. As the next step, the specific initialization of fields and resources as coded in the constructor are performed. Within a descendant class constructor, it is customary to write a statement such as inherited Create; or inherited;, or a similar statement before writing any other statements. The purpose of this statement is to invoke the ancestor class constructor before executing the descendant class constructor.
Similar to the constructor, a Delphi class can have its own destructor method, which will clean up the memory occupied by the object before it goes out of scope. The destructor method is named Destroy in Delphi classes. Destructor methods of base classes can also be overridden in the descendant classes to aid additional resource cleanup tasks. Unlike the case of constructors, the inherited Destroy statement must be executed as the last statement in the destructor of descendant classes. To individually destroy objects created within an object, it is recommended to call the Free method on the objects rather than Destroy because the Free method does not result in error when the object points to nil and only attempts free objects that have associated memory on the heap. The Self reserved word may be used as an identifier in object’s method implementations to refer to the object itself. Two class operators, is and as, are very useful while working with objects. The is operator is used to check whether the object is an instance of the specified class and return True if so, otherwise return False. The general syntax of using the is operator is given here.
if <object> is <class> then . . . else . . . end;
The use of as operator is different; it is used to dynamically typecast an object reference variable of an ancestor class to an object of one of its descendant’s class. Considering the example discussed in the class inheritance demo, code similar to the following may be used if necessary.
with newCar as MyCar do begin . . . . . . end;
In this example, the newCar is an object reference variable of base class Car type. However, if the typecasting fails due to improper use of objects in the class hierarchy, an exception will be thrown.
The concept of class inheritance as has been discussed in the previous subsection supports only single-inheritance, which means that a descendant class can be derived only from one base class. In this respect, Delphi behaves similarly to the Java programming language. Therefore, as one could expect, Delphi supports interfaces. An interface definition looks very similar to the class definition, with a few exceptions; the reserved word class is replaced by the reserved word interface, and all the methods declared in an interface are abstract. This means that none of the methods are implemented within the interface; rather they should be implemented by the descendant classes that are derived from the specific interface, and therefore we cannot instantiate objects from interfaces. As we cannot instantiate objects from interfaces, they do not have constructor and destructor methods. Interfaces only specify what the method signatures are and do not dictate how they should be implemented. For example, if there is an interface called IDraw, then a Circle class, a Rectangle class and a Polygon class (which implement the IDraw interface), all should implement the complete set of methods specified in the IDraw interface, but each of the derived classes could implement the methods in their own way. Conventionally, the interface names start with the letter I, though it is not syntactically wrong to name the interfaces differently. Although a class can inherit from only one base class, it can still implement more than one interface at a time. The general syntax of an interface definition with an example is shown here.
type <interface name> = interface <method declarations> end; type ShapeColor = (white, black, red, yellow, green, blue); type IMyDraw = interface procedure drawOutline(x, y: Integer); procedure fillDrawing(color: ShapeColor); end; type MyShape = class(MyBasicShape, IMyDraw) Procedure setName(name: string); procedure drawOutline(x, y: Integer); procedure fillDrawing(color: ShapeColor); end;
From the example, it can be noticed that the MyShape class is derived from the MyBasicShape class (whose definition is not provided in the example) and the IMyDraw interface.
Very similar to classes, an interface can descend from an ancestor interface definition, in which case we might add some new methods to the descendent interface. There is an additional interesting feature related to the interfaces. Object reference variables that are defined of an interface type can be assigned with objects of instances of classes that implement the interface. Through such object references, the methods that are implemented in the class and are declared in the interface from which the class is derived can be accessed. However, these object references do not permit one to access those implemented methods in the class that are not declared in the interface definition. Consider the sample code using the interface and class definitions of the previous example.
var obj: IMyDraw; // object reference of interface type begin obj := MyShape.Create; // MyShape implements IMyDraw obj.drawOutline(4,5); // permitted obj.setName('MyShape'); // not permitted obj.fillDrawing(red); // permitted end;
Interfaces are very useful in modern distributed computing. Based on the Delphi interfaces, the Kylix C++ also extends the concept of interfaces, as discussed later in the chapter.
Pointers are very useful for accessing variables and objects through their addresses. Variables declared as pointers of specific data types hold addresses of variables or objects of that data type. For example, a pointer variable declared to point to an Integer data type contains the address of an Integer variable; or a pointer variable declared to point to an object of MyShape class (as discussed in the previous example), can hold the address of a MyShape object. Pointer variables are declared by prefixing the ^ symbol before the data type or object type, and the address of a variable or object is obtained by prefixing the @ symbol before the variable or object name, as shown in the few examples here.
var ptr1: ^Integer; var ptr2: ^MyShape; var int1: Integer; var shape: MyShape; begin int1 := 10; ptr1 := @int1; shape := MyShape.Create; ptr2 := @shape; end;
The variable or object pointed to by the pointer variable is obtained by de-referencing the pointer variable by appending the ^ symbol after the variable or object name. For example, ptr2^ represents the object shape from the preceding code sample.
Delphi provides some predefined pointer types such as PChar, PString, PAnsiString, PCurrency, and PVariant, which point to data types Char, String, AnsiString, Currency, and Variant, respectively. The types discussed here are only a few; there are more types available, and they can be found in Delphi documentation.
Every language should have program control constructs to properly drive the logic, based on a set of rules built by the programmer; Delphi is no exception. In fact, Delphi provides a rich set of constructs, as evidenced from the discussion in this subsection. These constructs are discussed sequentially, with appropriate example code. It should be noted that the examples are not necessarily complete programs; rather, they are designed to demonstrate the specific concept being explained.
The statements executed in a Delphi program could be classified as simple statements, compound statements, or structured statements. Each of the simple statements performs one simple task; assignment statements that assign values to variables or calls to procedures or functions are examples of simple statements. Almost all the examples discussed so far have demonstrated simple statements. Compound statements are blocks of code embedded within the begin and end reserved words. Each individual statement within a block ends in a semicolon. Some of the examples discussed so far demonstrated such compound statements. Structured statements are those that include special constructs such as conditional and looping constructs. Compound statements could also contain structured statements.
The with . . . do construct is a structured construct that is used as a simple way to reference the fields of a record or properties and methods of objects without qualifying them with the record or object name. The employee record example provided earlier is rewritten using this construct as shown in Listing 5.8. For convenience, the complete program is provided so that it can be compiled and tested right away. This listing is available on the accompanying CD-ROM.
Listing 5.8: EmployeeDemo.dpr
program Employee; {$APPTYPE CONSOLE} type Employee = record firstname: string; lastname: string; gender: (Male, Female); empdate: string; title: string; basepay: Integer; end; var emp1, emp2: Employee; begin with emp1 do begin firstname := 'Satya'; lastname := 'Kolachina'; gender := Male; empdate := '01-01-2003'; title := 'Manager'; basepay := 90000; writeln(firstname + ' ' + lastname + ' is a ' + title); end; with emp2 do begin firstname := 'James'; lastname := 'Martin'; gender := Male; empdate := '10-01-2002'; title := 'Sr. Manager'; basepay := 95000; writeln(firstname + ' ' + lastname + ' is a ' + title); end; end.
The ‘if … then’ conditional construct evaluates the conditional expression. Only if the condition evaluates to true, the statement following the ‘then’ keyword will be executed. The statement to be executed when the condition is true is either a simple statement or a compound statement block, as shown here in the general syntax.
if <conditional expression> then <simple statement>; if <conditional expression> then begin <compound statement>; end;
The second form of the conditional construct is of the if … then … else … form. In this form, when the conditional expression is evaluated to true, one statement is executed, and if it is false, another statement is executed. The general syntax of this conditional construct is given here. In this construct, either or both of the statement1 and statement2 could be simple statements or compound statements.
if <conditional expression> then <statement1>; else <statement2>;
The relational operators =, <>, <, <=, >, and >= are used respectively for comparison of the first operand to be equal, unequal, less than, less than or equal to, greater than, and greater than or equal to compared to the second operand in the conditional expression. The logical operators and, or, and not can be used with one or more conditional expressions to produce a compound conditional expression, as shown here in the example.
if I <> 10 and A = 5 then <statement>;
The Boolean constants True and False are used to be assigned to Boolean variables or to check their values in a conditional expression. The if constructs can be nested. However, nested if constructs could make the whole conditional construct complex and confusing to the programmers. Therefore, the case statement may be used in place of complex if construct, whose general syntax is provided here in two forms. The two forms are identical in all respects, with the exception that the second form includes an else construct.
case <expression> of <value list1>: <statement1>; <value list2>: <statement2>; . . . . . . <value listN>: <statementN>; end; case <expression> of <value list1>: <statement1>; <value list2>: <statement2>; . . . . . . <value listN>: <statementN>; else <statement>; end;
In both forms of the case construct, the expression is evaluated and then compared to each of the value lists specified after the of keyword. If the value of the expression falls within a particular range of the value list, then the corresponding statement is executed, and then the case construct is exited. If the value of the expression does not fall in any of the value lists, then the statement specified in the else clause is executed if the else clause is provided, otherwise none of the statements is executed. It should be noted that the expression should be evaluated to a numerical (or enumerated) value, and therefore the value lists also should provide numerical or enumerated values. The value lists can include comma-separated values or a range of values specified in the <low value>…<high value> format as in 4..5, or a combination of comma-separated values and a range as in 5, 10..100. Listing 5.9, which is available on the companion CD-ROM, will demonstrate a case construct.
Listing 5.9: CaseDemo.dpr
program CaseDemo; {$APPTYPE CONSOLE} var InpStr: string; var StrSize: Integer; begin writeln('Please enter a string'); readln(InpStr); StrSize := Length(InpStr); case StrSize of 1..5: writeln('You entered too small string'); 6..10: writeln('You entered a medium size string'); 11..20: writeln('You entered a long string'); 21..40: writeln('You entered very long string'); else writeln('Sorry, I cannot process this long string'); end; end.
There are primarily three loop constructs provided in Delphi, as discussed here. The repeat … until … construct in Delphi is similar to the do … while construct found in some other programming languages and is a very useful loop construct. In this construct, the loop is executed at least one iteration as the conditional expression is tested only at the end of an iteration. Another important feature of this construct is that the statements within the loop are executed as long as the conditional expression evaluates to False and exits when it is evaluated to True. The while … do … construct also provides a loop control, but in a different way. Compared with the repeat loop, the while loop evaluates the conditional expression to be True in order for the statements in the loop to be executed and exits the loop when it evaluates to False. In addition, the while loop evaluates the expression before every iteration and hence does not guarantee the execution of first iteration. The third loop construct is the for construct, which executes for a specific number of iterations and is available in two forms. The first form of the loop works with an incrementing loop counter, while the second form works with a decrementing loop counter. The for loop construct is useful to iterate over the number of records in a table or query dataset, or over the members of array variables. The general syntax for all these constructs is provided here.
repeat <statement1>; <statement2>; <statementN>; until <conditional expression>; while <conditional expression> do begin <statement1>; <statement2>; <statementN>; end; for <loop counter := initial value> to <final value> do begin <statement1>; <statement2>; <statementN>; end; for <loop counter := initial value> downto <final value> do begin <statement1>; <statement2>; <statementN>; end;
| < Day Day Up > |
|