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:
async
deprecated
empty
external
implements
ISomeInterface.SomeMember
(See Explicit Interface Implementations)inline
iterator
(See Iterators)locked
locked on
Expression
mapped to
(See Mapped Members)optional
(Interface members only)partial
(See Partial Types)raises
unsafe
(See Unsafe Code on .NET)where
(see Generic Methods, above)
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.