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

Odpowiedz Nowy wątek
2018-09-11 07:29
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.
edytowany 7x, ostatnio: furious programming, 2018-09-12 15:39

Pozostało 580 znaków

2018-09-12 11:19
0

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

Pozostało 580 znaków

2018-09-12 11:25
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

Pozostało 580 znaków

2018-09-12 11:32

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.

edytowany 4x, ostatnio: Artur Protasewicz, 2018-09-13 04:59

Pozostało 580 znaków

2018-09-13 14:42
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.


edytowany 4x, ostatnio: furious programming, 2018-09-13 14:44

Pozostało 580 znaków

2018-09-13 16:33
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.

edytowany 3x, ostatnio: Artur Protasewicz, 2018-09-13 16:54

Pozostało 580 znaków

2018-09-14 16:38
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.


edytowany 2x, ostatnio: furious programming, 2018-09-14 16:39

Pozostało 580 znaków

2018-09-16 09:57
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.

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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