11

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?)

RRUZ
  • 134,889
  • 20
  • 356
  • 483
Darian Miller
  • 7,808
  • 3
  • 43
  • 62

7 Answers7

12

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.
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • 5
    Not the same performance characteristics, better performance because you no longer have the dummy getter that fixes the type for the return. And the enumerators were the "killer" for me. Suffice saying I've now got lots of classes that look like this `TSomethingList = class(TObjectList)`: Those are classes that once existed as hand-written descendants of `TObjectList` but now turned into generics containers so I can use the `for in` loop. – Cosmin Prund Mar 15 '11 at 15:29
  • 1
    @Cosmin I'm not sure that average performance improvement is real. Getter are not so time consuming, and it's always better to use a local variable to access several properties to an indexed item in a loop, e.g. Whereas all the duplicated code generated by generics (see Mason answer) could make the program slower: the CPU cache will be filled with all this unnecessary code. – Arnaud Bouchez Mar 15 '11 at 15:55
  • I agree that generic containers may increase performance, because Delphi versions that support generics have TDictionary which is probably the fastest container you can get if You need to find an item in a list by key value. – Linas Mar 15 '11 at 16:14
  • @Linas you could use TDictionary like feature without the generics, even with "old" Delphi versions - see http://synopse.info/forum/viewtopic.php?pid=1610#p1610 – Arnaud Bouchez Mar 15 '11 at 17:19
  • 1
    @A.Bouchez Interesting example but with generics it's much simpler and cleaner IMO. And your dynamic array uses binary search (if I investigated correctly) which is slower than dictionary's search. – Linas Mar 15 '11 at 17:39
  • @Linas Generics aren't much use in an older Delphi version. I'm sure if XE was the only Delphi in existence, @A.Bouchez would always use a TDictionary rather than the code he linked to. – David Heffernan Mar 15 '11 at 17:42
  • @David That's true. XE improved greatly when dealing with generics. But that's another topic. – Linas Mar 15 '11 at 17:48
  • +1 Thanks guys! I was going to mark this as the answer, but wasn't real convinced sure...then I read Deltics answer. – Darian Miller Mar 16 '11 at 02:36
  • @Linas About speed, you're right, from the mathematical POV. TDictionary uses a classic approach of hashing + modulo for its search. The hash window is growing by a power of two. Depending on the hash function used (the default hash for an object returned its memory address, so it's far far away from a perfect hash), its speed may vary. It's never truly O(1) but it will be faster than O(n) binary search in most cases. With the expend of bigger memory use. And if the hash is the default one (i.e. its memory address), you'll have a lot of collisions IMHO. So it could be slower. – Arnaud Bouchez Mar 16 '11 at 06:31
  • 1
    @darian the pitfall in your accepted answer is actually a limitation in the answer's author's knowledge. It's not a real problem. I'm sorry that you have been seduced by this argument. – David Heffernan Mar 16 '11 at 07:34
  • Note that it's not just Deltics example, Mason had an article link in his comment. I'm trying to use generics in new development, but in the original post I mentioned having just written code with the standard tlist and type-safe methods added and I had the desire to simply replace it all a generic list..which I did do with some units but then got to wondering what sort of trouble I might be creating. Going forward, I'll quit using my handy-dandy IDE tool that generates type-safe TList code in a few keystrokes. – Darian Miller Mar 22 '11 at 01:48
  • Another nail to the generics performance myth coffin: inlining is disabled for generics. IMHO never claim something is faster without actually measuring (or at least look at the asm code) – Marco van de Voort Mar 28 '13 at 09:34
10

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.
Cosmin Prund
  • 25,498
  • 2
  • 60
  • 104
  • I didn't say it was impossible with Generics, I said they made it a hindrance. Congratulations: You and David between you have very effectively proved exactly my point, thank you! ;) -1 because it doesn't answer the question at all. It's an answer that is a response to another answer - it should be a comment with a link to blog or other expanded medium for your observation/explanation. – Deltics Mar 16 '11 at 21:00
  • 1
    @Deltics, I get your point and respect your decision to downvote. None the less I did implement your *whole* animal feeding routine, using generic polymorphic lists in 79 lines of code, without even trying to keep it short. And note that's a complete console application, including tests. How can generics be a hindrance if they allowed me to do that? And my decision to use an other "SO Answer" to counter your example is in line with SO practices, since you can't have code in comments. And please don't hold my decision not to write a blog against me! – Cosmin Prund Mar 17 '11 at 08:20
  • 1
    @Deltics - This is an elaboration to the original question. Perfectly fine in my book. – Leonardo Herrera Mar 17 '11 at 19:06
  • Upvoting because this elaborated example code is very useful to me in my work. I appreciate the time taken, and want to offset the unwarranted downvote! – LaKraven Nov 30 '14 at 18:18
  • That's exactly what I was looking for. Thanks. – Nix Oct 13 '15 at 17:55
4

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

Robert Love
  • 12,447
  • 2
  • 48
  • 80
3

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.

Mason Wheeler
  • 82,511
  • 50
  • 270
  • 477
  • Most common usage patterns don't result in very much bloat. Nothing like that which the new RTTI produces. – David Heffernan Mar 15 '11 at 15:31
  • @Mason Wheeler - It is just a bloated EXE file or also a decrease in speed? If it is just the EXE size than is not a BIG problem. – Gabriel May 27 '14 at 14:12
2

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;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • `TAnimalList` inherits from `TList` and that inherits from `TObject`. You can't do `var L:TList := TAnimalList.Create`, that's not a polymorphic list. – Cosmin Prund Mar 16 '11 at 08:13
  • @Cosmin It solves Deltics' problem. I've seen your polymorphic list. I wasn't trying to replicate that. – David Heffernan Mar 16 '11 at 08:27
  • @David Deltic's answer was `"If you need polymoprhic lists then Generics are a hindrance"`. – Cosmin Prund Mar 16 '11 at 08:34
  • @Deltics But then his example is actually best solved by the code I present, I believe. – David Heffernan Mar 16 '11 at 09:01
  • @David I take Deltic's code as just an example for the apparent lack of polymorphic behavior in generic containers. It's a good example for that. His whole answer is actually summarized in the first sentence, everything else is the example. – Cosmin Prund Mar 16 '11 at 09:21
  • @Cosmin A global function is going to struggle to achieve polymorphism. You solution, whilst very clever, is complex. In reality, with real world problems, this is a non-issue. I'm a bit annoyed that Deltics has misled Darian in this way with what I see as groundless pessimism. – David Heffernan Mar 16 '11 at 09:25
  • @David, Polymorphism is a property of the class hierarchy, has nothing to do with the global function. Polymorphic lists are just as useful as any other form of OOP polymorphism, and just as necessary. Your example doesn't solve the polymorphic problem. And my "complex" code is just 10 lines long! I'm obviously only counting the generic class declaration plus the implementation of the `Add` and `GetItem` methods. The runtime complexity is that of the "old book" manual getter redefinition. Using it is just as easy as using any other generic container. – Cosmin Prund Mar 16 '11 at 09:49
  • @Cosmin Give me the real world example where you need your solution. I don't have one in my codebase. I'm not saying that there aren't cases where it's important. My point is that it feels silly to deprive yourself of the benefits of generic containers for a drawback that would never impact you! – David Heffernan Mar 16 '11 at 09:52
  • And I don't think Deltics misled Darian, I think Darian was looking for a reason to keep he's "old way" of doing things. But I do think Deltics was wrong: as proven, you can have generic and polymorphic containers. – Cosmin Prund Mar 16 '11 at 09:53
  • @Cosmin You are probably right. I'm probably over-reacting. After all we are all grown-ups and can take our own decisions. – David Heffernan Mar 16 '11 at 10:00
  • @David, you're right, this is a problem that might not impact many people, and you can even create the polymorphic list using "old school" techniques if the need ever comes, by simply inheriting from `TList`. In fact I'm reading a book on `asp.net` right now where the author's saying `polymorphism is the most talked about and least useful feature` (highly paraphrased because I don't have the book with me and I'm not even sure if the author was talking about OOP in general or just the asp.net framework). (to be continued...) – Cosmin Prund Mar 16 '11 at 10:08
  • Giving an example of useful polymorphic lists requires a non-trivial class hierarchy with a minimum two levels. I know for sure I used polymorphic lists in my Delphi 2010 code, but of course I'm so focused on the "solution" it never crossed my mind to write that generic class, I simply implemented the solution using "old school" techniques and moved on. Since an example from my own code is not helpful, here's an made up one: Let's say we have a routine taking a list of TControl objects. Let's say we've got a list of custom TFrames that we created at run time and we need to keep track of... – Cosmin Prund Mar 16 '11 at 10:12
  • ... caling the routine that takes `(TList)` with `(TList` is not possible; Yet `TMyFrame` inherits from `TControl` and the initial routine may be applied to the list of frames. If we had a polymorphic list of `TMyFrame` that inherits from `TList` we'd be able to call the routine. I'm sorry if this still sounds far-fetched, but polymorphism is in itself highly "verbose" and putting an other level of OOP-ish polymorphism on top of it just makes it more verbose. – Cosmin Prund Mar 16 '11 at 10:15
  • I said it was a hindrance - the fact that you need to contrive non-intuitive solutions to a problem which has an intuitive solution without generics simply makes my point that generics make such things harder (not impossible). -1 because it doesn't answer the question at all. It's a response to another answer, "fixing" something that didn't need "fixing" as the "broken" code was provided to support an observation, not make a concrete case. Your rebuttal to that observation should be a comment with a link to blog or other expanded medium for your observation/explanation. – Deltics Mar 16 '11 at 21:04
  • +1 The downside is mainly the newness of Generics and the possible time sinks of hitting into an example as Deltics posted, which looks like I would. (Time sinks = distracts from the flow of development.) If adding a quick list of items to a server application wherein most likely the only thing you'll need is an Add(x) Count() and perhaps a custom Find() routine then I think it's best (for me) to stick with the old standbys until MY codebase on generics expands and they become less of a potential issue. This is all part of the iterative learning cycle though and it's very much appreciated! – Darian Miller Mar 16 '11 at 21:18
  • 2
    I, for one, will be using Cosmin's solution when I need simple lists of objects with a common ancestor. I usually do. I want to forget old times of using TStringLists as containers for objects! – Leonardo Herrera Mar 17 '11 at 19:11
1

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!

Arnaud Bouchez
  • 42,305
  • 3
  • 71
  • 159
  • +1 I originally stated not to worry about backwards compatibility, but you are right of course... I have to support Delphi 5 apps, Delphi 7 apps, Delphi 2006 apps, Delphi 2007 apps, and Delphi XE apps...I'm on a mission to convert everything to XE though. – Darian Miller Mar 16 '11 at 02:39
  • A.Bouchez, you're a "special case" because you write libraries for others to use, I fully understand your desire to use backwards compatible language constructs. But this doesn't apply to the rest of us: The moment we open the `Delphi (N+1)` box we know we're going to use features not available in `Delphi (N)` - that's why we bought `Delphi (N+1)` – Cosmin Prund Mar 16 '11 at 07:23
  • @Cosmin - Writing libraries is a "worse case" for backwards compatibility, you're right. But there are companies maintaining several software, on several version of Delphi. And migration to a newer Delphi is not always possible for technical reasons (third-party components) or business budget (migration costs, especially into a Delphi Unicode version). So you could have, like Darian admit, several version of Delphi still alive in the same company and your computer. – Arnaud Bouchez Mar 16 '11 at 10:10
1

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.

:)

Deltics
  • 22,162
  • 2
  • 42
  • 70
  • @Darian: In case you were wondering about the "clear but counter-intuitive" principles that keep the example here from working, I wrote an article on it a while back. http://tech.turbu-rpg.com/149/generics-and-the-covariance-problem – Mason Wheeler Mar 16 '11 at 05:00
  • @Deltics, your example is a limitation of the default TList in Generics.Collections, it's not an intrinsic limitation of generics. See my answer for an example of feeding different kinds of generic animals with a single routine! – Cosmin Prund Mar 16 '11 at 06:58
  • -1 It's trivially easy to avoid this so called pitfall with proper use of constraints. I'm surprised someone as experienced as you are not aware of this. – David Heffernan Mar 16 '11 at 07:32
  • @David, how do you fix this with "proper use of constraints"? I don't think constraints alone are enough... you still have the incompatible inheritance problem (`TList` essentially inherits directly from `TObject`) – Cosmin Prund Mar 16 '11 at 07:47
  • @David - I have not learned how to manipulate the new Generic Cat Skinning tools to enable me to skin the cats I need. I am far too busy skinning cats effectively, efficiently and productively using very effective and much easier to learn and teach cat skinning tools to learn redundant knowledge or to have to spend the time then teaching that redundant knowledge to others who have to work in the same cat skinning facility. – Deltics Mar 16 '11 at 20:57
  • I'd say that it's not that we don't want to learn, it's a cost-benefit type thing. The potential costs of using generics on everything, as I thought I'd switch to even on simple lists that you dash out daily, seems to outweigh the immediate benefits of doing so. Long-term I'd have to agree that it makes sense. – Darian Miller Mar 16 '11 at 21:24
  • @Darin, generics pay off *especially* for simple list you dash out daily. With generics you just declare the list and use it, nothing else. And if the polymorphic list issue worries you, you may always derive a new list from the generic one using "classic" techniques, so you're on the safe side with that as well. Not to mention you may always go back from generics to "manual", since you're only replacing one line of code to do that! In my opinion the only valid reason not to use generics is backwards compatibility. – Cosmin Prund Mar 17 '11 at 08:28