Niejawne wywołanie przeciążonej funkcji write we własności - co z wydajnością?

0

To kolejne podejście do stworzenia interpretera języka Atari Action!
Tym razem inicjuję obiekt zawierający wartość całkowitą i automatycznie, w zależności od zakresu tej wartości ustawiam typ całkowity.

Wartość jest podawana za pomocą ShortString, gdyż intepreter będzie interpretował tekst, czyli String i dokona ekstrakcji wartości całkowitej z tego tekstu.

Natomiast potem, po odczytaniu tekstu będzie mi potrzebna liczba całkowita np. do ustawienia zakresu pętli.
Napisałem kod przy pomocy którego ta zamiana następuje wraz z ustawieniem typu całkowitego Action! i dzieje się w nim coś ciekawego.

Główny program wygląda tak:

var
  intVar1: TACTION_INT;
begin
  try
    intVar1 := TACTION_INT.Create;
    intVar1.SetV('-1225'); // zapis - wartość w postaci String
    WriteLn(intVar1.Value.value_); // odczyt - wartość w postaci Integer
    WriteLn;
    Write('Press key Enter');
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Natomiast program po jawnym wywołaniu SetV(ShrotString) niejawnie wywołuje SetV(TACTION_INT_RECORD).
Moje przeświadczenie o tym bierze się z tego, że program pozbawiony fragmentu SetV(TACTION_INT_RECORD) się nie kompiluje.

Czy coś tu się nie dzieje 2 razy i czy nie będzie miało wpływu na wydajność?
Nadmienię, że inicjowanie za pomocą rekordu jest przewidywane na dalszym etapie, ale według mnie już tutaj działa.

//Delphi 10.2 Community Edition
program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Classes;

type
  TACTION_INT_TYPE =
  (
    BYTE,
    INT,
    CARD,
    OTHER
  );

  TACTION_INT_RECORD = record
    type_: TACTION_INT_TYPE;
    value_: Integer;
  end;

  TACTION_INT = class
  private
    FValue: TACTION_INT_RECORD;
    FInt: Integer;
    function  IsInt(AStr: ShortString; out AInt: Integer): Boolean;
    procedure SetTypeAndValue(AStr: ShortString; out AInt: Integer);
    function  GetV: TACTION_INT_RECORD;
    procedure SetV(AStr: ShortString); overload;            {1-jawne}
    procedure SetV(ARec: TACTION_INT_RECORD); overload;     {2-niejawne}
  public
    property  Value: TACTION_INT_RECORD read GetV write SetV;
  end;

function TACTION_INT.IsInt(AStr: ShortString; out AInt: Integer): Boolean;
begin
  Result := Integer.TryParse(AStr, AInt);
end;

procedure TACTION_INT.SetTypeAndValue(AStr: ShortString; out AInt: Integer);
begin
  if IsInt(AStr, AInt) then
  begin
    if (AInt >= 0) and (AInt <= 255) then
      FValue.type_ := BYTE
    else
    if (AInt >= -32768) and (AInt <= 32767) then
      FValue.type_ := INT
    else
    if (AInt >= 0) and (AInt <= 65535) then
      FValue.type_ := CARD
    else
      FValue.type_ := OTHER;
  end
  else
    FValue.type_ := OTHER;
  if FValue.type_ <> OTHER then
    FValue.value_ := AInt
  else
    FValue.value_ := 0;
end;

function TACTION_INT.GetV: TACTION_INT_RECORD;
begin
  Result := FValue;
end;

procedure TACTION_INT.SetV(AStr: ShortString);
var
  AInt: Integer;
begin
  SetTypeAndValue(AStr, AInt);
end;

procedure TACTION_INT.SetV(ARec: TACTION_INT_RECORD);
var
  AStr: ShortString;
  AInt: Integer;
begin
  AInt := ARec.value_;
  AStr := IntToStr(AInt);
  SetTypeAndValue(AStr, AInt);
end;

var
  intVar1: TACTION_INT;
begin
  try
    intVar1 := TACTION_INT.Create;
    intVar1.SetV('-1225');
    WriteLn(intVar1.Value.value_);
    WriteLn;
    Write('Press key Enter');
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
1

Metoda SetTypeAndValue będzie działać randomowo – przekazujesz do niej parametr przez pustą referencję (jako out) i używasz go w instrukcjach warunkowych (do odczytu wartości). Powinieneś otrzymać kupę ostrzeżeń podczas kompilacji, bo wartość tego parametru jest niezdefiniowana. Rozwiąż ten problem, a resztę się później sprawdzi i ew. poprawi.

Nie rozumiem też dlaczego używasz typu ShortString – jakie są z tego benefity?

Jeśli chcesz stosować klasy do opisu składowych analizowanego kodu, to polecam używać ich do przechowywania danych na temat całych tokenów (idziesz w dobrym kierunku). Wydziel podstawową, wspólną logikę do klasy bazowej, a dedykowaną logikę dla tokenów skonkretyzowanych, umieść w klasach z niej dziedziczących.

Część logiki warto też wydzielić do prostych helperów lub metod statycznych, abyś nie musiał dla prostych operacji ciągle tworzyć wymaganych instacji klas. Ewentualnie stwórz sobie osobną klasę walidatora danych, a klasy tokenów używaj tylko jako pojemniki na dane.


Znów widzę, że rzuciłeś w kąt uniwersalną konwencję nazewnictwa elementów kodu, przez co trudniej go analizować.

Dużych liter w stylu SNAKE_CASE używa się wyłącznie do stałych, i to też nie zawsze. Pozostałe identyfikatory powinny być zgodne z PascalCase z ustaloną dużą literką prefiksu, a wartości enumów z camelCase, połaczonym z notacją węgierską. Przy czym nigdy nie używamy słów zarezerwowanych, a Ty właśnie to zrobiłeś – używasz słówka type. Jako zamiennik możesz zastosować kind, co nie będzie kolidowało ze słowami zarezerwowanymi i kluczowymi.

Jeśli mowa o enumy – przyjęło się, że pierwsza wartość typu wyliczeniowego jest zarezerwowana i oznacza wartość niezdefiniowaną, nieznaną, inną czy jakkolwiek to ujmując szczególną (wyjątkową). Ty taką posiadasz – Other – więc ona powinna być pierwsza:

type
  TActionIntKind = (aikOther, aikByte, aikInteger, aikCardinal);

lub:

type
  TActionIntKind = (aikUnknown, aikByte, aikInteger, aikCardinal);

Notacja węgierska nie tylko z automatu informuje, że dany identyfikator to właśnie enum, ale też pomaga unikać kolizji. Ty użyłeś BYTE jako nazwy enumu, co przy case-insensitive kodu i wyłączonym {$SCOPEDENUMS} może być problematyczne.

0

Pozmieniałem wg. twoich zaleceń (mam taką nadzieję). Usunąłem też fragment Action, żeby go nie kojarzyć z TAction występującym w Delphi.

//Delphi 10.2 Community Edition
program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils, System.Classes;

type
  TIntKind = (ikUnknown, ikByte, ikInteger, ikCardinal);

  TIntRec = record
    IntKind: TIntKind;
    IntValue: Integer;
  end;

  TInt = class
  private
    FValue: TIntRec;
    function  IsInt(AStr: String; out AInt: Integer): Boolean;
    procedure SetTypeAndValue(AStr: String);
    function  GetValue: TIntRec;
    procedure SetValue(AStr: String); overload;            {1-jawne}
    procedure SetValue(ARec: TIntRec); overload;           {2-niejawne}
  public
    property  Value: TIntRec read GetValue write SetValue;
  end;

function TInt.IsInt(AStr: String; out AInt: Integer): Boolean;
begin
  Result := Integer.TryParse(AStr, AInt);
end;

procedure TInt.SetTypeAndValue(AStr: String);
var
  AInt: Integer;
begin
  if IsInt(AStr, AInt) then
  begin
    if (AInt >= 0) and (AInt <= 255) then
      FValue.IntKind := ikByte
    else
    if (AInt >= -32768) and (AInt <= 32767) then
      FValue.IntKind := ikInteger
    else
    if (AInt >= 0) and (AInt <= 65535) then
      FValue.IntKind := ikCardinal
    else
      FValue.IntKind := ikUnknown;
  end
  else
    FValue.IntKind := ikUnknown;
  if FValue.IntKind <> ikUnknown then
    FValue.IntValue := AInt
  else
    FValue.IntValue := 0;
end;

function TInt.GetValue: TIntRec;
begin
  Result := FValue;
end;

procedure TInt.SetValue(AStr: String);
begin
  SetTypeAndValue(AStr);
end;

procedure TInt.SetValue(ARec: TIntRec);
var
  AStr: String;
  AInt: Integer;
begin
  AInt := ARec.IntValue;
  AStr := IntToStr(AInt);
  SetTypeAndValue(AStr);
end;

var
  intVar1: TInt;
begin
  try
    intVar1 := TInt.Create;
    intVar1.SetValue('-1225');
    WriteLn(intVar1.Value.IntValue);
    WriteLn;
    Write('Press key Enter');
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
0
furious programming napisał(a):

Jeśli chcesz stosować klasy do opisu składowych analizowanego kodu, to polecam używać ich do przechowywania danych na temat całych tokenów (idziesz w dobrym kierunku). Wydziel podstawową, wspólną logikę do klasy bazowej, a dedykowaną logikę dla tokenów skonkretyzowanych, umieść w klasach z niej dziedziczących.

Część logiki warto też wydzielić do prostych helperów lub metod statycznych, abyś nie musiał dla prostych operacji ciągle tworzyć wymaganych instacji klas. Ewentualnie stwórz sobie osobną klasę walidatora danych, a klasy tokenów używaj tylko jako pojemniki na dane.

Tego jeszcze nie ma. Na razie starałem się poprawić czytelność. I usunąłem błąd z parametrem procedury SetTypeAndValue.

0

Nie mam pojęcia dlaczego próbujesz rozpoznać typ zmiennej po jej zawartości skoro Action! jest silnie typowany i typ zmiennej masz wprost podany.

Przechodząc do konkretów to jeśli sobie zadeklaruję
CARD c = [123] (chyba tak się deklaruje i inicjuje zmienną) to Twój kod zaklasyfikuje 123 jako BYTE a nie CARD

BTW ja cały czas staram się dostrzec sens w Twoim kodzie i nie potrafię :P. Owszem on się kompiluje, robi to co zapewne w założeniach miał ale albo dla mnie te założenia są wielką niewiadomą albo operujemy na całkiem innym poziomie abstrakcji.

0

Trochę popracowałem. Oto kod:

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  System.Classes;

type
  TIntKind = (ikUnknown, ikByte, ikInteger, ikCardinal);

  TIntRec = record
    IntKind: TIntKind;
    IntValue: Integer;
  end;

  TInt = class
  private
    FValue:   TIntRec;
    function  IsInt(AStr: String; out AInt: Integer): Boolean;
    function  Validate(AKind: String; AInt: String; out AOutKind: TIntKind; out AOutInt: Integer): Boolean;
  public
    procedure SetValue(AKind: String; AInt: String); 
    function  GetValue: TIntRec;
    function  KindToStr(AKind: TIntKind): String;
  end;

function TInt.IsInt(AStr: String; out AInt: Integer): Boolean;
begin
  Result := Integer.TryParse(AStr, AInt);
end;

function TInt.Validate(AKind: String; AInt: String; out AOutKind: TIntKind; out AOutInt: Integer): Boolean;
begin
    AOutKind := ikUnknown;
    Result := false;
    if IsInt(AInt, AOutInt) then
    begin
      if (AKind = 'BYTE')and (AOutInt >= 0) and (AOutInt <= 255) then
        AOutKind := ikByte
      else
      if (AKind = 'INT') and (AOutInt >= -32768) and (AOutInt <= 32767) then
        AOutKind := ikInteger
      else
      if (AKind = 'CARD') and (AOutInt >= 0) and (AOutInt <= 65535) then
        AOutKind :=  ikCardinal;
    end;
    if AOutKind <> ikUnknown then
      Result := true;
end;

function TInt.GetValue: TIntRec;
begin
  Result := FValue;
end;

procedure TInt.SetValue(AKind: String; AInt: String);
var
  OutKind: TIntKind;
  OutInt: Integer;
begin
  if Validate(AKind, AInt, OutKind, OutInt) then
  begin
    FValue.IntKind := OutKind;
    FValue.IntValue := OutInt;
  end
  else
  begin
    FValue.IntKind := ikUnknown;
    FValue.IntValue := 0;
  end;
end;

function TInt.KindToStr(AKind: TIntKind): String;
begin
  case AKind of
    ikByte: Result := 'BYTE';
    ikInteger: Result := 'INT';
    ikCardinal: Result := 'CARD';
  else
    Result := 'UNKNOWN';
  end;
end;

function DebugInt(AVar: TInt): String;
begin
  Result := IntToStr(AVar.FValue.IntValue) + ' ' + AVar.KindToStr(AVar.FValue.IntKind);
end;

var
  Var1: TInt;
  Var2: TInt;
  Var3: TInt;
begin
  try
    Var1 := TInt.Create;
    Var1.SetValue('INT', '-1225');
    WriteLn(DebugInt(Var1));
    Var1.Free;
    Var2 := TInt.Create;
    Var2.SetValue('CARD', '-1225');
    WriteLn(DebugInt(Var2));
    Var2.Free;
    Var3 := TInt.Create;
    Var3.SetValue('CARD', '70000');
    WriteLn(DebugInt(Var3));
    Var3.Free;
    WriteLn;
    Write('Press key Enter');
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.
{
-1225 INT
0 UNKNOWN
0 UNKNOWN

Press key Enter
}
0

Mnie zainteresował pierwszy post, czy wywaliłeś property i program się nie kompilował?

0
Miang napisał(a):

Mnie zainteresował pierwszy post, czy wywaliłeś property i program się nie kompilował?

Na początku spróbowałem wywalić SetV oznaczone w komentarzu jako 2-niejawne.

procedure SetV(AStr: ShortString); overload;            {1-jawne}
procedure SetV(ARec: TACTION_INT_RECORD); overload;     {2-niejawne}

Property nie było wyrzucane:

property  Value: TACTION_INT_RECORD read GetV write SetV;

i bez tego drugiego SetV nie chciało się kompilować.

0

no to zobacz z jakiej funkcji property korzysta

0

Property operuje na typie TACTION_INT_RECORD. Nie rozumiem jednak jak SetV('-1225') wywołuje inną metodę, niż wywołana.

intVar1 := TACTION_INT.Create;
intVar1.SetV('-1225');
WriteLn(intVar1.Value.value_);
0

a w jaki sposób to robi, przypadkiem nie za pomocą funkcji którą wywaliłeś?

0

No tak właśnie robi - przez SetV(TACTION_INT_RECORD). A skąd wie SetV(ShortString), że ma użyć wspomnianej metody?

1

No ale jak wykasujesz tę metodę to kompilator się o nią upomni jest potrzebna ,dla tej property nawet jeśli w kodzie jej nie używasz, kompilator kompiluje cały kod a nie tylko funkcje których używasz

0

Myślę, że moim błędem na początku było pisanie metody SetV dla dwóch typów parametru. Lepiej zrobić do końca jedną metodę, dla jednego typu, w pełni funkcjonalną dla tego typu, a dopiero potem zabierać się za metodę z innym typem parametru i kolejno te metody testować. Przy czym nie powinny być zależne jedna od drugiej. Mogą mieć części wspólne, ale muszą być niezależne od siebie w sensie wzajemnych wywołań.

Ze wspomnianych metod wspólnych powinno wyłaniać się to:

furious programming napisał(a):

Jeśli chcesz stosować klasy do opisu składowych analizowanego kodu, to polecam używać ich do przechowywania danych na temat całych tokenów (idziesz w dobrym kierunku). Wydziel podstawową, wspólną logikę do klasy bazowej, a dedykowaną logikę dla tokenów skonkretyzowanych, umieść w klasach z niej dziedziczących.

Część logiki warto też wydzielić do prostych helperów lub metod statycznych, abyś nie musiał dla prostych operacji ciągle tworzyć wymaganych instacji klas. Ewentualnie stwórz sobie osobną klasę walidatora danych, a klasy tokenów używaj tylko jako pojemniki na dane.

Również ważne jest to, o czym nie wiedziałem. Nigdy nie spotkałem takiej sytuacji, bo zwykle staram się mieć pełną kontrolę nad przepływem (flow) kodu.

Miang napisał(a):

No ale jak wykasujesz tę metodę to kompilator się o nią upomni jest potrzebna ,dla tej property nawet jeśli w kodzie jej nie używasz, kompilator kompiluje cały kod a nie tylko funkcje których używasz

Te dwa powyższe stwierdzenia uznaję za rozwikłanie problemu.

0
Artur Protasewicz napisał(a):

Przy czym nie powinny być zależne jedna od drugiej. Mogą mieć części wspólne, ale muszą być niezależne od siebie w sensie wzajemnych wywołań.

Wzajemne wykorzystywanie metod w danej klasie nie jest czymś nadzwyczajnym czy niepoprawnym – mnóstwo tego typu przykładów możesz znaleźć w kodzie RTL/VCL (sam też z takich metod korzystam). Metody wykorzystujące siebie wzajemnie, pozwalają skrócić implementację i uniknąć duplikowania tej samej funkcjonalności (zasada DRY).

Popularnym przykładem jest ładowanie danych do pamięci. W danej klasie jest jedna metoda faktycznie ładująca dane, a kilka innych to po prostu wrappery z niej korzystające:

type
  TFoo = class(TObject)
  {..}
  public
    procedure LoadFromFile(const AFileName: String); overload;
    procedure LoadFromString(const AString: String); overload;
    procedure LoadFromStream(AStream: TStream); overload;
  end;

{..}

procedure TFoo.LoadFromFile(const AFileName: String);
var
  Stream: TFileStream;
begin
  Stream := TFileStream.Create(AFileName, fmOpenRead);
  try
    Self.LoadFromStream(Stream);
  finally
    Stream.Free();
  end;
end;

procedure TFoo.LoadFromString(const AString: String);
var
  Stream: TStringStream;
begin
  Stream := TStringStream.Create(AString);
  try
    Self.LoadFromStream(Stream);
  finally
    Stream.Free();
  end;
end;

procedure TFoo.LoadFromStream(AStream: TStream);
begin
  // docelowe ładowanie danych ze strumienia do pamięci
end;

Jak widać istnieje możliwość załadowania danych z trzech różnych źródeł (plik dyskowy, dowolny strumień lub ciąg znaków), ale fizyczne ładowanie danych zaimplementowane jest tylko w jednej metodzie. Jeśli zajdzie potrzeba zmodyfikowania kodu ładującego to wystarczy zmodyfikować tylko jedną metodę – reszta sama się dostosuje.

To tak na przyszłość, w razie gdybyś miał wątpliwości.

1

Nie jestem przeciwnikiem DRY. Chodzi jednak o to, że na pewnym etapie tworzenia kodu (wczesnym) jeszcze jest trudno wydzielić części wspólne, które to powinny trafiać w górę hierarchii rodzic-dziecko (czyli w stronę rodzica) (niekoniecznie w modelu obiektowym) i lepiej się ostrożnie posługiwać wspólnymi funkcjonalnościami (co też na pewno inaczej wygląda w przypadku programistów doświadczonych, ciągle w zawodzie, a inaczej w moim, bo u mnie był długo zastój, zwłaszcza w Delphi). Prawdę mówiąc często nawet długo będąc w zawodzie, najpierw dublowałem pewne funkcjonalności, aby potem je wydzielić znowu w stronę rodzica. Często tak jest, kiedy pisze się program od podstaw, a ja tak miałem zwykle. Oczywiście taki proces wyłaniania wspólnych funkcjonalności, jest bardziej czasochłonny, ale podobnie w całej teorii systemów - systemy się wyłania w kierunku bottom-up, co ma również lepiej sprawdzające się w praktyce odzwierciedlenie w sysemach informatycznych, czy też samych programach. Przykład programu w tym wątku pokazuje, że można wpaść w pułapkę, niejawnego wywoływania kodu i nie wiedzieć za bardzo jak to się dzieje i z jaką wydajnością. Takich sytuacji, chciałbym unikać, a sam jestem przykładem tego, że wyjście z wprawy, przypominające brak doświadczenia może na takie manowce sprowadzić. Tak samo wrappery są po stronie dziecka, a metoda wspólna wędruje w stronę rodzica. (niekoniecznie w ujęciu obiektowym, a nawet intuicyjnym). Więc jeśli o czymś piszę to o ostrożności w dążeniu do DRY.

1
Artur Protasewicz napisał(a):

Nie jestem przeciwnikiem DRY. Chodzi jednak o to, że na pewnym etapie tworzenia kodu (wczesnym) jeszcze jest trudno wydzielić części wspólne, które to powinny trafiać w górę hierarchii rodzic-dziecko […]

Oczywiście, zdaję sobie z tego sprawę – szczególnie wtedy, gdy kodu się nie projektowało i w głowie ma się jedynie ogólny zarys. Nie musisz od razu wiedzieć co i jak podzielić, możesz wydzielać wspólne funkcjonalności w trakcie implementacji kolejnych klas, co jakiś czas przeprowadzając refaktoryzację dotychczas napisanego kodu.

Przykład programu w tym wątku pokazuje, że można wpaść w pułapkę, niejawnego wywoływania kodu i nie wiedzieć za bardzo jak to się dzieje i z jaką wydajnością.

Owa pułapka nie znajduje się po stronie języka, bo tutaj nie ma żadnych niespodzianek czy UB. Każde zachowanie jest określone, każda kombinacja jest przewidziana i opisana w dokumentacji. Jeśli długo nie programowałeś w Delphi to powinieneś w niej szukać odpowiedzi.

Jeśli nie rozumiesz dlaczego coś nie działa prawidłowo to możesz też użyć debuggera, sprawdzając przepływ sterowania linijka po linijce. To pozwoli szybko namierzyć problemy i się ich pozbyć, a dokumentacja dostarczy konkretnych odpowiedzi dlaczego coś działa tak a nie inaczej.

0
furious programming napisał(a):

każda kombinacja jest przewidziana i opisana w dokumentacji. Jeśli długo nie programowałeś w Delphi to powinieneś w niej szukać odpowiedzi.
dokumentacja dostarczy konkretnych odpowiedzi dlaczego coś działa tak a nie inaczej.

Uczyłem się programowania w epoce, w której (w Polsce) o dokumentację było bardzo trudno, i być może dlatego szukam najpierw rozwiązania bez korzystania z niej. Dzisiaj jest inna epoka i prawdopodobnie prędzej otrzymam odpowiedź zadając pytanie, choćby na forum 4p lub przeszukując internet. Jeśli do dokumentacji zaliczyć książki różnych autorów, z których korzystałem, to chętniej do nich sięgałem, ale dziś też to poszło w stronę internetu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1