6

I don't understand why C# doesn't infer a type in the following complete situation:

public interface IThing {}

public class Thing1 : IThing {}

public class Thing2 : IThing {}

public interface IContainer {}

public class Container1 : IContainer
{
    public IThing A { get { return new Thing1(); } }
    public IThing B { get { return new Thing2(); } }
}

public class Container2 : IContainer
{
    public IThing C { get { return new Thing1(); } }
    public IThing D { get { return new Thing2(); } }
}

public class SomeClass
{
    public void PerformTask() {}
}

public static class ExtensionMethods
{
    // This function behaves as I would expect, inferring TContainer
    public static TContainer DoStuffWithThings<TContainer>(this TContainer container, Func<TContainer, IThing> getSomething, Func<TContainer, IThing> getSomethingElse) 
        where TContainer : IContainer
    {
        var something = getSomething.Invoke(container);
        var somethingElse = getSomethingElse.Invoke(container);

        // something and something else are the things we specify in our lambda expressions with respect to the container

        return container;
    }

    // The method in question
    public static TCustomReturnType DoStuffWithThings<TContainer, TCustomReturnType>(this TContainer container, Func<TContainer, IThing> getSomething, Func<TContainer, IThing> getSomethingElse)
        where TContainer : IContainer
        where TCustomReturnType : new()
    {
        var something = getSomething.Invoke(container);
        var somethingElse = getSomethingElse.Invoke(container);

        // Do stuff with the things just as above

        // This time we return our custom type
        return new TCustomReturnType();
    }
}

public class Driver
{
    public static void Main(string[] args)
    {
        var container1 = new Container1();
        var container2 = new Container2();
        // I can do stuff with the things for each container, returning the container each time.

        container1.DoStuffWithThings(c => c.A, c => c.B)
                  .DoStuffWithThings(c => c.B, c => c.A);

        container2.DoStuffWithThings(c => c.C, c => c.D)
                  .DoStuffWithThings(c => c.D, c => c.C);

        // Now we try to do the same but with the custom return type
        container1.DoStuffWithThings<Container1, SomeClass>(c => c.A, c => c.B)
            .PerformTask();
        // As you can see, the compiler requires us to specify Container1 as the container type.
        // Why is it not inferred? It is called from an instance of Container1.
        // The behavior I expect is for container1.DoStuffWithThings<SomeClass>(...) to infer
        // the container type and return a new instance of SomeClass.
    }
}

The basic idea is that when there is only one generic type parameter, the compiler infers the type. When I add a second, the compiler doesn't infer either (It obviously can't infer the second, but I'm not sure why it can't infer the first). My question is why is the type of the container not inferred?

pagrick
  • 73
  • 5

1 Answers1

10

The reason is that the second type parameter it's not used in any of the function parameters. So there is no way it's type can be inferred purely from parameter usage.

If you were to have a method signature like this (obviously not equivalent to your code, just an example):

public static TResult DoStuffWithThings<TContainer, TResult>(
    this TContainer container,
    Func<TContainer, TResult> getSomething)

Then it would be able to infer the generic types from the parameters.

If you want to avoid specifying the first parameter you can split the method up into two functions and hide intermediate parameters in a generic type, like this:

public static IntermediateResult<T> DoStuffWithThings<T>(this T container)

public class IntermediateResult<T>
{
    public WithReturnType<TResult>()
}

Then it calling as

var result = container.DoStuffWithThings().WithReturnType<Result>();
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
  • I do not expect the compiler to infer the second parameter (as you said, it is not used in any of the parameters). But I do expect it to infer the first parameter (which is in the parameters). So I can call container1.DoStuffWithThings(...) and have it infer that TContainer is Container1. Are parameters an all or nothing deal when doing type inference? – pagrick Mar 16 '13 at 02:57
  • 4
    You must specify either *all* type parameters or *none*, but see my updated answer for an alternative. – p.s.w.g Mar 16 '13 at 03:05
  • Thanks. I didn't realize it was all or nothing. For posterity I'd be curious to see why; where would the design of inferring only the known types and specifying unknown types break down? – pagrick Mar 16 '13 at 03:22
  • @perrick My guess is it's because if you only specify `Foo(...)` it may be difficult (even impossible in some cases) for the compiler to know if you mean `Foo(...)` or `Foo(...)`. – p.s.w.g Mar 16 '13 at 03:26
  • It seems theoretically possible, so it must be too complicated to use/implement for the language designers to allow it. In an extreme case like this: T9 method(T0 t0, T1 t1, ..., T8 t8) the all or nothing deal becomes cumbersome because 9/10 types can be easily inferred when the method is called, but since T9 can't, they all need to be explicitly listed matching the parameters used, e.g. var str = method(0, 1, ..., 8) – pagrick Mar 16 '13 at 03:47
  • instead of var str = method(0, 1, ..., 8) But I suppose cases like this don't come up enough to make it worth it. Thanks for analysis, sir. – pagrick Mar 16 '13 at 03:47
  • 1
    @perrick also consider that it will be difficult to tell the difference between `Foo(...)` (one type param, explicitly declared) and `Foo` (two type params, only one explicitly declared). – p.s.w.g Mar 16 '13 at 03:58
  • 1
    It would be nice if the language allowed you to do `Foo<,T>(...)` to clarify that the second type parameter is the one you are specifying, while allowing the first to be inferred. `Foo(...)` then would just be a shortcut for `Foo<,>(...)` in this case. – jam40jeff Mar 16 '13 at 12:44
  • "You must specify either all type parameters or none" - that's exactly what I needed. – stuartd Jan 03 '14 at 11:27