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

后端 未结 3 1295
情深已故
情深已故 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 = 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).

提交回复
热议问题