Sunday, July 5, 2020

Modern Object Pascal Introduction for Programmers

6.1. Input/output using streams

Modern programs should use TStream class and it’s many descendants to do input / output. It has many useful descendants, like TFileStream, TMemoryStream, TStringStream.

{$mode delphi}
uses SysUtils, Classes;
var
  S: TStream;
  InputInt, OutputInt: Integer;
begin
  InputInt := 666;

  S := TFileStream.Create('my_binary_file.data', fmCreate);
  try
    S.WriteBuffer(InputInt, SizeOf(InputInt));
  finally FreeAndNil(S) end;

  S := TFileStream.Create('my_binary_file.data', fmOpenRead);
  try
    S.ReadBuffer(OutputInt, SizeOf(OutputInt));
  finally FreeAndNil(S) end;

  Writeln('Read from file got integer: ', OutputInt);
end.

In the Castle Game Engine: You should use the Download method to create a stream that operates of resources (which includes files, data downloaded from URLs and Android assets). Moreover, to open the resource inside your game data (typically in data subdirectory) use the ApplicationData function.

EnableNetwork := true;
S := Download('https://castle-engine.io/latest.zip');
S := Download('file:///home/michalis/my_binary_file.data');
S := Download(ApplicationData('gui/my_image.png'));

To read text files, we advice using the TTextReader class. It provides a line-oriented API, and wraps a TStream inside. The TTextReader constructor can take a ready URL, or you can pass there your custom TStream source.

Text := TTextReader.Create(ApplicationData('my_data.txt'));
while not Text.Eof do
  WritelnLog('NextLine', Text.ReadLine);

6.2. Containers (lists, dictionaries) using generics

The language and run-time library offer various flexible containers. There are a number of non-generic classes (like TList and TObjectList from the Contnrs unit), there are also dynamic arrays (array of TMyType). But to get the most flexibility and type-safety, I advice using generic containers for most of your needs.

The generic containers give you a lot of helpful methods to add, remove, iterate, search, sort…​ The compiler also knows (and checks) that the container holds only items of the appropriate type.

There are three libraries providing generics containers in FPC now:

We advice using the FGL unit (if you need to work with the stable FPC 3.0.x or even older FPC 2.6.x), or the Generics.Collections unit (only available since FPC 3.1.1, but on the other hand compatible with Delphi). They both offer lists and dictionaries with naming consistent with other parts of the standard library (like the non-generic containers from the Contnrs unit).

In the Castle Game Engine: We include a local copy of Generics.Collections even for FPC 3.0.x. We use the Generics.Collections throughout the engine, and advice you to use Generics.Collections too!

Most important classes from the Generics.Collections unit are:

TList

A generic list of types.

TObjectList

A generic list of object instances. It can "own" children, which means that it will free them automatically.

TDictionary

A generic dictionary.

TObjectDictionary

A generic dictionary, that can "own" the keys and/or values.

Here’s how to you use a simple generic TObjectList:

{$mode delphi}
uses SysUtils, Generics.Collections;

type
  TApple = class
    Name: string;
  end;

  TAppleList = TObjectList<TApple>;

var
  A: TApple;
  Apples: TAppleList;
begin
  Apples := TAppleList.Create(true);
  try
    A := TApple.Create;
    A.Name := 'my apple';
    Apples.Add(A);

    A := TApple.Create;
    A.Name := 'another apple';
    Apples.Add(A);

    Writeln('Count: ', Apples.Count);
    Writeln(Apples[0].Name);
    Writeln(Apples[1].Name);
  finally FreeAndNil(Apples) end;
end.

Note that some operations require comparing two items, like sorting and searching (e.g. by Sort and IndexOf methods). The Generics.Collections containers use for this a comparer. The default comparer is reasonable for all types, even for records (in which case it compares memory contents, which is a reasonable default at least for searching using IndexOf).

When sorting the list you can provide a custom comparer as a parameter. The comparer is a class implementing the IComparer interface. In practice, you usually define the appropriate callback, and use TComparer<T>.Construct method to wrap this callback into an IComparer instance. An example of doing this is below:

{$mode delphi}
uses SysUtils, Generics.Defaults, Generics.Collections;

type
  TApple = class
    Name: string;
  end;

  TAppleList = TObjectList<TApple>;

function CompareApples(constref Left, Right: TApple): Integer;
begin
  Result := AnsiCompareStr(Left.Name, Right.Name);
end;

type
  TAppleComparer = TComparer<TApple>;
var
  A: TApple;
  L: TAppleList;
begin
  L := TAppleList.Create(true);
  try
    A := TApple.Create;
    A.Name := '11';
    L.Add(A);

    A := TApple.Create;
    A.Name := '33';
    L.Add(A);

    A := TApple.Create;
    A.Name := '22';
    L.Add(A);

    L.Sort(TAppleComparer.Construct(@CompareApples));

    Writeln('Count: ', L.Count);
    Writeln(L[0].Name);
    Writeln(L[1].Name);
    Writeln(L[2].Name);
  finally FreeAndNil(L) end;
end.

The TDictionary class implements a dictionary, also known as a map (key → value), also known as an associative array. It’s API is a bit similar to the C# TDictionary class. It has useful iterators for keys, values, and pairs of key→value.

An example code using a dictionary:

{$mode delphi}
uses SysUtils, Generics.Collections;

type
  TApple = class
    Name: string;
  end;

  TAppleDictionary = TDictionary<string, TApple>;

var
  Apples: TAppleDictionary;
  A, FoundA: TApple;
  ApplePair: TAppleDictionary.TDictionaryPair;
  AppleKey: string;
begin
  Apples := TAppleDictionary.Create;
  try
    A := TApple.Create;
    A.Name := 'my apple';
    Apples.AddOrSetValue('apple key 1', A);

    if Apples.TryGetValue('apple key 1', FoundA) then
      Writeln('Found apple under key "apple key 1" with name: ' +
        FoundA.Name);

    for AppleKey in Apples.Keys do
      Writeln('Found apple key: ' + AppleKey);
    for A in Apples.Values do
      Writeln('Found apple value: ' + A.Name);
    for ApplePair in Apples do
      Writeln('Found apple key->value: ' +
        ApplePair.Key + '->' + ApplePair.Value.Name);

    
    

    Apples.Remove('apple key 1');

    
    A.Free;
  finally FreeAndNil(Apples) end;
end.

The TObjectDictionary can additionally own the dictionary keys and/or values, which means that they will be automatically freed. Be careful to only own keys and/or values if they are object instances. If you set to "owned" some other type, like an Integer (for example, if your keys are Integer, and you include doOwnsKeys), you will get a nasty crash when the code executes.

An example code using the TObjectDictionary is below. Compile this example with memory leak detection, like fpc -gl -gh generics_object_dictionary.lpr, to see that everything is freed when program exits.

{$mode delphi}
uses SysUtils, Generics.Collections;

type
  TApple = class
    Name: string;
  end;

  TAppleDictionary = TObjectDictionary<string, TApple>;

var
  Apples: TAppleDictionary;
  A: TApple;
  ApplePair: TAppleDictionary.TDictionaryPair;
begin
  Apples := TAppleDictionary.Create([doOwnsValues]);
  try
    A := TApple.Create;
    A.Name := 'my apple';
    Apples.AddOrSetValue('apple key 1', A);

    for ApplePair in Apples do
      Writeln('Found apple key->value: ' +
        ApplePair.Key + '->' + ApplePair.Value.Name);

    Apples.Remove('apple key 1');
  finally FreeAndNil(Apples) end;
end.

If you prefer using the FGL unit instead of Generics.Collections, here is a short overview of the most important classes from the FGL unit:

TFPGList

A generic list of types.

TFPGObjectList

A generic list of object instances. It can "own" children.

TFPGMap

A generic dictionary.

In FGL unit, the TFPGList can be only used for types for which the equality operator (=) is defined. For TFPGMap the "greater than" (>) and "less than" (<) operators must be defined for the key type. If you want to use these lists with types that don’t have built-in comparison operators (e.g. with records), you have to overload their operators as shown in the Operator overloading.

In the Castle Game Engine we include a unit CastleGenericLists that adds TGenericStructList and TGenericStructMap classes. They are similar to TFPGList and TFPGMap, but they do not require a definition of the comparison operators for the appropriate type (instead, they compare memory contents, which is often appropriate for records or method pointers). But the CastleGenericLists unit is deprecated since the engine version 6.3, as we advice using Generics.Collections instead.

If you want to know more about the generics, see Generics.

6.3. Cloning: TPersistent.Assign

Copying the class instances by a simple assignment operator copies the reference.

var
  X, Y: TMyObject;
begin
  X := TMyObject.Create;
  Y := X;
  
  Y.MyField := 123; 
  FreeAndNil(X);
end;

To copy the class instance contents, the standard approach is to derive your class from TPersistent, and override it’s Assign method. Once it’s implemented properly in TMyObject, you use it like this:

var
  X, Y: TMyObject;
begin
  X := TMyObject.Create;
  Y := TMyObject.Create;
  Y.Assign(X);
  Y.MyField := 123; 
  FreeAndNil(X);
  FreeAndNil(Y);
end;

To make it work, you need to implement the Assign method to actually copy the fields you want. You should carefully implement the Assign method, to copy from a class that may be a descendant of the current class.

{$mode delphi}
uses SysUtils, Classes;

type
  TMyClass = class(TPersistent)
  public
    MyInt: Integer;
    procedure Assign(Source: TPersistent); override;
  end;

  TMyClassDescendant = class(TMyClass)
  public
    MyString: string;
    procedure Assign(Source: TPersistent); override;
  end;

procedure TMyClass.Assign(Source: TPersistent);
var
  SourceMyClass: TMyClass;
begin
  if Source is TMyClass then
  begin
    SourceMyClass := TMyClass(Source);
    MyInt := SourceMyClass.MyInt;
    
  end else
    
    inherited Assign(Source);
end;

procedure TMyClassDescendant.Assign(Source: TPersistent);
var
  SourceMyClassDescendant: TMyClassDescendant;
begin
  if Source is TMyClassDescendant then
  begin
    SourceMyClassDescendant := TMyClassDescendant(Source);
    MyString := SourceMyClassDescendant.MyString;
    
  end;

  
  inherited Assign(Source);
end;

var
  C1, C2: TMyClass;
  CD1, CD2: TMyClassDescendant;
begin
  
  C1 := TMyClass.Create;
  C2 := TMyClass.Create;
  try
    C1.MyInt := 666;
    C2.Assign(C1);
    Writeln('C2 state: ', C2.MyInt);
  finally
    FreeAndNil(C1);
    FreeAndNil(C2);
  end;

  
  CD1 := TMyClassDescendant.Create;
  CD2 := TMyClassDescendant.Create;
  try
    CD1.MyInt := 44;
    CD1.MyString := 'blah';
    CD2.Assign(CD1);
    Writeln('CD2 state: ', CD2.MyInt, ' ', CD2.MyString);
  finally
    FreeAndNil(CD1);
    FreeAndNil(CD2);
  end;
end.

Sometimes it’s more comfortable to alternatively override the AssignTo method in the source class, instead of overriding the Assign method in the destination class.

Be careful when you call inherited in the overridden Assign implementation. There are two situations:

Your class is a direct descendant of the TPersistent class. (Or, it’s not a direct descendant of TPersistent, but no ancestor has overridden the Assign method.)

In this case, your class should use the inherited keyword (to call the TPersistent.Assign) only if you cannot handle the assignment in your code.

Your class descends from some class that has already overridden the Assign method.

In this case, your class should always use the inherited keyword (to call the ancestor Assign). In general, calling inherited in overridden methods is usually a good idea.

To understand the need when to call (or not to call) inherited from the Assign implementation, and how it relates to the AssignTo method, it’s best to look at the TPersistent.Assign and TPersistent.AssignTo implementations:

procedure TPersistent.Assign(Source: TPersistent);
begin
  if Source <> nil then
    Source.AssignTo(Self)
  else
    raise EConvertError...
end;

procedure TPersistent.AssignTo(Destination: TPersistent);
begin
  raise EConvertError...
end;

Note

This is not the exact implementation of TPersistent, I simplified it to hide the boring details about how the exception message is build.

The conclusions you can get from the above are:

  • If neither Assign nor AssignTo are overridden, then calling them will result in an exception.

  • Also, note that there is no code in TPersistent implementation that automatically copies all the fields (or all the published fields) of the classes. That’s why you need to do that yourself, by overriding Assign in all the classes. You can use RTTI (runtime type information) for that, but for simple cases you will probably just list the fields to be copied manually.

When you have a class like TApple, your TApple.Assign implementation usually deals with copying fields that are specific to the TApple class (not to the TApple ancestor, like TFruit). So, the TApple.Assign implementation usually checks whether Source is TApple at the beginning, before copying apple-related fields. Then, it calls inherited to allow TFruit to handle the rest of the fields.

Assuming that you implemented TFruit.Assign and TApple.Assign following the standard pattern (as shown in the example above), the effect is like this:

  • If you pass TApple instance to TApple.Assign, it will work and copy all the fields.

  • If you pass TOrange instance to TApple.Assign, it will work and only copy the common fields shared by both TOrange and TApple. In other words, the fields defined at TFruit.

  • If you pass TWerewolf instance to TApple.Assign, it will raise an exception (because TApple.Assign will call TFruit.Assign which will call TPersistent.Assign which raises an exception).

Note

Remember that when descending from TPersistent, the default visibility specifier is published, to allow streaming of TPersistent descendants. Not all field and property types are allowed in the published section. If you get errors related to it, and you don’t care about streaming, just change the visibility to public. See the Visibility specifiers.


from Hacker News https://ift.tt/2NYLTdG

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.