I really like managed records, because they allow to avoid the cumbersome try-finally-free constructs always present. For example, just reading something from a file requires the following with TFileStream:
fs := TFileStream.Create(FileName, fmOpenRead);
try
str := fs.ReadAnsiString;
finally
fs.Free;
end;
Of those 6 lines, 4 (i.e. 1/3) are just dedicated to the obligatory closing of the file without giving any additional information to the programmer. Basically this is just boilerplate code that makes the actualy functionality less readable.
You can use a managed record instead, which uses the Finalize Operator to close the file:
type
TFileStreamRec = record
FFile: THandle;
class operator Finalize(var rec: TFileStreamRec); // Closes the file
function Open(FileName: String); // Opens the file
function ReadString: String;
end;
[...]
// Usage
fs.Open(FileName);
str := fs.ReadString;
This is much easier to use and results in much smaller code with much less clutter (Just 2 meaningful lines, instead of 6 with 4 meaningless lines).
But the problem here is that Records do not support inheritance, and the power of TFileStream is exactly that. For example many classes like TStrings have a LoadFromStream method, where you can pass a any kind of stream, this allows to read data from a Network connection (TSocketStream), from memory (TMemoryStream) or from a file (TFileStream).
Oldschool objects which are located on the stack allow for inheritance, but don't allow for management operators.
But it is possible with a hack to also get managemant operators from Managed Records to also work in an Object. When an object has a managed record as member, then the compiler will call the records management operators when the objects gets initialized, finalized or copied.
Simultaniously, the first member of an object will be at offset 0 of the objects memory, meaning that the pointer of that field is equivalent to the pointer of the object. This allows to simply call functions on an object from that record.
With this I've built a small proof of concept. It tries to emulate the TStream inheritance hierachy to show that this actually works as with TFileStream shown above:
program project1;
{$mode objfpc}{$H+}
{$ModeSwitch advancedrecords}
uses
SysUtils;
type
generic TAutoDone<T> = record
class operator Finalize(var rec: specialize TAutoDone<T>);
end;
class operator TAutoDone.Finalize(var rec: specialize TAutoDone<T>);
type
PT = ^T;
begin
PT(@Rec)^.Done;
end;
type
TStream = object
private
AutoDone: specialize TAutoDone<TStream>;
public
function Read(var Buff; Size: SizeInt): SizeInt; virtual; abstract;
function Write(const Buff; Size: SizeInt): SizeInt; virtual; abstract;
function ReadLn: String;
procedure WriteLn(s: String);
destructor Done; virtual;
end;
function TStream.ReadLn: String;
var
c: Char;
begin
Result := '';
repeat
if Read(c, 1) <> 1 then
Exit;
Result += c;
until c = #10;
Result := Result.Trim;
end;
procedure TStream.WriteLn(s: String);
begin
s += LineEnding;
Write(s[1], s.Length);
end;
destructor TStream.Done;
begin
// To be overriden
end;
type
TIOStream = object(TStream)
private
FInFile: THandle;
FOutFile: THandle;
public
function Read(var Buff; Size: SizeInt): SizeInt; virtual;
function Write(const Buff; Size: SizeInt): SizeInt; virtual;
constructor Init(InHandle, OutHandle: THandle);
end;
function TIOStream.Read(var Buff; Size: SizeInt): SizeInt;
begin
Result := FileRead(FInFile, Buff, Size);
end;
function TIOStream.Write(const Buff; Size: SizeInt): SizeInt;
begin
Result := FileWrite(FOutFile, Buff, Size);
end;
constructor TIOStream.Init(InHandle, OutHandle: THandle);
begin
FInFile := InHandle;
FOutFile := OutHandle;
end;
type
TConsoleStream = object(TIOStream)
public
constructor Init;
end;
constructor TConsoleStream.Init;
begin
inherited Init(StdInputHandle, StdOutputHandle);
end;
type
TFileStream = object(TIOStream)
private
FFile: THandle;
public
constructor Open(const FileName: String);
destructor Done; virtual;
end;
constructor TFileStream.Open(const FileName: String);
begin
if not FileExists(FileName) then
FFile := FileCreate(FileName)
else
FFile := FileOpen(FileName, fmOpenReadWrite);
FileSeek(FFile, 0, 0);
inherited Init(FFile, FFile);
end;
destructor TFileStream.Done;
begin
FileClose(FFile);
end;
procedure TestConsoleIO;
var
stream: TConsoleStream;
s: String;
begin
stream.Init;
s := Stream.ReadLn;
stream.WriteLn('Hello: ' + s);
end;
procedure TestFileWrite;
var
fs: TFileStream;
begin
fs.Open('test.txt');
fs.WriteLn('Test Line');
end;
procedure TestFileRead;
var
fs: TFileStream;
begin
fs.Open('test.txt');
WriteLn(fs.ReadLn);
end;
begin
TestConsoleIO;
TestFileRead;
TestFileWrite;
end.
There are a few limitations. First the AutoDone must always be the first field, so it must be defined in the base class. But this can be handled by having a virtual destructor. This trick does not work for an analogous AutoInit, because the VMT is constructed by the constructor and so no virtual methods can be used.
Note that TAutoDone has no members, therefore it's size in the TStream is 0. This means that other records that use the same pointer casting trick to get their parents pointer, can be appended afterwards (e.g. to handle the Copy or AddRef operators).
This is a stupid hack and probably not intended behavior. But I just wanted to put this out there because I find this interesting, because I defenetly believe that:
fs.Open('test.txt');
WriteLn(fs.ReadLn);
Is much more readable than:
fs := TFileStream.Create
try
WriteLn(fs.ReadLn);
finally
fs.Free;
end;