Search code examples
delphianonymous-methods

How are anonymous methods implemented under the hood?


Does Delphi "instantiate" each anonymous method (like an object)?, if so when does Delphi create this instance, and most important, when does Delphi free it?

Because anonymous method also captures external variables and extends their life time, it's important to know when these variables will be "released" from the memory.

What are the possible drawbacks to declare an anonymous method inside another anonymous method. Are circular reference possible?


Solution

  • Anonymous methods are implemented as interfaces. This article has a good explanation of how it is done by the compiler: Anonymous methods in Delphi: the internals.

    In essence, the compiler generated interface has a single method named Invoke, behind which is the anonymous method that you provide.

    Captured variables have the same lifetime as any anonymous methods that capture them. The anonymous method is an interface and its lifetime is managed by reference counting. Therefore, the captured variables life extends as long as does the anonymous methods that capture them.

    Just as it is possible for circular references to be created with interfaces, it must equally be possible for circular references to be created with anonymous methods. Here is the simplest demonstration that I can construct:

    uses
      System.SysUtils;
    
    procedure Main;
    var
      proc: TProc;
    begin
      proc :=
        procedure
        begin
          if Assigned(proc) then
            Beep;
        end;
    end;
    
    begin
      ReportMemoryLeaksOnShutdown := True;
      Main;
    end.
    

    Behind the scenes the compiler creates a hidden class that implements the anonymous method interface. That class contains as data members any variables that are captured. When proc is assigned to, that increases the reference count on the implementing instance. Since proc is owned by the implementing instance, that instance therefore has taken a reference to itself.

    To make this a little clearer, this program presents the identical issue but re-cast in terms of interfaces:

    uses
      System.SysUtils;
    
    type
      ISetValue = interface
        procedure SetValue(const Value: IInterface);
      end;
    
      TMyClass = class(TInterfacedObject, ISetValue)
        FValue: IInterface;
        procedure SetValue(const Value: IInterface);
      end;
    
    procedure TMyClass.SetValue(const Value: IInterface);
    begin
      FValue := Value;
    end;
    
    procedure Main;
    var
      intf: ISetValue;
    begin
      intf := TMyClass.Create;
      intf.SetValue(intf);
    end;
    
    begin
      ReportMemoryLeaksOnShutdown := True;
      Main;
    end.
    

    It is possible to break the circularity by explicitly clearing the self-reference. In the anonymous method example that looks like this:

    procedure Main;
    var
      proc: TProc;
    begin
      proc :=
        procedure
        begin
          if Assigned(proc) then
            Beep;
        end;
      proc := nil;
    end;
    

    The equivalent for the interface variant is:

    procedure Main;
    var
      intf: ISetValue;
    begin
      intf := TMyClass.Create;
      intf.SetValue(intf);
      intf.SetValue(nil);
    end;