Implicit conversion from null

时间:2018-09-18 19:29:18

标签: c# implicit-conversion

Looking Zoran Horvats courses at PluralSight, I'm currently implementing a Maybe type, a bit like Zoran has on its GitHub account: https://github.com/zoran-horvat/option

Generally, a Maybe is a wrapper around objects, which are either set or have a null value, avoiding null reference exceptions. To make the code a bit shorter, I would like to use implicit conversion to map the values / nulls to their corresponding maybe types. Here an example of my code:

    public void Hook(Maybe<Action<Keys>> onKeyDown, Maybe<Action<Keys>> onKeyUp)
    {
        _keyDownCallback = onKeyDown;
        _keyUpCallback = onKeyUp;

        _hookService.Hook(HookType.KeyBoardLowLevel, OnHookReceived);
    }

As you can see, you can hook and pass two optional callbacks, one for keyDown and one for keyUp. I would like to pass code like this:

nativeKeyboardHookService.Hook(new Action<Keys>(OnNativeKeyDown), null);

The implicit conversion on the Maybe is currently implemented like this:

    public static implicit operator Maybe<T>(T value)
    {
        return ToMaybe(value);
    }

    public static implicit operator T(Maybe<T> maybe)
    {
        return ToT(maybe);
    }

    public static Maybe<T> ToMaybe(T value)
    {
        if (value == null)
        {
            return new None<T>();
        }

        return new Some<T>(value);
    }

    public static T ToT(Maybe<T> maybe)
    {
        return maybe.Evaluate(
            value => value,
            () => default(T));
    }

My question: It works fine, if I pass an actual object, mapping it to an Maybe, but if I pass NULL, I still get a NULL object, not a None object. Am I doing here something wrong or is it just not possible? I didn't find any further information regarding such a conversion.

2 个答案:

答案 0 :(得分:2)

When you pass null to Hook() that's literally all you are doing because your implicit casts aren't being invoked at all. That's because null is a valid value for a reference type, and thus no need to cast.

You can't change Maybe to a struct if you want to keep Some and None because then these would have to be structs too, which means you run into the issue that you can't inherit structs.

You can't implement a common IMaybe<T> interface either because interfaces can't be used with casts.

What I recommend is keep your behavior as is, but don't use null. Instead of passing null, pass something else like Maybe<T>.None:

class Maybe<T>
{
    public static Maybe<T> None { get; } = new None<T>();
}

void Hook(..., Maybe<T>.None) { ... }

Or None<T>.Instance:

class None<T>
{
    public static None<T> Instance{ get; } = new None<T>();
}

void Hook(..., None<T>.Instance) { ... }

This has the advantage of being more readable and explicit.

答案 1 :(得分:1)

Your Maybe<T> is still a reference type, so null is a valid value for it:

Maybe<string> foo = null;

If you want to prevent that, you will need to make it a value type, for example something like this:

public struct Maybe<T>
{
    public T Value { get; }
    public bool IsEmpty => Value == null;

    public Maybe(T value)
    {
        Value = value;
    }

    public static implicit operator Maybe<T>(T value)
    {
        return new Maybe<T>(value);
    }
}

Then you can pass null to a method expecting a Maybe<T> and it will properly construct an empty Maybe<T> object.

But note that it being a value type, this now means that it is copied on every method call, so it has a different behavior to a reference type implementation.

In the end, you cannot really implement this nicely in C# simply because there is the null reference in the language. It’s only with C# 8’s nullable reference types that you will be able to prevent nulls altogether.