You can encapsulate what you desire in a generic type. Like this:
type
TFixedLengthArray<T> = record
strict private
FItems: TArray<T>;
FLength: Integer;
function GetItem(Index: Integer): T; inline;
procedure SetItem(Index: Integer; const Value: T); inline;
public
property Length: Integer read FLength;
property Items[Index: Integer]: T read GetItem write SetItem; default;
class function New(const Values: array of T): TFixedLengthArray<T>; static;
end;
{ TFixedLengthArray<T> }
class function TFixedLengthArray<T>.New(const Values: array of T): TFixedLengthArray<T>;
var
i: Integer;
begin
Result.FLength := System.Length(Values);
SetLength(Result.FItems, Result.FLength);
for i := 0 to Result.FLength-1 do begin
Result.FItems[i] := Values[i];
end;
end;
function TFixedLengthArray<T>.GetItem(Index: Integer): T;
begin
Result := FItems[Index];
end;
procedure TFixedLengthArray<T>.SetItem(Index: Integer; const Value: T);
begin
FItems[Index] := Value;
end;
Create a new one like this:
var
MyArray: TFixedLengthArray<Integer>;
....
MyArray: TFixedLengthArray<Integer>.New([1, 42, 666]);
Access items like this:
for i := 0 to MyArray.Length-1 do
Writeln(MyArray[i]);
This just wraps a dynamic array. Elements are contiguous. The length of the array is determined once and for all then a new instance is created.
One thing to watch out for here is that the type will behave like a reference type since its data is stored in a reference type. That is, the assignment operator on this type will behave in the same manner as dynamic array assignment.
So if we have two variables of this type, arr1 and arr2 then the following occurs:
arr1 := arr2;
arr1[0] := 42;
Assert(arr2[0] = 42);
If you wanted to make the type behave like a true value then you would implement copy-on-write inside SetItem.
Update
Your edit to the question changes is significantly. It seems that you are in fact concerned more with performance than encapsulation.
The inlining of the item accessor methods in the above type means that the performance characteristics should be close to that of an array. The access will still be O(1), but it is quite plausible that the inliner/optimiser is weak and fails to emit the most optimal code.
Before you decide that you must use arrays to obtain the absolute ultimate performance, do some real world benchmarking. It seems to me to be quite unlikely that the code to read/write from an array is really a bottleneck. Most likely the bottleneck will be what you then do with the values in the array.