tags:

views:

3090

answers:

10

In Delphi, I want to be able to create an private object that's associated with a class, and access it from all instances of that class. In Java, I'd use:

public class MyObject {
    private static final MySharedObject mySharedObjectInstance = new MySharedObject();
}

Or, if MySharedObject needed more complicated initialization, in Java I could instantiate and initialize it in a static initializer block.

(You might have guessed... I know my Java but I'm rather new to Delphi...)

Anyway, I don't want to instantiate a new MySharedObject each time I create an instance of MyObject, but I do want a MySharedObject to be accessible from each instance of MyObject. (It's actually logging that has spurred me to try to figure this out - I'm using Log4D and I want to store a TLogLogger as a class variable for each class that has logging functionality.)

What's the neatest way to do something like this in Delphi?

A: 

Before version 7, Delphi didn't have static variables, you'd have to use a global variable.

To make it as private as possible, put it in the implementation section of your unit.

CL
This is true until Delphi 7. There are class variables (see other answers) for higher versions.
Ralph Rickenbach
A: 

In Delphi static variables are implemented as variable types constants :)

This could be somewhat misleading.

procedure TForm1.Button1Click(Sender: TObject) ;
const
   clicks : Integer = 1; //not a true constant
begin
  Form1.Caption := IntToStr(clicks) ;
  clicks := clicks + 1;
end;

And yes, another possibility is using global variable in implementation part of your module.

This only works if the compiler switch "Assignable Consts" is turned on, globally or with {$J+} syntax (tnx Lars).

squadette
This only works if the compiler switch AssignableConsts is turned on
Lars Truijens
+3  A: 
 TMyObject = class
    private
      class var FLogger : TLogLogger;
      procedure SetLogger(value:TLogLogger);
      property Logger : TLogLogger read FLogger write SetLogger;
    end;

procedure TMyObject.SetLogger(value:TLogLogger);
begin
  // sanity checks here
  FLogger := Value;
end;

Note that this class variable will be writable from any class instance, hence you can set it up somewhere else in the code, usually based on some condition (type of logger etc.).

Edit: It will also be the same in all descendants of the class. Change it in one of the children, and it changes for all descendant instances. You could also set up default instance handling.

 TMyObject = class
    private
      class var FLogger : TLogLogger;
      procedure SetLogger(value:TLogLogger);
      function GetLogger:TLogLogger;
      property Logger : TLogLogger read GetLogger write SetLogger;
    end;

function TMyObject.GetLogger:TLogLogger;
begin
  if not Assigned(FLogger)
   then FLogger := TSomeLogLoggerClass.Create;
  Result := FLogger;
end;

procedure TMyObject.SetLogger(value:TLogLogger);
begin
  // sanity checks here
  FLogger := Value;
end;
Lars Fosdal
Could this have an issue with thread safety? Might multithreading lead to an instance of TMyObject trying to use an FLogger that hasn't been fully initialized?
MB
In theory, yes. Adding a crit.section would help - but that comes at a price. Best thing would be a class method, such as in the init section example, combined with an assert in the get routine. You might even want an assert in the setter to avoid multiple setters. YMMV.
Lars Fosdal
+2  A: 

The keywords you are looking for are "class var" - this starts a block of class variables in your class declaration. You need to end the block with "var" if you wish to include other fields after it (otherwise the block may be ended by a "private", "public", "procedure" etc specifier). Eg

(Edit: I re-read the question and moved reference count into TMyClass - as you may not be able to edit the TMySharedObjectClass class you want to share, if it comes from someone else's library)

  TMyClass = class(TObject)
  strict private
    class var
      FMySharedObjectRefCount: integer;
      FMySharedObject: TMySharedObjectClass;
    var
    FOtherNonClassField1: integer;
    function GetMySharedObject: TMySharedObjectClass;
  public
    constructor Create;
    destructor Destroy; override;
    property MySharedObject: TMySharedObjectClass read GetMySharedObject;
  end;


{ TMyClass }
constructor TMyClass.Create;
begin
  if not Assigned(FMySharedObject) then
    FMySharedObject := TMySharedObjectClass.Create;
  Inc(FMySharedObjectRefCount);
end;

destructor TMyClass.Destroy;
begin
  Dec(FMySharedObjectRefCount);
  if (FMySharedObjectRefCount < 1) then
    FreeAndNil(FMySharedObject);

  inherited;
end;

function TMyClass.GetMySharedObject: TMySharedObjectClass;
begin
  Result := FMySharedObject;
end;

Please note the above is not thread-safe, and there may be better ways of reference-counting (such as using Interfaces), but this is a simple example which should get you started. Note the TMySharedObjectClass can be replaced by TLogLogger or whatever you like.

Graza
I can see that something like this might make sense if you were sharing the object outside the class, but it's a lot of scaffolding code for an object that's only shared between instances of the class.
MB
+1  A: 

Please note that class variables are not available before Delphi 7.

CL
+4  A: 

Here is how I'll do that using a class variable, a class procedure and an initialization block:

unit MyObject;

interface

type

TMyObject = class
   private
     class var FLogger : TLogLogger;
   public
     class procedure SetLogger(value:TLogLogger);
     class procedure FreeLogger;
   end;

implementation

class procedure TMyObject.SetLogger(value:TLogLogger);
begin
  // sanity checks here
  FLogger := Value;
end;

class procedure TMyObject.FreeLogger;
begin
  if assigned(FLogger) then 
    FLogger.Free;
end;

initialization
  TMyObject.SetLogger(TLogLogger.Create);
finalization
  TMyObject.FreeLogger;
end.
Pierre-Jean Coudert
This is looking like the neatest solution to me so far. Though since it's for class-internal use only, if possible I'd be inclined to dispense with the SetLogger procedure, and just set the field directly in the initialization section. You could destroy it in a finalization section too I guess.
MB
If you need a private field, you'll need the SetLogger procedure. I agree with the finalization procedure comment.
Pierre-Jean Coudert
I think it should be possible to set a private field from the initializer, since private fields are accessible from within the unit. It has to be "strict private" for it to be accessible from within the class only.
MB
+1  A: 

Last year, Hallvard Vassbotn blogged about a Delphi-hack I had made for this, it became a two-part article:

  1. Hack#17: Virtual class variables, Part I
  2. Hack#17: Virtual class variables, Part II

Yeah, it's a long read, but very rewarding.

In summary, I've reused the (deprecated) VMT entry called vmtAutoTable as a variable. This slot in the VMT can be used to store any 4-byte value, but if you want to store, you could always allocate a record with all the fields you could wish for.

PatrickvL
+1  A: 

For what I want to do (a private class constant), the neatest solution that I can come up with (based on responses so far) is:

unit MyObject;

interface

type

TMyObject = class
private
  class var FLogger: TLogLogger;
end;

implementation

initialization
  TMyObject.FLogger:= TLogLogger.GetLogger(TMyObject);
finalization
  // You'd typically want to free the class objects in the finalization block, but
  // TLogLoggers are actually managed by Log4D.

end.

Perhaps a little more object oriented would be something like:

unit MyObject;

interface

type

TMyObject = class
strict private
  class var FLogger: TLogLogger;
private
  class procedure InitClass;
  class procedure FreeClass;
end;

implementation

class procedure TMyObject.InitClass;
begin
  FLogger:= TLogLogger.GetLogger(TMyObject);
end;

class procedure TMyObject.FreeClass;
begin
  // Nothing to do here for a TLogLogger - it's freed by Log4D.
end;

initialization
  TMyObject.InitClass;
finalization
  TMyObject.FreeClass;

end.

That might make more sense if there were multiple such class constants.

MB
There is a few drawback with putting stuff in init section: Even if you don't use the class, it will be linked in as long as you use the unit. Also, the init order may not always be what you think it is, and will change as you change the position of units in the uses clause.
Lars Fosdal
Ew, that sounds a bit ugly! Is it possible for an instance of a class to be created before its unit's initialization section has run? In other words could an instance of TMyObject try to use FLogger before it's been set in the initialization section?
MB
@MB: no, that's not possible - before any TMyObject is created or even class methods used, the unit that defines TMyObject would have been included, and it is at that point that the initialization section runs. You can't use a class in any unit before that unit's initialization section has executed
Graza
You could remove the use of the init section by using lazy instantiation; e.g. never use the FLogger var directly, but via a getter function as in the example below.TMyObject = classprivate class var FLogger: TLogLogger; function GetLogger: TLogLogger;end;procedure TMyObject.GetLogger: TLogLogger;begin if not Assigned(FLogger) then FLogger := TLogLogger.GetLogger(TMyObject); Result := FLogger;end;
Conor Boyd
+1  A: 

Two questions I think that need to be answered before you come up with a "perfect" solution..

  • The first, is whether TLogLogger is thread-safe. Can the same TLogLogger be called from multiple threads without calls to "syncronize"? Even if so, the following may still apply
  • Are class variables thread-in-scope or truly global?
  • If class variables are truly global, and TLogLogger is not thread safe, you might be best to use a unit-global threadvar to store the TLogLogger (as much as I don't like using "global" vars in any form), eg

Code:

interface
type
  TMyObject = class(TObject)
  private
    FLogger: TLogLogger; //NB: pointer to shared threadvar
  public
    constructor Create;
  end;
implementation
threadvar threadGlobalLogger: TLogLogger = nil;
constructor TMyObject.Create;
begin
  if not Assigned(threadGlobalLogger) then
    threadGlobalLogger := TLogLogger.GetLogger(TMyObject); //NB: No need to reference count or explicitly free, as it's freed by Log4D
  FLogger := threadGlobalLogger;
end;

Edit: It seems that class variables are globally stored, rather than an instance per thread. See this question for details.

Graza
No idea why the code formatting isn't working...
Graza
threadvar - cool, I didn't know you could do that in Delphi.I was going on the assumption that a class variable would only make more sense than an instance variable *if* the object (e.g. TLogLogger) was thread safe. But it looks like threadvar offers an in-between option.
MB
I'm still wondering whether Class variables are global or thread in scope. My guess is global, but perhaps I should add a new question...
Graza
A: 

Well, it's not beauty, but works fine in Delphi 7:

TMyObject = class
pulic
    class function MySharedObject: TMySharedObject; // I'm lazy so it will be read only
end;

implementation

...

class function MySharedObject: TMySharedObject;
{$J+} const MySharedObjectInstance: TMySharedObject = nil; {$J-} // {$J+} Makes the consts writable
begin
    // any conditional initialization ...
   if (not Assigned(MySharedObjectInstance)) then
       MySharedObjectInstance = TMySharedOject.Create(...);
  Result := MySharedObjectInstance;
end;

I'm curently using it to build singletons objects.

Gedean Dias