Using Generic containers in Delphi XE - always?

2020-05-21 08:09发布

问题:

Generic containers can be a time saver when having a item, and a strongly typed list of those items. It saves the repetitive coding of creating a new class with perhaps a TList internal variable, and typed Add/Delete type methods, among other benefits (such as all the new functionality provided by the Generic container classes.)

However, is it recommended to always use generic containers for strongly typed lists going forward? What are the specific downsides of doing so? (If not worried about backwards compatibility of code.) I was writing a server application yesterday and had a list of items that I created 'the old way' and was going to replace it with a generic list but decided to keep it lean, but mostly out of habit. (Should we break the habit and start a new one by always using generics?)

回答1:

In Delphi XE, there is no reason not to use generic containers.

Switching from the old method with casting will give you:

  • cleaner, type-safe, less error-prone code,
  • enumerators, for in loops,
  • the samebetter performance characteristics.


回答2:

This was prompted by Deltic's answer, I wanted to provide an counter-example proving you can use generics for the animal feeding routine. (ie: Polymorphic Generic List)

First some background: The reason you can feed generic animals using a generic base list class is because you'll usually have this kind of inheritance:

TBaseList = class
  // Some code to actually make this a list
end

TSpecificList = class(TBaseList)
  // Code that reintroduces the Add and GetItem routines to turn TSpecificList
  // into a type-safe list of a different type, compatible with the TBaseList
end

This doesn't work with generics because you'll normally have this:

TDogList = TList<TDog>
end

TCatList = TList<TCat>
end

... and the only "common ancestor" for both lists is TObject - not at all helpful. But we can define a new generic list type that takes two class arguments: a TAnimal and a TSpecificAnimal, generating a type-safe list of TSpecificAnimal compatible with a generic list of TAnimal. Here's the basic type definition:

TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
private
  function GetItem(i: Integer): T2;
public
  procedure Add(A:T2);
  property Item[i:Integer]:T2 read GetItem;default;
end;

Using this we can do:

TAnimal = class; 
TDog = class(TAnimal); 
TCat = class(TAnimal);

TDogList = TCompatibleList<TAnimal, TDog>;
TCatList = TCompatibleList<TAnimal, TCat>;

This way both TDogList and TCatList actually inherit from TObjectList<TAnimal>, so we now have a polymorphic generic list!

Here's a complete Console application that shows this concept in action. And that class is now going into my ClassLibrary for future reuse!

program Project23;

{$APPTYPE CONSOLE}

uses
  SysUtils, Generics.Collections;

type

  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TCat = class(TAnimal)
  end;

  TCompatibleList<T1:class;T2:class> = class(TObjectList<T1>)
  private
    function GetItem(i: Integer): T2;
  public
    procedure Add(A:T2);
    property Item[i:Integer]:T2 read GetItem;default;
  end;

{ TX<T1, T2> }

procedure TCompatibleList<T1, T2>.Add(A: T2);
begin
  inherited Add(T1(TObject(A)));
end;

function TCompatibleList<T1, T2>.GetItem(i: Integer): T2;
begin
  Result := T2(TObject(inherited Items[i]));
end;

procedure FeedTheAnimals(L: TObjectList<TAnimal>);
var A: TAnimal;
begin
  for A in L do
    Writeln('Feeding a ' + A.ClassName);
end;

var Dogs: TCompatibleList<TAnimal, TDog>;
    Cats: TCompatibleList<TAnimal, TCat>;
    Mixed: TObjectList<TAnimal>;

begin
  try
    // Feed some dogs
    Dogs := TCompatibleList<TAnimal, TDog>.Create;
    try
      Dogs.Add(TDog.Create);
      FeedTheAnimals(Dogs);
    finally Dogs.Free;
    end;
    // Feed some cats
    Cats := TCompatibleList<TAnimal, TCat>.Create;
    try
      Cats.Add(TCat.Create);
      FeedTheAnimals(Cats);
    finally Cats.Free;
    end;
    // Feed a mixed lot
    Mixed := TObjectList<TAnimal>.Create;
    try
      Mixed.Add(TDog.Create);
      Mixed.Add(TCat.Create);
      FeedTheAnimals(Mixed);
    finally Mixed.Free;
    end;
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.


回答3:

Should we break the habit and start a new one by always using generics? YES



回答4:

In most cases, yes, generic containers are a good thing. However, the compiler generates a lot of duplicate code, and unfortunately the linker doesn't know how to remove it yet, so heavy use of generics could result in a bloated executable. But other than that, they're great.



回答5:

In the spirit of Cosmin's answer, essentially a response to Deltic's answer, here is how to fix Deltic's code:

type
  TAnimal = class
  end;

  TDog = class(TAnimal)
  end;

  TAnimalList<T:TAnimal> = class(TList<T>)
    procedure Feed;
  end;
  TDogList = TAnimalList<TDog>;

Now you can write:

var
  Dogs: TDogList;
...
  Dogs.Feed;


回答6:

You wrote about backwards compatibility... this is my biggest concern, if (like me) you are writing libraries which should better compile with most common versions of Delphi.

Even if you're using only XE for a closed project, you are probably making some custom libraries of your own, even if you never publish the code. We all have such favorite units at hand, just available not to reinvent the wheel for every project.

In a future assignment, you may have to maintain some older code, with no possibility to upgrade to a newer Delphi version (no money for the 1,000,000 code lines migration and review). In this case, you could miss your XE-only libraries, with shiny generic-based lists...

But for a 100% "private" application, if you are sure that you will never have to maintain older Delphi code, I don't see any reason not to use generics. My only concern is the duplicated code issue (as quoted by Mason): the CPU cache can be filled with unnecessary code, so execution speed could suffer. But in real app, I think you won't see any difference.

Note: I've just added some new features to my TDynArray wrapper. I tried to mimic the sample code from EMB docwiki. So you could have generic-like features, with good old Delphi versions... Of course, generics are better for working with classes, but with some arrays and records, it just rocks!



回答7:

If you need polymoprhic lists then Generics are a hindrance, not a help. This does not even compile, for example, because you cannot use a TDogList where a TAnimalList is required:

  uses
    Generics.Collections;

  type
    TAnimal = class
    end;

    TDog = class(TAnimal)
    end;

    TAnimalList = TList<TAnimal>;
    TDogList = TList<TDog>;


  procedure FeedTheAnimals(const aList: TAnimalList);
  begin
    // Blah blah blah
  end;


  var
    dogs: TDogList;
  begin
    dogs := TDogList.Create;
    try
      FeedTheAnimals(dogs);

    finally
      dogs.Free;
    end;
  end;

The reasons for this are quite clear and easily explained, but it is just as equally counter intuitive.

My own view is that you can save a few seconds or minutes (if you are a slow typist) by using a generic instead of rolling a type safe container more specific and appropriate to your needs, but you may well end up spending more time working around the problems and limitations of Generics in the future than you saved by using them to start with (and by definition if you haven't been using generic containers up to now then you don't know what those problems/limitations might be until you run into them).

If I need a TAnimalList then the chances are I need or could benefit from additional TAnimal specific methods on that list class that I would like to inherit in a TDogList, which in turn may introduce additional specific members relevant to it's TDog items.

(Animal and Dog being used for illustrative purposes only, of course. I don't actually work on veterinarian code currently - LOL)

The problem is, you don't always know this at the start.

Defensive programming principles suggest (to me, ymmv) that painting yourself into a corner for the sake of a bit of time saving is likely to end up costing a lot in the future. And if it doesn't, the additional "cost" of not taking the up front saving is itself negligible.

Plus your code is more shareable with users of older Delphi versions, if you are inclined to be so generous.

:)