Can I return a different type from a function in a generic class depending on if the generic type used for the class is a value or a reference type?

Viewed 93

I want to return a Nullable<Value> from a function in a generic class if Value is a value type, and just 'Value' if value is already a reference type.

I've been trying to work with where clauses to achieve this, but I can't seem to use a where class on a non-generic function, even in a generic class.

    public class HashTable<Key, Value>
    {
        private List<ValueSlot> valueSlots = new List<ValueSlot>();

        private struct ValueSlot
        {
            public bool hasValue;
            public List<KeyValuePair<Key, Value>> values;
        }

        //...

        public Nullable<Value> TryGetValue(Key key) where Value : struct
        {
            (bool result, Value value) = TryGetValue(valueSlots, key);

            if (result == true)
            {
                Nullable<Value> myVal = value;
                return myVal;
            }
            else
            {
                return null;
            }
        }

        public Value TryGetValue(Key key) where Value : class
        {
            (bool result, Value value) = TryGetValue(valueSlots, key);

            return result == true ? value : (Value)null;
        }

        private (bool, Value) TryGetValue(List<ValueSlot> valueSlots, Key key)
        {
            //...
        }

        //...
    }

Unfortunately, this doesn't compile an generates a bunch of errors. I would rather not resort to returning tuples out from the class. Is there a simple way to make this approach work that I am missing? I've been trying to get this to work for nearly a week now...

4 Answers

Polymorphism by return type is not supported by C# yet. Generic type constraint is a hint to the compiler and not making method signatures different.

I'd suggest using Maybe<T> so that you can do this:

public class HashTable<Key, Value>
{
    public Maybe<Value> TryGetValue(Key key)
    {
        (bool result, Value value) = TryGetValue(valueSlots, key);

        if (result == true)
        {
            return new Maybe<Value>(value); // or value.ToMaybe() // with the extension method
        }
        else
        {
            return Maybe<Value>.Nothing;
        }
    }
}

This works regardless of what type the Value is.

Here's the implementation that I use:

public static class MaybeEx
{
    public static Maybe<T> ToMaybe<T>(this T value)
    {
        return new Maybe<T>(value);
    }

    public static T GetValue<T>(this Maybe<T> m, T @default) => m.HasValue ? m.Value : @default;
    public static T GetValue<T>(this Maybe<T> m, Func<T> @default) => m.HasValue ? m.Value : @default();

    public static Maybe<U> Select<T, U>(this Maybe<T> m, Func<T, U> k)
    {
        return m.SelectMany(t => k(t).ToMaybe());
    }

    public static Maybe<U> SelectMany<T, U>(this Maybe<T> m, Func<T, Maybe<U>> k)
    {
        if (!m.HasValue)
        {
            return Maybe<U>.Nothing;
        }
        return k(m.Value);
    }

    public static Maybe<V> SelectMany<T, U, V>(this Maybe<T> @this, Func<T, Maybe<U>> k, Func<T, U, V> s)
    {
        return @this.SelectMany(x => k(x).SelectMany(y => s(x, y).ToMaybe()));
    }
}

public class Maybe<T>
{
    public class MissingValueException : Exception { }

    public readonly static Maybe<T> Nothing = new Maybe<T>();

    private T _value;
    public T Value
    {
        get
        {
            if (!this.HasValue)
            {
                throw new MissingValueException();
            }
            return _value;
        }
        private set
        {
            _value = value;
        }
    }
    public bool HasValue { get; private set; }

    public Maybe()
    {
        HasValue = false;
    }

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

    public T ValueOrDefault(T @default) => this.HasValue ? this.Value : @default;
    public T ValueOrDefault(Func<T> @default) => this.HasValue ? this.Value : @default();

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

    public override string ToString()
    {
        return this.HasValue ? this.Value.ToString() : "<null>";
    }

    public override bool Equals(object obj)
    {
        if (obj is Maybe<T>)
            return Equals((Maybe<T>)obj);
        return false;
    }

    public bool Equals(Maybe<T> obj)
    {
        if (obj == null) return false;
        if (!EqualityComparer<T>.Default.Equals(_value, obj._value)) return false;
        if (!EqualityComparer<bool>.Default.Equals(this.HasValue, obj.HasValue)) return false;
        return true;
    }

    public override int GetHashCode()
    {
        int hash = 0;
        hash ^= EqualityComparer<T>.Default.GetHashCode(_value);
        hash ^= EqualityComparer<bool>.Default.GetHashCode(this.HasValue);
        return hash;
    }

    public static bool operator ==(Maybe<T> left, Maybe<T> right)
    {
        if (object.ReferenceEquals(left, null))
        {
            return object.ReferenceEquals(right, null);
        }

        return left.Equals(right);
    }

    public static bool operator !=(Maybe<T> left, Maybe<T> right)
    {
        return !(left == right);
    }
}

I believe what you are looking for is a Maybe Monad. It requires some work. But it is worth it imho. Implementation for Maybe Monad for C# is well described here.

You need to make some compromises to make this work:

public V? GetNullable<V>(Key key) where V : struct, Value
{
    (bool result, Value value) = TryGetValue(valueSlots, key);
    return result ? (V)value : (V?)null;
}

public V GetValueOrNull<V>(Key key) where V : class, Value
{
    (bool result, Value value) = TryGetValue(valueSlots, key);
    return result == true ? (V)value : null;
}

You must give different names to the two methods, and provide the type as parameter every time you call them:

var h1 = new HashTable<int, DateTime>(); // Value Type
DateTime? date = h1.GetNullable<DateTime>(1);

var h2 = new HashTable<int, Exception>(); // Reference Type
Exception ex = h2.GetValueOrNull<Exception>(1);

I guess that this defeats the purpose though.

Related