English Русский Español 日本語 Português
preview
Von den Grundlagen bis zur Mittelstufe: Direkter Dateizugriff (II)

Von den Grundlagen bis zur Mittelstufe: Direkter Dateizugriff (II)

MetaTrader 5 — Beispiele |
20 0
CODE X
CODE X

Einleitung

Im vorherigen Artikel „Von den Grundlagen bis zur Mittelstufe: Direkter Dateizugriff (I)“ haben wir kurz das Direktzugriffsmodell zum Lesen und Schreiben von Daten in Dateien vorgestellt. Um jedoch in jenem Artikel nicht zu viele Informationen auf einmal zu präsentieren, haben wir eines der Probleme nicht erläutert, das jeden, der dateibezogene Funktionen implementiert, besonders stark betrifft.

Achten Sie jedoch ganz genau auf das, was nun folgt, denn das richtige Verständnis dieses Punktes ist weitaus wichtiger als das Verständnis aller anderen Aspekte, die in diesem Artikel behandelt werden. Es ist nicht immer notwendig, im Voraus zu wissen, welcher Datentyp in einer Datei gespeichert wird. In den meisten Fällen sind Dateien auf eine bestimmte Art und Weise strukturiert, wie auch immer diese Struktur aussehen mag. Die Struktur einer Datei bestimmt weitgehend – oder, wenn man so will, ermöglicht – die von uns verwendeten Datentypen, ohne dass wir über jeden einzelnen Low-Level-Lese- oder -Schreibvorgang nachdenken müssen.

Das mag vielleicht etwas seltsam klingen, aber in der Praxis läuft es meistens genau so ab. Abgesehen von ganz bestimmten Situationen müssen wir selten den genauen Datentyp kennen, der an einer bestimmten Dateiposition gespeichert ist. Wenn eine solche Situation jedoch eintritt, ist es weitaus wichtiger, zu wissen und zu verstehen, was zu tun ist, als es auf den ersten Blick erscheinen mag. Das liegt daran, dass Sie als Programmierer – je nachdem, wie gut Sie die Funktionsweise dieser Mechanismen verstehen – bei der Lösung bestimmter dateibezogener Probleme auf erhebliche Schwierigkeiten stoßen können. Aus diesem Grund bitte ich Sie, lieber Leser, jedem in diesem Artikel erläuterten Punkt besondere Aufmerksamkeit zu schenken, denn das Verständnis der hier vorgestellten Inhalte kann den Unterschied ausmachen zwischen dem korrekten Lesen einer Datei und der völligen Unfähigkeit, deren Inhalt auch nur ansatzweise zu verstehen.


Direkter Dateizugriff (Teil 2)

Wenn es um Unions geht, habe ich in früheren Artikeln dieser Reihe gezeigt, dass wir genau auf den Datentyp und die Anzahl der Bytes achten müssen, die jeder Typ belegt. Ohne dieses Wissen ist es sehr schwierig zu verstehen, was ein bestimmter Datentyp tatsächlich darstellt.

Viele Entwickler neigen dazu, Unions zu ignorieren oder ihre Verwendung gänzlich zu vermeiden. Was Strukturen und Arrays betrifft, sieht die Situation jedoch ganz anders aus. In diesem Fall ist es oft sehr schwierig, wenn nicht sogar fast unmöglich, bestimmte Datenmodelle zu erstellen, ohne ein wirkliches Verständnis dieser Programmierkonzepte zu haben, da Strukturen und Arrays im alltäglichen Code weitaus häufiger vorkommen als Unions.

Viele Entwickler stoßen jedoch irgendwann auf ein schwierigeres Problem. In diesem Fall kann die Sache deutlich komplizierter werden, da manche Datenmodelle Strukturen, Arrays und Unions kombinieren. In diesem Fall muss man verstehen, wie diese Konzepte zusammenwirken, da die Umsetzung sonst schnell unübersichtlich wird. Diese Art der Modellierung kann unter anderem Dateien mit direktem Zugriff beinhalten.

„Wollen Sie mir Angst machen? Ich glaube nicht, dass Dateien so komplex sein können, wie Sie sagen.“ Das ist nicht ganz richtig, lieber Leser. Möglicherweise ist Ihnen das Ausmaß der damit verbundenen Komplexität noch nicht bewusst. Dateien sind zweifellos die komplexeste Art der Datenorganisation, die ein Programmierer erstellen und entwerfen kann. Diese Aussage trifft in so hohem Maße zu, dass bei der Einführung eines neuen Dateiformats in der Regel eine Dokumentation mitgeliefert wird, in der erklärt wird, wie das Format zu interpretieren ist.

Ohne eine Dokumentation, die die interne Struktur einer Datei beschreibt, liefert selbst das Öffnen der Datei kaum Aufschluss über ihren Inhalt, es sei denn, es handelt sich um eine reine Textdatei. Dies ist vielleicht der einzige Fall, in dem eine Dokumentation des internen Formats nicht unbedingt erforderlich ist. Um dies zu veranschaulichen, nehmen wir das einfachste Beispiel: das Bitmap-Format (BMP). Dieses Format wird in verschiedenen im Internet verfügbaren Dokumenten beschrieben; es handelt sich um ein sehr einfaches und recht praktisches Format, das speziell für die Speicherung von Bildern ohne komplexe Komprimierung entwickelt wurde.

Zwar kommt es in einigen Fällen zu einer geringfügigen Komprimierung, doch handelt es sich hierbei lediglich um ein Detail, das wir vorerst außer Acht lassen können. Das Wesentliche ist etwas anderes. In diesem Fall stellt sich die Frage, ob wir genau wissen, was in einer Datei gespeichert ist. Wenn Sie beispielsweise ein Bild in einem Hex-Editor öffnen, sehen Sie etwas Ähnliches wie in der folgenden Abbildung:

Abbildung 01

Sehen Sie sich Abbildung 01 an. Hier gibt es etwas recht Merkwürdiges, mein lieber Leser. Beachten Sie, dass der Inhalt der Datei grün hervorgehoben ist. Anhand dieser Daten lässt sich jedoch keineswegs genau feststellen, was diese Datei enthält – abgesehen davon, dass bestimmte Marker dabei helfen könnten, das Format zu identifizieren. Zum Beispiel die ersten drei Werte, die in dieser Datei enthalten sind. Natürlich könnte ein anderer Programmierer – oder eine andere Anwendung – dieselben drei Werte verwenden, um einen völlig anderen Dateityp darzustellen. Auch wenn dies nicht üblich ist, spricht nichts dagegen, dass jemand dies tut.

Geht man jedoch davon aus, dass dies nicht der Fall ist, kann man vernünftigerweise schließen, dass es sich um eine Bitmap-Datei handelt. Daher sollte sie als Bitmap-Datei behandelt werden. Nun frage ich Sie: Wissen Sie, wie man Daten aus dieser Datei extrahiert, ohne über eine Dokumentation zu verfügen, die deren Struktur erläutert? Wahrscheinlich nicht. Das liegt daran, dass – abgesehen davon, dass die ersten drei Werte auf das Bitmap-Format (BMP) hinweisen – nichts anderes in den Rohdaten Aufschluss darüber gibt, wie die Datei intern aufgebaut ist. Wenn Sie also nicht wissen, mit welchem Programm sich der tatsächliche Inhalt der Datei anzeigen lässt, können Sie nicht feststellen, welche Informationen in dieser Datei enthalten sind.

So funktionieren Dateizuordnungen im Wesentlichen. Es klingt wie Zauberei, aber das Betriebssystem überprüft den Datei-Header. Je nach Header oder Signatur kann die Datei einer bestimmten Anwendung zugeordnet sein. Wenn eine Zuordnung vorliegt, startet das Betriebssystem beim Aufruf zum Anzeigen des Dateiinhalts die Anwendung, die diesem Header oder dieser Signatur zugeordnet ist. Auch wenn viele Menschen glauben, dass dies mit anderen Dingen zusammenhängt. Manchmal stimmt das. Aber das spielt im Moment keine Rolle.

Na gut, aber worauf will ich damit hinaus? Nun, im vorherigen Artikel haben wir uns damit befasst, wie Daten per direktem Zugriff mithilfe von Aufrufen wie FileSeek und anderen Hilfsfunktionen an beliebigen Positionen aus einer Datei gelesen und in sie geschrieben werden können. Dort haben wir jedoch jeweils ein Byte gelesen, und jedes gelesene oder geschriebene Byte ließ die Dateiposition um ein Byte vorrücken. Aber trifft das immer zu? Um diese Frage zu beantworten, müssen wir auf einen weiteren Punkt eingehen, der bereits in anderen Artikeln behandelt wurde, beispielsweise als wir über Unions, Strukturen und Arrays gesprochen haben. Obwohl wir diese Themen getrennt und schrittweise behandelt haben, werden wir hier alle diese Konzepte gleichzeitig anwenden. Dies geschieht, damit wir nachvollziehen können, wie sich die Dateiposition bei jedem von uns ausgeführten Lese- oder Schreibvorgang weiterbewegt.

Na gut, vergessen wir nun einmal, was in Abbildung 01 gezeigt wurde, denn im Moment hilft uns das nicht weiter. Wir sollten mit etwas Einfacherem beginnen, das aber dennoch für das, was erklärt werden soll, geeignet ist. Dazu verwenden wir den folgenden Code:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int handle;
07.     const datetime dt = D'31.10.2024 15:30:10';
08.     const int i32 = 356248;
09. 
10.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE| FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
11.     {
12.         Print("Error...");
13.         return;
14.     };
15. 
16.     FileWrite(handle, i32, "Info", dt);
17. 
18.     FileFlush(handle);
19.     
20.     FileSeek(handle, 0, SEEK_SET);
21.     while (!FileIsEnding(handle))
22.         Print(FileTell(handle), " >> ", FileReadString(handle));
23. 
24.     FileClose(handle);
25. }
26. //+------------------------------------------------------------------+

Code 01

Gut, wenn Code 01 ausgeführt wird, erstellt er eine Datei, deren Inhalt unten zu sehen ist:

Abbildung 02

Dieser Code 01 erzeugt jedoch auch die folgende Ausgabe im Terminal:

Abbildung 03

Nun kommen wir zu dem Teil, auf den es am meisten ankommt. Beachten Sie, dass in Abbildung 03 vor jedem aus der Datei gelesenen Wert ein numerischer Wert steht. Nun, dieser Zahlenwert, den wir vor der Information sehen, gibt die Position an, an der der aktuelle Wert endet und der nächste beginnt. Wenn wir also die vorherige Dateiposition für das Lesen oder Schreiben kennen, können wir ermitteln, wie viele Einheiten gelesen oder geschrieben wurden. „Aber Moment mal, wäre es nicht korrekter, von gelesenen oder geschriebenen Bytes zu sprechen?“ Nein, lieber Leser, und Sie werden bald verstehen, warum.

Gut, dann ändern wir den Code, um zu verstehen, wie viele Einheiten bei einem einzelnen Vorgang gelesen und – analog dazu – auch geschrieben werden. Hier geht es zwar nur ums Lesen, aber dasselbe Prinzip gilt auch fürs Schreiben. Wir haben also den folgenden Code:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle;
07.     ulong   old;
08. 
09.     const datetime dt = D'31.10.2024 15:30:10';
10.     const int i32 = 356248;
11. 
12.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE| FILE_READ | FILE_ANSI)) == INVALID_HANDLE)
13.     {
14.         Print("Error...");
15.         return;
16.     };
17. 
18.     FileWrite(handle, i32, "Info", dt);
19. 
20.     FileFlush(handle);
21.     
22.     FileSeek(handle, 0, SEEK_SET);
23.     while (!FileIsEnding(handle))
24.     {
25.         old = FileTell(handle);
26.         Print(FileTell(handle) - old, " || ", FileTell(handle), " >> ", FileReadString(handle));
27.     }
28. 
29.     FileClose(handle);
30. }
31. //+------------------------------------------------------------------+

Code 02

Bei der Ausführung liefert Code 02 das folgende Ergebnis:

Abbildung 04

Da Code 02 und seine Ausgabe selbsterklärend sind, können wir zur nächsten Stufe übergehen. Aber ich möchte, dass Sie Folgendes verstehen, mein lieber Leser. An dieser Stelle führt der von uns durchgeführte Lesevorgang dazu, dass das System jeweils einen Wert nach dem anderen liest und dabei das in der Datei gespeicherte Trennzeichen verwendet. Hier ist das Trennzeichen das Tabulatorzeichen, wie in der folgenden Abbildung zu sehen ist:

Abbildung 05

Abbildung 05 hebt dieses Tabulatorzeichen hervor. Aber was passiert, wenn wir den Code ein wenig ändern? Um das zu sehen, verwenden wir den folgenden Code:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle;
07.     ulong   old;
08. 
09.     const datetime dt = D'31.10.2024 15:30:10';
10.     const int i32 = 356248;
11. 
12.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE| FILE_READ | FILE_ANSI | FILE_TXT)) == INVALID_HANDLE)
13.     {
14.         Print("Error...");
15.         return;
16.     };
17. 
18.     FileWrite(handle, i32, "Info", dt);
19. 
20.     FileFlush(handle);
21.     
22.     FileSeek(handle, 0, SEEK_SET);
23.     while (!FileIsEnding(handle))
24.     {
25.         old = FileTell(handle);
26.         Print(FileTell(handle) - old, " || ", FileTell(handle), " >> ", FileReadString(handle));
27.     }
28. 
29.     FileClose(handle);
30. }
31. //+------------------------------------------------------------------+

Code 03

Wenn Code 03 ausgeführt wird, sehen Sie das, was in der folgenden Abbildung dargestellt ist:

Abbildung 06

„Was für ein Chaos ist das denn? Ich habe nicht verstanden, was im Code geändert wurde und zu diesem Ergebnis geführt hat.“ Nun, das werde ich Ihnen nicht verraten. Sie müssen den Code selbst durchsehen, um herauszufinden, was geändert wurde. Wenn Sie sich jedoch den binären Inhalt der Datei ansehen, sehen Sie Folgendes:

Abbildung 07

„Was haben Sie getan? Sagen Sie mir, wo Sie die Änderung vorgenommen haben.“ Noch einmal, mein lieber Leser: Sie müssen sich den Code ansehen, um zu verstehen, was ich geändert habe. Auf jeden Fall können Sie sehen, dass das Ergebnis nicht mehr dasselbe ist wie zuvor. Das liegt daran, dass wir das Dateiformat selbst geändert haben. Da die Anwendung nicht mehr weiß, wie sie die Datei lesen muss, um die Daten korrekt wiederherzustellen, ist das Ergebnis diese völlig chaotische Ausgabe.

Achten Sie jetzt gut darauf: Um diese Ausgabe zu interpretieren, müssen wir dem Programm irgendwie mitteilen, wo ein Wert endet und der nächste beginnt. Dies kann auf verschiedene Weise geschehen. Hier werden wir nur eine der vielen möglichen Vorgehensweisen betrachten. Da jeder Fall einzigartig ist, gibt es keine allgemeingültige Lösung. Aber lassen Sie uns nichts überstürzen, denn ich möchte, dass Sie verstehen, wie man das richtig macht.

Zunächst werden wir den Code so ändern, dass er in etwa dem unten gezeigten entspricht:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle;
07.     ulong   old;
08. 
09.     const datetime dt = D'31.10.2024 15:30:10';
10.     const int i32 = 356248;
11. 
12.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE | FILE_READ | FILE_ANSI | FILE_BIN)) == INVALID_HANDLE)
13.     {
14.         Print("Error...");
15.         return;
16.     };
17.     FileWriteString(handle, (string)i32);
18.     FileWriteString(handle, "info");
19.     FileWriteString(handle, StringFormat("%s", TimeToString(dt, TIME_DATE | TIME_SECONDS)));
20. 
21.     FileFlush(handle);
22.     
23.     FileSeek(handle, 0, SEEK_SET);
24.     while (!FileIsEnding(handle))
25.     {
26.         old = FileTell(handle);
27.         Print(FileTell(handle) - old, " || ", FileTell(handle), " >> ", FileReadString(handle));
28.     }
29. 
30.     FileClose(handle);
31. }
32. //+------------------------------------------------------------------+

Code 04

Ausgezeichnet. Obwohl die Terminalausgabe dieselbe ist wie bei Code 03, erzeugt Code 04 eine etwas andere Datei, die unten dargestellt ist:

Abbildung 08

Also, passen Sie gut auf, mein lieber Leser. Wie bereits erwähnt, gibt es verschiedene Möglichkeiten, dasselbe zu erreichen. Hier werden wir jedoch eine Methode verwenden, die einer Methode sehr ähnlich ist, die es früher in der Programmiersprache BASIC gab und die inzwischen nicht mehr verwendet wird. Daher sieht der nächste Schritt wie folgt aus:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle;
07.     ulong   old;
08. 
09.     const datetime dt = D'31.10.2024 15:30:10';
10.     const int i32 = 356248;
11. 
12.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE | FILE_READ | FILE_ANSI | FILE_BIN)) == INVALID_HANDLE)
13.     {
14.         Print("Error...");
15.         return;
16.     };
17.     FileWriteInteger(handle, 0, CHAR_VALUE);
18.     FileWriteString(handle, (string)i32);
19.     FileWriteInteger(handle, 0, CHAR_VALUE);
20.     FileWriteString(handle, "info");
21.     FileWriteInteger(handle, 0, CHAR_VALUE);
22.     FileWriteString(handle, StringFormat("%s", TimeToString(dt, TIME_DATE | TIME_SECONDS)));
23. 
24.     FileFlush(handle);
25.     
26.     FileSeek(handle, 0, SEEK_SET);
27.     while (!FileIsEnding(handle))
28.     {
29.         old = FileTell(handle);
30.         Print(FileTell(handle) - old, " || ", FileTell(handle), " >> ", FileReadString(handle));
31.     }
32. 
33.     FileClose(handle);
34. }
35. //+------------------------------------------------------------------+

Code 05

Achten Sie jetzt bitte ganz genau darauf. Mit Code 05 wird eine Datei erstellt, die der in der folgenden Abbildung gezeigten ähnelt:

Abbildung 09

Bei der Analyse von Abbildung 09 können Sie feststellen, dass ich bestimmte Stellen markiert habe, an denen der Hexadezimalwert null ist. Warum? Der Grund dafür ist, dass wir die genauen Positionen, an denen sich diese Werte befinden, nutzen, um der Anwendung mitzuteilen, wie die Zeichenkette korrekt gelesen werden soll. Dies geschieht, damit die Anwendung weiß, wie sie die Zeichenkette korrekt lesen muss, und somit den richtigen Text direkt im Terminal ausgeben kann. Wenn Sie während der Ausführung von Code 05 einen Blick auf das Terminal werfen, wird Ihnen etwas Seltsames auffallen, obwohl dies für diejenigen, die den Erklärungen und Beispielen in diesen Artikeln gefolgt sind, vollkommen nachvollziehbar ist. Das liegt daran, dass beim Versuch, etwas im Terminal auszugeben, das erste Zeichen in der Datei einen Wert von null hat. Daher erhalten wir ein Ergebnis, das auf den ersten Blick vielleicht etwas ungewöhnlich erscheint.

Wenn Sie jedoch selbst in Code 05 den in der Funktion FileSeek in Zeile 26 verwendeten Wert so ändern, dass dieser erste Nullwert, der ganz am Anfang der Datei steht, übersprungen wird, werden Sie feststellen, dass bestimmte Daten im Terminal angezeigt werden. Probieren Sie das später einmal aus, damit Sie sehen und verstehen können, wie sich das Programm verhält. Das bleibt also ein Vorschlag für ein Experiment, das Sie später durchführen können. Wir werden dies jedoch nicht hier im Artikel tun, da ich möchte, dass Sie selbst sehen, wie sich die Ergebnisse bei Änderungen am Code steuern lassen. Doch kommen wir zurück zu unserer Hauptfrage.

Da die Datei, die wir nun erstellen, als Binärdatei behandelt werden muss, muss die Funktion FileReadString wissen, wie viele Zeichen – in dieser Darstellung also Bytes – gelesen werden sollen. Dies ist erforderlich, um den korrekten und vollständigen Text zu erhalten, der im Terminal ausgegeben wird. Dazu werden wir den Code erneut anpassen, wie unten gezeigt.

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle;
07. 
08.     const datetime dt = D'31.10.2024 15:30:10';
09.     const int i32 = 356248;
10. 
11.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE | FILE_READ | FILE_ANSI | FILE_BIN)) == INVALID_HANDLE)
12.     {
13.         Print("Error...");
14.         return;
15.     };
16.     FWriteString(handle, (string)i32);
17.     FWriteString(handle, "info");
18.     FWriteString(handle, StringFormat("%s", TimeToString(dt, TIME_DATE | TIME_SECONDS)));
19. 
20.     FileFlush(handle);
21.     
22.     FileSeek(handle, 0, SEEK_SET);
23.     while (!FileIsEnding(handle))
24.     {
25.         uchar old = (uchar) FileReadInteger(handle, CHAR_VALUE);
26.         Print(old, " || ", FileTell(handle), " >> ", FileReadString(handle, old));
27.     }
28. 
29.     FileClose(handle);
30. }
31. //+------------------------------------------------------------------+
32. void FWriteString(int &handle, const string szArg)
33. {
34.     long offs = (long) FileWriteInteger(handle, 0, CHAR_VALUE);
35.     long size = (long) FileWriteString(handle, szArg);
36.     ulong ftell = FileTell(handle);
37.     FileSeek(handle, (size + offs) * -1, SEEK_CUR);
38.     FileWriteInteger(handle, (uchar)size, CHAR_VALUE);
39.     FileSeek(handle, ftell, SEEK_SET);
40. }
41. //+------------------------------------------------------------------+

Code 06

Ausgezeichnet. Jetzt haben wir tatsächlich Code, der im Terminal ein interessantes Ergebnis liefert. Wenn dieser Code ausgeführt wird, sehen Sie das in der folgenden Abbildung gezeigte Ergebnis:

Abbildung 10

Kommen wir nun zur Datei. Wenn Sie die Datei in einem Hex-Editor öffnen, sehen Sie etwas Ähnliches wie in der folgenden Abbildung.

Abbildung 11

Beachten Sie, dass in diesem Fall, wie in Abbildung 11 dargestellt, mehrere Werte hervorgehoben sind. Sie sind für das Format unverzichtbar. Das liegt daran, dass sie die Anzahl der Zeichen angeben, die die Funktion FileReadString lesen soll. Mit anderen Worten: Jeder hervorgehobene Wert gibt an, wie viele Zeichen als Nächstes gelesen werden müssen. Tatsächlich kann man ganz deutlich erkennen, dass genau diese Werte auch in Abbildung 10 dargestellt sind. Warum hat dann Code 06 funktioniert, während die anderen lediglich eine Datei erzeugt haben, die im Terminal kein plausibles Ergebnis lieferte?

Um dies zu verstehen, müssen wir die Prozedur, die in Zeile 32 von Code 06 erstellt wurde, genau untersuchen. Diese Prozedur ist gewissermaßen das Herzstück unserer Anwendung, da sie per direktem Zugriff auf die Datei zugreift, um den Datenabschnitt zu erstellen, der geschrieben werden soll. Diese Prozedur arbeitet jedoch nur mit einem bestimmten Typ – oder genauer gesagt mit Werten von fester Breite in Bytes –, sodass alles weitgehend vom Datentyp abhängt, den wir in der Datei speichern werden.

Beachten Sie nun bitte einige Punkte, denn sie sind wichtig. In Zeile 34 schreiben wir ein Byte. Dasselbe Byte wird später zur Anpassung des Offsets verwendet. Dieser Offset ist hier der entscheidende Punkt. Aber darauf kommen wir noch zu gegebener Zeit zurück. Lassen Sie uns zunächst einmal verstehen, wie die Subroutine aus Zeile 32 funktioniert. Nachdem also Platz für das Längenbyte reserviert wurde, schreiben wir den Inhalt der Zeichenkette in die Datei. Das geschieht in Zeile 35. Hinweis: Die Zeichenkette DARF NICHT MEHR ALS 255 Zeichen enthalten, da dies die durch das zuvor geschriebene Byte vorgegebene Obergrenze ist. Nun speichern wir in Zeile 36 vorübergehend die aktuelle Dateiposition. Das liegt daran, dass wir zu dem Byte zurückkehren, in dem die Nachrichtenlänge gespeichert ist, und den Inhalt dieses Bytes ändern. Das geschieht in Zeile 37. Das eigentliche Schreiben erfolgt in Zeile 38. Beachten Sie, dass all diese Datenträgerzugriffe durch eine einzige Array-Operation ersetzt werden könnten. Aber vorerst lassen wir alles so, wie es ist, damit man genau nachvollziehen kann, was wir gerade tun.

Gut, sobald die Daten geschrieben wurden, erzwingt Zeile 20, dass alle gepufferten Daten sofort geschrieben werden, falls sich noch Daten im Puffer befinden. Dann setzt Zeile 22 die Dateiposition auf den Anfang der Datei, und an dieser Stelle gelangen wir in die Schleife in Zeile 23, wo der Lesevorgang beginnt: Zunächst lesen wir das Byte, das die Anzahl der folgenden Zeichen angibt. Anschließend lesen wir in Zeile 26 dieselben Zeichen ein und geben sie aus.

Beachten Sie, dass die Zeilen 25 und 34 übereinstimmen müssen. Dadurch wird sichergestellt, dass die Anwendung das Längenfeld mit derselben Breite in Bytes interpretiert, mit der es geschrieben wurde. Genau hier kommt das Offset-Problem ins Spiel. Ausgezeichnet. Dasselbe Datenlayout lässt sich jedoch auf unterschiedliche Weise umsetzen, was meiner Meinung nach die Codeentwicklung sowie die Wartung und Weiterentwicklung erheblich vereinfacht.

Halten Sie nun einen Moment inne und denken Sie nach, mein lieber Leser. Was wir in der Datei tun, die wir hier erstellen, ist genau das, was wir bereits in einem anderen Artikel dieser Reihe behandelt haben. Damals verfügten wir jedoch noch nicht über genügend Informationen, um einige Punkte zu erläutern, die wir hier in der Praxis anwenden können. Was ich damit sagen will, ist: Wenn wir uns ansehen, wie das alles umgesetzt wird, arbeiten wir eigentlich mit einer Datenstruktur. Daher ist es wichtig, sich nicht nur auf den Code zu konzentrieren, sondern vielmehr zu versuchen, die verwendeten Konzepte zu verstehen und darüber nachzudenken, wie man zu einem ähnlichen Ergebnis gelangen könnte. Manche davon sind einfacher, andere effizienter. Auf jeden Fall ist es wichtiger, das Konzept zu verstehen, als sich Code auswendig zu lernen und ihn ohne Verständnis nachzuahmen.

Na gut, um zu zeigen, dass wir auf verschiedene Arten vorgehen können und trotzdem immer zum gleichen Ergebnis kommen, wollen wir ein wenig mit dem experimentieren, was wir bisher untersucht haben. Dazu werden wir mit Code arbeiten, der sich in einigen Punkten unterscheidet. Dieser Code ist unten zu sehen:

01. //+------------------------------------------------------------------+
02. #property copyright "Daniel Jose"
03. //+------------------------------------------------------------------+
04. void OnStart(void)
05. {
06.     int     handle,
07.             start = 1;
08.     uchar   arr[];
09. 
10.     const datetime dt = D'31.10.2024 15:30:10';
11.     const int i32 = 356248;
12. 
13.     if ((handle = FileOpen("Hello World.txt", FILE_WRITE | FILE_READ | FILE_BIN)) == INVALID_HANDLE)
14.     {
15.         Print("Error...");
16.         return;
17.     };
18. 
19.     start += arr[start - 1] = (uchar) StringToCharArray((string)i32, arr, start);
20.     start += arr[start - 1] = (uchar) StringToCharArray("info", arr, start);
21.     start += arr[start - 1] = (uchar) StringToCharArray(StringFormat("%s", TimeToString(dt, TIME_DATE | TIME_SECONDS)), arr, start);
22. 
23.     FileWriteArray(handle, arr, 0, start - 1);
24.     FileFlush(handle);
25. 
26.     ArrayFree(arr);
27. 
28.     FileSeek(handle, 0, SEEK_SET);
29.     FileReadArray(handle, arr);
30. 
31.     FileClose(handle);
32. 
33.     for (uint i = 0; i < arr.Size(); i+= arr[i])
34.         Print(i, " >> ", CharArrayToString(arr, i + 1, arr[i]));
35. }
36. //+------------------------------------------------------------------+

Code 07

Wenn Code 07 ausgeführt wird, sieht das Ergebnis im Terminal wie folgt aus:

Abbildung 12

Nun gibt es da etwas, das wir verstehen müssen, mein lieber Leser. Im Gegensatz zu den anderen bisher betrachteten Codebeispielen führen wir hier einige Operationen auf etwas andere Weise durch. Das liegt daran, dass wir zahlreiche Datenträgerzugriffe durch Operationen ersetzen, die direkt im Arbeitsspeicher (RAM) ausgeführt werden. Das ist hier in Code 07 deutlich zu sehen. Welche Auswirkungen hat dies auf den Code und auf die Anwendungsleistung? Letztendlich scheinen sowohl der Zweck als auch das Ergebnis unverändert zu bleiben.

Durch die Vermeidung wiederholter Datenträgerzugriffe und die Verlagerung der meisten Vorgänge in den Arbeitsspeicher wird die Anwendung schneller; außerdem werden Verzögerungen reduziert und unnötige Leistungseinbußen bei MetaTrader 5 vermieden, die durch ständige Schreibanforderungen an den Datenträger verursacht werden. Tatsächlich erfolgt das eigentliche Schreiben auf den Datenträger nur in diesem Teil des Codes. Allerdings gibt es in Code 07 einige Dinge, die für Programmieranfänger möglicherweise nicht ganz klar sind. Auch wenn die vorherigen Artikel einen Großteil des Codes bereits erläutern, möchte ich kurz erklären, warum Code 07 funktioniert und dasselbe Ergebnis liefert wie die vorherigen Beispiele.

Zunächst ist zu beachten, dass in Zeile 07 eine Variable initialisiert wird, die für uns sehr nützlich sein wird. Außerdem deklarieren wir in Zeile 08 ein dynamisches Array. Dieses Array stellt unsere Datei im Arbeitsspeicher dar. Achten Sie nun darauf, was in den Zeilen 19, 20 und 21 passiert. In diesen Zeilen von Code 07 machen wir dasselbe wie in den Zeilen 16, 17 und 18 von Code 06. In Code 07 führen dieselben drei Zeilen jedoch auch eine Operation aus, die der Prozedur FWriteString entspricht, die ebenfalls in Code 06 vorkommt.

„Aber Moment mal. Jetzt verwirrt mich Code 07 ein wenig. „Meinen Sie damit, dass die Zeilen 19, 20 und 21 alles ersetzen, was wir in Code 06 getan haben, um Daten in die Datei zu schreiben?“ Ja, das ist möglich, mein lieber Leser. „Aber meiner Ansicht nach leisten diese Zeilen in Code 07 das nicht. Zumindest sehe ich es im Moment nicht. „Könnten Sie das genauer erklären?“ Natürlich kann ich das erklären. Schauen wir uns also einmal an, was eine der Codezeilen bewirkt, da die anderen auf die gleiche Weise funktionieren.

Vielleicht rührt die Verwirrung daher, dass diese Zeilen stark zusammengefasst sind. Schauen wir uns also einmal an, wie Zeile 19 funktioniert. Denken Sie zunächst daran, dass die Variable start zum Zeitpunkt der Ausführung der Funktion StringToCharArray bereits einen Wert hat. Der in start gespeicherte Wert gibt also an, ab welcher Position im Array die in der Zeichenkette enthaltenen Zeichen eingefügt werden sollen. Weitere Informationen zur Funktion StringToCharArray finden Sie in der Dokumentation. Nachdem alle Zeichen zum Array hinzugefügt wurden, gibt die Funktion die Anzahl der hinzugefügten Zeichen zurück. Daher verwenden wir den Wert von start, um die Anzahl der Zeichen am Anfang des Arrays abzulegen. Auf diese Weise erhalten wir genau dasselbe Datenlayout, das in Code 06 implementiert wurde.

Allerdings gibt es hier einen Unterschied. StringToCharArray zählt jedes Zeichen, einschließlich des abschließenden NULL-Zeichens. Mit anderen Worten: Die zurückgegebene Anzahl weicht von dem in Code 06 ermittelten Wert ab. Achten Sie besonders genau auf diesen Punkt. Der letzte Schritt dieser Zeile – in diesem Fall Zeile 19 – besteht jedoch darin, die durch die Variable start angegebene Position zu aktualisieren.

Kommen wir nun zum interessanten Teil des Codes, nämlich zu Zeile 23. Wenn Sie verstanden haben, was in den Zeilen 19, 20 und 21 passiert ist, ist Ihnen wahrscheinlich aufgefallen, dass die Variable start immer auf das abschließende NULL-Zeichen im Array zeigt. Dieses Zeichen ist das letzte im Array. Da dieses Zeichen für uns jedoch nicht von Interesse ist, verwenden wir die Funktion FileWriteArray, um alle Zeichen außer dem abschließenden NULL-Zeichen auf dem Datenträger zu speichern. Als Ergebnis erhalten wir eine Binärdatei, die der von Code 06 sehr ähnlich ist. Dennoch wird sie mit der von Code 06 erzeugten Datei nicht kompatibel sein, gerade weil das vorangestellte Längenbyte anders berechnet wird. Aus diesem Grund haben wir auch den Lesealgorithmus auf den in Zeile 29 gezeigten geändert. Dort lesen wir die gesamte Datei in einem einzigen Vorgang ein. Um den Inhalt jedoch korrekt anzuzeigen und somit das erwartete Ergebnis zu erhalten, verwenden wir die Schleife in Zeile 33.


Abschließende Gedanken

In diesem Artikel haben wir untersucht, wie ein direkter Zugriff auf Dateien zum Schreiben von Daten in eine Datei implementiert werden kann. Dadurch können wir ein einfaches Protokoll zum Speichern von Daten definieren. Es ist jedoch nicht immer ratsam, dies direkt in der Datei zu tun.

Oft ist es am besten, die Datei im Arbeitsspeicher als Array aufzubauen. Erst nachdem alle Werte im Array abgelegt wurden, schreiben wir die Daten auf den Datenträger. Dadurch wird der Prozess effizienter, da der Arbeitsspeicher (RAM) wesentlich schneller ist als Speichermedien. Selbst Solid-State-Drives (SSDs) sind langsamer als der Arbeitsspeicher (RAM). Außerdem ist es im Allgemeinen nicht besonders sinnvoll, eine Datei Byte für Byte zu lesen. Ein besserer Ansatz besteht darin, einen großen Block direkt in den Arbeitsspeicher einzulesen und dort zu verarbeiten; dort können wir Byte für Byte nach bestimmten Daten suchen.

Obwohl wir hier nur mit Ein-Byte-Typen gearbeitet haben, können Sie alles Gezeigte auf größere oder komplexere Typen erweitern. Dies ist ohne Weiteres möglich, da sich derselbe Ansatz über Strukturen oder Unions erweitern lässt. Diese wurden hier in dieser Artikelserie bereits behandelt.

MQ5-Datei Beschreibung
Code 01 Demonstration des Dateizugriffs
Code 02 Demonstration des Dateizugriffs
Code 03 Demonstration des Dateizugriffs
Code 04 Demonstration des Dateizugriffs
Code 05 Demonstration des Dateizugriffs
Code 06 Demonstration des Dateizugriffs
Code 07 Demonstration des Dateizugriffs

Übersetzt aus dem Portugiesischen von MetaQuotes Ltd.
Originalartikel: https://www.mql5.com/pt/articles/16271

Beigefügte Dateien |
Anexo.zip (4.13 KB)
Von den Grundlagen bis zur Mittelstufe: Navigation durch die Sandbox Von den Grundlagen bis zur Mittelstufe: Navigation durch die Sandbox
In diesem Artikel sehen wir uns zwei Möglichkeiten an, den Inhalt der Sandbox zu prüfen und sogar mit ihr zu interagieren, wobei MetaTrader 5 als Basisplattform dient. Das Verständnis des in diesem Artikel behandelten Materials ist unerlässlich, um die Themen der folgenden Artikel nachvollziehen zu können.
MQL5 meistern – Vom Anfänger zum Profi (Teil VII): Grundlagen des Debuggings von MQL-Anwendungen MQL5 meistern – Vom Anfänger zum Profi (Teil VII): Grundlagen des Debuggings von MQL-Anwendungen
Debugging ist ein wesentlicher Bestandteil des Programmierzyklus. In diesem Artikel werden gängige Techniken für das Debugging jeder Anwendung behandelt, die in der MetaTrader-5-Umgebung ausgeführt wird.
Eine alternative Log-datei mit der Verwendung der HTML und CSS Eine alternative Log-datei mit der Verwendung der HTML und CSS
In diesem Artikel werden wir eine sehr einfache, aber leistungsfähige Bibliothek zur Erstellung der HTML-Dateien schreiben, dabei lernen wir auch, wie man eine ihre Darstellung einstellen kann (nach seinem Geschmack) und sehen wir, wie man es leicht in seinem Expert Advisor oder Skript hinzufügen oder verwenden kann.
Von den Grundlagen zur Mittelstufe: Objektereignisse (II) Von den Grundlagen zur Mittelstufe: Objektereignisse (II)
In diesem Artikel werden wir uns ansehen, wie die letzten drei Arten von Ereignissen funktionieren, die von einem Objekt ausgelöst werden. Das ist sehr interessant, denn am Ende werden wir etwas tun, das vielen Menschen vielleicht verrückt erscheinen mag, das aber durchaus möglich ist und zu einem sehr überraschenden Ergebnis führt.