0

I have a class defined which contains only strings as properties, and I need to get the property name based on its value as in the example below. In the example there are only 3 properties, in the real life class there are almost 1000. The problem is that this class is heavily used, and I want to know if I can get the property name by its value in a faster way.

unit Unit5;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs,RTTI, StdCtrls, Diagnostics;

type
  TConstDBElem = class
  public
    CCFN_1 : String;
    CCFN_2 : String;
    CCFN_3 : String;
    constructor Create;
  end;

  TForm5 = class(TForm)
    Memo1: TMemo;
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  end;

var
  Form5: TForm5;
  Obj: TConstDBElem;

implementation


{$R *.dfm}

procedure TForm5.Button1Click(Sender: TObject);
var iPos:Integer;
    timer:TStopwatch;
    Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
  var
    vrttiContext: TRttiContext;
    vrttiField : TRttiField;
    vType : TRttiType;
  begin
      vType := vrttiContext.GetType(TConstDBElem);

      for vrttiField in vType.GetFields do
        if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
        begin
           result := vrttiField.Name;
         end;
  end;

begin
  timer := TStopwatch.Create;
  timer.Start;
  Memo1.Lines.Clear;
  for iPos := 0 to 100000 do
    GetName(Obj,'TEST3');
  timer.Stop;
  Memo1.Lines.Add(FloatToStr(timer.Elapsed.TotalSeconds));
end;

constructor TConstDBElem.Create;
begin
  CCFN_1 := 'TEST1';
  CCFN_2 := 'TEST2';
  CCFN_3 := 'TEST3' ;
end;

initialization
  Obj := TConstDBElem.Create;

finalization
  Obj.Free;


end.

Yes,I know this is a very bad design, and this should not be done like this. Is there any option to make this search faster?

RBA
  • 12,337
  • 16
  • 79
  • 126
  • Do the values of the fields change at runtime? And if so, how can you do better than iterating over all fields looking for one with the desired name? I'd suggest a different design, but it looks as though the class has already been designed and it's too late. – David Heffernan Jul 29 '14 at 14:20
  • What made you decide to call `Free` on those three references? It's wrong to do so on many levels. Not needed on `vrttiContext`. Nor is the call to `TRttiContext.Create`. Completely wrong on `vType` and `vrttiField`. Don't destroy them. `vrttiField` is a loop variable. You are calling `Free` on whatever value it has at the end of the loop. Perhaps that's well defined for a for in loop. I'm not even sure. Bogus no matter what. And finally, the try is in the wrong place. You could easily be calling `Free` on uninitialized variables. – David Heffernan Jul 29 '14 at 14:24
  • No, the values are not changed at runtime. Yes, changing the design is not suitable... – RBA Jul 29 '14 at 14:40
  • If the values aren't changed at runtime, why does every instance have its own copies? Surely these can be constants. Perhaps class constants. You could readily populate a dictionary and use dictionary lookup which would be far more efficient. RTTI is never going to be efficient. – David Heffernan Jul 29 '14 at 14:41
  • 5
    BTW, a class with almost 1000 properties/fields doesn't look like a good idea in the first place. – Uwe Raabe Jul 29 '14 at 14:56
  • @UweRaabe: I just wanted to write the same. – Rudy Velthuis Jul 29 '14 at 14:56
  • Yes, i know is a bad design. My question is if there is any faster solution to this. – RBA Jul 29 '14 at 15:11
  • 2
    If all the properties are String, then you can use a `TStringList` with its name/value pairs. Then you only need one thing, not thousands. That's what the `Values` property of the `TStringList` was designed for. By the way, just glancing at your code, you don't have any "properties" but only "fields". – Jerry Dodge Jul 29 '14 at 15:20
  • Having 1000 fields is definitely a bad design. Not just from the memory overhead, but also the RTTI overhead. It is going to take `TRttiType.GetFields()` some time to build up its list of 1000 entries before you can then iterate through them. If you look at the implementation of `GetFields()`, it actually performs at least **3 loops** (actually more) - one loop to calculate the length of an internal 2D array to hold the fields of `Self` and all of its ancestor classes, one loop to fill that 2D array, and one loop to concat that 2D array into a 1D array that is returned in the `Result`. – Remy Lebeau Jul 29 '14 at 16:47
  • Actually more loops, because the search for the declared fields in each step of `GetFields()`'s logic are themselves looped, so a lot of duplicated effort. – Remy Lebeau Jul 29 '14 at 16:52
  • To the downvoter, please explain your vote. To all, this is a legacy code, is not made by me, and I only asked if this can be improved. – RBA Jul 30 '14 at 08:09

3 Answers3

3

When GetName() finds a match, it is not stopping its loop, so it keeps searching for more matches. Assigning a function's Result does not exit the function, like you clearly think it does. As such, GetName() ends up returning the last match, not the first match. The loop should be calling Exit when it finds the first match:

Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
  vrttiContext: TRttiContext;
  vrttiField : TRttiField;
  vType : TRttiType;
begin
  vType := vrttiContext.GetType(TConstDBElem);

  for vrttiField in vType.GetFields do
    if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
    begin
      result := vrttiField.Name;
      Exit; // <-- add this
    end;
end;

Alternatively, use the version of Exit() that takes a parameter:

Function GetName(const DBElemInstance : TConstDBElem; valueName: string) : string;
var
  vrttiContext: TRttiContext;
  vrttiField : TRttiField;
  vType : TRttiType;
begin
  vType := vrttiContext.GetType(TConstDBElem);

  for vrttiField in vType.GetFields do
    if (vrttiField.GetValue(DBElemInstance).ToString = valueName) then
    begin
      Exit(vrttiField.Name); // <-- assigns Result and exits at the same time
    end;
end;

In your simple example, the time wasted to search 3 fields is hardly noticeable, but when searching 1000 fields, it makes a difference.

Remy Lebeau
  • 555,201
  • 31
  • 458
  • 770
  • 1
    +1 for `Exit(vrttiField.Name); // <-- assigns Result and exits at the same time`, I did not even know you could do that! –  Jul 29 '14 at 21:01
  • Per the [System.Exit documentation](http://docwiki.embarcadero.com/VCL/2010/en/System.Exit): "Beginning with Delphi 2009, Exit can take a parameter specifying a result. The parameter must be of the same type as the result of the function." – Remy Lebeau Jul 29 '14 at 21:47
  • @Blobby it's not as good an idea as you might think. Now there are multiple ways to return a value. Either set `Result` or pass a parameter to `Exit()`. This is inconsistent. How I yearn for a proper `return` statement, even better one that returned by value rather than using a `var` parameter. – David Heffernan Jul 30 '14 at 06:35
  • @DavidHeffernan Ok I read your comment and understand it, it's just I seem to pick up these keywords I have never seen before and instantly think it's going to make life easier :) Funny enough I would have used `Break` instead of `Exit` and returned the value after the loop, it is not often I use `Exit`. –  Jul 30 '14 at 18:33
  • 1
    @blobby if you know the value to be returned, set it and exit. Only use break if you have more work to do. – David Heffernan Jul 30 '14 at 18:37
  • @DavidHeffernan: `Exit(param)` *is* Delphi's equivalent of a "prop `return` statement". The parameter is taken by value, not by reference, so why do you think there is a `var` involved? Or are you referring to the hidden `var` that the compiler may or may not generate for the function `Result` that `Exit(param)` assigns to? – Remy Lebeau Jul 30 '14 at 18:43
  • 1
    @RemyLebeau `Exit(...)` is not a statement, it's a procedure. The `Result` variable is available throughout the life of the function. Delphi return values are indeed sometimes implemented as `var` parameters, and sometimes optimised into registers with by value passing. In the latter case there is a new local variable declared to hold `Result`. When it's a `var` parameter, the compiler has to do some juggling to make sure that value semantics are achieved. It's all a bit of a mess. It doesn't follow platform ABI. Although that is hardly very well defined. I like C, C++, C# `return`. – David Heffernan Jul 30 '14 at 18:56
  • @DavidHeffernan thanks for the tip, I always thought of `Exit` as a bad thing, like a get out of jail card to escape the routine. –  Jul 31 '14 at 00:04
1

You state in a comment that the values never change at runtime. In which case you can simply build a single dictionary at startup that has the property values as the dictionary key, and the property name as the dictionary value.

I'm assuming that all instances of the class have the same property values. If that's not the case then you'll need one dictionary per instance.

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
0

It's a "bad design" because someone wrote a class that they're treating like a C-style Struct. As has been said already, there are NO PROPERTIES defined in the class, just a bunch of PUBLIC DATA MEMBERS, aka, "fields".

There's no encapsulation, so anything you do to change the structure could have far-reaching implications on any unit that uses this. I agree that replacing the IMPLEMENTATION with a TStringList or a TDictionary would be smart, but ... there are no interfaces to adhere to! You have 1000-odd hard-wired references to public data members.

(The last time I saw something like this was some code written by a bunch of VB programmers who wrote classes as if they were C-style structs containing public data members, and then they wrote external functions to access the data, just as you'd do in C. Only they buried business logic inside of the accessor methods as well, which causes random pieces of the code to make direct references to the data members of the class.)

Off-hand, I'd say you're totally mis-using the RTTI code. Sure, the optimizations above will improve performance, but so what? It's the wrong solution!

If you really want to refactor this (and you certainly should!), first I'd look to see how widespread the use of this poor class is by changing the public to private and see how many errors you get.

Then I'd derive it from TStringList, delete all of the local fields, and move the GetName function inside of the class:

type
  TConstDBElem = class( TStringList )
  public
    constructor Create;
    function GetName( aName : string ) : string;
  end;

Now, if I'm interpreting your example correctly, you want to do this to initialize the object:

constructor TConstDBElem.Create;
begin
  Add( 'TEST1=CCFN_1' );
  Add( 'TEST2=CCFN_2' );
  Add( 'TEST3=CCFN_3' );
end;

Then replace all of the references in other units with a call to obj.GetName():

function TConstDBElem.GetName( aName : string ) : string;
begin
  Result := Values[aName];
end;

You're replacing a reference to obj.CCFN_1 (?) or GetName(obj,'TEST1') with obj.GetName('TEST1').

(Maybe I'm totally off-base at this point. Sorry, but I just don't get how you're using this class from the example, and it doesn't make a whole lot of sense to me anyway. It would make more sense if you said what you're mapping between. I mean ... who needs to look up a local field name from a value associated with it? And what do you do with it once you've found it? Whomever wrote this had to go through some incredible contortions to make the code work because s/he sure didn't understand OOP when this was designed!)

At this point, you will have succeeded in decoupling the clients of this class (other units) from its internal implementation, and replacing those references with calls to an interface (a method) defined in the class instead.

Then you can do some testing to see what happens if you change the implementation, like from a TStringList to a TDictionary. But no matter how you slice it, I cannot imagine that either the TStringList or TDictionary will be slower than how you're abusing the RTTI system in your example. :)

David Schwartz
  • 1,756
  • 13
  • 18
  • Whilst I agree that the current design is poor, I definitely think that deriving from `TStringList` is also very poor. You've now got a class that inherits all sorts of unneeded functionality. It's got methods like `Sort` and `InsertObject` and `SaveToFile`. Properties like `CommaText`, `ValuesFromIndex[]`, `StrictDelimiter`, `WriteBOM` and so on. Inheritance is not a good option here. Encapsulation is what is needed. And since you want a dictionary, why `TStringList`. Surely `TDictionary` is what is needed. – David Heffernan Jul 31 '14 at 11:14
  • You make my point very well! The gist of my reply wasn't to suggest THE BEST solution, but merely to show how to get to a point where THE IMPLEMENTATION CAN BE CHANGED without affecting how it's used by clients. Anyway, I'm sure that if I posted an example using a container as a member, someone surely would have suggested I inherit from the object directly. It's irrelevant. He needs to break the dependency on directly accessing the data members first. The rest is mere implementation details. Both Add() and GetName() methods will work with virtually anything, inheritance or composition. – David Schwartz Jul 31 '14 at 16:18