C#: Implicit conversion from null to struct (and more)

MyStruct some = null; // some == MyStruct(...)

This is a small post about a rather specific construct in C# syntax, permitting to assign "null" to struct-typed variables (which are non-nullable) and treat this specific case as you please.

If you did not come here looking for this particular thing, you may or may not be in a dire need of the post's subject, but explanation may be of interest.

Prehistory

I have stumbled upon the problem with implicitly converting "primitive" values to structs while configuring C# code generation for a Haxe project. While the Haxe code works perfectly fine by itself, working with it from C# currently can be a bit problematic.

Say, you have the following code:

function some(a:String, ?b:Bool):Void

which turns into this upon compilation:

void some(string a, haxe.lang.Null<bool> b)

Aside of the optional parameter no longer being optional (that's a story for another day), doesn't look too suspicious, right? But let's take a look at how you would call this procedure with a single parameter (leaving the second to be null):

some("Hello", new haxe.lang.Null<bool>(default(bool)));

... pretty convenient, huh? Except not really. The thing here is that Null<T> has two constructors - one taking a single (default) value for "nulled" value, and another one taking two for "non-nulled".

As you can expect, being able to just pass "true", "false", or "null" would be more convenient.

Context

For a simple example, let's take a look at another "nullable type" kind of struct:

struct Null<T> {
	public readonly bool hasValue;
	public readonly T value;
	public Null(bool hasValue, T value) {
		this.hasValue = hasValue;
		this.value = value;
	}
	public override string ToString() {
		return hasValue ? value.ToString() : "null";
	}
}

So here we have two fields (containing the value and whether there is one), a nondescript constructor, and a simple ToString override. Now, to implicit conversions:

Implicit conversion from primitive type

This one's easy. You define a "implicit operator" that takes our parameter-type, returns a new struct\object, and basically just calls the constructor accordingly:

public static implicit operator Null<T>(T value) {
	return new Null<T>(true, value);
}

These shown structures go into the "struct", just in case.

Implicit conversion to primitive type

The process logically should go both ways, and so there's this:

public static implicit operator T(Null<T> obj) {
	if (obj.hasValue) {
		return obj.value;
	} else throw new Exception("Value is null.");
}

If the value is present, it is returned. If the value is not present, an exception is thrown, as it would not be possible to convert "null" to the chosen primitive type (that's why you would make a struct like this, after all). Other option would be to return default(T) instead of throwing an exception, but to me that looks like a decision that you may later regret.

Implicit conversion from null

Now, this is where the things get interesting.

public static implicit operator Null<T>(NoValue noValue) {
	return new Null<T>(false, default(T));
}
public class NoValue { }

Depending on your familiarity with these things, from a glance at the above code you may already have a couple of questions. So let me explain a little:

  • "null" itself is not a "type" in C#, thus cannot be used directly.
  • At the same time, null "belongs to" any "reference" type as the default value.
  • So we declare a special "no value" type that a null-value can be safely cast to.
  • When an implicit cast of null to the struct is required, having no other logical options, the compiler assumes that "null" is a NoValue, calling the conversion function.

So, there - it's weird but it does work just fine. And a small test being

Null<int> i = 3;
Console.WriteLine("i:" + i);
i = null;
Console.WriteLine("n:" + i);

will output

i:3
n:null

just like you would expect it to. Add a couple extra operators, and you'll have a nice system that both works optimally and is convenient to use.

Have fun!

Related posts:

Leave a Reply

Your email address will not be published. Required fields are marked *