I recently came across an interesting params gotcha (Or more like a trap) recently while working around a method in C# that allowed both a params and an IEnumerable parameter. If that sounds confusing, let me give a quick refresher.
Let’s say I have a method that looks like so :
static void Call(IEnumerable<object> input) { Console.WriteLine("List Object"); }
Pretty normal. But sometimes when calling this method, I have say two items, and I don’t want to “new up” a list. So using this current method, I would have to do something like so :
var item1 = new object(); var item2 = new object(); Call(new List<object> { item1, item2 });
Kind of ugly. But there is also the params keyword that allows us to pass in items seperated by commas, and by magic, it turns into an array inside the method. For example :
static void Call(params object[] input) { Console.WriteLine("Object Params"); }
Now we can just do :
var item1 = new object(); var item2 = new object(); Call(item1, item2);
Everything is perfect! But then I ran into an interesting conundrum that I had never seen before. Firstly, let’s suppose I pass in a list of strings to my overloaded call. The code might look like so :
static void Main(string[] args) { Call(new List<string>()); } static void Call(params object[] input) { Console.WriteLine("Object Params"); } static void Call(IEnumerable<object> input) { Console.WriteLine("List Object"); }
If I ran this code, what would be output? Because I’ve passed it a List<string>, which is a type of IEnumerable<object>, you might think it would output “List Object”. And… You would be right! It does indeed use the IEnumerable method which makes total sense because List<string> is a type of IEnumerable<object>. But interestingly enough… List<string> is also an object… So theoretically, it could indeed actually be passed to the params call also. But, all is well for now and we are working correctly.
Later on however, I decide that I want a generic method that does some extra work, before calling the Call method. The code looks like so :
static void Main(string[] args) { GenericalCall(new List<string>()); } static void GenericalCall<T>(List<T> input) { Call(input); } static void Call(params object[] input) { Console.WriteLine("Object Params"); } static void Call(IEnumerable<object> input) { Console.WriteLine("List Object"); }
Well theoretically, we are still giving it a List of T. Now T could be anything, but in our case we are passing it a list of strings same as before so you might expect it to output “List Object” again. Wrong! It actually outputs “Object Params”! Why?!
Honestly. I’m just guessing here. But I think I’ve deduced why. Because the type T could be anything, the compiler isn’t actually sure that it should be able to call the IEnumerable overload as whatever T is, might actually not inherit from object (Although, we know it will, but “theoretically” it could not). Because of this, it treats our List<T> as a single object, and passes that single item as a param into the params call. Crazy! I actually thought maybe at runtime it might try and inspect T and see what type it is to deduce the right call path, but it looks like it happens at compile time.
This is confirmed if we actually add a constraint to our generic method that says the type of T must be a class (Therefore has to be derived from object). For example :
static void Main(string[] args) { GenericalCall(new List<string>()); } static void GenericalCall<T>(List<T> input) where T : class { Call(input); } static void Call(params object[] input) { Console.WriteLine("Object Params"); } static void Call(IEnumerable<object> input) { Console.WriteLine("List Object"); }
Now we return the List Object output because we have told the compiler ahead of time that T will be a class, which all objects inherit from Object. Easy!
Another way to solve this is to force cast the List<T> to IEnumerable<object> like so :
static void GenericalCall<T>(List<T> input) { Call((IEnumerable<object>)input); }
Anyway, hopefully that wasn’t too much of a ramble. I think this is one of those things that you sort of store away in the back of your head for that one time it actually occurs.
Where Did I Actually See This?
Just as a little footnote to this story. I actually saw this when trying to use EntityFramework’s “HasData” method.
I had this line inside a generic class that helped load CSV’s as data into a database.
modelBuilder.Entity(typeof(T)).HasData(seedData);
I kept getting :
The seed entity for entity type ‘XXX’ cannot be added because there was no value provided for the required property ‘YYY’
And it took me a long time to realize HasData has two overloads :
HasData(Enumerable<object> data); HasData(params object[] data);
So for me, it was as easy as casting my seedData input to IEnumerable.
modelBuilder.Entity(typeof(T)).HasData((IEnumerable<object>)seedData)