Friday, September 11, 2009

Using Attributes and TCustomAttribute descendants

With Delphi 2010 attributes have been added as a language feature. They have been available in Delphi Prism (Targets .NET) and this now adds similar functionality to Win32.

Attributes are a way of associating additional metadata information with a given type or member of a type.

They can be applied in many places, the following code shows several of the places you can place attributes.


// Declaring an attribute.
TAttrTest = class(TCustomAttribute)
end;

// Places you can use an attribute.
[TAttrTest]
TRec = Record
[TAttrTest]
value : String;
[TAttrTest]
procedure DoThis([TAttrTest]arg1: String);
End;

[TAttrTest]
TMyEnum = (enOne,enTwo,enThree);

[TAttrTest]
TMySet = set of TMyEnum;

[TAttrTest]
TObj = class(TObject)
private
[TAttrTest]
FID: Integer;
public
[TAttrTest]
FName : String;
[TAttrTest]
property Id : Integer read FID write FID;
[TAttrTest]
constructor Create;
[TAttrTest]
destructor Destroy; override;
[TAttrTest]
procedure DoThis;
end;

var
[TAttrTest]
I : Integer;


So how do attributes work in Delphi 2010?

Attributes must descend from TCustomAttribute, if you look at the declaration of TCustomAttribute you will find that there is nothing special.


{ The base class for all custom attributes. Attribute
instances created by the RTTI unit are owned by those
members to which they apply. }
TCustomAttribute = class(TObject)
end;


Passing just the name of the new attribute is only practical in a few cases, usually
you need additional data associated. This is done through the constructor. The following example shows how to setup the call to the constructor in the attribute.


Type
TAttrTest2 = class(TObject)
private
FId : Integer;
public
constructor Create(aID : Integer);
property ID : Integer read FID write FID;
end;

[TAttrTest2(123)]
TMyObject = Class(TObject)
end;


So its simple to declare an Attribute and decorate your types with them. Accessing the attributes stored in a given type involves using rtti.pas, I covered some of the basics of how this works in the previous post

Anything that can have attributes has an associated .GetAttributes() method that returns
the array of the attributes associated with that code.

The following code shows how to access the attributes.


program Project10;

{$APPTYPE CONSOLE}

uses
SysUtils, RTTI;
type
TAttrTest2 = class(TCustomAttribute)
private
FId : Integer;
public
constructor Create(aID : Integer);
property ID : Integer read FID write FID;
end;

[TAttrTest2(1)]
[TAttrTest2(2)]
[TAttrTest2(3)]
TMyObject = Class(TObject)
end;

{ TAttrTest2 }

constructor TAttrTest2.Create(aID: Integer);
begin
FID := aId;
end;

var
c : TRttiContext;
t : TRttiType;
a : TCustomAttribute;
begin
c := TRttiContext.Create;
try
t := c.GetType(TMyObject);
for a in t.GetAttributes do
begin
Writeln((a as TAttrTest2).ID);
end;
finally
c.Free
end;
readln;
end.

Output:

1
2
3



Attributes also have a few other special items that the compiler implements.

If you have an attribute named like this...


type
TestAttribute = class(TCustomAttribute)
end;

It can be refered to in two different ways

[TestAttribute]
TExample = class(Tobject)
end;

[Test]
TExample2 = class(TObject)
end;


The compiler will look for the type, if it is not found it will automatically append "Attribute" to the name and search again. This is done to mimic the .NET behavior.

The compiler also has some special support for types allow you to get TRttiType
easily from the pTypeInfo pointer. The following code segment shows how that pTypeInfo can be interchanged for TRttitype in attributes.


uses
SysUtils, Rtti;

type
TestAttribute = class(TCustomAttribute)
public
constructor Create(aType : TRttiType);
end;

[Test(typeinfo(Integer))]
TEmployee = class(TObject)
end;


There are many practical applications for Attributes, I will explore many of these in later articles.

RTTI Article List

12 comments:

  1. Thank you for illustrating how "noisy" code quickly becomes with attributes. :)

    This is another one of those language features that appeals to the laziness inherent in most developers.

    We constantly seek to create code as quickly as possible, neglecting the fact that unless the code is "throw away", it will spend 99.9% of its life being maintained, not created.

    Attributes make reading code physically harder by adding noise and conflating concerns (SQL attributes mixed in with test framework attributes, mixed in with xyz attributes and scattered through the declarations).

    I'm not saying that attributes don't have some practical use, only that caution should be exercised in their use.

    Where there is an alternative approach that keeps unrelated concerns separated and out of the declarations where they have no business being, that approach should be preferred, even if it takes a little longer to create that code.

    You will reap the rewards in having something much more maintainable for the many months and years that follow those exciting few minutes of churning out some new code.

    ReplyDelete
  2. As with all launguage features you need to use them in the correct places.

    However, I don't think they lead to harder to read code. It's different but it's not harder to read. I believe they can lead to cleaner easier to maintain code, when used correctly.

    ReplyDelete
  3. Your TCustomAttribute descendents basically contain values only. Can they have methods that do something else, like popping up a message dialog, as well? Do you think that would make sense?

    ReplyDelete
  4. You can have additional functionality on your attributues. They are just classes.

    One big thing I neglected is you should avoid doing any validation at the time of the construction, by raising an exception. Instead you should do it querying the object that uses the Attributes.

    This way you have context and a class stack that makes sense. The Attribute Contruction will show you in deep in the rtti.pas and be confusing to debug. There are also other reasons, you can see them by reading the comments in RTTI.pas.

    ReplyDelete
  5. Thanks!
    I look forward to studying the rest of your blog posts.

    ReplyDelete
  6. .NET attributes allow you to set property values on the attribute, as well as pass constructor parameters. You didn't specifically show an example, but I assume the Delphi/Win32 attributes can do this too?

    ReplyDelete
  7. I wondered the same, but I have not seen anything in the documentation, or indication that properties setting is currently supported.

    Neither is the AttributeUsage although I did roll a runtime version of it together. Just nothing at compile time.

    ReplyDelete
  8. Me understanding the attribute-concept halts on one reason. I still haven't found a place where it is useful to me. I don't want to stress you but you did mentioned coming articles of where custom attributes are practical.
    P.S. Great articles about the RTTI - a much overlooked topic.

    ReplyDelete
  9. Many of these use Attributes.

    http://robstechcorner.blogspot.com/2009/10/rtti-practical-examples.html

    ReplyDelete
  10. On first code snippet, lines 38-40; You are adding an attribute to a variable.

    Is this really possible? Is there a way to get a reference to the variable itself and read the attribute?

    ReplyDelete
  11. Hi Robert, maybe I'm wrong, but is the third example should not be in place:
    TAttrTest2 = class (TObject)
    here is
    TAttrTest2 = class (TCustomAttribute)

    ReplyDelete
  12. Very late, but another use for attributes which we are finding very compelling at present is in unifying documentation, source and test for concurrency and threading: http://marc.durdin.net/2014/05/using-delphi-attributes-to-unify-source-test-and-documentation/

    ReplyDelete