Why can't I assign my function reference to a matching variable? E2555 is raised

后端 未结 3 1277
情深已故
情深已故 2021-01-01 20:49

I\'m trying to build an custom comparer which allows the assignment of the comparison function to an internal field. In order to ease the creation of the comparer, I tried t

相关标签:
3条回答
  • 2021-01-01 20:54

    I don't think that this is a bug. Critically, you've defined TConstFunc as an anonymous method type. These are managed, reference counted, very special types that are quite different from regular object methods. By compiler magic they are usually assignment compatible, but with several important caveats. Consider the more concise :

    program Project1;
    
    {$APPTYPE CONSOLE}
    
    type
      TFoo = reference to procedure;
    
      TDemo = class
      private
        FFoo : TFoo;
        procedure Foo;
      public
        class function Construct(): TDemo;
      end;
    
    procedure TDemo.Foo;
    begin
      WriteLn('foo');
    end;
    
    class function TDemo.Construct: TDemo;
    begin
      result := TDemo.Create();
      result.FFoo := result.foo;
    end;
    
    end.
    

    This also produces the same compiler error (E2555). Because the member method is a procedure of object (object method) type, and you are assigning it to a reference to procedure (anonymous method) type, this is equivalent to (and I suspect that the compiler is expanding this as) :

    class function TDemo.Construct: TDemo;
    begin
      result := TDemo.Create();
      result.FFoo := procedure
                     begin
                       result.foo;
                     end;
    end;
    

    The compiler cannot assign the method reference directly (since they are of different types), and therefore (I guess) has to wrap it in an anonymous method which is implicitly requiring capture of the result variable. Function return values cannot be captured by anonymous methods, however - only local variables can.

    In your case (or, indeed, for any function type), the equivalent cannot even be expressed due to the anonymous wrapper hiding the result variable, but we can imagine the same in theory as:

    class function TDemo.Construct: TDemo;
    begin
      Result := TDemo.Create();
      Result.FVar := function(const L, R : string) : integer
                     begin
                       result := result.CompareInternal(L,R);  // ** can't do this
                     end;
    end;
    

    As David has shown, introducing a local variable (which can be captured) is one correct solution. Alternatively, if you don't need the TConstFunc type to be anonymous, you can simply declare it as a regular object method :

    TConstFunc<T1, T2, TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;
    


    Another example where attempting to capture result fails :

    program Project1;
    
    {$APPTYPE CONSOLE}
    
    type
      TBar = reference to procedure;
      TDemo = class
      private
        FFoo : Integer;
        FBar : TBar;
      public
        class function Construct(): TDemo;
      end;
    
    class function TDemo.Construct: TDemo;
    begin
      result := TDemo.Create();
      result.FFoo := 1;
      result.FBar := procedure
                     begin
                       WriteLn(result.FFoo);
                     end;
    end;
    
    end.
    

    The fundamental reason why this does not work is because a method's return value is effectively a var parameter and the anonymous closure captures variables, not values. This is a critical point. Similarly, this is also not allowed :

    program Project1;
    
    {$APPTYPE CONSOLE}
    
    type
      TFoo = reference to procedure;
    
      TDemo = class
      private
        FFoo : TFoo;
        procedure Bar(var x : integer);
      end;
    
    procedure TDemo.Bar(var x: Integer);
    begin
      FFoo := procedure
              begin
                WriteLn(x);
              end;
    end;
    
    begin
    end.
    

    [dcc32 Error] Project1.dpr(18): E2555 Cannot capture symbol 'x'

    In the case of a reference type, as in the original example, you really are only interested in capturing the value of the reference and not the variable that contains it. This does not make it syntactically equivalent and it would not be proper for the compiler to create a new variable for you for this purpose.

    We could rewrite the above as this, introducing a variable :

    procedure TDemo.Bar(var x: Integer);
    var
      y : integer;
    begin
      y := x;
      FFoo := procedure
              begin
                WriteLn(y);
              end;
    end;
    

    And this is allowed, but the expected behaviour would be very different. In the case of capturing x (not allowed), we would expect that FFoo would always write the current value of whatever variable was passed in as argument x to Bar, regardless of where or when it may have been changed in the interim. We would also expect that the closure would keep the variable alive even after it fell out of whatever scope created it.

    In the latter case, however, we expect FFoo to output the value of y, which is the value of the variable x as it was the last time Bar was called.


    Returning to the first example, consider this :

    program Project1;    
    {$APPTYPE CONSOLE}    
    type
      TFoo = reference to procedure;    
      TDemo = class
      private
        FFoo : TFoo;
        FBar : string;
        procedure Foo;
      public
        class function Construct(): TDemo;
      end;
    
    procedure TDemo.Foo;
    begin
      WriteLn('foo' + FBar);
    end;
    
    class function TDemo.Construct: TDemo;
    var
      LDemo : TDemo;
    begin
      result := TDemo.Create();
      LDemo := result;
      LDemo.FBar := 'bar';
      result.FFoo := LDemo.foo;
      LDemo := nil;
      result.FFoo();  // **access violation
    end;
    
    var
     LDemo:TDemo;
    begin
      LDemo := TDemo.Construct;
    end.
    

    Here it is clear with :

    result.FFoo := LDemo.foo;
    

    that we have not assigned a normal reference to the method foo beloning to the instance of TDemo stored in LDemo, but have actually captured the variable LDemo itself, not the value it contained at the time. Setting LDemo to nil afterwards naturally produces an access violation, even thought the object instance it referred to when the assignment was made is still alive.

    This is radically different behaviour than if we simply defined TFoo as a procedure of object instead of a reference to procedure. Had we done that instead, the above code works as one might naively expect (output foobar to the console).

    0 讨论(0)
  • 2021-01-01 21:08

    This is not a full fledged answer, rather notes to David's answer and to the topicstarter's question.

    Using answer mode for posting source snippets.

    class function TDemo.Construct: TDemo;
    begin
      Result := TDemo.Create();
      Result.FVar := Result.CompareInternal;
    end;
    
    class function TDemo.Construct: TDemo;
    var
      Demo: TDemo;
    begin
      Demo := TDemo.Create();
      Demo.FVar := Demo.CompareInternal;
      Result := Demo;
    end;
    

    Those both snippets use the same template:

    1. Create an object ( and memory management responsibilities attached to it )
    2. Tune and adjust the object
    3. Pass the object to outer world ( and m/m responsibilities with it )

    Sure, the p.2 here is just one single line, still

    1. It has a function call, which might be error prone. Twice so if function would be virtual overrode by inheriting subclasses.
    2. Patterns are to work not in the easiest situations, but rather in hardest ones.

    So I think we should assume that p.2 has risk of runtime error, risk of exception thrown. Then it is a textbook memory leak. The local function still holds the memory management responsibilities, since it did not passed the result outside. But it also does not fulfill the required cleanup.

    From my perspective the correct pattern - and the one giving one more incentive to using a dedicated local variable than mere Result/Result compiler confusion - should be

    class function TDemo.Construct: TDemo;
    var
      Demo: TDemo;
    begin
    
      Demo := TDemo.Create();  // stage 1: creating an object
      try                      // stage 1: accepting M/M responsibilities
    
         Demo.FVar := Demo.CompareInternal; // stage 2: tuning and facing
         // Demo.xxx := yyy;                //   ...potential risks of exceptions
         // Demo.Connect(zzz);  etc
    
         Result := Demo;   // stage 3: passing the object outside
         Demo := nil;      // stage 3: abandoning M/M responsibilities
         //  function exit should follow this line immediately, without other fault-risky statements
      finally
        Demo.Free;         // proceeding with M/M in case of faults in stage 2
      end;
    end;                   // stage 3: passing the object outside - immediately after the assignments!
    

    UPD: ventiseis: And as a side node: I would try to instantiate the configurated comparer TDemo only once. The comparison function should be a stateless function

      TDemo = class(TComparer<string>)
      private
        class var FVar: TConstFunc<string, string, Integer>;
       // function CompareInternal(const L, R: string): Integer; STATIC; // also possible
        class constructor InitComp;
      ...
      end;
    
      // would only be called once, if the class is actually used somewhere in the project
      class constructor TDemo.InitComp; 
      begin
        FVar := function(const L, R: string): Integer
        begin
          Result := StrToInt(R) - StrToInt(L)
        end 
      end;
    
    0 讨论(0)
  • 2021-01-01 21:13

    The compiler error on my English Delphi reads:

    [dcc32 Error] E2555 Cannot capture symbol 'Result'

    This is due to a defective design. There's no reason for any variable capture to be taking place here at all. The right hand side of the assignment is an instance method rather than an anonymous method. But the compiler handles that by wrapping the method in an anonymous method. The compiler translates

    Result.FVar := Result.CompareInternal;
    

    to

    Result.FVar := 
      function(const Arg1, Arg2: string): Integer
      begin
        InnerResult := OuterResult.CompareInternal(Arg1, Arg2);
      end;
    

    Leaving aside the confusion over the two separate result variables, the compiler rejects this because the outer result variable is not a local, it's a var parameter. And so cannot be captured.

    But the whole design is wrong in my view. There's no need for any variable capture. When you write Result.CompareInternal you intend to refer to a normal of object method. With a better design, the compiler would allow this assignment without creating an anonymous method.

    You can work around the problem like this:

    class function TDemo.Construct: TDemo;
    var
      Demo: TDemo;
    begin
      Demo := TDemo.Create();
      Demo.FVar := Demo.CompareInternal;
      Result := Demo;
    end;
    

    Here the local variable Demo can be captured.

    Or as I would suggest, like this:

    program ConsoleDemo1;
    
    {$APPTYPE CONSOLE}
    
    uses
      Generics.Defaults,
      System.SysUtils;
    
    type
      TConstFunc<T1, T2, TResult> = reference to function(const Arg1: T1; 
        const Arg2: T2): TResult;
    
      TDemo = class(TComparer<string>)
      private
        FVar: TConstFunc<string, string, Integer>;
        function CompareInternal(const L, R: string): Integer;
      public
        constructor Create;
        function Compare(const L, R: string): Integer; override;
      end;
    
    constructor TDemo.Create;
    begin
      inherited;
      FVar := CompareInternal;
    end;
    
    function TDemo.Compare(const L, R: string): Integer;
    begin
      Result := FVar(L, R);
    end;
    
    function TDemo.CompareInternal(const L, R: string): Integer;
    begin
      Result := AnsiCompareStr(L, R);
    end;
    
    end.
    
    0 讨论(0)
提交回复
热议问题