C# generic interface specialization

Viewed 16872

I wonder if it is in any way possible to specialize generic interface methods somehow in C#? I have found similar questions, but nothing exactly like this. Now I suspect that the answer is "No, you can't" but I would like to have it confirmed.

What I have is something like the following.

public interface IStorage
{
    void Store<T>(T data);
}

public class Storage : IStorage
{
    public void Store<T>(T data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

class Program
{
    static void Main(string[] args)
    {
        IStorage i = new Storage();
        i.Store("somestring"); // Prints Generic
        i.Store(1); // Prints Generic
        Storage s = (Storage)i;
        s.Store("somestring"); // Prints Generic
        s.Store(1); // Prints Specific
    }
}

Is there any way to make it use the specialized version of Store when called through the interface? And if not, does anyone know the exact reason why C# treats Generic arguments this way?

Edit: The issue could be worked around if it wasn't so that C# cannot resolve template arguments in more than one step.

void Foo<T>(T t)
{
    SubFoo(t);
}

void SubFoo<T>(T t)
{
    Console.WriteLine("Generic");
}

void SubFoo(int t)
{
    Console.WriteLine("Specific");
}

A call to Foo(1) here will print "Generic" as well, shouldn't the compiler be able to resolve this? Or does the JIT prevent this?

6 Answers

Overload resolution is performed at compile-time, not at run-time based on the actual type of the passed value.

IStorage i = new Storage();
i.Store("somestring"); // Prints Generic
i.Store(1); // Prints Generic

This will always call the "generic" method, because there is only one overload of Store in IStorage and the compiler doesn't know that i actually contains a Storage object. How can the compiler know about the other overload in Storage?

Storage s = (Storage)i;
s.Store("somestring"); // Prints Generic
s.Store(1); // Prints Specific

Here, the compiler knows that s contains a Storage object (or one deriving from Storage), because s is declared that way. So it sees two overloads. It chooses the specific overload for int values, because overload resolution rules say to prefer specific overloads over generic overloads.


It's technically possible to determine typeof(T) in the generic method at run-time and forward the method call to a specific method. But if you think about it, this doesn't make a lot of sense. A generic method means that the same implementation works for arguments of different, unrelated types. If you want different implementations for different types, you shouldn't use generics for this.


void Foo<T>(T t)
{
    SubFoo(t);
}

void SubFoo<T>(T t);
void SubFoo(int t);

Generics work quite a bit different from C++ templates. The C# compiler compiles Foo only once -- to a generic method. Remember: generic means same implementation for different types. The C# compiler does not know at compile-time if T is going to be an int or a string or any other type. So the only possible implementation of Foo that works for any T is to call SubFoo<T>. If one of the SubFoo overloads would be called depending on T, the implementation of Foo wouldn't be the same for all T any more.

If you want to take advantage of compile-time overload resolution you may as well extend the interface with a method that takes an int:

public interface IStorage
{
    void Store<T>(T data);
}

public interface IIntStorage: IStorage
{
    void Store(int data);
}

public class Storage : IIntStorage
{
    public void Store<T>(T data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

Now if you call Store(1) through the IIntStorage interface it will use the specialized method (similar to how you called Storage's method directly), but if you call it through IStorage it will still use the generic version.

You could do something like this:

public interface IStorage<T>
{
    void Store(object data);
    void Store<T>(T data);
}

public class Storage : IStorage<int>
{
    public void Store(object data)
    {
        Console.WriteLine("Generic");
    }

    public void Store(int data)
    {
        Console.WriteLine("Specific");
    }
}

You've typed i as IStorage and that interface doesn't define the overloaded Store method.

You can do it by introducing additional type information (e.g. implementing an interface). Here is an example.

// no need to modify the original interface
public interface IStorage
{
    void Store<T>(T data);
}

Implementation with specialization based on a generic interface

public class Storage : IStorage,
    Storage.ISpecializedFor<int>,
    Storage.ISpecializedFor<double>
{
    // a private interface to provide additional type info
    private interface ISpecializedFor<T>
    {
        void Store(T data);
    }

    public void Store<T>(T data)
    {
        // resolve specialization
        if (this is ISpecializedFor<T> specialized)
        {
            specialized.Store(data);
        }
        else
        {
            // unspecialized implementation goes here
            Console.WriteLine("Unspecialized");
        }
    }

    // specialized implementation
    void ISpecializedFor<int>.Store(int data)
    {
        Console.WriteLine("Specialized for int");
    }

    void ISpecializedFor<double>.Store(double data)
    {
        Console.WriteLine("Specialized for double");
    }
}

Results

void Main()
{
    IStorage storage = new Storage();

    storage.Store("hello"); // prints "Unspecialized"
    storage.Store(42); // prints "Specialized for int"
    storage.Store(1.0); // prints "Specialized for double"
}
Related