Type Declarations

From RAD Studio
Jump to: navigation, search

Go Up to Delphi’s Object Pascal Style Guide

Types are declared in a type section (there are no strict rules in terms of how many type sections there can be in an interface or implementation section of a unit). Type identifiers are listed in one or more lines following the line with the type keyword and indented 2 spaces.

As a general rule, all type name identifiers are prefixed with a capital T, as in TMyType.

There are the following exceptions to this naming rule:

  • Exception classes (derived from the class Exception) start with a capital E letter instead of capital T, as in EMyException
  • Interface types are prefixed with the capital letter I instead of capital T, as in IMyInterface
  • Pointer data types use the capital letter P
  • Custom attribute classes (that is, classes descending from TCustomAttribute), which do not use the initial T and should end with the word Attribute (see the specific section later on)
  • Data structures mapped to platform API (like WinAPI record types used as parameters) should retain their original name in the platform API

Basic Types Declarations

Data types are declared in specific blocks, which can be repeated in a source code file. The type keyword should appear on a specific line, flush to the left, with declarations indented in the following lines. The exception is nested types, which are indented within the type they are part of.

As already indicated, data type names should follow the Pascal Casing rules and start with a letter T (with the specific exceptions discussed). After the type name and on the same line you should have the equal sign with spaces on both sides, followed by the actual type declaration.

These are examples of an enumerated type, a set type, a type alias, and a subrange type:

type
  TFontStyle = (fsBold, fsItalic, fsUnderline, fsStrikeOut);
  TFontStyles = set of TFontStyle;
  TFontName = type string;
  TFontCharset = 0..255;

Other specific examples of more complex type declarations are listed in the following sections.

Special Rules for Enumerated Types

There is one exception for the naming rule (use Pascal casing) for the constant values of an old-style enumeration (before scoped enumerations were introduced). For the values, it was recommended to use value with the enumeration type name as a prefix - sort of an Hungarian exception. For example, FontStyle values would start with fs and ButtonKind values with bk. The reason lies in the fact that these constant values are global and a prefix helps reduce the risk of a name clash. Example:

type
  TSeekOrigin = (soBeginning, soCurrent, soEnd);
  TBitBtnKind = (bkCustom, bkOK, bkCancel, bkHelp, 
    bkYes, bkNo, bkClose, bkAbort, bkRetry, bkIgnore, bkAll);

For scoped enumerations, given they are used with the type as a prefix, use regular Pascal Casing notation with no prefix, for example:

type
  TOSPlatform = (Windows, OSX, iOS, Android, Linux);
  TSide = (Top, Left, Bottom, Right);

In general, we recommend using scoped enumerations over the traditional syntax. However, when working on existing libraries that use traditional enumerations (like VCL) it is a good practice to follow the library standard approach.

Array declarations

There should not be a space before the opening bracket "[" or within the square brackets:

type
  TMyArray = array[0..100] of Char;

Similarly, a space is not present before the open square bracket when using an array variable.

Array1[20] := 22;
Note: Currently in the core Delphi libraries the rule about not having a space before the open square bracket is not applied consistently (for example in SysUtils or FMX.Types, but it is the rule in Generics.Collections).

Function pointer types

Function pointer type declarations follow the general rules for procedure and functions declarations. Not having a name, the keyword is followed by the open parentheses with the parameters list (if needed) without a space.

Note: Currently in the core Delphi libraries the rule about not having a space before the open parentheses is not applied consistently.

Example:

type
  TListSortCompare = function(Item1, Item2: Pointer): Integer;

Class (and Record) Declarations

Following the general rules, a class declaration has an identifier prefaced by a capital T and using Pascal Casing, followed by a space, an equals sign, and the lowercase word class.

Rules for records (which can have methods and a subset of the access specifiers) are similar and will be highlighted if relevant.

If you want to specify the ancestor for a class, add a parenthesis (with no space), the name of the ancestor class, and the closing parenthesis:

type
  TMyClass = class(TObject)

Access specifier (or scoping) directives should use the same indentation of the class name they belong to), and declared in the order shown in this example:

type
  TMyClass = class(TObject)
  strict private
  private
  strict protected
  protected
  public
  published
  end;

There are six access levels for class members in Object Pascal: published, public, protected, strict protected, private, and strict private -- in order of decreasing accessibility.

Note: Nested classes and nested types in general) follow the same rules of type declaration, and further are indented from the type they declared to be nested in.

Class Body Organization

The body of a class declaration should be organized in the following order, for each of the access specifier blocks:

  • Field declarations
  • Method declarations
  • Property declarations

In the original style guide, there was a requirement that “fields, properties, and methods in your class should be arranged alphabetically by name” in the respective section. This is often the case in the RTL and VCL libraries and it remains the recommended approach, but it is considered acceptable to keep related fields and methods together. For fields, the position can also affect the memory layout. So we are dropping the alphabetic order from the requirement, but if an existing class has alphabetically sorted members it is recommended to add new members respecting that arrangement.

Field and Property Naming

Fields (member variables) use Pascal Casing and begin each type declaration with a capital F. Fields are generally not accessible to external classes. For records, fields might be public, and in this case, the initial F is omitted.

By contrast, properties use Pascal Casing with no prefix. Name methods for accessing properties (if needed) with an initial Get or Set word followed by the property name (with no underscore or other special elements). We also recommend to avoid writing a setter and getter method when this has no additional code than what’s needed to write or read a local field.

An example of fields and properties:

type
  TMyClass = class(TObject)
  private
    FMyData: Integer;
    procedure SetData(Value: Integer);
  published
    property MyData: Integer read FData write SetData;
  end;

Method Naming

Method names should use the regular rules for naming and follow the rules for procedures and functions listed earlier. Method names should be imperative verbs or verb phrases.

Examples:

// GOOD method names:
ShowStatus, DrawCircle, AddLayoutComponent

// BAD method names:
MouseButton // noun phrase; doesn't describe function
drawCircle // starts with lower-case letter
Add_layout_component // uses underscores
ServerRunning     // verb phrase, but not imperative (use IsServerRunning or StartServer)

As a general recommendation, a method to test some boolean property or condition of the class may start with Is and use the property of value name (IsVisible), start with Has for a generic condition (HasParent), or use Can for a feature (CanPaint). This is a general recommendation that needs to be adapted to the specific case.

If possible, a method declaration should appear on one line. A broken line is aligned two spaces in from the left, like for procedures and functions.

Event naming

Event handlers' names start with the word On and are directly mapped (with no setter and getter method) to a field having the same name (for example, OnEvent property, FOnEvent field). Calls that trigger the event execution should be wrapped in a method starting with Do and followed by the event name (as in DoEvent).

Constructor declarations and C++ compatibility

Place your constructors and destructors at the head of the list of methods in the public section (or the section they are part of).

If there is more than one constructor, and if you choose to give them all the same name, then sort them lexically by formal parameter list, with constructors having more parameters always coming after those with fewer parameters. This implies that a constructor with no arguments (if it exists) is always the first one.

Note: For greatest compatibility with C++Builder, try to make the parameter lists of your constructors unique by types, even when the constructors have different names. C++ cannot call constructors by name, so the only way to distinguish between multiple constructors is by the types in the parameter list.

The following constructors cannot be distinguished as different by C++ because both contain a single Integer parameter, and will be an error when HPP generation is turned on:

constructor Create(AValue : Integer);
constructor CreateWithWidth(AWidth : Integer);

Instead, use different types (preferred) or add a dummy parameter to distinguish the parameter list:

constructor Create(AValue : Integer);
constructor CreateWithWidth(AWidth : Integer; Dummy : Boolean);

Event handler types

Event handler types follow the same rules of function pointer type declarations (which in turn borrow from procedure and function declarations). As a rule, there is no space between the procedure and function keyword and the open parentheses. Examples:

type
  TNotifyEvent = procedure(Sender: TObject) of object;
  TGetStrProc = procedure(const S: string) of object;
Note: Currently in the core Delphi libraries the rule about not having a space before the open parentheses is not applied consistently.

Type helpers

Record and class helpers follow the same rules of records and classes. The name of the identifier should start with the letter T as any type, refer to the type it helps, and end with the word Helper. Example:

type
  TUInt32Helper = record helper for UInt32 ...
  TSideHelper = record helper for TSide ...

Interfaces

Interfaces are declared very similarly to classes. Differently from other types, interfaces don’t use the letter T as prefix, but the letter I instead.

Following general rules, an interface declaration should be indented two spaces. The body of the interface is indented by the standard indentation of two further spaces. The closing end keyword should also be indented two spaces, the same indentation as the interface type name. There should be a semi-colon following the closing end keyword.

All interface methods are inherently public and abstract; do not explicitly include these keywords in the declaration of an interface method.

Except as otherwise noted, interface declarations follow the same style guidelines as classes.

The body of an interface declaration should be organized in the following order:

  • Interface method declarations
  • Interface property declarations

The declaration styles of interface properties and methods are identical to the styles for class properties and methods. The GUID part of the interface declaration is indented as the body of the interface:

type
  IStreamPersist = interface
    ['{B8CD12A3-267A-11D4-83DA-00C04F60B2DD}']
    procedure LoadFromStream(Stream: TStream);
    procedure SaveToStream(Stream: TStream);
  end;

As for classes implementing multiple interfaces, in the case of a long list use the regular rules for line continuation, going to a new line, and indenting two additional spaces:

type
  TMyObject = class(TInterfacedObject, ISerializable, IObservable, 
    IChangeListener, IChangeable)
  private
    ...

Generic Types Declarations

Generic data types and generic methods within non-generic types are written following the same general rules and records, classes, interfaces, or methods. The angle brackets containing the generic types are not preceded or followed by extra spaces (on either side). If multiple generic types appear in a declaration, they are separated by a comma and a space.

Note: Currently in the core Delphi libraries the rule about having a space before the open-angle bracket for generic type is not applied consistently. Also, the use of a space after the comma separating multiple typos is not used consistently.

The placeholder for a generic data type is generally indicated by the letter stand-alone letter T. When multiple generic type placeholders are present, it is recommended to give the generic type placeholders a more descriptive name, like the type placeholders TKey and TValue in TDictionay<T>, rather than using the following letters of the alphabet: T, U, V.

Examples:

type
  TDictionary<TKey, TValue> = class
  TList<T> = class(TEnumerable<T>)
  Tuple<T,U>

Anonymous Method Types

Anonymous method type declarations follow the same rules of function and procedure declarations and of function pointers declarations but add some specific rules mostly related to the call site.

In the type declaration, as for any function pointer or reference type, there is no space between the procedure or function keyword and the open parenthesis indicating the parameters and their types, as in:

type
  TListSortCompareFunc = reference to function(Item1, Item2: Pointer): Integer;
  TInterceptBeforeNotify = reference to procedure(Instance: TObject;
    Method: TRttiMethod; const Args: TArray<TValue>; out DoInvoke: Boolean;
    out Result: TValue);
Note: Currently in the core Delphi libraries the rule about not having a space before the open parentheses is not applied consistently. The examples above are different in the actual code.

When you assign or pass a value to an anonymous method, you can write the function or procedure code directly, in place. In this case, the function or procedure code should be written following the standard rules and respecting the indentation level of the call site. Begin and end keywords need to be on separate lines; the end statement can be followed immediately by a comma, when additional parameters follow, by the closing parenthesis, or by a semicolon in case of an assignment of the anonymous method.

Examples of correct and incorrect anonymous methods invocations (from the actual Delphi libraries code, another rule not applied consistently):

  // correct
  ForEachOutputParameter(ProxyMethod,
    procedure(Param: TDSProxyParameter)
    begin
      if Param.ParameterDirection = TDBXParameterDirections.ReturnParameter then
        R := Param;
    end);

  // incorrect, as begin is on the same line as procedure  
  TThread.Queue(nil, procedure begin
    FRootNode.Delete;
    FRootNode := nil;
  end);

  // incorrect, writing an anonymous method fully inlined is an error
  TThread.Synchronize(nil, procedure begin FCallback(FQueueName, Success, FForceExpand); end);

Attributes

Custom attribute class names do not use the initial T for types and end with the word Attribute. This final word can be omitted when referring to the attribute type between square brackets.

Custom attributes appear in code before the symbol they refer to within square brackets and with no extra spaces inside the square brackets. There are two scenarios (not followed consistently in the Delphi libraries code):

  • For types and methods, they should be placed on a separate line preceding the symbol they refer to
  • For fields and parameters, they should be inline and separated with a space from the symbol they refer to (the exception being a very long attribute, which can be placed on its own line before the symbol).

Parameters of custom attributes follow the general rules of functions parameters.

Examples:

type
  WeakAttribute = class(TCustomAttribute)
  UnsafeAttribute = class(TCustomAttribute)
  AlignAttribute = class(TCustomAttribute)

  TAggregatedObject = class(TObject)
  private
    [Unsafe] FController: IInterface;

  [HPPGEN(HPPGenAttribute.mkFriend, 'DELPHICLASS TList__1<T>')]
  TListHelper = record