Benutzerdefinierter ==-Operator. Sollen wir ihn beibehalten?

Wenn Sie dies in Unity tun:
if (myGameObject == null) {}
Unity macht etwas Besonderes mit dem ==-Operator. Anstelle dessen, was die meisten Leute erwarten würden, haben wir eine spezielle Implementierung des ==-Operators.
Dies dient zwei Zwecken:
1) Wenn ein MonoBehaviour Felder hat (nur im Editor[1]), setzen wir diese Felder nicht auf „echte null“, sondern auf ein „Fake null“-Objekt. Unser benutzerdefinierter ==-Operator kann prüfen, ob etwas eines dieser gefälschten null ist, und verhält sich entsprechend. Obwohl dies ein exotisches Setup ist, können wir damit Informationen im gefälschten null speichern, die Ihnen mehr Kontextinformationen liefern, wenn Sie eine Methode darauf aufrufen oder das Objekt nach einer Eigenschaft fragen. Ohne diesen Trick würden Sie nur eine NullReferenceException und einen Stacktrace erhalten, hätten aber keine Ahnung, welches GameObject das MonoBehaviour mit dem Feld hatte, das null war. Mit diesem Trick können wir das GameObject im Inspektor hervorheben und Ihnen auch weitere Anweisungen geben: „Sieht aus, als würden Sie hier auf ein nicht initialisiertes Feld in diesem MonoBehaviour zugreifen. Verwenden Sie den Inspektor, um das Feld auf etwas verweisen zu lassen.“
Zweck zwei ist etwas komplizierter.
2) Wenn Sie ein ac#-Objekt vom Typ „GameObject“[2] erhalten, enthält es fast nichts. Das liegt daran, dass Unity eine C/C++-Engine ist. Alle tatsächlichen Informationen zu diesem GameObject (sein Name, die Liste seiner Komponenten, seine HideFlags usw.) befinden sich auf der C++-Seite. Das einzige, was das C#-Objekt hat, ist ein Zeiger auf das native Objekt. Wir nennen diese C#-Objekte „Wrapperobjekte“. Die Lebensdauer dieser C++-Objekte wie GameObject und alles andere, was von UnityEngine.Object abgeleitet ist, wird explizit verwaltet. Diese Objekte werden zerstört, wenn Sie eine neue Szene laden. Oder wenn Sie für sie Object.Destroy(myObject); aufrufen. Die Lebensdauer von C#-Objekten wird auf C#-Art mit einem Garbage Collector verwaltet. Dies bedeutet, dass es möglich ist, dass noch ein AC#-Wrapperobjekt vorhanden ist, das ein bereits zerstörtes C++-Objekt umschließt. Wenn Sie dieses Objekt mit null vergleichen, gibt unser benutzerdefinierter ==-Operator in diesem Fall „true“ zurück, obwohl die eigentliche C#-Variable in Wirklichkeit nicht wirklich null ist.
Während diese beiden Anwendungsfälle ziemlich sinnvoll sind, bringt die benutzerdefinierte null auch eine Reihe von Nachteilen mit sich.
– Es ist kontraintuitiv.
- Der Vergleich zweier UnityEngine.Objects miteinander oder mit null ist langsamer als erwartet.
– Der benutzerdefinierte ==-Operator ist nicht threadsicher, daher können Sie Objekte nicht außerhalb des Hauptthreads vergleichen. (dieses könnten wir beheben).
– Es verhält sich inkonsistent mit dem ??-Operator, der ebenfalls eine null durchführt, aber dieser führt eine reine C# null durch und kann nicht umgangen werden, um unsere benutzerdefinierte null aufzurufen.
Wenn wir alle diese Vor- und Nachteile durchgehen und unsere API von Grund auf neu erstellen würden, würden wir uns dafür entscheiden, keine benutzerdefinierte null durchzuführen, sondern stattdessen eine myObject.destroyed-Eigenschaft zu haben, mit der Sie prüfen können, ob das Objekt tot ist oder nicht. Und wir müssten einfach damit leben, dass wir keine besseren Fehlermeldungen mehr ausgeben können, wenn Sie eine Funktion für ein Feld aufrufen, das null ist.
Wir überlegen, ob wir dies ändern sollten oder nicht. Dies ist ein Schritt in unserem nie endenden Streben, die richtige Balance zwischen „alte Dinge reparieren und bereinigen“ und „alte Projekte nicht kaputt machen“ zu finden. In diesem Fall fragen wir uns, was Sie denken. Für Unity5 haben wir daran gearbeitet, dass Unity Ihre Skripte automatisch aktualisieren kann (mehr dazu in einem nachfolgenden Blogbeitrag). Leider können wir Ihre Skripte in diesem Fall nicht automatisch aktualisieren. (da wir nicht zwischen „Dies ist ein altes Skript, das tatsächlich das alte Verhalten erfordert“ und „Dies ist ein neues Skript, das tatsächlich das neue Verhalten erfordert“ unterscheiden können.)
Wir tendieren dazu, den „benutzerdefinierten ==-Operator zu entfernen“, zögern jedoch, da dadurch die Bedeutung aller null geändert würde, die Ihre Projekte derzeit durchführen. Und für Fälle, in denen das Objekt nicht „wirklich null“, sondern ein zerstörtes Objekt ist, gab eine Nullprüfung früher „true“ zurück. Wenn wir dies ändern, wird es „false“ zurückgeben. Wenn Sie überprüfen möchten, ob Ihre Variable auf ein zerstörtes Objekt verweist, müssen Sie den Code ändern, um stattdessen „if (myObject.destroyed) {}“ zu überprüfen. Das macht uns etwas nervös, denn wenn Sie diesen Blogbeitrag nicht gelesen haben (und wenn doch, ist das höchstwahrscheinlich der Fall), ist es sehr leicht, dieses geänderte Verhalten nicht zu bemerken, insbesondere, da den meisten Leuten nicht bewusst ist, dass diese benutzerdefinierte null überhaupt existiert.[3]
Wenn wir es ändern, sollten wir es allerdings für Unity5 tun, da die Schwelle dafür, wie viel Upgrade-Schmerz wir den Benutzern zumuten möchten, bei kleineren Releases noch niedriger ist.
Was sollen wir Ihrer Meinung nach tun? Sollen wir Ihnen ein saubereres Erlebnis bieten, auf Kosten der Änderung von null in Ihrem Projekt, oder sollen wir alles so belassen, wie es ist?
Tschüß, Lucas (@lucasmeijer)
[1] Wir tun dies nur im Editor. Aus diesem Grund wird beim Aufrufen von GetComponent() zum Abfragen einer nicht vorhandenen Komponente eine C#-Speicherzuweisung angezeigt, da wir diese benutzerdefinierte Warnzeichenfolge innerhalb des neu zugewiesenen gefälschten null generieren. Diese Speicherzuweisung erfolgt in integrierten Spielen nicht. Dies ist ein sehr gutes Beispiel dafür, warum Sie beim Profilieren Ihres Spiels immer den eigentlichen Standalone-Player oder mobilen Player und nicht den Editor profilieren sollten, da wir im Editor viele zusätzliche Sicherheits-/Schutz-/Nutzungsprüfungen durchführen, um Ihnen das Leben zu erleichtern, allerdings auf Kosten der Leistung. Führen Sie beim Profilieren hinsichtlich Leistung und Speicherzuweisung niemals ein Profil des Editors durch, sondern immer ein Profil des erstellten Spiels.
[2] Dies gilt nicht nur für GameObject, sondern für alles, was von UnityEngine.Object abgeleitet ist.
[3] Lustige Geschichte: Ich bin beim Optimieren der Leistung von GetComponent<T>() darauf gestoßen und habe beim Implementieren einiger Caching-Funktionen für die Transform-Komponente keine Leistungsvorteile festgestellt. Dann hat sich @jonasechterhoffdas Problem angesehen und ist zum selben Schluss gekommen. Der Caching-Code sieht folgendermaßen aus:
private Transform m_CachedTransform
public Transform transform
{
get
{
if (m_CachedTransform == null)
m_CachedTransform = InternalGetTransform();
return m_CachedTransform;
}
}
Es stellte sich heraus, dass zwei unserer eigenen Ingenieure übersehen hatten, dass die null aufwändiger als erwartet war und dies der Grund dafür war, dass sich durch das Caching kein Geschwindigkeitsvorteil ergab. Dies führte zu der Frage „Wenn sogar wir es verpassen, wie viele unserer Benutzer werden es dann verpassen?“, woraus dieser Blogbeitrag resultierte :)