Switch-Case pattern for non-constant types Edit: Following answers, I modified my code, and wrote an other question.



History

Recently, I (stupidly) tried to make a switch on myObj.GetType(). Of course, it didn't work. Then I made an ugly (this is subjective) if ... else if ... else if ... else ... list.
I ask myself if I couldn't make something to keep the switch-case architecture, and then I started experienced things.

Expected

To keep the structure of a real switch-case, I want to use it in this way:

Switch(myInt)
    .Case(1, EqualsOneMethod)
    .Case(2, EqualsTwoMethod)
    .CaseWhen(i =&gt; i &lt;= 0, NullOrNegativeMethod)
    .Default(DefaultMethod);




static void EqualsOneMethod() { ... }
static void EqualsTwoMethod() { ... }
static void NullOrNegativeMethod() { ... }
static void DefaultMethod() { ... }


Code

Everything starts with this interface:

public interface ISwitchCase&lt;T&gt;
{
    ISwitchCase&lt;T&gt; Case(T value, Action action);
    ISwitchCase&lt;T&gt; Case(T value, IEqualityComparer&lt;T&gt; comparer, Action action);
    ISwitchCase&lt;T&gt; CaseWhen(Predicate&lt;T&gt; predicate, Action action);
    void Default(Action action);
}




The normal implementation is this one:

internal class SwitchCase&lt;T&gt; : ISwitchCase&lt;T&gt;
{
    private T Value { get; }

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

    public ISwitchCase&lt;T&gt; Case(T value, Action action)
    {
        if (Value is IEquatable&lt;T&gt;)
            return CaseWhen(t =&gt; ((IEquatable&lt;T&gt;)t).Equals(value), action);

        if (value is IEquatable&lt;T&gt;)
            return CaseWhen(t =&gt; ((IEquatable&lt;T&gt;)value).Equals(t), action);

        return Case(value, EqualityComparer&lt;T&gt;.Default, action);
    }

    public ISwitchCase&lt;T&gt; Case(T value, IEqualityComparer&lt;T&gt; comparer, Action action)
    {
        return CaseWhen(t =&gt; comparer.Equals(Value, value), action);
    }

    public ISwitchCase&lt;T&gt; CaseWhen(Predicate&lt;T&gt; predicate, Action action)
    {
        if (predicate(Value))
        {
            action();
            return EmptySwitchCase&lt;T&gt;.Instance;
        }

        return this;
    }

    public void Default(Action action) =&gt;
        action();
}




Whenever a case fullfils its predicate, it returns an EmptySwitchCase, which is the following:

internal class EmptySwitchCase&lt;T&gt; : ISwitchCase&lt;T&gt;
{
    public static EmptySwitchCase&lt;T&gt; Instance { get; }

    static EmptySwitchCase() { Instance = new EmptySwitchCase&lt;T&gt;(); }

    private EmptySwitchCase() { }

    public ISwitchCase&lt;T&gt; Case(T value, Action action) =&gt; this;

    public ISwitchCase&lt;T&gt; Case(T value, IEqualityComparer&lt;T&gt; comparer, Action action) =&gt; this;

    public ISwitchCase&lt;T&gt; CaseWhen(Predicate&lt;T&gt; predicate, Action action) =&gt; this;

    public void Default(Action action) { }
}


Thanks to this EmptySwitchCase, once a case is executed, no other case / default will be triggered.



Finally, as all these classes are internal, a simple static class is used as a facade:

public static class Bar // couldn't find a correct name...
{
    public static ISwitchCase&lt;T&gt; Switch&lt;T&gt;(T value) =&gt; new SwitchCase&lt;T&gt;(value);
}
The co-occurrence of Value and value in the same methods is irritating and likely to lead to bugs: I would rename value to query or something like that (and shall call value by query for the rest of this answer in the interests of not making any mistakes myself).



I was initially dubious of Flater's suggestion that creating the SwitchCase objects would be a problem given all the predicates flying around, so I looked at the memory characteristics of his example loop (only with a few more zeros on the end), and saw that I was completely wrong:



Note all the predicates and boxed ints (always a bad sign). All those predicates are not the ones the caller wrote: those are predicates created by the Case(T, ...) methods, and all the Int32s are integers being boxed so they can be cast as IEquatable&lt;int&gt;. If you change Flater's code to one which only uses the Case(Predicate&lt;T&gt;, ...) overload, everything disappears with the exception of the SwitchCases (the caller's predicates are reused), and as Flater says, you can remove those too by caching and improve the method memory characteristics dramatically.



Now to diverge from the code itself... there may be little point in your if (Value is IEquatable&lt;T&gt;) condition, since the EqualityComparer will do that for you. The only thing it gives you, is that it will use T's implementation of IEquatable&lt;T&gt; by default rather than query's: this sort of thing needs to carefully documented, because it could cause someone down the line no-end of confusion when they try to 'override' the comparison by implementing IEquatable&lt;T&gt; in a super-class of T, only to have it ignored. This feels the wrong way round to me; however, personally I wouldn't do the check for either, and would solely depend on the EqualityComparer (and maybe a comparer passed to the constructor). If I did want to use query's implementation, then I probably ought to know either that it has an implementation to use (and when it will be used), in which case the caller can decide if they want to use it or not. I'd rather provide a performant version with statically determinable behaviour:



public ISwitchCase&lt;T&gt; Case&lt;S&gt;(S query, Action action) where S : IEquatable&lt;T&gt;
{
    if (query.Equals(Value))
    {
        action();
        return EmptySwitchCase&lt;T&gt;.Instance;
    }

    return this;
}


Such a method also has the advantage of being efficient when handling structs, as it doesn't involve any boxing. I've also in-lined the conditional logic, because it avoids the allocation of the Predicate&lt;T&gt; and its body (which can't be reused).

This doesn't solve the boxing int problem in Flater's example, because the Case(T, etc.) overloads are better, but if you replace those with these, you can prevent any repetitive allocations occurring when these methods are called.

/// &lt;summary&gt;
/// Compares the query and internal value using the DefaultEqualityComparer, and invokes the given action if they match
/// &lt;/summary&gt;
public ISwitchCase&lt;T&gt; Case(T query, Action action)
{
    return Case(query, EqualityComparer&lt;T&gt;.Default, action);
}

/// &lt;summary&gt;
/// Compares the query and internal value using the given comparer, and invokes the given action if they match
/// &lt;/summary&gt;
public ISwitchCase&lt;T&gt; Case(T query, IEqualityComparer&lt;T&gt; comparer, Action action)
{
    if (comparer.Equals(Value, query))
    {
        action();
        return EmptySwitchCase&lt;T&gt;.Instance;
    }

    return this;
}


Obviously it's your decision what your API does, and it is important to stress that the behaviour is different like this, but that the behaviour was unclear before. With the added inline documentation, the new behaviour is clear and completely transparent at compile time. The difference is that that a value which provides an alternative implementation of IEquatable&lt;T&gt; will only be compared using this implementation if it is not a T and it can be determined at compile time that it is IEquatable&lt;T&gt;. I'd make a case for this behaviour being more 'intuitive', because it become equivalent to using a predicate of the form v =&gt; query.Equals(v) when query is not of type T. Without a : T constraint, this means that stuff which is comparable to T without necessarily being a T can also be used, which may or may nor be desirable.



Naturally, it is not nice having many methods implementing the if (predicate(Value)) logic, so I'd be inclined to put this login in a simple private ISwitchCase&lt;T&gt; Case(bool condition, Action action), essentially replacing your current Case(Predicate&lt;T&gt;, etc.) in this role. If you want to keep the dynamic IEquatable&lt;T&gt; behaviour, then you can still rid yourself of all the anonymous methods by using such a method instead of deferring everything to Case(Predicate&lt;T&gt;, etc.). For example:

/// &lt;summary&gt;
/// Compares the query with the internal value, first trying to use implementation of `IEquatable&amp;lt;t&amp;gt;` provided by the internal value, then that provided by the query, falling back to the DefaultComparer otherwise
/// &lt;/summary&gt;
public ISwitchCase&lt;T&gt; Case(T query, Action action)
{
    if (Value is IEquatable&lt;T&gt;)
        return Case(((IEquatable&lt;T&gt;)Value).Equals(query), action);

    if (query is IEquatable&lt;T&gt;)
        return Case(((IEquatable&lt;T&gt;)query).Equals(Value), action);

    return Case(query, EqualityComparer&lt;T&gt;.Default, action);
}

/// &lt;summary&gt;
/// Compares the query and internal value using the given comparer, and calls the given action if they match
/// &lt;/summary&gt;
public ISwitchCase&lt;T&gt; Case(T query, IEqualityComparer&lt;T&gt; comparer, Action action)
{
        return Case(comparer.Equals(Value, query), action);
}

/// &lt;summary&gt;
/// Invokes the given action if the condition is true
/// &lt;/summary&gt;
private ISwitchCase&lt;T&gt; Case(bool condition, Action action)
{
    if (condition)
    {
        action();
        return EmptySwitchCase&lt;T&gt;.Instance;
    }

    return this;
}


Or, more tidily (using pattern matching):

/// &lt;summary&gt;
/// Compares the query with the internal value, first trying to use implementation of `IEquatable&amp;lt;t&amp;gt;` provided by the internal value, then that provided by the query, falling back to the DefaultComparer otherwise
/// &lt;/summary&gt;
public ISwitchCase&lt;T&gt; Case(T query, Action action)
{
    if (Value is IEquatable&lt;T&gt; v)
        return Case(v.Equals(query), action);

    if (query is IEquatable&lt;T&gt; q)
        return Case(q.Equals(Value), action);

    return Case(query, EqualityComparer&lt;T&gt;.Default, action);
}


Note the inline documentation, which should appear on the interface.