Wednesday, July 20, 2011

Property references in Delphi - possible?

What is the worst thing about how bindings can be realized in Delphi? One may argue but I think it's the magic strings specifying what property to bind. I cannot think of any solution that could get rid of that. Using magic strings (and not only in that case) makes your software vulnerable to certain problems. Just mentioning a few:
  • refactoring may break your code
  • typos may break your code
  • no possible compiler checks for correctness
  • no code completion support
Well, a few versions ago we got method references. You prolly know how they are implemented behind the scenes but let's take a quick look at them. You can do something like (as defined in SysUtils):
type
  TFunc<T> = reference to function: T;
As you also may know this is a reference counted type. Guess how it is implemented? Correct, with an interface. This is similar to:
type
  IFunc<T> = interface
    function Invoke(): T;
  end;
You can even use those method reference types as Interface when you implement a class:
type
  TMyFuncClass = class(TInterfacedObject, TFunc<Integer>)
  public
    function Invoke: Integer;
  end;
Or you can inherit your interface from it (and of course implement that interface into your class):
type
  IFunc<T> = interface(TFunc<T>)
  end;
Ok, but we were talking about properties, right? Well a property basically is not much more than the combination of a function and a procedure - the getter and the setter.
We could define it like this:
type
  IProp<T> = interface
    function GetValue: T;
    procedure SetValue(Value: string);
    property Value: T read GetValue write SetValue;
  end;
So it is nothing more than (of course this does not work):
type
  IProp<T> = interface(TFunc<T>, TProc<T>)
    property Value: T read Invoke write Invoke;
  end;
But what you can do is this:
type
  TProp<T> = class(TInterfacedObject, TFunc<T>, TProc<T>)
  private
    function Invoke: T; overload;
    procedure Invoke(Value: T); overload;
  public
    property Value: T read Invoke write Invoke;
  end;
We can take this and use it as property reference passing it around, getting and setting values using RTTI but there we are again, how to create an instance? Yes, specifying the instance and the property by string. Another possibility would be with anonymous methods wrapping the property.

How about some built in type like this?
type
  TProp<T> = reference to property: T;
So I could write something like this:
procedure ShowAndInc(AProp: TProp<NativeInt>);
begin
  ShowMessageFmt('%d', [AProp]);
  AProp := AProp + 1; 
end;

procedure Form1Button1Click(Sender: TObject);
begin
  ShowAndInc(Self.Tag);
end;

What the compiler actually needs to do is something like this (ignoring possible read or write only properties for now):
ShowAndInc(TProp<NativeInt>.Create(
  function: NativeInt
  begin
    Result := Self.Tag;
  end,
  procedure(Value: NativeInt)
  begin
    Self.Tag := Value;
  end));
This is the code of my actual working TProp class and interface:
type
  IProp<T> = interface
    function Invoke: T; overload;
    procedure Invoke(Value: T); overload;
    property Value: T read Invoke write Invoke;
  end;

  TProp<T> = class(TInterfacedObject, IProp<T>)
  private
    FGetter: TFunc<T>;
    FSetter: TProc<T>;
    function Invoke: T; overload;
    procedure Invoke(Value: T); overload;
  public
    constructor Create(Getter: TFunc<T>; Setter: TProc<T>);
    property Value: T read Invoke write Invoke;
  end;

{ TProp<T> }

constructor TProp<T>.Create(Getter: TFunc<T>; Setter: TProc<T>);
begin
  FGetter := Getter;
  FSetter := Setter;
end;

function TProp<T>.Invoke: T;
begin
  Result := FGetter;
end;

procedure TProp<T>.Invoke(Value: T);
begin
  FSetter(Value);
end;
The compiler could do it way better by directly putting the call to the getter and setter into the right places.

So what do you think? Do you want reference to property in one of the next Delphi versions? Any drawbacks I am not seeing right now?

3 comments:

  1. Cool!

    But for binding and other stuff I would be happy to have at least a simple compiler magic function like we have for TypeOf() and TypeInfo(), say PropInfo():

    TObject = class
    TestProp: Integer;
    end;

    var
    p: PPropInfo;

    p := PropInfo(TObject.TestProp);

    But with your "property reference" more cool stuff can be done! Then you can use a property like an object (or pointer): change it in one place and all "pointers" read the same value.

    The compiler could check for read/write properties. For read only properties a "reference to readonly property" could be used.

    ReplyDelete
  2. One problem is that the default property reference doesn't have a Getter.

    For now, I've ended up using explicit anonymous setters and getters for each property, which do have other benefits such as allowing for conditional value trimming.

    In a way, the string reference issue could be worked around by introcuing what I imagine to be a fairly simple compiler magic function RefNameToStr(SomeProperty) that returns 'SomeProperty'.

    I'd like a CurrentMethodName function as well :P.

    ReplyDelete
  3. Direct access to field variables would not be a big problem if the whole feature was implemented into the compiler.

    CurrentMethodName? JclDebug.ProcByLevel() is your friend. :)

    ReplyDelete