Einzelnen Beitrag anzeigen

FragenderHerbert

Registriert seit: 4. Dez 2013
47 Beiträge
 
#8

AW: Verständnisfrage Dateien in Objekten

  Alt 13. Dez 2013, 18:50
@Sir Rufo:

Ja das ist zum Üben gedacht. Ich möchte verstehen, wie ein echter Stream intern funktioniert. Den Quelltext aus der Unit Classes verstehe ich noch nicht gut genug dazu. Eigene Experimente geben mir da mehr Aufschluss.

Zitat:
OK, dann halt zum Üben....

Im Prinzip wurde hier einfach nur ein "File Of Byte" weggekapselt, insofern ist das schon OK.
Auch wenn da TFileStream und Co. wohl eine bessere Alternative wären.



TByteStream ist wohl kein ganz guter Name, da es ja nicht von TStreams abgeleitet ist und sonst auch kein "Stream" intern verwendet wird, bzw. es auch nur irgendwas damit zu tun hat.
Wie wäre es TBytesFile?

Wie gesagt, ich will die Funktionsweise des Streams verstehen lernen. Aber für meine Klasse wäre natürlich TBytesFile ein besserer Name.

Zitat:
Wieso wird bei Write der Puffer zum Schreiben übergeben? (Var-Parameter)
An dem Buffer soll doch nichts verändert werden.

Ok, das kann ich ändern. Allerdings gab es in Turbo Pascal, von wo ich das nach Delphi portiert habe, noch keine const Parameter. Heute in Delphi kann man lt Handbuch schreiben:

procedure Write(Const Buffer; Size: Longint); in Turbo Pascal noch keine const Parameter, ging es nur so:

procedure Write(var Buffer; Size: Longint);
Zitat:
Wozu brauchst du das BufSize?
Die länge steht doch schon im Array drin. (Length)
Auch wieder wahr. Für das OpenArray stimmt das. Hatte aber ursprünglich obiges Konstrukt vorgesehen. Write(var Buffer;...


Zitat von himitsu:
Einem Array-Parameter kann man zwar schön statische Arrays über geben, oder ein idrekt definiertes Array (weiß jetzt nicht wie das heißt).
x.Write([1, 2, 3, 4]);
aber? Diese Möglichkeit ist doch praktisch.

Zitat von himitsu:
markieren
Delphi-Quellcode:
procedure Write(Buffer: array of byte); overload; // für statische Arrays und direkte Angaben
Ok, Danke, hab ich verstanden. Wenn ich Array of Byte verwende gibt es ja Low() und High(), womit ich die Arraygrenzen abfragen kann. Dann muss ich halt ein Array passender Größe übergeben und das dann schreiben.

Zitat von himitsu:
procedure Write(Buffer: TBytes); overload; // für dynamische Arrays
Wo ist TBytes gleich noch mal definiert? Ist das einfach so ein Array of Byte?

Zitat von himitsu:
procedure Read(var Buffer: TBytes; Count: Longint); // nur für dynamische Arrays möglich
Das verstehe ich jetzt nicht. Oben ist gesagt, das ich Count bei einem Array of Byte weglassen kann.

Außerdem kann ich doch an ein Open Array, wie das in Turbo Pascal hieß, ein beliebig großes Array übergeben. Wenn ich dann aber aus diesem Array nicht alle Bytes lese/schreibe, wenn also Count kleiner ist als die Größe des Arrays?

(Bis gleich groß sollte ja auch noch klappen).

Zitat von himitsu:
// oder

function Read(Count: Longint): TBytes; // ebenso
Oder so. Ok!

Zitat von himitsu:
Was sind fail und done?
Statt Done muss es Destroy heißen. Die Prozedur fail gab es in Turbo Pascal zum Aufruf im Konstruktor, wenn die Instantiierung meines Objektes fehl schlägt, hier zum Beispiel, wenn meine Datei nicht geöffnet werden konnte, weil sie nicht gefunden wurde. Fail sorgt dann dafür, das der Speicher korrekt wieder frei gegeben wird.

Zitat von himitsu:
Wenn du kein Delphi, sondern Lazarus/FPC verwendest, dann erwähn das bitte auch.
Ok, werde ich zuküftig tun und ich hab ja auch schon darauf hingewiesen, das ich vorher mit Turbo Pascal programmiert habe, nun aber wirklich mit Delphi programmiere/programmieren will, nicht mit Lazarus.

Zitat von himitsu:
Aber dennoch ... wieso inherited Destroy eigentlich Done und nicht das übergeordnete Destroy?
Weil ich den Code aus Turbo Pascal übersetzt habe, das heißt, in Turbo Pascal entwickelt und dann nach Delphi portiert. In TP gab es statt der Definition

type
TMeinObjekt = Object

in Delphi

type
TMeinObjekt = class

Der Constructor hieß dort Init, der Destructor hieß Done. Ich denke, im Interesse guten Programmierstils sollte man diese Namensgebung beibehalten und erst bei den Delphi Klassen stattdessen Create und Destroy verwenden.
Aber beim Portieren habe ich wohl vergessen, das Done abzuändern. Richtig muss es heißen:

inherited Destroy.

Habe das nicht korrekt portiert.

Zitat von himitsu:
Die Variable IResult kann weg, da du den Wert eh nur einmal auswertest und das gleich in der nächsten Zeile.

Was passiert, wenn Mode "ungültig" ist, also keinem deiner behandelten Werte entspricht?
Dann darf die Datei nicht geöffnet werden und fail muss aufgerufen werden.

Delphi-Quellcode:
...
if mode=fmCreate then
begin

end else fail; //weil ha bei ungültigem Mode die Datei nicht geöffnet wird
Zitat von himitsu:
Wieso "verwirfst" du quasi das IOResult?
Das gehört an fail übergeben, oder es sollte man sonstwie auswerten und weiterreichen .... und was macht fail eigentlich?
Der Grund, also IOResult, gehört, wenn möglich übersetzt, in die Fehlermeldung mit rein, damit man weiß warum es nicht ging.
Da du die Datei komplett kapselst, gehört auch die Fehlerbehandlung koplett da rein. (ja, IOResult könnte man extern eventuell noch abfragen, aber jenachdem was fail macht, könnte das dann schon ungültig sein)
IResult habe ich eingeführt, weil ich mit Turbo Pascal meine ersten Programmierversuche gemacht habe und da habe ich gelernt, das IOResult eine Funktion ist, deren Wert direkt nach seiner Abfrage wieder auf NULL zurück gesetzt wird.

Hmmm, IOResult an fail übergeben??? Da muss ich wohl moch mal meine alten Turbo Pascal Handbücher rausholen. Ich dachte Fail sei eine Prozedur, die ich im Konstruktor aufrufen kann, wenn die Erzeugung meines Objektes fehl schlägt.

Zitat von himitsu:
Nochmal zu den "unnötigen" Variablen, wie vorhin schon das IResult.
markieren
Delphi-Quellcode:
function TByteStream.ReadByte: Byte;
begin
System.Read(FFile, Result);
end;
Da das Result von TByteStream.Read immer dem BufSize entspricht, ist es nutlos und es kann eine Prozedur werden.
Ok, Danke! Werd ich mir merken.

Zitat von himitsu:
TByteStream.Read(Buffer, BufSize) : entweder du nimmst die größe von Buffer (Length) oder den Wert von BufSize.
- bei BufSize wird der Buffer auf die "richtige" länge gebracht und nicht blind drin rumgeschrieben
- bei Length(Buffer) ist BufSize "nutzlos" und flieg raus, aus dem Code
Ok, in Delphi geht das ja. Da gibt es ja wie ich durch Handbuchgstudium soeben gelernt habe, den const Parameter. Den gibt es in Turbo Pascal noch nicht. Ursprünglich wollte ich auch statt Array of Byte eine typlose Variable nehmen.

Zitat von himitsu:
Und wozu Read/Write das Current/FilePos brauchen, hab ich nicht verstanden, danie wirklich verwendet wird, außer es im Result wieder rauszurechnen ... hätte man bei 0 angefangen, bräuchte man es auch nicht rausrechnen, bzw. -0 ist ja nichts.

Warum macht Seek bei 2zwei Drittel des Codes Nichts? (CurrentPos und End)
Will die Seek Methode so implementieren, wie die Originale und da kann ich den Parameter Origin angeben. Da kann ich ja mitgeben, ob ich ab Beginn, ab aktueller Position oder ab Ende suchen will. Diese Methode ist noch nicht fertig implementiert.

Zitat von himitsu:
Weil diese Methode noch nicht fertig implementiert ist. In Turbo Pascal nimmt die Prozedur Seek und auch die Stream Methode Seek nur die gewünschte Position entgegen, ich Delphi kann man noch angeben, ob vom Beginn des Streams, soFromBeginning, von der aktuellen Position aus, soFromCurrent oder vom Streamende aus soFromEnd gesucht werden soll. Das möchte ich implementieren.

Wenn du schon OpenRead und Open aka OpenReadWrite unterscheidest, dann gehort vor das Reset noch der richtige FileMode zugewiesen.
Hmmm, wie mache ich das?

Habe den Quellcode von Turbo Pascal aus nach Delphi übersetzt. Delphi besitze ich erst seit kurzer Zeit.

Zitat von himitsu:
TReader ist ein blöder Name ... Was liest der denn und außerdem Delphi-Referenz durchsuchenTReader?
Wie wäre es mit TBytesReader, für das TBytesFile?
TReader steht für meine TByteStream Klasse. Ist halt schlampig übernommen. Hätte besser denselben Namen TByteStream verwenden sollen.

Zitat von himitsu:
Und daß beim TReader in der Implementation der klassenname fehlt ist bestimmt nur schlimmes Copy&Paste.
Korrekt, ist nur schlimmes Copy&Paste. Hab extra noch mal meinen Quelltext daraufhin überprüft. Hab ja auch die Korrekturen und Tipps von hier eingearbeitet und die Klasse nochmals getestet.

Und wie kommen Objekte in die Datei?

TStream, der echte Stream ist doch erst mal eine Klasse, in der etwas auf die externen Datenträger geschrieben wird. Das kann erst mal so weit mit gewöhnlichen Dateifunktionen passieren. Aber wie kommt nun ein Objekt in so eine Datei? Wie gesagt, ist mir der Quelltext aus der Unit Classes da noch nicht klar zumal dort in TStream + Nachfolgern bereits Methoden aufgerufen werden, die wohl in anderen Units und anderen Klassen verborgen sind.

In der Unit Classes lese ich zum Beispiel das hier:

Delphi-Quellcode:
function TStream.ReadComponent(Instance: TComponent): TComponent;
var
  Reader: TReader;
begin
  Reader := TReader.Create(Self, 4096);
  try
    Result := Reader.ReadRootComponent(Instance);
  finally
    Reader.Free;
  end;
end;

procedure TStream.WriteComponent(Instance: TComponent);
begin
  WriteDescendent(Instance, nil);
end;
Wenn ich dann weiter schaue, finde ich zunächst das hier:

Delphi-Quellcode:
procedure TWriter.WriteDescendent(Root: TComponent; AAncestor: TComponent);
begin
  FRootAncestor := AAncestor;
  FAncestor := AAncestor;
  FRoot := Root;
  FLookupRoot := Root;
  WriteSignature;
  WriteComponent(Root);
end;
Da weiß ich aber immer noch nicht, wie WriteComponent intern arbeitet.

Aber hier wird es jetzt interessant:

Delphi-Quellcode:
procedure TWriter.WriteComponent(Component: TComponent);

  function FindAncestor(const Name: string): TComponent;
  var
    I: Integer;
  begin
    for I := 0 to FAncestorList.Count - 1 do
    begin
      Result := FAncestorList[I];
      if SameText(Result.Name, Name) then Exit;
    end;
    Result := nil;
  end;

var
  OldAncestor: TPersistent;
  OldRootAncestor: TComponent;
  AncestorComponent: TComponent;
  I: Integer;
begin
  OldAncestor := Ancestor;
  OldRootAncestor := RootAncestor;
  try
    Include(Component.FComponentState, csWriting);
    for I := 0 to Component.ComponentCount - 1 do
      if csSubComponent in Component.Components[I].ComponentStyle then
        Include(Component.Components[I].FComponentState, csWriting);
    if Assigned(FAncestorList) then
      Ancestor := FindAncestor(Component.Name);
    if Assigned(FOnFindAncestor) and ((Ancestor = nil) or
    (Ancestor is TComponent)) then
    begin
      AncestorComponent := TComponent(Ancestor);
      FOnFindAncestor(Self, Component, Component.Name, AncestorComponent,
        FRootAncestor);
      Ancestor := AncestorComponent;
    end;
    Component.WriteState(Self);
    Exclude(Component.FComponentState, csWriting);
    for I := 0 to Component.ComponentCount - 1 do
      if csSubComponent in Component.Components[I].ComponentStyle then
        Exclude(Component.Components[I].FComponentState, csWriting);
  finally
    Ancestor := OldAncestor;
    FRootAncestor := OldRootAncestor;
  end;
end;
Das möchte ich jetzt gerne verstehen.

Schreibt diese Methode die Eigenschaften und Datenfelder in den Stream? Dann müßte bei Instantiierung ein Konstruktor aufgerufen werden, nachdem die Felder der Klasse vom Reader belegt worden sind.

Das hier ist ja die Methode WriteComponent aus TWriter, offenbar wird zum Schtreiben eine Klasse verwendet, die halt das Objekt nur in den Stream schreiben kann, zum Lesen gibt es dann eine andere Klasse.

Aber bei TReader.ReadComponent() wird es richtig umfangreich. Da blick ich derzeit noch gar nicht durch. Da gibt es in der Methode jede Menge lokale Prozeduren, die verstanden sein wollen. Und dann ist da immer noch der Code der eigentlichen Methode.


.

Geändert von FragenderHerbert (13. Dez 2013 um 19:40 Uhr)
  Mit Zitat antworten Zitat