Da LegacyClonk seit Clonk Rage einige Neuerungen bezüglich der Scriptsprache bekommen hat, möchte ich hier eine kurze Übersicht und Tipps geben wie man mithilfe der Neuerungen sauberere Scripts schreiben kann. Dies ist ein Wikipost, kann also von jedem ergänzt werden.
Die meisten Neuerungen funktionieren nur mit #strict 3.
#strict 3
geht nochmal strenger mit Typkonvertierungen der Werts 0
und false
um, weshalb es wichtig ist sich darüber im Klaren zu sein. Im Gegensatz zu vorher ist es nicht mehr erlaubt diese als Wert für „nichts“ beliebiger Typen zu verwenden.
Der neue Wert für „nichts“ ist nil
.
Das erlaubt das saubere Unterscheiden zwischen 0
, false
und „nichts“.
Im Gegensatz zu 0
darf nil
aber nicht in Berechnungen verwendet werden. Das führt zu Errors.
Konvertierungen zu bool
Es können weiterhin beliebige Typen zu bool
konvertiert werden, wobei alle außer die folgenden Werte zu true
werden: nil
, 0
, false
, NONE
.
Für den booleschen Negations-Operator !
gilt natürlich die umgekehrte Logik.
Prüfen auf nil, false und 0 und Vergleiche damit
Beispiele
Alle folgenden Ausdrücke ergeben true
:
nil == nil;
nil != 0;
nil != false;
0 == 0;
0 != false;
false == false;
!false;
!0;
!nil;
Erklärung
Allgemein gelten Werte für ==
und !=
nur als identisch wenn sie auch den selben Typen haben.
Da nicht nur nil
bei bool
-Konvertierung zu false
wird, kann if (!foo)
nicht zur korrekten Erkennung auf nil
verwendet werden.
Stattdessen muss ein direkter Vergleich mit nil
stattfinden, also if (foo == nil)
oder if (foo != nil)
.
Das ist aber natürlich nur notwendig, wenn nil
getrennt von anderen false
-artige Werten behandelt werden soll.
Aus diesem Grund müssen Vergleiche mit false
oder 0
mit Vorsicht behandelt werden.
Speziell für false
wird meistens das gewünschte Verhalten mit if (!foo)
erreicht, während if (foo == false)
für 0
und nil
nicht zutrifft.
Verwendung von nil
Tipps für den Umgang mit und Einsatz von nil
:
- Soll
nil
in Berechnungen wie0
behandelt werden, hilft der??
-Operator:
// Foo() gibt int oder nil zurück;
var x = Foo() ?? 0;
var y = x * 42;
- Default-Werte für Funktionsparameter
func Foo(int distance, bool stronger, object obj)
{
distance ??= 100;
stronger ??= true;
obj ??= this;
…
}
Foo(); // distance == 100, stronger == true, obj == this
Foo(0, false); // distance == 0, stronger == false, obj == this
Foo(nil, false); // distance == 100, stronger == false
- Kommunizieren von Fehlern
func Price(id def)
{
if (def == BEER) return 0; // free beer!
else if (def == FLNT) return 2;
return nil; // don’t know
}
Price(BEER); // returns 0, it’s free!
Price(SFLN); // returns nil, we don’t know!
Datentyp map
Codevereinfachungen
Nachfolgend ein paar Codevereinfachungen die möglich sind:
alt | => | neu | Hinweis |
---|---|---|---|
arr[GetLength(arr)] = 42 |
arr[] = 42 |
||
LocalN(obj, "foo") = 1337 |
obj.foo = 1337 |
||
var v = "foo"; LocalN(obj, v) = 1337; |
var v = "foo"; obj[v] = 1337; |
für den Fall, dass der Variablenname nicht konstant ist | |
if (obj) obj->RemoveObject(); |
obj?->RemoveObject(); |
es gibt auch arr?[index] , map?.key , obj?.var , obj?->~Foo()
|
|
if (obj) x = obj->Foo()[0]; else x = "RIP"; |
x = obj?->Foo()[0] ?? "RIP"; |
||
Format("%s %s", "Hello", "World") |
"Hello" .. " " .. "World" |
es gibt auch ..=
|
|
Format("Foo %d", 1234) |
"Foo " .. 1234 |
geht für strings, IDs und ints; bools werden zu "0" bzw. "1"
|
|
Format("%c", GetChar("Hello", 3)) |
"Hello"[3] |
nur lesend; negativer Index zählt vom Ende rückwärts (nur bei strings!) | |
CastObjects(FLNT, 100, 100, AbsX(50), AbsY(50)) |
global->CastObjects(FLNT, 100, 100, 50, 50) |
Aufrufe mit global-> haben globalen Kontext, dadurch gelten Koordinaten immer global |
|
ArrayConcatenate([42, 1337], ["hello world"]) |
[42, 1337] .. ["hello world"] |
ArrayConcatenate musste selbst definiert werden |
Weitere Tipps
-
IntializeDef(string section)
:
Diese Funktion wird in jeder Definition beim Laden des Szenarios und beim Laden einer Sektion aufgerufen.section
ist der Sektionsname der aktuellen Sektion odernil
für die Hauptsektion.
In diesem Callback können unter anderem zur Definition gehörende globale Variablen (mitstatic
definiert) auf einen Wert gesetzt werden, oder ein Regelobjekt erstellt werden welches immer existieren soll.
Der Aufruf erfolgt im Definitionskontext der jeweiligen Definition.
Interoperation zwischen #strict 3 und nicht-#strict 3
Kurzfassung in Beispielform
#strict 3
func Strict3(int anInt, bool aBool, dontKnow)
{
return 0;
}
func TestInStrict3()
{
// Strict3(false); // Error: false is not int
Strict3(0, 0); // Ok: converts 0 to false
Strict3(); // in Strict3(): anInt == nil, aBool == nil, dontKnow == nil
Strict3(0, false, 0); // in Strict3(): anInt == 0, aBool == false, dontKnow == 0
var x = Strict3(0, false, false); // in Strict3(): anInt == 0, aBool == false, dontKnow == false
// x == 0 && x != false && x != nil
x = NonStrict3(0, 0, 0); // in NonStrict3(): anInt == 0 && anInt == false, aBool == 0 && aBool == false, dontKnow == 0 && dontKnow == false
// x == nil && x != 0 && x != false
x = NonStrict3() ?? 0; // x == 0
x = NonStrict3() ?? false; // x == false
}
#strict 2 // or #strict or non-#strict
func NonStrict3(int anInt, bool aBool, dontKnow)
{
// all four variants below have the exact same result
return;
return 0;
return false;
// <no return>
}
func TestInNonStrict3()
{
var x = Strict3(0, 0, 0); // in Strict3(): anInt == 0, aBool == false, dontKnow == nil
// x == 0 && x == false
Strict3(false, false, false); // in Strict3(): anInt == 0, aBool == false, dontKnow == nil
}
Erklärung
Um eine möglichst gute Integration von #strict 3
mit alten Scripten zu ermöglichen gibt es in einigen Fällen automatische Konvertierungen zwischen 0
bzw. false
und nil
.
Je nach Konvertierungsrichtung gelten unterschiedliche Regeln.
Hintergründe
Zur Besserung Nachvollziehbarkeit sei hier ein kleines Detail erwähnt:
Variablen in C4Script haben einen gewissen Typen. Zusätzlich zu den üblichen Typen int
, bool
, id
, string
, object
, array
und map
gibt es den speziellen Typen any
. In der Dokumentation und Typangaben von Funktionsargumenten steht any
für beliebiger Typ.
Intern hat any
drei unterschiedliche Bedeutungen:
- Als Typ einer C4Script-Variable hat es eine Doppelbedeutung:
-
Bei Rohwert 0: Die Variable hat den Wert „nichts“ bzw.
nil
in#strict 3
.
0
,false
,nil
undNONE
haben alle Rohwert 0. - Bei Rohwert != 0: Typ ist unbekannt, in diesem Fall muss er „erraten“ werden.
-
Bei Rohwert 0: Die Variable hat den Wert „nichts“ bzw.
- Bei Typangabe für Funktionsargumente steht es für beliebige Typen.
Der Rohwert ist der Wert, der für die Variable tatsächlich im Arbeitsspeicher steht.
Ohne #strict 3
werden alle Variablen mit Rohwert 0 implizit zu any
. Das führt dazu, dass 0
und false
als Wert für „nichts“ verwendet werden können. Gleichzeitig ist es dadurch auch unmöglich diese Werte zu unterscheiden.
Mit #strict 3
behält eine Variable auch mit Rohwert 0 ihren Typen bei. Nur explizites Setzen auf nil
ändert den Typen zu any
.
Konvertierungsregeln
Prinzipiell gibt es, aufgrund oben erklärter Umstände, einen Typ-Verlust für 0
und false
beim Übergang von #strict 3
zu nicht-#strict 3
.
Diese Werte werden dabei zum Wert „nichts“ (äquivalent zu 0
und false
ohne #strict 3
, bzw. nil
in #strict 3
).
Dies passiert einerseits mit den Funktionsargumenten beim Aufruf einer nicht-#strict 3
-Funktion aus einer #strict 3
-Funktion und andererseits mit dem Rückgabewert von #strict 3
-Funktionen die aus nicht-#strict 3
-Funktionen aufgerufen werden.
Beim Übergang von nicht-#strict 3
zu #strict 3
gibt es unter gewissen Umständen eine automatische Konvertierung von „nichts“ zu 0
oder false
.
Beim Aufruf einer #strict 3
-Funktion aus einem nicht-#strict 3
-Script wird „nichts“ als Funktionsargument zu 0
oder false
konvertiert, wenn das Funktionsargument entsprechend als int
oder bool
definiert ist.
Bei Funktionsargumenten ohne Typangabe und bei Rückgabewerten von nicht-#strict 3
-Funktionen die aus einem #strict 3
-Script aufgerufen werden gibt es keine automatische Konvertierung. In solchen Fällen muss auf #strict 3
-Seite unter Umständen ?? 0
oder ?? false
verwendet werden.
Unter Beachtung dieser Umstände können Scripte mit unterschiedlichen #strict
-Leveln mithilfe von #appendto
und #include
wie gehabt kombiniert werden.
Warum der ganze Aufwand mit #strict 3?
#strict 3
kann als ersten Eindruck aussehen als wäre es unnötiger Mehraufwand. Immerhin löst es in einigen Fällen Errors aus, die vorher oftmals tadellos funktionierten.
In der allgemeinen Scriptpraxis stellt sich aber heraus, dass zu lose Typisierung oft zu versteckten Fehlern führen kann.
JavaScript und vor allem PHP sind wahrscheinlich die bekanntesten Scriptsprachen die aufgrund sehr schwacher Typisierung für viele Überraschungen sorgen.
Durch strengere Typisierung wird zwar etwas mehr Disziplin beim Scripten gefordert, allerdings hilft sie dabei gewisse Fehler schneller aufzudecken und im Endeffekt robustere Scripte zu schreiben.
Außerdem erlaubt sie das Unterscheiden der Werte 0
, false
und „nichts“, was wie oben illustriert unter anderem für Default-Werte hilfreich ist. Hätte es diese Unterscheidung schon immer gegeben, müsste zum Beispiel bei gewissen Funktionen nicht die Spielernummer um 1 erhöht angegeben werden.