Dieser Leitfaden enthält eine Reihe von empfohlenen Standards für das Schreiben von konsistentem Code, der leicht zu pflegen ist. Durch die Einhaltung dieser Richtlinien können Entwickler die Lesbarkeit des Codes verbessern, Fehler reduzieren und die Zusammenarbeit erleichtern. Ein standardisierter Codestil ist notwendig, um sicherzustellen, dass der Code sowohl für aktuelle als auch für zukünftige Entwickler, die an einem Projekt arbeiten, einfach zu verstehen und zu pflegen ist.
Dieser Leitfaden enthält Empfehlungen, aber letztlich liegt die Entscheidung bei deinem Team.
1. Gebräuchliche Benennungsschemata
Die Benennung ist entscheidend für lesbaren und wartbaren Code. Versuche, in deinem Code eine einheitliche Benennung zu verwenden.
1.1 Do's
-
IsX
: Wird oft zur Benennung von Logikvariablen verwendet, um eine Frage zu stellen (z. B. IsEmpty). -
OnX
: Eine überladbare Funktion, die vom Framework aufgerufen wird. -
SubscribeX
: Abonniert ein Framework-Event mit dem Namen X, oft mit Übergabe einer OnX-Funktion an dieses Ereignis. -
MakeC
: Macht eine Instanz der Klasse c, ohne den c-Construktor zu überladen. -
CreateC
: Erstellt eine Instanz der Klasse c, die ihr logisches Leben beginnt. -
DestroyC
: Beendet die logische Lebensdauer. -
C:c
: Wenn du mit einer einzelnen Instanz der Klasse c arbeitest, ist es in Ordnung, sie C zu nennen.
1.2 Don'ts
-
Anhängen von Typnamen. Nenne es einfach
thing
, nichtthing_typ
oderthing_klasse
. -
Anhängen von Enum-Werten. Nicht
color := enum{COLOR_Red, COLOR_Green}
, sonderncolor := enum{Red, Green}
verwenden.
2. Namen
2.1 Typen verwenden lower_snake_case
Typnamen sollten immer lower_snake_case
sein. Dazu gehören alle Typen: Structs, Klassen, Typedefs, Traits/Interfaces, Enums, usw.
my_new_type := class
2.2 Interfaces sind Adjektive
Interfaces sollten wenn möglich Adjektive sein, wie z.B. printable, enumerable. Wenn Adjektive nicht geeignet erscheinen, füge stattdessen _interface
an den Namen an.
my_new_thing_interface := interface
2.3 PascalCase für alles andere
Alle anderen Namen sollten PascalCase sein. Module, Mitgliedsvariablen, Parameter, Methoden und so weiter.
MyNewVariable:my_new_type = …
2.4 Parametrische Typen
-
Benenne parametrische Typen t oder thing, wobei thing erklärt, was der Typ darstellen soll. Zum Beispiel:
Send(Payload:payload where payload:type)
Du sendest einige parametrisierte Daten,Payload
, von einem beliebigenpayload
-Typ. -
Wenn es mehr als einen Parametertyp gibt, vermeide die Verwendung einzelner Buchstaben, wie
t
,u
,g
. -
Verwende niemals das Suffix
_t
.
3. Formatierung
Es ist wichtig, dass du bei der Formatierung deiner gesamten Codebasis konsistent bleibst. Dadurch wird der Code für dich und andere Entwickler einfacher zu lesen und zu verstehen. Wähle einen Formatierungsstil, der für das Projekt geeignet ist.
Als Beispiel für eine konsistente Vorgehensweise könntest du eines der folgenden Abstandsformate wählen und es in der gesamten Codebasis verwenden:
MyVariable : int = 5
MyVariable:int = 5
3.1 Einrückung
-
Verwende vier Leerzeichen für die Einrückung, niemals Tabulatoren.
-
Codeblöcke sollten eingerückte Blöcke (spaced) und keine geschweiften Klammern (braced) verwenden:
my_class := class: Foo():void = Print("Hallo, Welt!")
- Ausname ist das Schreiben von einzeiligen Ausdrücken wie
option{a}
,my_class{A := b}
, etc.
- Ausname ist das Schreiben von einzeiligen Ausdrücken wie
3.2 Leerzeichen
- Verwende Leerzeichen um Operatoren, es sei denn, es ist sinnvoll, den Code für seinen Kontext kompakt zu halten. Füge Klammern hinzu, um die Reihenfolge der Operationen explizit festzulegen.
MyNumber := 4 + (2 * (a + b))
-
Füge keine Leerzeichen am Anfang und Ende von Klammern ein. Mehrere Ausdrücke innerhalb von Klammern sollten durch ein einzelnes Leerzeichen getrennt werden.
enum{Red, Blue, Green} MyObject:my_class = my_class{X := 1, Y := 2} Vector := vector3{X := 1000.0, Y := -1000.0, Z := 0.0} Foo(Num:int, Str:[]char)
- Halte Kennung und Typ zusammen; füge ein Leerzeichen um den Zuweisungsoperator
=
hinzu. Füge ein Leerzeichen um Typdefinitionen und konstante Initialisierungsoperatoren (:=
) hinzu.MyVariable:int = 5 MyVariable := 5 my_type := class
- Befolge die gleichen Empfehlungen für Klammern, Kennungen und Typenabstände für Funktionssignaturen.
Foo(X:t where t:subtype(class3)):tuple(t, int) = (X, X.Property) Foo(G(:t):void where t:type):void Const(X:t, :u where t:type, u:type):t = X
3.3 Zeilenumbrüche
-
Verwende eine beabstandete, mehrzeilige Form, um einen Zeilenumbruch einzufügen.
Do MyTransform := transform: Translation := vector3: X := 100.0 Y := 200.0 Z := 300.0 Rotation := rotation: Pitch := 0.0 Yaw := 0.0 Roll := 0.0
Besser lesbar und leichter zu bearbeiten. Don't MyTransform := transform{Translation := vector3{X := 100.0, Y := 200.0, Z := 300.0}, Rotation := rotation{...}}
Schwer zu lesen in einer einzigen Zeile
- Definiere Enums in beabstandeter, mehrzeiliger Form, wenn sie Kommentare pro Enumeration benötigen oder wenn du einen Zeilenumbruch einfügen musst.
enum: Red, # Desc1 Blue, # Desc2
3.4 Klammern
Verwende keine Klammern für nicht vererbende Klassendefinitionen.
Do |
|
Don't |
|
3.5 Vermeide die Punkt-Leerzeichen-Schreibweise
Vermeide die Verwendung der Punkt-Leerzeichen-Notation ". " anstelle von Klammern. Dies macht es visuell schwieriger, Leerzeichen zu erkennen und ist eine potenzielle Quelle der Verwirrung.
Don't |
|
Don't |
|
4. Funktionen
4.1 Implizite Rückgabe als Standard
Funktionen geben ihren letzten Ausdruckswert zurück. Verwende das als implizite Rückgabe.
Sqr(X:int):int =
X * X # Implizite Rückgabe
Wenn explizite Rückgaben verwendet werden, sollten alle Rückgaben in der Funktion explizit sein.
4.2 GetX-Funktionen sollten
Getter oder Funktionen mit ähnlicher Semantik, die möglicherweise keine gültigen Werte zurückgeben, sollten mit <decides><transacts>
gekennzeichnet werden und einen Nicht-Options-Typ zurückgeben. Der Aufrufer sollte mit möglichen Fehlern umgehen.
GetX()<decides><transacts>:x
Eine Ausnahme bilden Funktionen, die bedingungslos in eine var
schreiben müssen. Ein Fehlschlag würde die Mutation zurücksetzen, daher müssen sie als Rückgabetyp logic
oder option
verwenden.
4.3 Bevorzugung von Erweiterungsmethoden gegenüber Ein-Parameter-Funktionen
Verwende Erweiterungsmethoden anstelle einer Funktion mit einem einzigen typisierten Parameter.
Dies hilft Intellisense. Durch die Eingabe von MyVector.Normalize()
anstelle von Normalize(MyVector)
kann es mit jedem Zeichen des eingegebenen Methodennamens Namen vorschlagen.
Do
(Vector:vector3).Normalize<public>():vector3
Don't
Normalize<public>(Vector:vector3):vector3
5. Fehlerprüfungen
5.1 Begrenzung der Anzahl der einzeiligen fehlbaren Ausdrücke auf drei
-
Begrenze die bedingten Prüfungen/fehlbaren Ausdrücke in einer Zeile auf maximal drei.
if (Damage > 10, Player := FindRandomPlayer[], Player.GetFortCharacter[].IsAlive[]):
EliminatePlayer(Player)
-
Verwende die Form if
mit Klammern ()
, wenn die Anzahl der Bedingungen weniger als drei beträgt.
Do
if (Damage > 10, Player := FindRandomPlayer[]):
EliminatePlayer(Player)
Hält den Code kurz, aber lesbar.
Don't
if:
Damage > 10
Player := FindRandomPlayer[]
then:
EliminatePlayer(Player)
Unnötige Aufteilung des Codes auf mehrere Zeilen, ohne dass die Lesbarkeit verbessert wird.
-
Wenn du mehr als zwei Wörter für jeden Ausdruck verwendest, sind maximal zwei Ausdrücke in einer Zeile oft besser lesbar.
if (Player := FindAlivePlayer[GetPlayspace().GetPlayers()], Team := FindEmptyTeam[GetPlayspace().GetTeamCollection().GetTeams()]):
AddPlayerToTeam(Player, Team)
-
Du kannst auch die Regel anwenden, dass du in einer einzigen Zeile nicht mehr als neun Wörter verwendest.
Bei Überschreitung der Grenze ist die mehrzeilige Form mit Abstand zu verwenden.
Do
if:
Player := FindAlivePlayer[GetPlayspace().GetPlayers()]
Team := FindEmptyTeam[GetPlayspace().GetTeamCollection().GetTeams()]
Character := Player.GetFortCharacter[]
Character.GetHealth() < 50
then:
AddPlayerToTeam(Player, Team)
Character.SetHealth(100)
Der Text liest sich besser und der Zusammenhang ist über mehrere Zeilen hinweg verständlich.
Don't
if (Player := FindAlivePlayer[GetPlayspace().GetPlayers()], Team := FindEmptyTeam[GetPlayspace().GetTeamCollection().GetTeams()], Character := Player.GetFortCharacter[], Character.GetHealth() < 50):
AddPlayerToTeam(Player, Team)
Character.SetHealth(100)
Der Text ist schwer zu entziffern.
-
Es ist zu prüfen, ob die Zusammenfassung mehrerer fehlbarer Bedingungen in einer einzigen <decides>
-Funktion die Lesbarkeit und Wiederverwendung des Codes erleichtern würde. Wenn der Code nur an einer Stelle verwendet wird, kann ein "section"-Kommentar ohne Ad-hoc-Funktion ausreichend sein.
if:
Player := FindRandomPlayer[]
IsAlive[Player]
not IsInvulnerable[Player]
Character := Player.GetFortCharacter[]
Character.GetHealth < 10
then:
EliminatePlayer(Player)
-
Kann umgeschrieben werden als:
GetRandomPlayerToEliminate()<decides><transacts>:player=
Player := FindRandomPlayer[]
IsAlive[Player]
not IsInvulnerable[Player]
Character := Player.GetFortCharacter[]
Character.GetHealth < 10
Spieler
if (Player := GetRandomPlayerToEliminate[]):
Eliminate(Player)
-
Die gleiche Richtlinie gilt für Ausdrücke in for
-Schleifen.
Zum Beispiel:
set Lights = for (ActorIndex -> TaggedActor : TaggedActors, LightDevice := customizable_light_device[TaggedActor], ShouldLightBeOn := LightsState[ActorIndex]):
Logger.Print("Adding Light at index {ActorIndex} with State:{if (ShouldLightBeOn?) then "On" else "Off"}")
if (ShouldLightBeOn?) then LightDevice.TurnOn() else LightDevice.TurnOff()
LightDevice
-
Besser als:
set Lights = for:
ActorIndex -> TaggedActor : TaggedActors
LightDevice := customizable_light_device[TaggedActor]
ShouldLightBeOn := LightsState[ActorIndex]
do:
if (ShouldLightBeOn?) then LightDevice.TurnOn() else LightDevice.TurnOff()
LightDevice
5.2 Gruppiere abhängige fehlbare Ausdrücke
Wenn eine Bedingung in einem Fehlerkontext davon abhängt, dass ein vorhergehender Fehlerkontext erfolgreich ist, sollten die beiden Bedingungen, wenn möglich, im selben Fehlerkontext bleiben und die Richtlinie 5.1 befolgt werden.
Dadurch wird die Lokalität des Codes verbessert, was das logische Verständnis und die Fehlersuche vereinfacht.
Do
EliminatingCharacter := EliminationResult.EliminatingCharacter
if (FortCharacter := EliminatingCharacter?, EliminatingAgent := FortCharacter.GetAgent[]):
GrantNextWeapon(EliminatingAgent)
Abhängige oder verwandte Bedingungen werden gruppiert.
Do
if:
FortCharacter := Player.GetFortCharacter[]
set AgentMap[Player] = 1
FirstItemGranter:item_granter_device = ItemGranters[0]
then:
FortCharacter.EliminatedEvent().Subscribe(OnPlayerEliminated)
FirstItemGranter.GrantItem(Player)
Abhängige oder verwandte Bedingungen werden gruppiert.
Don't
EliminatingCharacter := Result.EliminatingCharacter
if:
FortCharacter := EliminatingCharacter?
then:
if:
EliminatingAgent := FortCharacter.GetAgent[]
then:
GrantNextWeapon(EliminatingAgent)
Unnötige Einrückungen können die Übersichtlichkeit des Textes beeinträchtigen.
Don't
if:
FortCharacter := Player.GetFortCharacter[]
then:
FortCharacter.EliminatedEvent().Subscribe(OnPlayerEliminated)
if:
set AgentMap[Player] = 1
if:
FirstItemGranter:item_granter_device = ItemGranters[0]
then:
FirstItemGranter.GrantItem(Player)
Unnötige Einrückungen können die Übersichtlichkeit des Textes beeinträchtigen.
Es ist akzeptabel, Fehlerkontexte aufzuteilen, wenn du jeden potenziellen Fehler (oder Fehlergruppen) separat behandelst.
if (Player := FindPlayer[]):
if (Player.IsVulnerable[]?):
EliminatePlayer(Player)
else:
Print("Der Spieler ist unverwundbar und kann nicht eliminiert werden.")
else:
Print("Spieler kann nicht gefunden werden. Das ist ein Einrichtungsfehler.")
6. Verkapselung
6.1 Bevorzuge Interfaces gegenüber Klassen
Verwende Interfaces anstelle von Klassen, wo es sinnvoll ist. Dies trägt zur Verringerung der Implementierungsabhängigkeiten bei und ermöglicht es den Benutzern, Implementierungen bereitzustellen, die vom Framework verwendet werden können.
6.2 Bevorzuge privaten Zugang und schränke den Umfang ein
Klassenmitglieder sollten in den meisten Fällen "privat" sein.
Methoden von Klassen und Modulen sollten so restriktiv wie möglich skaliert werden – <internal>
oder <private>
, wo immer dies angebracht ist.
7. Events
7.1 Postfix-Events mit Event- und Prefix-Handlern mit On
Namen von abonnierbaren Events oder Delegatenlisten sollten mit dem Postfix Event
versehen werden, und Namen von Event-Handlern sollten mit dem Präfix On
versehen werden.
MyDevice.JumpEvent.Subscribe(OnJump)
8. Parallelität
8.1 Verzichte darauf, Funktionen mit Async auszustatten
Vermeide es, <suspends>
Funktionen mit Async
oder ähnlichen Begriffen auszustatten.
Do
DoWork()<suspends>:void
Don't
DoWorkAsync()<suspends>:void
Es ist akzeptabel, das Präfix Await
zu einer <suspends>
-Funktion hinzuzufügen, die intern auf etwas wartet, das passieren soll.
Dies kann klären, wie eine API verwendet werden soll.
AwaitGameEnd()<suspends>:void=
# Andere Dinge einrichten, bevor das Spielende abgewartet wird...
GameEndEvent.Await()
OnBegin()<suspends>:void =
race:
RunGameLoop()
AwaitGameEnd()
9. Attribute
9.1 Attribute trennen
Setze die Attribute in eine eigene Zeile. Es ist besser lesbar, insbesondere wenn mehrere Attribute zur gleichen Kennung hinzugefügt werden.
Do
@editable
MyField:int = 42
Don't
@editable MyField:int = 42
10. Ausdrücke importieren
10.1 Sortiere Importausdrücke in alphabetischer Reihenfolge
Zum Beispiel:
using { /EpicGames.com/Temporary/Diagnostics }
using { /EpicGames.com/Temporary/SpatialMath }
using { /EpicGames.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }
Getter oder Funktionen mit ähnlicher Semantik, die möglicherweise keine gültigen Werte zurückgeben, sollten mit <decides><transacts>
gekennzeichnet werden und einen Nicht-Options-Typ zurückgeben. Der Aufrufer sollte mit möglichen Fehlern umgehen.
GetX()<decides><transacts>:x
Eine Ausnahme bilden Funktionen, die bedingungslos in eine var
schreiben müssen. Ein Fehlschlag würde die Mutation zurücksetzen, daher müssen sie als Rückgabetyp logic
oder option
verwenden.
4.3 Bevorzugung von Erweiterungsmethoden gegenüber Ein-Parameter-Funktionen
Verwende Erweiterungsmethoden anstelle einer Funktion mit einem einzigen typisierten Parameter.
Dies hilft Intellisense. Durch die Eingabe von MyVector.Normalize()
anstelle von Normalize(MyVector)
kann es mit jedem Zeichen des eingegebenen Methodennamens Namen vorschlagen.
Do |
|
Don't |
|
5. Fehlerprüfungen
5.1 Begrenzung der Anzahl der einzeiligen fehlbaren Ausdrücke auf drei
-
Begrenze die bedingten Prüfungen/fehlbaren Ausdrücke in einer Zeile auf maximal drei.
if (Damage > 10, Player := FindRandomPlayer[], Player.GetFortCharacter[].IsAlive[]): EliminatePlayer(Player)
-
Verwende die Form
if
mit Klammern()
, wenn die Anzahl der Bedingungen weniger als drei beträgt.
Do |
|
Hält den Code kurz, aber lesbar. |
Don't |
|
Unnötige Aufteilung des Codes auf mehrere Zeilen, ohne dass die Lesbarkeit verbessert wird. |
-
Wenn du mehr als zwei Wörter für jeden Ausdruck verwendest, sind maximal zwei Ausdrücke in einer Zeile oft besser lesbar.
if (Player := FindAlivePlayer[GetPlayspace().GetPlayers()], Team := FindEmptyTeam[GetPlayspace().GetTeamCollection().GetTeams()]): AddPlayerToTeam(Player, Team)
-
Du kannst auch die Regel anwenden, dass du in einer einzigen Zeile nicht mehr als neun Wörter verwendest. Bei Überschreitung der Grenze ist die mehrzeilige Form mit Abstand zu verwenden.
Do |
|
Der Text liest sich besser und der Zusammenhang ist über mehrere Zeilen hinweg verständlich. |
Don't |
|
Der Text ist schwer zu entziffern. |
-
Es ist zu prüfen, ob die Zusammenfassung mehrerer fehlbarer Bedingungen in einer einzigen
<decides>
-Funktion die Lesbarkeit und Wiederverwendung des Codes erleichtern würde. Wenn der Code nur an einer Stelle verwendet wird, kann ein "section"-Kommentar ohne Ad-hoc-Funktion ausreichend sein.if: Player := FindRandomPlayer[] IsAlive[Player] not IsInvulnerable[Player] Character := Player.GetFortCharacter[] Character.GetHealth < 10 then: EliminatePlayer(Player)
-
Kann umgeschrieben werden als:
GetRandomPlayerToEliminate()<decides><transacts>:player= Player := FindRandomPlayer[] IsAlive[Player] not IsInvulnerable[Player] Character := Player.GetFortCharacter[] Character.GetHealth < 10 Spieler if (Player := GetRandomPlayerToEliminate[]): Eliminate(Player)
-
Die gleiche Richtlinie gilt für Ausdrücke in
for
-Schleifen. Zum Beispiel:set Lights = for (ActorIndex -> TaggedActor : TaggedActors, LightDevice := customizable_light_device[TaggedActor], ShouldLightBeOn := LightsState[ActorIndex]): Logger.Print("Adding Light at index {ActorIndex} with State:{if (ShouldLightBeOn?) then "On" else "Off"}") if (ShouldLightBeOn?) then LightDevice.TurnOn() else LightDevice.TurnOff() LightDevice
-
Besser als:
set Lights = for: ActorIndex -> TaggedActor : TaggedActors LightDevice := customizable_light_device[TaggedActor] ShouldLightBeOn := LightsState[ActorIndex] do: if (ShouldLightBeOn?) then LightDevice.TurnOn() else LightDevice.TurnOff() LightDevice
5.2 Gruppiere abhängige fehlbare Ausdrücke
Wenn eine Bedingung in einem Fehlerkontext davon abhängt, dass ein vorhergehender Fehlerkontext erfolgreich ist, sollten die beiden Bedingungen, wenn möglich, im selben Fehlerkontext bleiben und die Richtlinie 5.1 befolgt werden.
Dadurch wird die Lokalität des Codes verbessert, was das logische Verständnis und die Fehlersuche vereinfacht.
Do |
|
Abhängige oder verwandte Bedingungen werden gruppiert. |
Do |
|
Abhängige oder verwandte Bedingungen werden gruppiert. |
Don't |
|
Unnötige Einrückungen können die Übersichtlichkeit des Textes beeinträchtigen. |
Don't |
|
Unnötige Einrückungen können die Übersichtlichkeit des Textes beeinträchtigen. |
Es ist akzeptabel, Fehlerkontexte aufzuteilen, wenn du jeden potenziellen Fehler (oder Fehlergruppen) separat behandelst.
if (Player := FindPlayer[]):
if (Player.IsVulnerable[]?):
EliminatePlayer(Player)
else:
Print("Der Spieler ist unverwundbar und kann nicht eliminiert werden.")
else:
Print("Spieler kann nicht gefunden werden. Das ist ein Einrichtungsfehler.")
6. Verkapselung
6.1 Bevorzuge Interfaces gegenüber Klassen
Verwende Interfaces anstelle von Klassen, wo es sinnvoll ist. Dies trägt zur Verringerung der Implementierungsabhängigkeiten bei und ermöglicht es den Benutzern, Implementierungen bereitzustellen, die vom Framework verwendet werden können.
6.2 Bevorzuge privaten Zugang und schränke den Umfang ein
Klassenmitglieder sollten in den meisten Fällen "privat" sein.
Methoden von Klassen und Modulen sollten so restriktiv wie möglich skaliert werden – <internal>
oder <private>
, wo immer dies angebracht ist.
7. Events
7.1 Postfix-Events mit Event- und Prefix-Handlern mit On
Namen von abonnierbaren Events oder Delegatenlisten sollten mit dem Postfix Event
versehen werden, und Namen von Event-Handlern sollten mit dem Präfix On
versehen werden.
MyDevice.JumpEvent.Subscribe(OnJump)
8. Parallelität
8.1 Verzichte darauf, Funktionen mit Async auszustatten
Vermeide es, <suspends>
Funktionen mit Async
oder ähnlichen Begriffen auszustatten.
Do |
|
Don't |
|
Es ist akzeptabel, das Präfix Await
zu einer <suspends>
-Funktion hinzuzufügen, die intern auf etwas wartet, das passieren soll.
Dies kann klären, wie eine API verwendet werden soll.
AwaitGameEnd()<suspends>:void=
# Andere Dinge einrichten, bevor das Spielende abgewartet wird...
GameEndEvent.Await()
OnBegin()<suspends>:void =
race:
RunGameLoop()
AwaitGameEnd()
9. Attribute
9.1 Attribute trennen
Setze die Attribute in eine eigene Zeile. Es ist besser lesbar, insbesondere wenn mehrere Attribute zur gleichen Kennung hinzugefügt werden.
Do |
|
Don't |
|
10. Ausdrücke importieren
10.1 Sortiere Importausdrücke in alphabetischer Reihenfolge
Zum Beispiel:
using { /EpicGames.com/Temporary/Diagnostics }
using { /EpicGames.com/Temporary/SpatialMath }
using { /EpicGames.com/Temporary/UI }
using { /Fortnite.com/UI }
using { /Verse.org/Simulation }