Design Guidelines: Provide type inference friendly Create function for generic objects


Really this guideline is a bit longer but putting it all in a blog title seemed a bit too much.  The full guideline should read: "If a generic class constructor arguments contain types of all generic parameters, provide a static method named Create on a static class of the same class name as the generic class which takes the same arguments and calls the constructor."  Quite a mouth full. 

Lets look at a specific example with Tuples.  Tuples are generic with respect to the values they are representing.  Without any type inference help we would have to write the following code to create a simple tuple.

            var tuple = new Tuple<int, string>(5, "astring");

Not too bad because we are using simple types.  But what happens when we are using really long type names? 

            var tuple2 = new Tuple<string,Dictionary<string, List<int>>>(val1, val2);

As we can see, the code is getting quite a bit uglier.  This pattern is in fact not maintainable once we start using un-namable types such as anonymous types or generics of anonymous types. 

The problem here is we are not leveraging the compilers type inference capabilities.  The compiler can easily infer the types of a tuple argument and hence create a tuple.  We just need to provide a mechanism to do so.  The best way is to define a static method on a static class with the same name as the generic.  Lets call this method Create.

    public static class Tuple {
        public static Tuple<TA, TB> Create<TA, TB>(TA valueA, TB valueB) { 
            return new Tuple<TA, TB>(valueA, valueB); 
        }
    }

The method Tuple.Create still has two generic parameters.  However since we are providing a set of values which contain types for every generic parameter, the compiler can infer the generic arguments.  Now we can create a Tuple without specifying any generic arguments.  Because no types are specified this will work with any value in the code including un-namable types. 

            var tuple = Tuple.Create(6, "astring");
            var tuple2 = Tuple.Create(6, new { name = "aname", value = 42 });

Comments (3)

  1. There are a few things I don’t understand about this…

    1) What’s so special about the static method of a static class that the compiler does type inference on it, but not on the constructor? The constructor has the same parameters; what information is the compiler missing that makes inference impossible in this case, while being possible in the other?

    2) I didn’t even know you could declare a static class with the same name as a "normal" class. Are you sure that’s what’s going on, and it’s not some kind of weird semi-partial thing where you can declare your class’s static methods separately with "static" as a stand-in for "partial"?

    3) Why not just use a static method for the normal class? (Obviously related to the above.)

    I mean, these are pretty obvious so I’m sure I’m not pointing out something new. Rather, why are my preconceptions about how things should work so wrong?

    Thanks!

  2. 1) Really there is no difference other than syntax.  The compiler is capable of infering the types in both cases it just doesn’t support syntax for it.  One idea i’ve seen tossed around is the following.

    C#  new Tuple<,>(true,"again")

    VB  new Tuple(Of,)(True,"again")

    2) It’s definately two classes.  The reason why this is possible is when you declare a class with a generic parameter it gets emmited with a different name.  For instance Foo<T> will be emitted as Foo`1 and Foo<T,U> will be emmited as Foo`2.  AFAIK, this is not a CLI standard, it’s a compiler convention.  If you switch Reflector to IL mode you can see this behavior when a generic method/type is referenced in a function body.  

    3) Adding a static method for the normal class won’t work because you still have to specify the generic parameters when calling the static method.  

    C# and VB did a great job of integrating generics into the language and making them easy.  The problem is their quite hard and making them easy allows people to use them without fully understanding the implication of doing so.  In addition the BCL did a fairly good job of integrating generics and hence many of these little issues don’t show up.  

    One way to start learning more about generics is to turn on FxCop if you define a generic class.  It will point out many pitfalls and good design practices around using generics.  

  3. Ah OK. I’ve definitely done reflection on generics; I think the use case that I was missing was when you have both generic and non-generic types with the same name (e.g. IEnumerable).

    In this case the behavior I was expecting was something like "if there is no non-generic IEnumerable, then let IEnumerable(x) do type inference on x and call into the constructor of IEnumerable<T>. But if there is, then it should just call the IEnumerable constructor. Unless x doesn’t match any IEnumerable constructors, and _does_ match the IEnumerable<T> type restriction—then use IEnumerable<T>." But obviously this is pretty bad in a few ways… lots of potential for introducing new code creating breaking changes etc.

    Thanks!