Pytanie Dlaczego nie mogę przypisać odwołania do funkcji do odpowiedniej zmiennej? E2555 jest podniesiony


Próbuję zbudować niestandardowego porównywalnika, który umożliwia przypisanie funkcji porównania do wewnętrznego pola. Aby ułatwić tworzenie porównywania, próbowałem dodać funkcję klasy podobną do konstruktora Construct który inicjuje porównywarkę.

Teraz, jeśli spróbuję skompilować poniższy przykład, wyświetli się kompilator

[dcc32 Fehler] ConsoleDemo1.dpr (37): E2555 Symbol "Wynik" nie może być śledzony

Mam następujący kod przykładowy:

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.

14
2018-01-08 12:46


pochodzenie


Przypuszczam, że dzieje się tak z powodu jakiejś magii kompilatora. Jeśli używam TConstFunc<T1, T2, TResult> = function(const Arg1: T1; const Arg2: T2): TResult of object;, kod kompiluje się i działa. - ventiseis


Odpowiedzi:


Nie sądzę, że to błąd. Krytycznie, zdefiniowałeś TConstFunc jako anonimowa metoda rodzaj. Są one zarządzane, zliczanie odwołań, bardzo specjalne typy, które różnią się od metod zwykłych obiektów. Według magii kompilatora są zwykle kompatybilne z przypisaniem, ale z kilkoma ważnymi zastrzeżeniami. Rozważ bardziej zwięzłe:

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.

Powoduje to również ten sam błąd kompilatora (E2555). Ponieważ metoda członka jest procedure of object (metoda obiektu), a ty przypisujesz go do reference to procedure (metoda anonimowa), jest to równoważne (i podejrzewam, że kompilator rozwija to jako):

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

Kompilator nie może bezpośrednio przypisać referencji do metody (ponieważ są one różnych typów) i dlatego (jak sądzę) musi zawijać ją w anonimową metodę, która domyślnie wymaga przechwytywania result zmienna. Zwracane wartości funkcji nie mogą być przechwytywane przez metody anonimowejednak - mogą tylko zmienne lokalne.

W twoim przypadku (lub w rzeczywistości dla każdego function type), odpowiednik nie może nawet zostać wyrażony ze względu na ukrywanie anonimowego opakowania result zmienne, ale możemy wyobrazić sobie to samo w teorii, jak:

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;

Jak pokazał David, wprowadzenie zmiennej lokalnej (która może zostać przechwycona) jest jednym prawidłowym rozwiązaniem. Ewentualnie, jeśli nie potrzebujesz TConstFunc typ anonimowy, możesz po prostu zadeklarować go jako zwykłą metodę obiektową:

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


Inny przykład próby przechwycenia result zawiedzie:

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.

Podstawowym powodem, dla którego to nie działa, jest fakt, że wartość zwracana przez metodę jest efektywna var parametr i przechwytuje anonimowe zamknięcie zmienne, nie wartości. To jest punkt krytyczny. Podobnie jest to niedozwolone:

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.

[błąd dcc32] Project1.dpr (18): E2555 Nie można uchwycić symbolu "x"

W przypadku typu referencyjnego, jak w oryginalnym przykładzie, naprawdę interesuje Cię tylko przechwytywanie wartość odniesienia, a nie zmienna który to zawiera. Nie powoduje to, że jest to syntaktycznie równoważny i kompilator nie powinien tworzyć dla niego nowej zmiennej do tego celu.

Możemy przepisać powyższe w ten sposób, wprowadzając zmienną:

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

Jest to dozwolone, ale oczekiwane zachowanie będzie zupełnie inne. W przypadku przechwytywania x (niedozwolone), spodziewalibyśmy się tego FFoo zawsze zapisywałby bieżącą wartość dowolnej zmiennej, która została przekazana jako argument xdo Bar, niezależnie od miejsca i czasu, w którym mogła zostać zmieniona w międzyczasie. Spodziewalibyśmy się również, że zamknięcie utrzyma zmienną przy życiu nawet po tym, jak wypadłaby ona z jakiegokolwiek zakresu.

W tym drugim przypadku spodziewamy się FFoo aby wyprowadzić wartość y, która jest wartością zmiennej x tak jak było ostatni raz Bar został wywołany.


Wracając do pierwszego przykładu, rozważ to:

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.

Tutaj jest jasne:

result.FFoo := LDemo.foo;

że nie przypisaliśmy normalnego odniesienia do metody foo beloning do instancja z TDemo przechowywane w LDemo, ale faktycznie uchwycił zmienna  LDemo się, a nie wartość zawarte w tym czasie. Oprawa LDemodo nil następnie naturalnie powoduje naruszenie zasad dostępu, nawet jeśli instancja obiektu, do której się odwołała, gdy zadanie zostało wykonane, wciąż żyje.

To jest radykalnie inne zachowanie, niż po prostu zdefiniowaliśmy TFoo jak procedure of object zamiast reference to procedure. Gdybyśmy to zrobili, powyższy kod działa tak, jak można naiwnie oczekiwać (wyjście foobar do konsoli).


11
2018-01-08 16:33



Myślę, że twoja analiza, w jaki sposób kompilator opakowuje wywołanie metody jako metodę anon, jest natychmiastowa. Jeśli jednak kompilator może stwierdzić, że musi to zrobić, to powinien być w stanie wyobrazić sobie, że musi wprowadzić zmienną, którą można uchwycić, moim zdaniem. - David Heffernan
@DavidHeffernan Można by tak pomyśleć, zgadzam się. Może jednak zachodzić coś subtelnego. Muszę założyć, że istnieje dobry powód, dla którego wyniki funkcji nie mogą zostać przechwycone (ogólnie). Wprowadzanie zmiennej w ten sposób nie zawsze jest podejściem bez skutków ubocznych ... Nie jestem pewien. - J...
Przyczyną zwracania wartości nie może być przechwytywanie jest to, że nie są one zadeklarowane w funkcji. Są one implementowane jako parametry var, przekazywane po wszystkich innych parametrach. - David Heffernan
Ja robię. Myślę, że wyjaśniłeś szczegóły implementacji za ograniczeniem, które zrobiłeś bardzo dobrze. Zasługujesz na więcej niż moje jedyne głosowanie. Jest to jednak szczegół wdrażania. Jest to metoda obiektu. Nie ma dwuznaczności. Nie powinno być zmiennego przechwytywania. Kompilator powinien to zrobić. - David Heffernan
@Graymatter Nie. Nie ma takiej potrzeby zmienna zdobyć. Gdy metoda jest przypisana do ref proc, przydział może być wartość instancja i kod. Podobnie jak w przypadku of object typ metody. Projektanci wybrali inną drogę, ale nie jest to jedyny sposób. W pierwszym akapicie tej odpowiedzi znajduje się wzmianka magia. Sugeruję, że należy użyć innej magii. Oczywiście na to jest za późno. Każdy raport o błędzie zostanie zamknięty jako zgodnie z projektem. Po prostu uważam, że wybór projektu był kiepski. - David Heffernan


Błąd kompilatora na moim angielskim Delphi brzmi:

[błąd dcc32] E2555 Nie można uchwycić symbolu "Wynik"

Jest to spowodowane wadliwym projektem. Nie ma powodu, aby w ogóle odbywało się tutaj przechwytywanie zmiennych. Prawa strona przypisania to metoda instancji, a nie metoda anonimowa. Ale kompilator obsługuje to, owijając metodę w anonimową metodę. Kompilator tłumaczy

Result.FVar := Result.CompareInternal;

do

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

Pomijając zamieszanie związane z dwiema oddzielnymi zmiennymi wynikowymi, kompilator odrzuca to, ponieważ zewnętrzna zmienna wyniku nie jest lokalna, jest to var parametr. I tak nie można go złapać.

Ale moim zdaniem cały projekt jest błędny. Nie ma potrzeby przechwytywania zmiennych. Kiedy piszesz Result.CompareInternal masz zamiar odnosić się do normalnego of object metoda. Dzięki lepszemu projektowi kompilator pozwoliłby na to zadanie bez tworzenia anonimowej metody.

Możesz obejść problem w ten sposób:

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

Tutaj jest zmienna lokalna Demo można złapać.

Lub jak sugerowałbym, w ten sposób:

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.

10
2018-01-08 12:52



Przepraszam, przetłumaczyłem komunikat o błędzie na pół. - ventiseis
W porządku. Przy okazji, niesamowite MCVE. Automatycznie głosuj na mnie za MCVE w ten sposób. Gdyby tylko wszystkie pytania były tak postawione !! - David Heffernan
Napisałem to, aby samemu zrozumieć to zachowanie. Jeśli skompiluję mój oryginalny projekt, czekam dwie minuty i mam 2 GB pamięci RAM ... - ventiseis
Och, muzyka do moich uszu !! Gdyby tylko wszyscy myśleli w ten sposób, tak wielu ludzi byłoby w stanie samodzielnie rozwiązać znacznie więcej problemów! Co jest lepsze dla wszystkich. - David Heffernan
FWIW Rozwiązałem tak na XE4: class function TDemo.Construct: TDemo; begin Result := TDemo.Create; Result.AssignInternalComparer; end; i procedure TDemo.AssignInternalComparer; begin FVar := CompareInternal; end; - fantaghirocco


To nie jest pełna odpowiedź, raczej uwagi do odpowiedzi Davida i pytania do tematu.

Używanie trybu odpowiedzi do publikowania fragmentów kodu źródłowego.

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;

Oba te fragmenty korzystają z tego samego szablonu:

  1. Utwórz obiekt (i związane z nim obowiązki zarządzania pamięcią)
  2. Dostrój i dostosuj obiekt
  3. Przekaż obiekt do świata zewnętrznego (i związane z nim obowiązki m / m)

Jasne, że p.2 jest tu tylko jedną linią

  1. Ma wywołanie funkcji, które może być podatne na błędy. Dwa razy, więc jeśli funkcja byłaby wirtualnie nadpisana przez dziedziczenie podklas.
  2. Wzory mają działać nie w najłatwiejszych sytuacjach, ale raczej w najcięższych.

Myślę więc, że powinniśmy założyć, że p.2 ma ryzyko błędu wykonania, ryzyko wyjątku. Następnie jest to wyciek pamięci podręcznika. Funkcja lokalna nadal ma obowiązki związane z zarządzaniem pamięcią, ponieważ nie przekazała wyniku na zewnątrz. Ale także nie spełnia wymaganych porządków.

Z mojej perspektywy prawidłowy wzór - i ten, który daje jeszcze jedną zachętę do używania dedykowanej zmiennej lokalnej niż zwykły Result/Result zamieszanie kompilatora - powinno być

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: I jako węzeł boczny: Próbowałbym utworzyć instancję konfigurowanego porównywalnika TDemo tylko raz. Funkcja porównania powinna być funkcją bezpaństwową

  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;

2
2018-01-09 10:40



Zgadzam się, że jeśli zostaną zgłoszone jakiekolwiek wyjątki, powstanie przeciek pamięci. Nigdy nie wymyśliłem tego rodzaju próby w sam koniec - w zasadzie to fajny pomysł, ale myślę, że istnieje pewne niebezpieczeństwo, aby zapomnieć Demo := nil; . Dlaczego nie używać try .. except on oE: FreeAndNil(Demo); raise oE; end;? - ventiseis
I jako węzeł boczny: Próbowałbym utworzyć instancję konfigurowanego porównywalnika TDemo tylko raz. Funkcja porównania powinna być funkcją bezpaństwową, pracującą z dowolną listą lub tablicą, więc nie ma żadnej korzyści, aby tworzyć i niszczyć porównywarki. - ventiseis
@ventiseis, które można napisać prościej, ...except Demo.Free; raise; end. Odpowiedź brzmi dosłownie tak samo: "istnieje pewne niebezpieczeństwo" zapomnienia o ponownym podniesieniu wyjątku. Również zapomniałeś o początku gry :) Więc - wybierz swoją truciznę, są podobne. Również mój fragment udostępnia funkcję jednej dodatkowej opcji (na przykład w rodzaju funkcji Find-Or-Nil, np TDataSet.FindField), aby usunąć tymczasowy obiekt i powrócić nil bez zgłaszania wyjątków. - Arioch 'The
@ventiseis następnie TDemo.FVar może powinien być class variable przypisany w class constructor - Arioch 'The
@ventiseis zwraca RÓŻNE KLASY, tak jak Delphi TEncoding który został zaprojektowany również po .Net (a to nieprawda, że ​​IMHO jako native Delphi nie jest oparte na GC! TEncoding powinien być interfejsem, a nie klasą!) Tak jak z TEncoding masz zrobić function TDemo.Compare(const L, R: string): Integer; VIRTUAL; ABSTRACT;, musisz zrobić kilka zajęć TDemo1, TDemo2, TDemo3 z innym function TDemoNNN.Compare(const L, R: string): Integer; override; wdrożenia. A następnie zwróć różne KLASY, jak to robi przykład Twojej DotNet. - Arioch 'The