Vielen ist foreach und die Anwendung von foreach und Iteratoren ein Begriff, da foreach in Programmiersprachen wie PHP, C++, ... gängig sind. Nur leider in Pawn nicht, doch existiert bereits ein Include, womit man in Pawn foreach und Iteratoren verwenden kann und dadurch bessere Daten Strukturen bilden kann und folglich die Performance einer Anwendung steigern vermag. Dieses Thema ist für viele sicherlich nicht Neuland, doch ist mir aufgefallen, dass viele Lücken im Know-How über die richtige und nutzvolle Anwendung von foreach bestehen. Aus diesem Grund habe ich mich dazu entschlossen im folgenden einen besseren Einblick in die Welt von foreach und dessen Iteratoren zu ermöglichen, sodass es hoffentlich einigen möglich sein wird bessere, übersichtlichere, leichtere und performantere Daten Strukturen zu bilden.
Inhaltsverzeichnis
- Download
- Wieso ist foreach performanter im Vergleich zu normalen Schleifen?
- Deklaration von Iteratoren
- Vordefinierte Iteratoren und deren Anwendung
- foreach und Iteratoren in der Praxis
- Funktionen zur Behandlung von Iteratoren
- Fazit
1. Download
foreach ist in Form eines Includes zu downloaden, welches von Y_Less geschrieben wurde und sich bereits in der 14. Version befindet. Dieses Include werdet ihr in eurem Ordner unter folgendem Pfad einfügen müssen:
Download: http://forum.sa-mp.com/showthread.php?t=92679
2. Wieso ist foreach performanter im Vergleich zu normalen Schleifen?
Dieser erste Schritt dient lediglich dazu foreach genauer zu verstehen und die Systematik hinter diesem großen Versprechen, mehr Performance zu bringen, einleuchten zu lassen. Folglich ist es nicht von Bedeutung dies zu verstehen, wenn man foreach lediglich anwenden möchte, hilft aber zu großen Teilen bei dem Verständnis des Ganzen. Zusammenfassend lässt sich sagen, Iteratoren in verschiedenen Strukturen vorkommen können. foreach arbeitet bei Verwendung des Includes von Y_Less wie eine Liste. Im Vergleich zu herkömmlichen Schleifen, die i. d. R. von Index 0 bis hin zu Index n eines Arrays durchlaufen, "springt" foreach von Index zu Index in der Reihenfolge, wie die Indexe belegt wurden. Schauen wir uns die Ausgangssituation an: Zu Beginn sind alle Werte im Array als "invalide" definiert, dadurch, dass sie einen größeren Wert in sich tragen als der Iterator groß ist:
Es lässt sich erkennen, dass alle Werte im Iterator größer als die Größe des Iterators (10) sind (Achtung: Wir zählen von 0 bis 9!). foreach fügt eine weitere Zelle automatisch hinzu, die die Startzelle des Iterators definiert. Folglich ist der letzte Wert in der Startzelle beinahe valide. Bei der Initialisierung von Iteratoren wendet foreach also folgendes Muster an:
Dies ist die Ausgangssituation eines neuen, unverwundeten Iterators. Nun möchten wir jedoch Werte in den Iterator einsetzen. Ein Nachteil von foreach bildet sich hier bereits ab, denn es können nur Werte kleiner oder gleich der Größe des Iterators eingesetzt werden. Dies spielt jedoch später bei der Anwendung keine sehr große Rolle, denn die Vorteile von foreach überwiegen stark. Wir wollen nun also die Beispielwerte 2, 7 und 3 einsetzen. Da foreach von Index zu Index "springt", sieht unser Iterator nun wie folgt aus:
Was nun passiert ist ist Folgendes: Wir haben in unsere Startzelle (Index 10) unseren ersten Wert 2 eingefügt. Diese Zahl sagt uns, dass wir nun in Index 2 weiterarbeiten. Folglich haben wir unsere zweite Zahl (7) in Index 2 gespeichert. Dies wiederum sagt uns, dass wir nun zu Index 7 springen und dort unsere dritte Zahl (3) einfügen. Würden wir nun weitere Zahlen einfügen, würden wir bei Index 3 weiterarbeiten, da dies unser nächster "Sprung" wäre.
Hieraus lässt sich ableiten, wie foreach diese Versprechen für mehr Performance einhält. Anstatt durch 10 leere Indexe zu laufen, wie es eine herkömmliche Schleife tun würde, definiert foreach bei der Initialisierung von Iteratoren Indexe als invalide und springt von Index zu Index. Folglich werden nur Indexe durchlaufen, die tatsächlich in Verwendung sind und als valide definiert sind. Soviel zur Theorie und der Systematik hinter foreach, kommen wir zur Anwendung des Gelernten in Pawn.
3. Deklaration von Iteratoren
Zu Beginn einer neuen Anwendung gilt es natürlich die benötigten Iteratoren zu deklarieren, die wir für die spätere Verarbeitung von Daten benötigen. Dabei ist Acht zu geben, denn man muss zwischen ein-dimensionalen und mehr-dimensionalen Iteratoren unterscheiden und verschieden an die Deklaration herangehen. Folgender Codeausschnitt sollte die Deklaration vor Augen führen:
new Iterator:oneDimensionalIterator<10>,
Iterator:twoDimensionalIterator[7]<20>,
Iterator:threeDimensionalIterator[6][5]<5>,
Iterator:fourDimensionalIterator[7][2][5]<12>;
Iter_Init(twoDimensionalIterator);
for(new i = 0; i != Iter_InternalSize(threeDimensionalIterator); ++i) {
Iter_Init(threeDimensionalIterator[i]);
}
for(new i = 0; i != Iter_InternalSize(fourDimensionalIterator); ++i) {
for(new j = 0; j != Iter_InternalSize(fourDimensionalIterator[i]); ++j) {
Iter_Init(fourDimensionalIterator[i][j]);
}
}
Ein ganzes Stück Code für ein eigentlich kurzes Stück Arbeit. Für viele mag der obere Quellcode nun etwas verwirrend sein, doch bringen wir Licht ins Dunkle. Schauen wir uns vorerst die Deklaration des ersten Iterators (oneDimensionalIterator) an, der wie der Name des Iterators bereits verrät, nur eine Dimension mit der Größe 10 hat. Ein Iterator wird also wie eine Variable definiert, nur ist der Tag "Iterator:" vor dem Namen des Iterators nötig. Anschließend kommt die Größe der ersten Dimension in "<" und ">". Mit ein-dimensionalen Iteratoren wäre damit schon getan. Aber wie oben angesprochen müssen mehr-dimensionale Iteratoren anderst behandelt werden. Der erste Schritt bleibt jedoch gleich, es wird lediglich in eckigen Klammern eine neue Dimension hinzugefügt. Bei mehr-dimensionalen Iteratoren wird mit der Funktion Iter_Init() der Iterator vollständig deklariert. Der Grund dafür ist, dass mehr-dimensionale Iteratoren erst bei Ausführung der Anwendung initialisiert werden können. Daher ist die Funktion Iter_Init() anzuwenden, sodass beim Ausführen der Anwendung der Iterator komplett initialisiert werden kann.
Doch was ist nun bei diesen drei- und vier-dimensionalen Iteratoren passiert? Wofür diese Schleifen? Diese Schleifen werden benötigt, da die Funktion Iter_Init() lediglich zwei-dimensionale Iteratoren initialisieren kann und folglich wir den Iterator in Schleifen Stück für Stück zusammenschachteln müssen. Bei drei Dimensionen wird eine Schleife benötigt, bei vier Dimensionen zwei Schleifen, usw. (Dimensionen - 2 = Anzahl der Schleifen, da Iter_Init() bereits zwei Dimensionen initialisiert).
4. Vordefinierte Iteratoren und deren Anwendung
Das Include von Y_Less ist wirklich hilfreich, denn es liefert uns bereits von Haus aus drei vordefinierte Iteratoren. Diese sind "Player", welcher alle verbundenen Spieler beinhaltet, "Character", welcher alle verbundenen Spieler und Bots (NPCs) gespeichert hat, und "Bot", welcher über alle Bots (NPCs) verfügt. Dies ist hilfreich, denn nun können wir zum Beispiel mit dem Iterator "Player" sehr performant durch alle verbundenen Spieler springen, ohne per Schleife von 0 bis MAX_PLAYERS durch viele leere Indexe zu springen. Auch die Anwendung von IsPlayerConnected() wird uns erspart - ein weiterer Gewinn an Performance! Ein einfaches Beispiel, eine Schleife, welche uns die Scores aller verbundenen Spieler ausgibt (Achtet dabei auf die Schreibweise dieser neuen Schleife - erst fügen wir in foreach() die Variable ein, dann ein Doppelpunkt und dann den Namen des Iterators):
foreach(new i : Player) {
printf("Score of PlayerID %i: %i.", i, GetPlayerScore(i));
}
Ein weiteres Beispiel, eine Funktion, welche uns Die ID eines Spielers mit einem gegebenen Namen zurückliefert:
getPlayerID(name[]) {
new tempName[MAX_PLAYER_NAME];
foreach(new i : Player) {
GetPlayerName(i, tempName, MAX_PLAYER_NAME);
if(strcmp(tempName, name, false) == false) {
return i;
}
}
return -1;
}
Doch gehen wir einen Schritt weiter und arbeiten mit unseren eigenen Iteratoren für einen ganz bestimmten Anwendungsfall.
5. foreach und Iteratoren in der Praxis
Nehmen wir an, dass wir ein Haussystem basteln möchten, mit welchem wir im Spiel eigenhändig Häuser erstellen können ohne irgendetwas im Vorhinein im Quellcode für diese Häuser definiert zu haben. Normalerweise würden wir nun rangehen und MAX_HOUSES definieren und diesen Wert als Dimension in einem Array verwenden, der unter anderem mit einem Enumerator verlinkt ist. Später würden wir diesen Wert auch noch nehmen, um durch alle Häuser zu gehen, um ein bestimmtes Haus zu finden, zum Beispiel jenes Haus, zu welchem man am nächsten ist. In der Praxis würde man auch wenn man nur zehn Häuser erstellt hat bei einem MAX_HOUSES von 500 immer durch alle 500 Indexe laufen und diese zehn Häuser suchen und letztendlich vergleichen. Nun nehmen wir doch einfach einen Iterator und foreach - Resultat: Wir müssen lediglich durch jene Häuser laufen, die auch tatsächlich existieren. Doch wie sieht das im Quellcode denn nun aus? Machen wir uns dran:
#define MAX_HOUSES (500)
enum houseEnumerator {
Float:hX,
Float:hY,
Float:hZ
}
new Iterator:houseIterator<MAX_HOUSES>,
houseArray[MAX_HOUSES][houseEnumerator];
createHouse(Float:X, Float:Y, Float:Z) {
new idx = Iter_Free(houseIterator);
houseArray[idx][hX] = X;
houseArray[idx][hY] = Y;
houseArray[idx][hZ] = Z;
Iter_Add(houseIterator, idx);
}
getClosestHouse(Float:X, Float:Y, Float:Z, Float:minDistance = 10.0) {
new Float:distance,
Float:tempDistance,
houseID = -1;
foreach(new i : houseIterator) {
tempDistance = getDistanceBetweenPoints(houseArray[i][hX], houseArray[i][hY], houseArray[i][hZ], X, Y, Z);
if((houseID == -1 && tempDistance <= minDistance) || (houseID != -1 && tempDistance < distance)) {
distance = tempDistance;
houseID = i;
}
}
return houseID;
}
Float:getDistanceBetweenPoints(Float:X1, Float:Y1, Float:Z1, Float:X2, Float:Y2, Float:Z2) {
return floatsqroot(((X1 - X2) * (X1 - X2)) + ((Y1 - Y2) * (Y1 - Y2)) + ((Z1 - Z2) * (Z1 - Z2)));
}
Auf den Quellcode selbst werde ich nicht genauer eingehen, da dies nicht Teil des Tutorials ist, jedoch sage ich soviel, dass wir zwar noch die Datenmenge haben, denn wir haben weiterhin einen Array mit einer Dimension von MAX_HOUSES, jedoch wird der Array viel performanter genutzt, indem wir per Iterator nur durch jene Indexe springen, die auch tatsächlich belegt sind, also nur diese Häuser uns ansehen, die auch tatsächlich existieren. Dies bewerkstelligen wir, indem wir beim Erstellen des Hauses (Hier: createHouse()) einen freien Index im Iterator bestimmen und diesen dann als Index im houseArray verwenden und per Iter_Add() im Iterator belegen. Hier werden einige Funktionen wie Iter_Add() und Iter_Free() angewandt, schauen wir uns doch mal die Funktionen für Iteratoren noch genauer an.
6. Funktionen zur Behandlung von Iteratoren
Y_Less hat einige sehr hilfreiche Funktionen in das Include eingebaut, die uns bei der Anwendung des Includes sehr behilflich sind. Iter_Add() ist zweifellos die wichtigste Funktion, die eingebaut wurde, denn damit ist es uns möglich den Iterator zu füllen. Iter_Add() hat zwei Parameter, den Namen des Iterators und den Wert, der hinzugefügt werden soll. Im Folgenden werde ich nicht auf alle Funktionen detailliert eingehen, sondern lediglich die Funktionsköpfe als auch den Effekt der Funktion in Kommentaren niederschreiben und ein kurzes Beispiel hinzufügen. Dies sollte für die Erklärung dieser recht kleinen aber wirksamen Funktionen genügen:
/*
Iter_Add(name, value)
Effekt: Fügt einen Wert zu einem beliebigen Iterator hinzu.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
/*
Iter_Remove(name, value)
Effekt: Entfernt einen Wert aus einem beliebigen Iterator.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 1);
Iter_Add(exampleIterator, 2);
Iter_Remove(exampleIterator, 1);
/*
Iter_SafeRemove(name, value, &prev)
Effekt: Entfernt einen Wert aus einem beliebigen Iterator innerhalb einer foreach-Schleife. Iter_Remove() kann nicht angewandt werden, da aufgrund der Sprünge von Index zu Index die Anwendung nicht mehr wissen würde, wohin man als nächstes springen sollte. Daher liefert die Funktion Iter_SafeRemove() in einem dritten Parameter denjenigen Wert zurück, mit welchem nun "weitergesprungen" wird.
*/
new Iterator:exampleIterator<3>,
continualValue;
Iter_Add(exampleIterator, 1);
Iter_Add(exampleIterator, 2);
foreach(new i : exampleIterator) {
Iter_SafeRemove(exampeIterator, i, continualValue);
i = continualValue;
}
/*
Iter_Clear(name)
Effekt: Leert einen kompletten Iterator.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 1);
Iter_Add(exampleIterator, 2);
Iter_Clear(exampleIterator);
/*
Iter_Random(name)
Effekt: Liefert einen beliebigen Wert aus einem beliebigen Iterator.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 1);
Iter_Add(exampleIterator, 2);
printf("Eins oder zwei?: %i!", Iter_Random(exampleIterator));
/*
Iter_Count(name)
Effekt: Liefert die Anzahl an Werten in einem Iterator.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
printf("Der Iterator enthält %i Werte!", Iter_Count(exampleIterator));
/*
Iter_Free(name)
Effekt: Liefert der ersten freien Index in einem beliebigen Iterator.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
printf("Der erste freie Index ist: %i!", Iter_Free(exampleIterator));
/*
Iter_Contains(name, value)
Effekt: Prüft, ob ein Wert in einem Iterator vorhanden ist.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
printf("Ist der Wert eins im Iterator vorzufinden? %s!", (Iter_Contains(exampleIterator, 2) == 0) ? ("Nein") : ("Ja"));
/*
Iter_First(name)
Effekt: Liefert den ersten Wert eines Iterators.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
printf("Der erste Wert in diesem Iterator ist: %i!", Iter_First(exampleIterator));
/*
Iter_Next(name, cur)
Effekt: Liefert den nächsten Wert in der Reihe eines Iterators.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 0);
Iter_Add(exampleIterator, 2);
printf("Der nächste Wert ist: %i!", Iter_Next(exampleIterator, 0));
/*
Iter_Last(name)
Effekt: Liefert den letzten Wert eines Iterators.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
printf("Der letzte Wert ist: %i!", Iter_Last(exampleIterator));
/*
Iter_Prev(name, cur)
Effekt: Liefert den vorherigen Wert in der Reihe eines Iterators.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 1);
Iter_Add(exampleIterator, 2);
printf("Der vorherige Wert ist: %i!", Iter_Prev(exampleIterator, 2));
/*
Iter_Begin(name)
Effekt: Liefert einen invaliden Wert vor Beginn des Iterators (vergleiche "2. Wieso ist foreach performanter im Vergleich zu normalen Schleifen?").
Iter_End(name)
Effekt: Liefert einen invaliden Wert nach Ende des Iterators (vergleiche "2. Wieso ist foreach performanter im Vergleich zu normalen Schleifen?"). Dies kann mit Iter_Begin() hilfreich zum Bilden einer foreach()-Schleife per for() sein, wodurch man später weitere Parameter einbauen kann und zum Beispiel weitere if-Abfragen entfernen kann.
*/
new Iterator:exampleIterator<3>;
Iter_Add(exampleIterator, 2);
for (new i = Iter_Begin(exampleIterator); (i = Iter_Next(exampleIterator, i)) != Iter_End(exampleIterator); ) {
// ...
}
7. Fazit
Zusammenfassend lässt sich sagen, dass man mit foreach und Iteratoren eine Menge anstellen kann - und dies zum Guten! Wer performant arbeiten möchte, der wird um Iteratoren nicht herumkommen, denn die Vorteile in jedem Bereich sind massiv. Nicht nur performancetechnisch sind die Vorteile grandios, sondern auch im Quellcode selbst, denn dieser sieht direkt viel übersichtlicher und besser aus, denn mit Iteratoren kann man sich auch eine Menge Arbeit ersparen und Bugs können auch taktisch umlaufen werden. Zwar kann man mit Iteratoren noch nicht die Datenmenge, die man benötigt, auch wenn man Indexe nicht belegt schmälern, aber zumindest kann man damit schonmal diese unsinnigen Indexe umlaufen. In Verbindung mit Vektoren können Iteratoren Wunder wirken, doch leider haben wir in Pawno auch keine Vektoren. Aber dazu gibt es ja die nötigen Plugins oder kann sich selbst die nötigen Plugins schreiben. Aber das ist ein anderes Thema. Ich hoffe, dass ich euch helfen konnte und das Thema foreach und Iteratoren etwas klarer machen konnte, sodass ihr in Zukunft damit Anwendung finden könnt. Ich würde mich über Feedback und Verbesserungen freuen.
P.S.: Dies sind alles Beispielcodes, die ich nur hier im Editor geschrieben habe. Ich habe sie nicht nochmals durchgetestet. Falls ihr Fehler findet würde ich mich über eine Mitteilung freuen.