Notifications
Article
Fake nulle a operator "??" i "?." [PL]
Updated 2 months ago
73
2

Gdzie w pamięci przechowywane są obiekty?

Zanim powiem coś więcej na temat fake nulli muszę wyjaśnić jakie są sposoby zarządzania pamięcią i gdzie w pamięci znajdują się określone obiekty.

Gdzie przechowywane są instancje (zwykłych) klas?

Poniżej znajduję się przykład typowej klasy, którą możemy utworzyć z pomocą języka C#.
class ExampleClass { }
Instancje takich klas będą istnieć w pamięci tak długo dopóki będziemy mieć do nich referencje.

ExampleClass instancja = new ExampleClass();
A gdy nie będziemy mieć do nich referencji to zostaną one usunięte przez Garbage Collector. Instancje takich klas będą istnieć w pamięci zarządzanej (ang. managed memory)
ExampleClass instancja = new ExampleClass(); // przypisuję null tym samym tracę referencje do instancji klasy ExampleClass instancja = null;

Gdzie przechowywane są instancje klas dziedziczących z klasy UnityEngine.Object?

Z klasy UnityEngine.Object dziedziczą:
  • gameObjecty
  • wszystkie komponenty (np. klasa Transform, Rigidbody itd.)
  • wszystkie assety (np. klasa Texture, ScriptableObject, AudioClip itd.)
Te obiekty znajdują się jednocześnie w pamięci natywnej i zarządzanej.

Z czego to wynika?

Edytor Unity został napisany w języku C++. W tym języku zostało zaimplementowane działanie poszczególnych klas i funkcji edytora. Implementacje poszczególnych klas nie są dla nas dostępne bezpośrednio.
Kod związany z naszymi grami piszemy w języku C# z racji jego prostoty (względem języka C++).
Klasy związane z assetami, komponentami i gameObjectami, których używamy w naszych skryptach są wraperami, które pozwalają na obsługę instancji obiektów, które tak naprawdę znajdują się po stronie natywnej. Poniżej przykład klasy GameObject. Ta klasa nie implementuję sama w sobie działania gameObjectów. Pozwala natomiast na zarządzanie i komunikacje z instancją gameObjectu znajdującą się po stronie natywnej.

Podsumujmy

Instancje obiektów dziedziczących z klasy UnityEngine.Object istnieją jednocześnie w pamięci natywnej i zarządzanej a instancje pozostałych obiektów istnieją tylko w pamięci zarządzanej.

Sposoby na sprawdzenie czy obiekt istnieje

Zacznę od omówienia sposobów z klasy UnityEngine.Object.

Operator konwersji UnityEngine.Object na bool

Klasa UnityEngine.Object posiada specjalny operator, który pozwala na sprawdzenie czy obiekt istnieje.
https://docs.unity3d.com/ScriptReference/Object-operator_Object.html
using UnityEngine; public class FakeNullTest : MonoBehaviour { void Start() { GameObject gameObject = new GameObject(); //Możemy przypisać obiekt bezpośrednio do wartości typu bool aby sprawdzić czy istnieje bool obiektIstnieje = gameObject; // możemy także bezpośrednio zaprzeczyć istnieniu obiektu bool obiektNieIstnieje = !gameObject; // lub umieścić obiekt bęzpośrednio w ifie if (gameObject) { Debug.Log(gameObject); } //zamiast porównywania do nulla //if (gameObject!=null) //{ //} } }

Operator == i !=

Aby sprawdzić czy obiekt istnieje możemy także skorzystać z operatora == lub !=
using UnityEngine; public class FakeNullTest : MonoBehaviour { void Start() { GameObject gameObject = new GameObject(); // sprawdzamy czy obiekt nie jest nullem if (gameObject!=null) { Debug.Log(gameObject); } } }

Metoda System.Object.ReferenceEquals() i System.Object.Equals()

To dwa kolejne sposoby na sprawdzenie czy obiekt istnieje.
Jednak jak zaraz się przekonacie nie działają one zawsze poprawnie w edytorze Unity dla obiektów dziedziczących z UnityEngine.Object. W poniższym przykładzie sprawdzamy czy gameObject jest nullem gdy nie został zainicjowany. Test wykonujemy w edytorze.
using UnityEngine; public class FakeNullTest : MonoBehaviour { public GameObject myGameObject; void Update() { Debug.Log("Czy obiekt jest nullem?"); bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null); Debug.Log("referenceEquals: " + referenceEquals); bool equals = System.Object.Equals(myGameObject, null); Debug.Log("equals: " + equals); // Dla porównania korzystamy także z operatora == bool operatorPorównania = myGameObject == null; Debug.Log("operatorPorównania: " + operatorPorównania); } }
Do okna Consoli zostały wypisane następujące logi.
Zwróćcie uwagę na niepoprawne wartości w przypadku dwóch pierwszych metod. Te metody zwracają informacje, że obiekt nadal istnieje. Czemu tak się dzieje?

W jaki sposób powyższe operatory sprawdzają czy UnityEngine.Object jest nullem?

Na samym początku wspominałem, że instancje obiektów dziedziczących z klasy UnityEngine.Object istnieją w pamięci natywnej a w pamięci zarządzanej istnieje wrapper służący do komunikacji i zarządzania instancją w pamięci natywnej. Patrz rysunek niżej.
Metoda System.Object.ReferenceEquals() i System.Object.Equals() porównują instancje obiektów istniejące w pamięci zarządzanej.
Natomiast operatory ==, != i konwersji Object na bool odwołują się do instancji obiektów w pamięci natywnej ignorując przy tym istnienie instancji w części zarządzanej. Czyli możemy mieć czasami do czynienia z sytuacją, że te operatory będą "udawać, że dany obiekt jest nullem" mimo, że po stronie zarządzanej nadal ten obiekt istnieje. Ten mechanizm działania edytora nosi nazwę "fake null".

Czym są "fake nulle"?

Edytor Unity posiada specjalny mechanizm obsługi nulli dla obiektów dziedziczących z klasy UnityEngine.Object.
Gdy wczytujemy nową scenę lub niszczymy obiekt z pomocą metody Destroy() to niszczymy obiekt po stronie natywnej. Następnie Garbage Collector usuwa wrapper obiektu po stronie zarządzanej.
Obiekt jest "prawdziwym nullem" gdy nie istnieje po stronie natywnej i zarządzanej.
Obiekt będzie "fake nullem" jeśli obiekt nie istnieje po stronie natywnej i jednocześnie nadal istnieje po stronie zarządzanej.

W jakiej sytuacji obiekt będzie "fake nullem"?

Pierwsza sprawa - "fake nulle" istnieją tylko w edytorze. W skompilowanej aplikacji nie istnieją "fake nulle" - obiekt będzie albo istnieć albo nie.
Obiekt może być "fake nullem" np. w sytuacji gdy obiekt zostanie usunięty z pomocą metody Destroy (metoda niszczy obiekt po stronie natywnej) a po stronie zarządzanej nadal istnieje bo Garbage Collector go jeszcze nie usunął. (Poniżej przykładowy kod)
using System.Collections; using UnityEngine; public class FakeNullTest : MonoBehaviour { public GameObject myGameObject; private void Start() { myGameObject = new GameObject(); } void Update() { if (Input.GetKeyDown(KeyCode.P)) StartCoroutine(DestroyAndLogInfo()); } IEnumerator DestroyAndLogInfo() { // niszczymy obiekt Destroy(myGameObject); // obiekty są niszczone dopiero po zakończeniu wywoływania Update() // dlatego czekamy jedną klatkę na wypisanie logów yield return null; Debug.Log("Czy obiekt jest nullem?"); bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null); Debug.Log("referenceEquals: " + referenceEquals); bool equals = System.Object.Equals(myGameObject, null); Debug.Log("equals: " + equals); bool operatorPorównania = myGameObject == null; Debug.Log("operatorPorównania: " + operatorPorównania); } }
"Fake nullami" będą także niezainicjowane obiekty. (Poniżej przykładowy kod)
using UnityEngine; public class FakeNullTest : MonoBehaviour { // Niezainicjalizowany obiekt public GameObject myGameObject; void Update() { Debug.Log("Czy obiekt jest nullem?"); bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null); Debug.Log("referenceEquals: " + referenceEquals); bool equals = System.Object.Equals(myGameObject, null); Debug.Log("equals: " + equals); bool operatorPorównania = myGameObject == null; Debug.Log("operatorPorównania: " + operatorPorównania); } }

Dlaczego metoda System.Object.ReferenceEquals() i System.Object.Equals() nie działa poprawnie dla UnityEngine.Object?

Te dwie metody nie będą działać poprawnie dla "fake nulli" z racji tego, że te metody porównują referencje po stronie zarządzanej. W przypadku "fake nulli" obiekt nadal będzie istniał w pamięci zarządzanej mimo, że po stronie natywnej już nie istnieje.

Kiedy metoda System.Object.ReferenceEquals() i System.Object.Equals() będzie działać poprawnie dla UnityEngine.Object?

Te metody będą działać poprawnie po skompilowaniu aplikacji z racji tego, że "fake nulle" istnieją tylko w edytorze Unity.
Możemy także przypisać wartość null w kodzie. W ten sposób "fake null" stanie się "prawdziwym nullem".
using UnityEngine; public class FakeNullTest : MonoBehaviour { // Niezainicjalizowany obiekt public GameObject myGameObject; void Update() { //przypisujemy wartość null aby uzyskać "prawdziwego nulla" myGameObject = null; Debug.Log("Czy obiekt jest nullem?"); bool referenceEquals = System.Object.ReferenceEquals(myGameObject, null); Debug.Log("referenceEquals: " + referenceEquals); bool equals = System.Object.Equals(myGameObject, null); Debug.Log("equals: " + equals); bool operatorPorównania = myGameObject == null; Debug.Log("operatorPorównania: " + operatorPorównania); } }
Teraz te metody zwracają te same wartości co operator ==.

Jakie są korzyści korzystania z "fake nulli"?

Edytor Unity dzięki mechanizmowi "fake nulli" uzyskuje bardziej szczegółowe informacje na temat nieistniejących obiektów.
Poniżej możecie zobaczyć porównać błędy jak się wyświetlają dla każdego z nulli. Error w przypadku "fake nulla" jest bardziej szczegółowy zawiera informacje gdzie brakuję referencji. Error dla "prawdziwego nulla" wyświetla dużo uboższe informacje na temat brakującej referencji.
using UnityEngine; public class FakeNullTest : MonoBehaviour { // Niezainicjalizowany obiekt public GameObject myGameObject; void Update () { // jeśli przypisujemy wartość null to uzyskamy "prawdziwego nulla" // jeśli zakomentujemy tą linie to uzyskamy "fake nulla" myGameObject = null; // odwołuję się do wielkości activeSelf aby wywołać błąd bool a = myGameObject.activeSelf; } }
Bardziej szczegółowe informacje na temat "fake nulli" możecie znaleźć w tym poście:
https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/

Fake nulle a operator "??" i "?:".

Na początek kilka słów przypomnienia jak działają te operatory.

Jak działa operator "??"

Null-coalescing operator czyli "??" działa w taki sposób, że gdy obiekt istnieje to zwracany jest ten obiekt a jeśli obiekt jest nullem to zwrócona zostanie wartość z prawej strony operatora "??".
https://docs.microsoft.com/pl-pl/dotnet/csharp/language-reference/operators/null-coalescing-operator
Poniżej przykładowy kod:
using UnityEngine; public class NullCoalescingOperator : MonoBehaviour { void Start() { // Sytuacja gdy obiekt jest nullem string obiektNieIstnieje = null; string wynik = obiektNieIstnieje ?? "Wartość z prawej strony operatora ??"; Debug.Log("Gdy mamy referencje do nulla: "+ wynik); // Sytuacja gdy obiekt istnieje string obiektIstnieje = "Obiekt!"; string wynik2 = obiektIstnieje ?? "To zostanie zwrócone jeśli obiekt przed operatorem ?? jest nullem"; Debug.Log("Gdy mamy referencje do obiektu: " + wynik2); } }
Po wywołaniu powyższego kodu w oknie Consoli wyświetlą się następujące logi:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class ConditionalOperator : MonoBehaviour { void Start() { // Sytuacja gdy obiekt jest nullem int liczba = 5; Debug.Log("liczba = 5 "); string wynik = (liczba > 0) ? "Lewa strona " : "Prawa strona"; Debug.Log("liczba > 0: "+wynik); string wynik2 = (liczba > 10) ? "Lewa strona " : "Prawa strona"; Debug.Log("liczba < 10: " + wynik); } }

Jak działa operator "?."

Null conditional operators czyli "?." lub "?[]".
Operator "?." jest skróconą wersją porównania do nulla. Jeśli obiekt jest nullem to kod po "?." nie zostanie wywołany.
Poniżej przykładowy kod, ułatwiający zrozumienie tego operatora:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class NullConditionalOperator : MonoBehaviour { void Start() { string wynik = ""; string obiektNieIstnieje = null; wynik = obiektNieIstnieje?.Replace(oldChar: 'A', newChar: 'Z'); Debug.Log("Gdy mamy referencje do nulla: "+ wynik); // Odpowiednik powyższego kodu korzystającego z operatora ?. //if (obiektNieIstnieje != null) //{ // wynik = obiektNieIstnieje.Replace(oldChar: 'A', newChar: 'Z'); //} //Debug.Log("Gdy mamy referencje do nulla: " + wynik); string obiektIstnieje = "AAAAAAA!"; wynik = obiektIstnieje.Replace(oldChar: 'A', newChar: 'Z'); Debug.Log("Gdy mamy referencje do obiektu: " + wynik); } }
Po wywołaniu powyższego kodu w oknie Consoli wyświetlą się następujące logi:


Dlaczego powinniśmy uważać gdy korzystamy z operatora "??" i "?:".

Powyższe operatory są ściśle związane z sprawdzaniem czy obiekt ma wartość null. Porównują one obiekt do nulla po stronie zarządzanej dlatego nie będą one działać poprawnie dla "fake nulli".
Poniżej przykład różnicy w działaniu tych operatorów w przypadku "prawdziwych i fake nulli". Wszystkie assety, komponenty i gameObjecty dziedziczą z klasy UnityEngine.Object.
using UnityEngine; public class FakeNullTest : MonoBehaviour { // Niezainicjalizowany obiekt public GameObject myGameObject; void Update () { // jeśli przypisujemy wartość null to uzyskamy "prawdziwego nulla" // jeśli zakomentujemy tą linie to uzyskamy "fake nulla" myGameObject = null; string name = "AAAA"; // Jeśli myGameObject istnieje to zwrócona zostanie jego nazwa // A jeśli nie nic nie zostanie zwrócone name = myGameObject?.name; Debug.Log("?. "+ name); // Jeśli myGameObject istnieje to zwrócony // A jeśli nie to utworzony nowy gameObject GameObject temp = myGameObject ?? new GameObject("Nowo utworzony GameObject"); Debug.Log("?? " + temp.name); } }
Dlatego też nie używajcie tych operatorów gdy dla obiektów dziedziczących z klasy UnityEngine.Object. Skorzystajcie jako zamiennik z operatorów !=, == i konwersji na bool bo obsługują one "fake nulle".
Tu możecie znaleźć informacje na temat tego jak zastąpić te dwa operatory:
https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object


Bibliografia i inne materiały na temat "fake nulli".

https://blogs.unity3d.com/2014/05/16/custom-operator-should-we-keep-it/
https://github.com/JetBrains/resharper-unity/wiki/Possible-unintended-bypass-of-lifetime-check-of-underlying-Unity-engine-object
https://answers.unity.com/questions/1465702/getcomponent-does-never-return-null-possible-bug.html?childToView=1466149#comment-1466149
https://answers.unity.com/questions/1378330/-operator-not-working-as-expected.html?childToView=1378682#comment-1378682
https://answers.unity.com/questions/1398006/2d-array-initialized-but-send-null.html
https://answers.unity.com/questions/1236014/-operator-not-working.html
https://www.youtube.com/watch?v=feRt_q9wRz0&list=PLfurnpjsyGQItrxGMNLzphKIemfXWhVmg
Tags:
Rafał Tadajewski
I haven't duck. I took dino. - Programmer
10
Comments
Rafał Tadajewski
3 months ago
I haven't duck. I took dino.
Nikita GoncharukDzięki za ten artykuł. Przetłumaczę na rosyjski i opublikuję na moim kanale: https://t.me/Game_Dev_Channel
Super :)
0
NG
Nikita Goncharuk
3 months ago
Dzięki za ten artykuł. Przetłumaczę na rosyjski i opublikuję na moim kanale: https://t.me/Game_Dev_Channel
0