Jasica.Net

C#, .Net, SQL i nie tylko

Zabawy z null'em

Post ma na celu ukazanie kilku ciekawych własności C# i CLR, o których niewielu programistów pamiętam. 

Czasami metoda do sprawdzania czy dany obiekt jest pusty mogłaby być pomocna:

        public class SimpleClass
        {
            public bool IsNull()
            {
                return this == null;
            }
        }

        static void Main(string[] args)
        {
            SimpleClass sc = null;
            if (sc.IsNull())
                Console.WriteLine("null");
            else
                Console.WriteLine("not null");
        }

Oczywiście ten kod nie będzie działać poprawnie, a każde takie wywołanie skończy się rzuceniem wyjątku NullReferenceException. Jako, że metoda nie wykorzystuje żadnych składowych klasy można zastosować pewną sztuczkę. Wygenerowany kod il metody Main będzie wyglądał następująco:

.method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    .maxstack  2
    .locals init ([0] class Null.Program/SimpleClass sc,
             [1] bool CS$4$0000)
    IL_0000:  nop
    IL_0001:  ldnull
    IL_0002:  stloc.0
    IL_0003:  ldloc.0
    IL_0004:  callvirt   instance bool Null.Program/SimpleClass::IsNull()
    IL_0009:  ldc.i4.0
    IL_000a:  ceq
    IL_000c:  stloc.1
    IL_000d:  ldloc.1
    IL_000e:  brtrue.s   IL_001d

    IL_0010:  ldstr      "null"
    IL_0015:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_001a:  nop
    IL_001b:  br.s       IL_0028

    IL_001d:  ldstr      "not null"
    IL_0022:  call       void [mscorlib]System.Console::WriteLine(string)
    IL_0027:  nop
    IL_0028:  ret
  }

Na uwagę zasługuje linia oznaczona IL_0004- wystarczy w niej podmienić wywołanie callvirt na call, a w czasie uruchomienia na konsole zostanie wypisany komunikat "null". Dzieje się tak, gdyż jedną z różnic pomiędzy tymi dwoma sposobami wywoływania metod jest to, że ta druga nie sprawdza, czy obiekt, na którym wołamy metodę nie jest nullem. Call jest stosowany głównie przy wywoływaniu metod statycznych, jednak w tym przypadku może się nadać.

Ręczna edycja nie jest zbyt miła- teoretycznie można skorzystać z narzędzi typu Fody, do zautomatyzowania tej pracy. Istnieje jednak prostszy sposób na otrzymanie podobnego efektu- extensions method:

 

    public static class SimpleClassExtensions
    {
        public static bool IsNull(this SimpleClass instance)
        {
            return instance == null;
        }
    }

 Inny ciekawy przypadek stanowią dwie poniższe metody:

        public static string GetDefaultString<T>() where T : new()
        {
            T instance = new T();
            return instance.ToString();
        }

        public static Type GetInstanceType<T>() where T : new()
        {
            T instance = new T();
            return instance.GetType();
        }

Pierwsza z nich zawsze będzie działać prawidłowo. Drugą można spróbować popsuć wołając np. GetInstanceType<long?>, czego wynikiem będzie niespodziewane rzucenie wyjątku NullReferenceException. Wszystkiemu winny jest kod wygenerowanej metody:

.method public hidebysig static class [mscorlib]System.Type 
        GetInstanceType<.ctor T>() cil managed
{
  .maxstack  1
  .locals init ([0] !!T 'instance',
           [1] class [mscorlib]System.Type CS$1$0000,
           [2] !!T CS$0$0001)
  IL_0000:  nop
  IL_0001:  ldloca.s   CS$0$0001
  IL_0003:  initobj    !!T
  IL_0009:  ldloc.2
  IL_000a:  box        !!T
  IL_000f:  brfalse.s  IL_001c
  IL_0011:  ldloca.s   CS$0$0001
  IL_0013:  initobj    !!T
  IL_0019:  ldloc.2
  IL_001a:  br.s       IL_0021
  IL_001c:  call       !!0 [mscorlib]System.Activator::CreateInstance<!!0>()
  IL_0021:  nop
  IL_0022:  stloc.0
  IL_0023:  ldloca.s   'instance'
  IL_0025:  constrained. !!T
  IL_002b:  callvirt   instance class [mscorlib]System.Type [mscorlib]System.Object::GetType()
  IL_0030:  stloc.1
  IL_0031:  br.s       IL_0033
  IL_0033:  ldloc.1
  IL_0034:  ret
}

Jest to metoda generyczna, więc aby mogła ona działać prawidłowo zarówno z klasami jak i strukturami kompilator przed wywołaniem metody instrukcją constrained. W przypadku metody ToString problem nie powstanie, gdyż jest ona wirtualna. Niestety GetType nie ma takiej własności i zostanie zastosowany boxing, a tym samym ostatecznie wywołanie metody na null'u.

To już koniec ciekawostek na dzień dzisiejszy. Zapraszam w przyszłości.