1

I created the following class, after reading about the significant performance improvement of TDictionary over TStringList:

    TAnsiStringList = class(TObjectDictionary<AnsiString,TObject>)
    public
      constructor Create(const OwnsObjects: Boolean = True); reintroduce;
      destructor Destroy; override;
      procedure Add(const AString: AnsiString);
      procedure AddObject(const AString: AnsiString; AObject: TObject);
    end;

I coded the constructor like this:

  { TAnsiStringList }

    constructor TAnsiStringList.Create(const OwnsObjects: Boolean = True);
    begin
      if OwnsObjects then
        inherited Create([doOwnsKeys,doOwnsValues])
      else
        inherited Create;
    end;

...expecting that this TObjectDictionary constructor would be called:

    constructor Create(Ownerships: TDictionaryOwnerships; ACapacity: Integer = 0); overload;

...if the Ownerships parameter were specified. If the Ownerships parameter is not specified, I expected that the following inherited TDictionary constructor would be called:

    constructor Create(ACapacity: Integer = 0); overload;

The code compiles and runs, but when I call

    inherited Create([doOwnsKeys,doOwnsValues]) I get the following error:

Invalid class typecast

Does anyone see what I'm doing wrong, and is there a proper way to do this?

TIA

DelphiCoder
  • 95
  • 1
  • 7

1 Answers1

5

The problem is that you are asking the container to call Free on your keys when items are removed. But your keys are not classes and so that is an invalid request. This is trapped at runtime rather than compile time because the ownership is not determined until runtime.

You only want doOwnsValues and should remove doOwnsKeys.

 if OwnsObjects then
    inherited Create([doOwnsValues])
 else
   inherited Create;

For what it is worth, if you are trying to maked an AnsiString equivalent to TStringList, then your approach is flawed. A string list is an ordered container, but a dictionary is not. You won't be able to index by integer, iterate in order and so on. I also do not understand why you would want to force all consumers of this class to declare the objects to be of type TObject. You should leave that parameter free for the consumer to specify. That's the beauty of generics.

Perhaps you don't want an ordered container, in which case a dictionary is what you need. But in that case you simply don't need to create a new class. You can simply use TObjectDictionary as is.

If you are dead set on creating a sub class then I'd do it like this:

type
  TAnsiStringDict<T: class> = class(TObjectDictionary<AnsiString, T>)

That will allow the consumer of the class to decide which type of objects they put in the dictionary, and maintain compile time type safety.

So, when you want a dictionary of list boxes your declare a variable like this:

var
  ListBoxDict: TAnsiStringDict<TListBox>;
David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • Thanks for your prompt reply. The reason I took this approach is simply due to the performance improvement when performing lookups on large lists. I've often used a stringlist as a text based indexing system on lists of objects. If I don't have a need to look up a string or object using the numeric index, why wouldn't it be a better approach to use TObjectDictionary, since it is so much faster on the lookup? – DelphiCoder Apr 12 '13 at 14:42
  • You can simply use `TObjectDictionary` as is. If you want a version that explicitly forces your ownership choice, and chooses `AnsiString` for the keys, at least leave the value type parameterized. Then you can have type safety. You might find that performance is worse with `AnsiString`. Unless you use `AnsiString` everywhere else and don't touch RTL/VCL methods that will convert to and from UTF-16. – David Heffernan Apr 12 '13 at 14:44
  • *If I don't have a need to look up a string or object using the numeric index* That's fine. That's my last paragraph. But then your class name is wrong. Every Delphi coder that reads `TAnsiStringList` is going to assume an ordered array like container. – David Heffernan Apr 12 '13 at 14:45
  • Incidentally, I put a small test app together to test the performance difference, and I found that adding items to a TDictionary is 30-50% slower than a TStringList. But it's so much faster to do lookups on a TDictionary that it offsets the performance loss. The reason I created this class the way I did is because our legacy software uses a similar class that doens't need the indexed ordering, and uses the old ".indexOf(string) > -1" approach, which is quite competently handled by TDictionary's ".ContainsKey(string)" function. – DelphiCoder Apr 12 '13 at 14:51
  • I totally get the performance trade-offs. String list lookup is greatly improved if you set `Sorted` to `True`. Because then it can use binary search. I think I answered the question in the first part of the answer. The rest was general advice. 1. I think the class is named incorrectly. 2. I think you can use `TObjectDictionary` as is without subclassing. 3. If you do subclass, leave the value parameter open for consumers to specify. – David Heffernan Apr 12 '13 at 14:53
  • I changed the class name to TAnsiStringDict. Your solution of course works. I'm new to generics, and the principles are not completely clear to me yet. However, with the <...,TObject> parameter, I thought that would leave open the option to use any object type desired, and still allow the object type to be freed, as long as it is a descendant of TObject. Are you suggesting leaving the object type open so that it doesn't have to be a TObject descendant? I.E. Record types etc.? – DelphiCoder Apr 12 '13 at 15:23
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/28115/discussion-between-delphicoder-and-david-heffernan) – DelphiCoder Apr 12 '13 at 15:26