[Gelöst] Methoden ... Virtual? Abstract? Keins davon?

Für Fragen von Einsteigern und Programmieranfängern...
Antworten
Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

[Gelöst] Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Nimral »

Im Moment bin ich noch in der EInsteigerphase ... hinter jeder Antwort eine neue Frage. Ich belästige euch daher mal wieder.

Ich war mir unsicher, ob ich bestimmtes Objekt explizit freigeben muss oder nicht. Es ist über einige Ecken von TObject abgeleitet. Der Quellcode von TObjext schaut so aus:

Code: Alles auswählen

 
       TObject = class
       public
          constructor Create;
          destructor Destroy;virtual;
          ...
 
und später:
 
      constructor TObject.Create;
        begin
        end;
 
      destructor TObject.Destroy;
        begin
        end;
 
 


Hoppla. Warum ist der Destructor als "virtual" markiert, und der Constructor nicht? Für beide gibt es eine (leere) Implementierung für TObject. Beide habe ich schon gelegentlich überschrieben. Ich habe die Doku zu "virtual" befragt. Zu Lazarus fasste ich ins Leere. Zu Delphi gibt es eine vage Beschreibung http://www.delphibasics.co.uk/RTL.asp?Name=Virtual: "The Virtual directive allows a method in a class to be overriden (replaced) by a same named method in a derived class.". Wäre das so richtig, würde man wohl auch den Constructor "virtual" markeiren müssen, denn die hinterlegte Implementierung tut rein gar nichts, wer immer also irgendeine Funktionalität im Constructor haben möchte *muss* den Constructor überschreiben. Er ist aber nicht "virtual" markiert, also sollte das eigentlich nicht gehen.

Ich habe schon mal vor Wochen beim Einstieg in OOP mit FPC an dem Thema rumgeknabbert, "warf" hat mir eine Erklärung geliefert die ich damals verständlich fand (https://www.lazarusforum.de/viewtopic.php?p=112159#p112159), nur ... das kann so wohl kaum zutreffen, denn ich kann - ausgetestet - *jede* Methode in einer abgeleiteten Klasse überschreiben, egal ob sie "virtual" markiert war oder nicht. Ein paar Runden Trial and Error später bin ich mir ziemlich sicher: das Einzige was ich mir mit "virtual" einfange ist, dass ich beim Überschreiben der Methode in der abgeleiteten Klasse ein "Override" angeben muss. Damit bricht "warf"s Erklärungsbaum für mich zusammen, und ich bin so schlau wie vorher.

Einmal verunsichert, habe ich auch noch mit abstract gespielt ... ich dachte, wenn ich es angebe zwinge ich jemanden, der von der Basisklasse ableitet dazu, eine eigene Implementierung zu hinterlegen, und verbiete gleichzeitig mir selbst, in meiner Basisklasse eine Implementierung zu hinterlegen. Auch nicht richtig, der Compiler übersetzt mein Programm ohne Fehler, obwohl ich eine abstrakte Methode nie definiert habe - erst beim Versuch, sie aufzurufen, gibt es einen SIGSEV - das hätte ich ohne "abstract" auch haben können.

Wer kann Licht ins Dunkel bringen?

Armin.
Zuletzt geändert von Nimral am Mo 10. Feb 2020, 19:16, insgesamt 1-mal geändert.

Socke
Lazarusforum e. V.
Beiträge: 3158
Registriert: Di 22. Jul 2008, 19:27
OS, Lazarus, FPC: Lazarus: SVN; FPC: svn; Win 10/Linux/Raspbian/openSUSE
CPU-Target: 32bit x86 armhf
Wohnort: Köln
Kontaktdaten:

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Socke »

Constructor und Destructor sind keine normalen Methoden. Hier erzeugt der Compiler ein wenig mehr Code um den eigentlichen Aufruf herum. Konkret ist das: Vor dem Aufruf des Konstruktors wird Speicher reserviert und nach Ende des Destruktors wieder freigegeben. Daher enthalten beide Funktionen im TObjekt auch keinen Code.

Benutzeravatar
fliegermichl
Lazarusforum e. V.
Beiträge: 1430
Registriert: Do 9. Jun 2011, 09:42
OS, Lazarus, FPC: Lazarus Fixes FPC Stable
CPU-Target: 32/64Bit
Wohnort: Echzell

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von fliegermichl »

Nimral hat geschrieben:Einmal verunsichert, habe ich auch noch mit abstract gespielt ... ich dachte, wenn ich es angebe zwinge ich jemanden, der von der Basisklasse ableitet dazu, eine eigene Implementierung zu hinterlegen, und verbiete gleichzeitig mir selbst, in meiner Basisklasse eine Implementierung zu hinterlegen. Auch nicht richtig, der Compiler übersetzt mein Programm ohne Fehler, obwohl ich eine abstrakte Methode nie definiert habe - erst beim Versuch, sie aufzurufen, gibt es einen SIGSEV - das hätte ich ohne "abstract" auch haben können.

Wer kann Licht ins Dunkel bringen?

Armin.


Die abgeleitete Klasse muß als abstract definierte Methoden selbst nicht überschreiben. Der Compiler erzeugt aber eine Warnung wenn man eine Instanz dieser Klasse erstellt.
Klassen von denen Instanzen erstellt werden müssen also als abstract deklarierte Methoden überschreiben.

Man kann damit eine mehrschichtige Klassenhirarchie erstellen. Auf dem Weg dahin bis zur Klasse von der Instanzen erstellt werden, sollten dann aber alle als abstract deklarierten Methoden überschrieben worden sein.

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Nimral »

Socke hat geschrieben:Daher enthalten beide Funktionen im TObjekt auch keinen Code.


Hm. Damit habe ich herumgespielt:

Code: Alles auswählen

 
  TMyTestClass = class(TObject)
 
    public
      procedure Plain;
      procedure PlainVirtual; virtual;
      procedure PlainVirtualAbstract; virtual; abstract;
  end;
 
  TMyDerivedClass = class(TMyTestClass)
    public
      // procedure PlainVirtualAbstract;
  end;   
 
 


- ich kann sowohl Plain als auch PlainVirtual überschreiben, beide können "inherited" benützen, ich bekomme exakt das Selbe Systemverhalten, egal ob ich virtual angebe oder nicht. Nur - wenn ich "virtual" angebe, muss ich beim Überschreiben "override" angeben --> "virtual" und "override" sind ohne erkennbaren Nutzen.

- ich habe PlainVirtualAbstract "abstract" definiert, aber in MyDericedClass keine Implementierung hinterlegt, was doch eigentlich der Sinn von "abstract" wäre - den Compiler stört es nicht. Das Einzige was "abstract" verhindert ist, dass ich TMyTestClass.PlainVirtualAbstract implementiere. So wie ich die Doku lese, würde man gezwungen, in einer abgeleiteten Klasse eine Implementierung vorzunehmen, aber der Zwang erfolgt nicht, das Programm kompiliert auch ohne, fliegt dann aber mit einem SIGSEV auf die Nase, wenn ich versuche, TMyDerivedClass.PlainVirtualAbstract aufzurufen.

...?

Armin

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Nimral »

fliegermichl hat geschrieben:Die abgeleitete Klasse muß als abstract definierte Methoden selbst nicht überschreiben. Der Compiler erzeugt aber eine Warnung wenn man eine Instanz dieser Klasse erstellt.


"Project1.pas(40,17) Warning: An inherited method is hidden by "PlainVirtualAbstract;"

Die da etwa?

"Warnung: Kartoffeln können erröten, wenn man die Batterie öffnet"

Code: Alles auswählen

 
  { TMyTestClass }
 
  TMyTestClass = class(TObject)
 
    public
      procedure Plain;
      procedure PlainVirtual; virtual;
      procedure PlainVirtualAbstract; virtual; abstract;
  end;
 
  { TMyDerivedClass }
 
  TMyDerivedClass = class(TMyTestClass)
    public
      procedure Plain;
  end;
 
  TAnotherDerivedClass = class(TMyDerivedClass)
 
 
  end;
 
var
  TC : TMyTestClass;
  DC : TMyDerivedClass;
  ADC : TAnotherDerivedClass;
 
 


... hier gibt es dann überhaupt keine Warnung mehr wegen der "abstract" Klasse.

Armin.

Benutzeravatar
fliegermichl
Lazarusforum e. V.
Beiträge: 1430
Registriert: Do 9. Jun 2011, 09:42
OS, Lazarus, FPC: Lazarus Fixes FPC Stable
CPU-Target: 32/64Bit
Wohnort: Echzell

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von fliegermichl »

Die Warnung besagt nur, daß du in TMyDerivedClass die Methode Plain nicht mit Override markiert hast.

In deinem Beispiel wird ja auch keine Instanz erzeugt also gibt es auch keine Warnung:

Code: Alles auswählen

 
type
 TMyBase = class
  procedure Plain;
  procedure PlainVirtual; virtual;
  procedure PlainVirtualAbstract; virtual; abstract;
 end;
 
 TMyDerived = class ( TMyBase )
  procedure Plain; // Das ist möglich. Plain gibt es jetzt zwei mal!
  procedure PlainVirtual; override; // <-- das override hatte gefehlt
 end;
 
 TMySecond = class ( TMyDerived )
  procedure PlainVirtualAbstract; override;
 end;
 
var b : TMyBase;
begin
 b := TMyDerived.Create; // <-- Warnung, daß eine Klasseninstanz mit abstrakten Methoden erstellt wird
 TMyBase(b).Plain; // <- ruft TMyBase.Plain auf
 TMySecond(b).Plain; // <- ruft TMyDerived.Plain auf
end;
 

Warf
Beiträge: 1908
Registriert: Di 23. Sep 2014, 17:46
OS, Lazarus, FPC: Win10 | Linux
CPU-Target: x86_64

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Warf »

Kurze fassung: Konstruktor Virtual macht keinen sinn, da der Konstruktor nur zum zeitpunkt der Creation aufgerufen wird, und der destruktor von TObject ist mit virtual markiert, und jede andere klasse muss ihn als override markieren (nicht als virtual).

Hier mal eine lange erklärung, indem wir OOP selbst bauen, mit Records:

Beginnen wir mit einer basis klasse für Formen:

Code: Alles auswählen

TShape = record
  Position: TPoint;
end;

Mit zwei Methoden MoveBy, ToString und einen Konstruktor:

Code: Alles auswählen

procedure moveBy(var self: TShape; const offset: TPoint);
begin
  Self.Position.X += offset.X;
  Self.Position.Y += offset.Y;
end;
 
function toString(var self: TShape): String;
begin
  Result := 'Shape: (' + Self.Position.X + ', ' + Self.Position.Y+ ')';
end;
 
function Shape(const InitialPosition: TPoint): TShape;
begin
  Result.Position := InitialPosition;
end;


Jetzt erzeugen wir einen abkömmling:

Code: Alles auswählen

TCircle = record
  super: TShape;
  Radius: Double;
end;

Inklusive einem Konstruktor:

Code: Alles auswählen

fucntion Circle(const InitialPosition: TPoint; Radius: TPoint);
begin
  Result.super := Shape(InitialPosition); // das gleiche wie inherited Create; in OOP
  Result.Radius := Radius;
end;

 
Das selbe machen wir noch für ein Quadrat:

Code: Alles auswählen

TSquare = record
  super: TShape;
  SideLength: Double;
end;

Inklusive einem Konstruktor:

Code: Alles auswählen

fucntion Square(const InitialPosition: TPoint; SideLength: TPoint);
begin
  Result.super := Shape(InitialPosition); // das gleiche wie inherited Create; in OOP
  Result.SideLength := SideLength;
end;

 
Außerdem überladen wir die toString funktion für beide:

Code: Alles auswählen

function toString(var self: TCircle): String;
begin
  Result := 'Circle: (' + Self.Super.Position.X + ', ' + Self.Super.Position.Y+ ', ' + Self.Radius + ')';
end;
function toString(var self: Square): String;
begin
  Result := 'Square: (' + Self.Super.Position.X + ', ' + Self.Super.Position.Y+ ', ' + Self.SideLength + ')';
end;

 
Da die superclass das erste element im record ist kann man die ableitungshierachie ziemlich einfach runtergehen, wenn man pointer hat:

Code: Alles auswählen

var circ: PCircle;
  sp: PShape;
 
circ^ := Circle; // angenommen der speicher exsistiert, z.b. via new/GetMem geholt
sp := PShape(circ); // weil super das erste element ist gilt sp = circ^.super, somit wird die hierachie runter gegangen
MoveBy(sp^, offset); // selbe wie MoveBy(circ^.super, offset)M
circ := PCircle(sp); // somit kann wieder die hierachieleiter hochgegangen werden

Aber jetzt beobachten wir das folgende:

Code: Alles auswählen

circ^ := Circle;
sp := PShape(circ);
toString(circ^); // Circle (...)
toString(sp^); // Shape (...)

Obwohl beide die selbe funktion bereitstellen, ist das ergebnis anders, je nachdem was man für einen typen übergibt. Das nennt sich shadowing, da die funktion toString von circ toString von shape überschattet, da sie den selben namen hat.
Das ist das selbe verhalten wenn du klassen überlädts und keine annotationen wie virtual hinzufügst, dann wird wenn du das Objekt in die Unterklasse castest eine andere funktion aufgerufen als wenn du in die überklasse castest.
 
Jetzt willst du das aber nicht, sondern willst viel mehr das egal was für ein objekt aus der Hierachie verwendet wird, immer die toString funktion des obersten elements der hierachie verwendet wird. Dafür ist virtual und override da. Intern wird das über funktionspointer umgesetzt:

Code: Alles auswählen

TShape = record
  Position: TPoint;
  ToString: function(var Self: Pointer); // same as virtual keyword in OOP
end;

Welche dann im Konstruktor gesetzt wird (sagen wir mal die toString methoden werden umbenannt zu xxx_toString):

Code: Alles auswählen

function Shape(const InitialPosition: TPoint): TShape;
begin
  Result.Position := InitialPosition;
  Result.ToString := @shape_ToString; // virtual keyword
end;
 
fucntion Circle(const InitialPosition: TPoint; Radius: TPoint);
begin
  Result.super := Shape(InitialPosition);
  Result.super.toString := @circle_ToString; // override keyword
  Result.Radius := Radius;
end;
 
fucntion Square(const InitialPosition: TPoint; SideLength: TPoint);
begin
  Result.super := Shape(InitialPosition);
  Result.super.toString := @square_ToString; // override keyword
  Result.SideLength := SideLength;
end;

Das wichtige an dieser stelle ist das die Signatur der toString Methode sich daher nicht mehr ändern darf, da sonst der funktionspointer nicht mehr passt. Daher heißt das ganze auch override, da es tatsächlich einfach überschrieben wird.
 
Wenn man jetzt das folgende schreibt:

Code: Alles auswählen

var circ: PCircle;
sq: PSquare;
sp: PShape;
 
sp := PShape(circ);
sp^.toString(sq); // Circle (...)
sp := PShape(sq);
sp^.toString(sq); // Square (...)


OOP macht intern nix anderes, nur werden alle virtuellen funktionen zusammen in eine so genannte vTable geschrieben (somit hat man einen pointer pro objekt statt einem pointer pro methode, das spart speicher), und macht ne ganze menge syntaktischen zucker drum herum, das du nicht die ganze zeit x.super schreiben musst oder hin und her casten musst

Wenn du eine funktion die als virtual in einer Basis klasse markiert ist nochmal erzeugst in einer oberklasse, (egal ob mit virtual oder ohne) wird der Funktionspointer auch geshadowed. In deinem beispiel shadowst du also das TObject Destroy mit deinem eigenen Destroy

Fassen wir also zusammen: Virtual macht aus einer normalen funktion im sourcecode einen funktionspointer im Objekt. Override überschreibt diesen funktionspointer mit einer neuen funktion. Wenn du eine normale Methode aufrufst ist das wie ein normaler funktionsaufruf, wenn du eine virtuelle/override Methode aufrufst wird der funktionspointer aus der vTable aufgerufen. Wichtig: Funktionspointer chasen (aka vtable benutzen) kostet laufzeit, normale funktionen (also ohne virtual) können geinlined werden, und sind damit im allgemeinen schneller. Also benutze virtual nur wenn dus musst, ansonsten benutz normale.

Damit können wir auch die Frage mit Create und Destroy beantworten:
Create virtual zu machen bringt nix. Create wird nur ein einziges mal aufgerufen, und niemals aus einer niedrigeren stufe der Ableitungshierachie aufgerufen. Daher macht ein eintrag in dier VTable für den Konstruktor keinen Sinn. Der destruktor hingegen muss overriden, damit egal in welcher stufe der Ableitungshierachie du dich befindest, immer der oberste konstruktor ausgeführt wird, sonst kann es sein das nur der super teil aufgeräumt wird, und dein konstruktor nie aufgerufen wird, weil .Free auf einem objekt der Hierachie aufgerufen wurde, von dem du den Destruktor geshadowed hast

Nimral
Beiträge: 390
Registriert: Mi 10. Jun 2015, 11:33

Re: Methoden ... Virtual? Abstract? Keins davon?

Beitrag von Nimral »

Warf, Du hast Dir unendliche Mühe gemacht, vielen Dank dafür.

Ich habs inzwischen auch geschnallt - ich habe, bevor ich Deine Antwort gelesen habe, endlich aus einem Nebensatz von Fliegermichi den richtigen Schluss gezogen: ich habe die ganze Zeit auf Klassenmethoden herumgeritten, daher hat alles was irgendwie mit virtual zu tun hat erst mal nicht funktioniert, die statischen Methoden dagegen schon.

Code: Alles auswählen

 
 
var TC:TMyTestClass;
 
begin
TC.Plain;
end;
 
macht genau das Selbe wie
 
begin
TC := TMyClass.Create;  // 1
TC.Plain;
end;
 


1: ist immer nötig, auch wenn ich selber weder einen Constructor create geschrieben noch von einer Klasse abgeleitet habe die einen Constructor hat. Teuflischer Weise funktioneren statische Methoden aber trotzdem, ich kann auch Unterklassen ableiten, Methoden hinzufügen oder überschrieben oder auch nicht, mit inherited; arbeiten ... aber eben alles nur so lange ich keine als virtual deklarierte Methode anfasse. So wie ich meine Beipiele aufgbaut habe bin ich also sehr weit gekommen ohne den create, und daher nicht auf die Idee gekommen, das Problem ganz am Anfang des Codes zu suchen.

Jungs, ihr seid echt eine enorme Arbeitserleichterung, ich danke euch von Herzen, und hoffe zuversichtlich, dass ich in 1-2 Jahren so weit bin dass ich auch selber Neulingen helfen kann.

HG aus Bayern (ja, es steht noch - Sabine war kaum mehr als ein laues Lüftchen)

Armin.

Benutzeravatar
fliegermichl
Lazarusforum e. V.
Beiträge: 1430
Registriert: Do 9. Jun 2011, 09:42
OS, Lazarus, FPC: Lazarus Fixes FPC Stable
CPU-Target: 32/64Bit
Wohnort: Echzell

Re: [Gelöst] Methoden ... Virtual? Abstract? Keins davon?

Beitrag von fliegermichl »

Aehöm virtual constructor macht schon Sinn.

Code: Alles auswählen

 
{$apptype console}
program Project1;
 
{$R *.res}
 
type
 tbase1 = class
  constructor create; virtual;
 end;
 tbase1class = class of tbase1;
 
 tderived = class ( tbase1 )
  constructor create; override;
 end;
 
constructor tbase1.create;
begin
 writeln('tbase1');
end;
 
constructor tderived.create;
begin
 writeln('tderived');
end;
 
var t : tbase1;
     tt : tbase1class;
begin
 tt := tderived;
 t := tt.create; // Hier wird jetzt tderived ausgegeben. Wäre der constructor nicht virtuell, so wäre das tbase1
 readln;
end.
 

Antworten