Почему я не могу назначить ссылку на мою функцию на соответствующую переменную? E2555 поднят

Я пытаюсь создать пользовательский сопоставитель, который позволяет назначить функцию сравнения внутреннему полю. Чтобы облегчить создание сопоставления, я попытался добавить конструктор-подобную функцию класса Construct, которая инициализирует компаратор.

Теперь, если я попытаюсь скомпилировать следующий пример, компилятор отобразит

[dcc32 Fehler] ConsoleDemo1.dpr(37): E2555 Символ "Результат" не может быть отслежен

У меня есть следующий пример-код:

program ConsoleDemo1;

{$APPTYPE CONSOLE}
{$R *.res}

uses
  Generics.Collections, 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
    class function Construct(): TDemo;
    function Compare(const L, R: string): Integer; override;
  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;

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

end.

Ответы

Ответ 1

Я не думаю, что это ошибка. Критически, вы определили TConstFunc как анонимный тип метода. Они управляются, подсчитываются ссылки, очень специальные типы, которые сильно отличаются от обычных методов объектов. По магии компилятора они обычно совместимы с назначением, но с несколькими важными оговорками. Рассмотрим более краткий:

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.

Это также создает ту же ошибку компилятора (E2555). Поскольку метод member является типом procedure of object (object method), и вы назначаете его типу reference to procedure (анонимный метод), это эквивалентно (и я подозреваю, что компилятор расширяет это как):

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

Компилятор не может напрямую ссылаться на ссылку на метод (поскольку они имеют разные типы), и поэтому (я думаю) должен обернуть его анонимным методом, который неявно требует захвата переменной result. Возвращаемые значения функции не могут быть записаны анонимными методами, однако - могут использоваться только локальные переменные.

В вашем случае (или, действительно, для любого типа function) эквивалент не может быть даже выражен из-за анонимной оболочки, скрывающей переменную result, но мы можем представить ее теоретически как:

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;

Как показал Дэвид, введение локальной переменной (которая может быть записана) является одним из правильных решений. В качестве альтернативы, если вам не нужен тип TConstFunc для анонимности, вы можете просто объявить его как обычный метод объекта:

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


Другой пример, когда попытка захвата result терпит неудачу:

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.

Основная причина, по которой это не работает, состоит в том, что возвращаемое значение метода эффективно является параметром var, а анонимное закрытие захватывает переменные, а не значения. Это критический момент. Аналогично, это также не допускается:

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 Невозможно записать символ 'x'

В случае ссылочного типа, как и в исходном примере, вам действительно интересно только захватить значение ссылки, а не переменную, которая ее содержит. Это не делает его синтаксически эквивалентным, и для этой цели было бы нецелесообразно создавать для вас новую переменную.

Мы могли бы переписать вышеупомянутое как это, введя переменную:

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

И это разрешено, но ожидаемое поведение будет совсем другим. В случае захвата x (не разрешено) мы ожидаем, что FFoo всегда будет записывать текущее значение любой переменной, переданной в качестве аргумента x в Bar, независимо от того, где и когда были изменены в промежуточный период. Мы также ожидаем, что закрытие сохранит переменную в живых даже после того, как она выпадет из какой бы то ни было возможности ее создания.

В последнем случае мы ожидаем, что FFoo выведет значение y, которое является значением переменной x, как это было в последний раз, когда был вызван Bar.


Возвращаясь к первому примеру, рассмотрим следующее:

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.

Здесь видно, что:

result.FFoo := LDemo.foo;

что мы не назначили нормальную ссылку на метод foo, который ссылается на экземпляр TDemo, хранящийся в LDemo, но фактически захватил переменную LDemo, а не значение , которое оно содержало в то время. Установка LDemo в nil впоследствии естественным образом приводит к нарушению доступа, даже если предположить, что экземпляр объекта, на который он ссылался при выполнении задания, все еще жив.

Это радикально другое поведение, чем если бы мы просто определяли TFoo как procedure of object вместо reference to procedure. Если бы мы это сделали, приведенный выше код работает так, как можно было бы наивно ожидать (вывод foobar на консоль).

Ответ 2

Ошибка компилятора на моем английском языке Delphi гласит:

[ошибка dcc32] E2555 Невозможно записать символ "Результат"

Это связано с дефектным дизайном. Нет никаких оснований для того, чтобы какой-либо захват переменной здесь происходил вообще. Правая часть задания - это метод экземпляра, а не анонимный метод. Но компилятор справляется с этим, обернув метод анонимным методом. Компилятор переводит

Result.FVar := Result.CompareInternal;

к

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

Оставив в стороне путаницу по двум отдельным переменным результата, компилятор отвергает это, потому что внешняя переменная результата не является локальным, это параметр var. И поэтому невозможно захватить.

Но вся конструкция ошибочна, на мой взгляд. Нет необходимости в каком-либо захвате переменной. Когда вы пишете Result.CompareInternal, вы намерены ссылаться на обычный метод of object. С лучшей конструкцией компилятор разрешил бы это назначение без создания анонимного метода.

Вы можете решить эту проблему следующим образом:

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

Здесь локальная переменная Demo может быть захвачена.

Или, как я бы сказал, вот так:

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.

Ответ 3

Это не полноценный ответ, скорее заметки для ответа Дэвида и вопрос о главном.

Использование режима ответа для публикации исходных фрагментов.

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;

Эти оба фрагмента используют один и тот же шаблон:

  • Создать объект (и связанные с ним обязанности управления памятью)
  • Настройка и настройка объекта
  • Передайте объект во внешний мир (и обязанности m/m с ним)

Конечно, p.2 здесь всего одна строка, еще

  • Он имеет вызов функции, который может быть подвержен ошибкам. Дважды, поэтому, если функция будет виртуальной перегрузкой, наследуя подклассы.
  • Шаблоны должны работать не в самых простых ситуациях, а в самых сложных.

Поэтому я думаю, что мы должны предположить, что p.2 имеет риск ошибки во время выполнения, риск возникновения исключения. Тогда это утечка памяти учебника. Локальная функция по-прежнему сохраняет обязанности управления памятью, поскольку она не передала результат снаружи. Но он также не выполняет требуемую очистку.

С моей точки зрения правильный шаблон - и тот, который дает еще один стимул использовать выделенную локальную переменную, чем просто путаницу компилятора Result/Result, должен быть

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: И как сторона node: я попытался бы создать экземпляр конфигурационного компаратора TDemo только один раз. Функция сравнения должна быть функцией без сохранения

  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;