Jasica.Net

C#, .Net, SQL i nie tylko

Tworzenie obiektów bez wykonywania konstruktora

Tworzenie obiektów w .Net jest utożsamiane z zarezerwowaniem dla nich pewnej pamięci, a następnie wykonaniem konstruktora(domyślnego lub parametryzowanego). Jednak w pewnych przypadkach wykonywanie kodu zawartego w konstruktorze może być niepożądane. Istnieje jednak sposób by tego uniknąć.

Brak angażowania logiki w konstruktorze jest zazwyczaj pożądany podczas odtwarzania obiektu np. po odczycie danych z dysku twardego lub danych przesłanych przez sieć. Dodatkowo, jeśli obiekt posiada bezparametryczny konstruktor utworzenie obiektu danego typu przy pomocy refleksji nie jest trudne. Sprawa staje się bardziej skomplikowana jeśli obiekt wymaga do inicjalizacji innych danych. W takim przypadku z pomocą przychodzi statyczna metoda FormatterServices.GetUninitializedObject, która jako jedyny parametr przyjmuje typ obiektu jaki chcemy utworzyć. Należy uważać z jej użyciem, gdyż tworzy ona obiekt, którego przestrzeń w pamięci jest wypełniona samymi zerami. Może implikować stan obiektu, który nie jest dla niego dozwolony i może objawiać się niepożądanym działaniem . Taki obiekt należy jak najszybciej wypełnić danymi przed jego wykorzystaniem. Przykład użycia:

    public class Foo
    {
        private int bar;
        private int baz;

        public Foo(int bar)
        {
            this.bar = bar;
            this.baz = 8;
        }

        public override string ToString()
        {
            return string.Format("bar={0}, baz={1}", this.bar, baz);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var test = (Foo)FormatterServices.GetUninitializedObject(typeof(Foo));
            Console.WriteLine(test);
        } 
    }

Na ekranie, zgodnie z oczekiwaniami, zostanie wypisana informacja, z której wynika, że obiekt nie został zainicjalizowany- "bar=0, baz=0".

Samo działanie metody jest interesujące- jak obchodzi ona taki element języka jak wywołanie konstruktora? Dekompilując jej kod nie dowiemy się dużo:

    [SecurityCritical]
    public static object GetUninitializedObject(Type type)
    {
      if (type == null)
        throw new ArgumentNullException("type");
      if (type is RuntimeType)
        return FormatterServices.nativeGetUninitializedObject((RuntimeType) type);
      throw new SerializationException(Environment.GetResourceString(
        "Serialization_InvalidType", 
        new object[1] { (object) type.ToString() }));
    }

    [MethodImpl(MethodImplOptions.InternalCall)]
    [SecurityCritical]
    private static extern object nativeGetUninitializedObject(RuntimeType type);

Oznaczenie metody jako extern wraz z MethodImplOptions.InternalCall mówi, że metoda jest zaimplementowana wewnątrz CLR. Jeśli chcemy pogrzebać głębiej zawsze można zerknąć na kod SSCLI

FCIMPL1(Object*, ReflectionSerialization::GetUninitializedObject, ReflectClassBaseObject* objTypeUNSAFE) {
    CONTRACTL {
        THROWS;
        DISABLED(GC_TRIGGERS);
        MODE_COOPERATIVE;
        SO_TOLERANT;
    }
    CONTRACTL_END;
    
    OBJECTREF           retVal  = NULL;
    REFLECTCLASSBASEREF objType = (REFLECTCLASSBASEREF) objTypeUNSAFE;

    HELPER_METHOD_FRAME_BEGIN_RET_ATTRIB_NOPOLL(Frame::FRAME_ATTR_RETURNOBJ);

    if (objType == NULL) {
        COMPlusThrowArgumentNull(L"type", L"ArgumentNull_Type");
    }

    TypeHandle type = objType->GetType();

    // Don't allow arrays, pointers, byrefs or function pointers.
    if (!type.IsUnsharedMT())
        COMPlusThrow(kArgumentException, L"Argument_InvalidValue");

    MethodTable *pMT = type.GetMethodTable();
    PREFIX_ASSUME(pMT != NULL);

    //We don't allow unitialized strings.
    if (pMT == g_pStringClass) {
        COMPlusThrow(kArgumentException, L"Argument_NoUninitializedStrings");
    }

    // if this is an abstract class or an interface type then we will
    //  fail this
    if (pMT->IsAbstract()) {
        COMPlusThrow(kMemberAccessException,L"Acc_CreateAbst");
    }
    else if (pMT->ContainsGenericVariables()) {
        COMPlusThrow(kMemberAccessException,L"Acc_CreateGeneric");
    }

    // Never allow the allocation of an unitialized ContextBoundObject derived type, these must always be created with a paired
    // transparent proxy or the jit will get confused.
    if (pMT->IsContextful())
        COMPlusThrow(kNotSupportedException, L"NotSupported_ManagedActivation");

    // If it is a nullable, return the underlying type instead.  
    if (Nullable::IsNullableType(pMT)) 
        pMT = pMT->GetInstantiation()[0].GetMethodTable();
 
    retVal = pMT->Allocate();

    HELPER_METHOD_FRAME_END();
    return OBJECTREFToObject(retVal);
}
FCIMPLEND

Jest tu sprawdzana cała masa warunków, aż ostatecznie alokowana jest po prostu pamięć - nie ma tu żadnego wołania konstruktora.