Methods

Methods are the building block of any program, as they contain what we commonly think of as "code": the actual logic that makes up the functionality of a class.

Simply put, a method is a block of code that can be called from elsewhere within the project to perform its function.

Methods are Object Oriented Programming's logical replacement for the plain "procedures" and "functions" defined by basic Pascal. Because methods are such an important part of OOP languages, and because what we call something defines how we think of it, Oxygene introduced the new method keyword back in 2004 to define methods.

Note: many of the details about methods covered in this topic also apply to other "method-like" Members, including Constructors, Finalizers, Custom Operators and Property getters/setters.

This includes the sections on Parameters, Method Result, Method Body, Pre- and Post-Conditions and Generics, below.

Method Declaration Syntax

In its simplest form, a method declaration consists of the method keyword, followed by a method name and an optional list of parameters that can be passed to the method, in parenthesis. Methods can also have an optional result type, separated from the rest of the method with a colon.

The method declaration can optionally be followed by a number of Modifiers, separated by semicolons. The most important modifiers will be covered in more detail, below.

method MyMethodName(aParameter: String; aNotherParameter: Integer): Boolean; virtual;

Method Implementation Syntax

Unless it is empty, abstract or external (more on those below), each method declaration needs to provide an implementation. The implantation can be provided right behind the declaration, using what Oxygene refers to as Unified Class Syntax, or it can be provided below the type declaration in the implementation section of the file.

If the implementation is provided separarely, the method declaration (minus any modifiers) is repeated, but expanded to include the type name in front of the method name:

method MyClass.MyMethodName(aParameter: String; aNotherParameter: Integer): Boolean;
begin
  // code goes here
end;

The method body must consist of at least a begin/end block that can contain the zero or more Statements that make up the method body. There can also optionally be a require block at the beginning and an ensure block at the end, both containing pre- and post-conditions to be checked before the method starts and after it ends. Pre- and post-conditions are covered in more detail, below.

For compatibility with legacy Pascal compilers such as Delphi, methods can also have an optional var section at the start, where variables for use throughout the method can be declared. Use of a var section is discouraged in favor of using var Statements in the main body of the method body, as needed.

method MyClass.DoSomeWork(aData: String): String;
var
  lCount: Integer;          // not recommended
begin
  lCount := aData.Length;
  //...
end;

Parameters

A method can take zero or more parameters, listed after the method name, and enclosed in parenthesis. Individual parameters are separated by semicolons, and each parameter must at the very least consist of a name and a type, separated by a colon.

Unlike other languages, including C# and Swift, an empty set of parenthesis is optional (and discouraged) for declaring methods that take no parameters.

method DoSomething(aParameter: String);
method DoSomethingElse;

Optionally, a default value can be provided via the := operator. If a default is provided, callers can omit the parameter in question, and the default will be used instead:

method DoSomething(aParameter: String := 'Use this by default.');

'out' and 'var' Parameters

Parameters can also have a modifier of either var or out. By default, parameters are passed into the method, but not back out. Inside the method, the parameter becomes a local variable, and any changes made to it inside the method are not propagated to the caller.

Parameters declared with var will be passed into the method, and any changes made by the method will be passed back out when the method exits. Parameters declared with out will start off as having their default() value when the method starts, and any changes made to them will then be passed out when the method is finished.

method GetValues(var a: Integer; out b: Integer);
begin
  a := a + 15;
  b := 10;
end;

Note that when passing heap-based Reference Types such as Classes, the var and out modifiers apply to the reference, and not the data stored within the type. Changes made to a class passed in as regular "in-wards" parameter will still affect the caller, as it is holding as reference to the same class. Changing the parameter to a different instance will not propagate outwards. When a reference type is passed via var, changes to the reference itself will propagate back, and the caller will have a reference to the new object.

method GetValues(a: SomeClass; var b: SomeClass);
begin
  a.SomeValue := a.SomeValue+15; // affects the instance that the caller passed in
  a := new SomeClass();          // this change will not affewct ehe caller
  b := new SomeClass();          // this change will affect ehe caller
end;

GetValues(x, y);
// x is changed, but still the same class. y is now a new class

'const var' Parameters

Parameters can optionally be marked as const var. If marked as such, they will be passed in the same fashion as var parameters, discussed above, but cannot be modified within the method.

This option is mainly used for performance reasons, allowing for example a large struct to be passed without causing a second copy to be created on the stack, while still preventing changes to the original value from the caller's side.

'params' Method Parameters

The optional params keyword can be used on the last parameter to capture all extra parameters in a single array. To use params, this parameter has to be a simple Dynamic Array.

method Write(a: String; params args: array of String);
begin 
end;

...
Write('hello', 'this', 'world'); // a is 'hello', args is ['this', 'world']
Write('hello', 'world'); // a is 'hello', args is ['world']
Write('hello'); // a is 'hello', args is [] (empty array)

Method Result

If the method is defined to return a value, an implicit result Variable is available in scope, within the method body. This variable is of the same type as the method's return type, and initialized to the default() value of the type (typically 0 or nil).

The result variable can be both written and read, allowing you to use it to store an incremental status and be updated during the course of the method's execution. This is a significant improvement over other languages, including C, C# or Swift, where a return value can only be set in the same step as the method is exited.

Of course the exit Statement can still exit the method and return a value in one go at any time. If exit is called with a value, that value is used as result. If it is called without value, any previously set value in result will be used, just as if the method terminated by reaching its end.

Multi-Part Method Names

In order to fit in well with the API conventions on the Cocoa platform, Oxygene has support for multi-part method names.

Multi-part method names are essentially the ability for a method's name to be split into separate parts, each followed by a distinct parameter. This is "required" on the Cocoa platform because all the platform's APIs follow this convention, and we wanted Oxygene to be able to both consume and implement methods alongside those conventions without resorting to awkward attributes or other adornments, and to feel at home on the Cocoa platform.

A multi-part method has parameter parts for each part, both when being declared:

method RunCommand(aString: String) Arguments(params aArguments: array of String): Boolean;

...and when being called:

myClass.RunCommand('ebuild') Arguments('MyProject.sln', '--configuration:Debug');

While the feature was created for Cocoa, multi-part method names are supported on all platforms, and considered a regular feature of the Oxygene language (and also compatible with C#, Swift and Java). We encourage to use and embrace them, as they are a great tool to make code more readable and understandable.

Method Body

The method body is what makes up the bulk of a method, and provides its implementation. It starts with the begin keyword and ends with a matching end or – id a post-condition is provided – ensure keyword.

The method body is made up of xero or more Statements, separated by semicolons (and, traditionally but optionally, a line break). Most (mut not all) Expressions can also be used as standalone statements. When doing so, the value of the expression is simply ignored.

Statements can be simple one-line constructs (such as a Method Call or a var Declaration, or they can be more complex structures that might even contain their own sub-list of statements (such as a repeat/until Block.

Pre- and Post-Conditions

Methods can provide optional pre-conditions that will be checked before the main mody of the method starts executing, or post-conditions that will be checked after the method exits.

Both pre-conditions and post-conditions consist of a list of Boolean expressions, separated by semicolons. Each condition will be evaluated in row, and if any of them evaluates to false, execution will be aborted with a fatal Assertion.

Pre-conditions are provided before the method body, in an optional require section. Post-conditions are provided after the method body, in an optional ensure section:

method MyClass.IncrementCount(aBy: Int32);
require
  aBy > 0;
begin
  fCount := fCount + aBy
ensure
  fCount - aBy = old fCount;
end;

In the ensure section, the old Operator can be used to refer to the original value a parameter or field had before the method started. This can be useful for checking the validity od the result compared to previous state. Note that for heap-based Reference Types (except Strings such as Classes, which receive special handling), the old operator only captures the old reference, but not the old state of the object it contains.

In both the require and ensure section, an optional more detailed error message can be provided, to be included in the assertion when a check fails. This must be in the form of a constant String Literal, separated from the expression with a colon. (If the expression itself contains a colon, the whole expression needs to be wrapped in parenthesis).

method MyClass.IncrementCount();
require
  (SomeValue:SomeField = 0) : "SomeField may not be zero!";
begin
  ...

Note that pre- and post-conditions will only be compiled and executed if the "Enable Asserts" Compiler Option is on. By default, this option is off for Release builds, to optimize performance. It is important to not rely on pre- and-postconditions to execute for the regular operation of the project, and to avoid conditions with side effects.

Pre- and post-conditions should be used only for testing, not for general code flow.

Generic Methods

Similar to Generic Types, individual methods can be declared to have one or more generic parameters. The type parameters are provided enclosed in angle brackets after the method name. These types are then used as placeholders for a more concrete type, allowing you to define methods that are, well, generic, and not tied down to working with a specific type as parameter or result.

method ArrayHelpers.FindIndexInArray<T>(list: array of T; item: T): Integer;
begin
  for i: Integer := 0 to list.Length - 1 do begin
    if list[i] = item then
      exit i;
  exit -1;
end;

For example, the FindIndexInArray<T> method above can work with any type of array to find an item's index.

ArrayHelpers.FindIndexInArray<Int>([1,2,3], 2);
ArrayHelpers.FindIndexInArray<String>(["One","Two","Three"], "Four");

Optional Generic Gonstraints can be provided, using the where Member Modifier. Co- and contra-variance does not apply to generic methods.

method ArrayHelpers.SomeMethod<in T>(aSomeParaneter T); where T has constructor;
begin
  ...

Please refer to the Generic Types topic for a more detailed discussion on generics, including many details that apply to generics in members, as well.

Static Methods

Like most type members, methods are by default defined on the instance – that means the method can be called on and will execute in the context of an instance of the class. A method can be marked as static by prefixing the method declaration with the class keyword, or by applying the static Member Modifier:

class method MyClassMethod: String;        // static method on the class itself
method MyOtherClassMethod: String; static; // static method on the class itself

Visibility

The visibility of methods is governed by the Visibility Section of the containing type the method is declared in, or the Visibility Modifiers applied to the method.

Virtuality

The Virtuality of methods can be controlled by applying one of the Virtuality Member Modifiers.

method OverrideMe; virtual;

Other Modifiers

A number of other Member Modifiers can be applied to methods:

The Legacy procedure and function keywords

For compatibility with legacy Pascal languages, the procedure and function keywords can be used instead of method, when Delphi Compatibility is turned on. Methods declared with function must have a result type, while those declared with procedure may not.

We discourage the use of procedure and function and strongly encourage you to embrace the method keyword.

See Also