In the last post I discussed how System.Void
is meant as a placeholder and not meant to actually be instantiated.
Being of a rebellious nature, I need no more motivation than that to want to instantiate one. However the .NET team has made some effort to stop attempts to create voids.
However if I really wanted one of these delicious voids, could I get one?
Activator
The go-to API for creating objects where the type is only known at runtime is System.Activator.CreateInstance(Type)
. The API is as standard as it gets - consider the following code.
public class Factory<T> where T : new()
{
public void Create() => new T();
}
We have a Factory<T>
class which calls the parameterless constructor of the type it is given. It all looks straightforward, however when looking at the CIL we notice something interesting.
.method public hidebysig instance void Create() cil managed
{
// Code size 7 (0x7)
.maxstack 8
IL_0000: call !!0 [System.Runtime]System.Activator::CreateInstance<!T>()
IL_0005: pop
IL_0006: ret
} // end of method Factory`1::Create
Since the C# compiler does not know what T
will be at runtime, it must resort to the trusty Activator
API.
We can try using Activator
directly in an attempt to replicate what Factory<void>.Create()
would do. Then, surely, we will have our void.
var type = typeof(void);
var myVoid = Activator.CreateInstance(type);
// Previous line throws NotSupportedException("Cannot dynamically create an instance of System.Void")
Our plan falls apart, the framework is one step ahead of us. However, the message is suspiciously specific, as if instead of it leading to an illegal state, they don't want us to create a void!
If that is the case, there might be other APIs which have not considered this scenario. Digging through the runtime source code, we find that the guard (shown below) forms part of the CreateInstance
API.
if (ReferenceEquals(elementType, typeof(void)))
throw new NotSupportedException(SR.Acc_CreateVoid);
So the Activator
API has outsmarted us - but that leaves open the possibility that other APIs might not.
Uninitialized Objects
One useful technique in older serializers (e.g. Binary Serialization) was to create a zeroed-out object and then set its fields directly, skipping all user code such as constructors and property setters. The resulting object would theoretically be in the same state as when it was first serialized. The APIs used to create these zeroed-out ("uninitialized") objects are available on the FormatterServices
class. There's a conveniently named GetUninitializedObject
method which does exactly what it's named, and, digging down, it internally calls RuntimeHelpers.GetUninitializedObject
which is bound to a C++ internal method. Below is an excerpt of the implementation with some bits cut out.
// Don't allow arrays, pointers, byrefs or function pointers.
if (type.IsTypeDesc() || type.IsArray())
COMPlusThrow(kArgumentException, W("Argument_InvalidValue"));
// We don't allow unitialized Strings or Utf8Strings.
if (pMT == g_pStringClass) {
COMPlusThrow(kArgumentException, W("Argument_NoUninitializedStrings"));
}
// if this is an abstract class or an interface type then we will fail this
if (pMT->IsAbstract()) {
COMPlusThrow(kMemberAccessException,W("Acc_CreateAbst"));
}
if (pMT->ContainsGenericVariables()) {
COMPlusThrow(kMemberAccessException,W("Acc_CreateGeneric"));
}
if (pMT->IsByRefLike()) {
COMPlusThrow(kNotSupportedException, W("NotSupported_ByRefLike"));
}
// Never allow allocation of generics actually instantiated over __Canon
if (pMT->IsSharedByGenericInstantiations()) {
COMPlusThrow(kNotSupportedException, W("NotSupported_Type"));
}
retVal = pMT->Allocate();
So the API guards against types which are arrays, strings, abstract classes, pointer-like, COM objects, and unbound generic types. This looks promising as System.Void
is none of those!
var type = typeof(void);
object myVoid = RuntimeHelpers.GetUninitializedObject(type);
Console.WriteLine(myVoid.GetType().Name);
Success; we have obtained a void.
Boxing
No implosions - the universe still stands strong. I feel cheated. Surely .NET doesn't allow me to have an object of 0 bytes in size? It doesn't. My precious void has been boxed.
The point of value types in .NET is to be laid out directly in memory without headers (e.g. 4 bytes for a 32 bit integer). This is indeed the case when the value exists on the stack, in the fields of another type, or as array elements.
However to be treated as an object
, they must be "boxed". This "boxing" process prepends a header to the actual data, giving the boxed value type the same memory layout as all objects. Boxing allows classes such as List<object>
to assume all elements are pointers to a header followed by data, regardless of whether they are reference types (e.g. string
) or value types (e.g. int
), or a mix of anything.
All these boxes go against the spirit of instantiating System.Void
- I want to have nothingness, not an empty box. I will attempt this next time. Stay tuned.
Join the Discussion
You must be signed in to comment.