C# 6.0 – Params IEnumerable

[7/30/2015] This article was written against a pre-release version of C# 6.0 and didn’t make it into the final product. Be sure to check out the list of my five favorite C# 6.0 features for content written against the release!

One of the items currently listed as “planned” on the Language Feature Implementation Status page is params IEnumerable. This is one of those features that I’ve thought could be nice but I’ve never really felt strongly about it one way or another mainly because I avoid params arrays whenever possible. I think the only time I actually use them consistently is with String.Format and its cousin methods. Before diving into the new feature, let’s revisit params arrays so I can better explain why I avoid them and why I’m so apathetic about this proposed feature.

The idea behind params arrays is that they let us pass an arbitrary number of arguments to a method by defining said method such that its final parameter is an array modified by the params keyword. When we invoke the function, the compiler implicitly creates an array from the final parameters and is then passed to the method. In the method body, we typically access those values through the array. The following function shows this in action:

string MyStringFormat(string format, params string[] tokens)
{
  return
    Regex.Replace(
      format,
      @"\{(?<index>\d+)\}",
      m => tokens[Int32.Parse(m.Groups["index"].Value)].ToString());
}

We could then invoke this function as follows:

var format = "[{0}] Hello, {1}.";
var time = DateTime.Now.ToShortTimeString();
var name = "Guest";

var str = MyStringFormat(format, time, name);
// [10:35 PM] Hello, Guest.

Back in the early days of the .NET Framework, before we had collection initializers, the above was far more convenient than using an array directly. Now that collection initializers let us define arrays inline, we can simply write this:

var str = MyStringFormat(format, new [] { time, name });
// [10:35 PM] Hello, Guest.

Arguably, the former is a bit cleaner than the later because of the inline array definition but I have two problems with it. First, it masks the fact that an array is involved, and second, params arrays can really wreak havoc on overload resolution if you’re not careful with them. Assume we need to overload the MyStringFormat method to accept another string that will control some formatting options. We can’t tack the string on to the end of the parameter list because params arrays must appear last so we insert it at the front like this:

Note: I’m using string constants here for illustration purposes. A less contrived example would likely use an enum, thus avoiding the problem.

static class StringTransform
{
  public const string Uppercase = "Uppercase";
  public const string Lowercase = "Lowercase";
}

string MyStringFormat(string transformation, string format, params string[] tokens)
{
  var str = MyStringFormat(format, tokens);
  
  switch (transformation)
  {
    case StringTransform.Uppercase: return str.ToUpper();
    case StringTransform.Lowercase: return str.ToLower();
  }      

  return str;
}

Now we can invoke this new overload as follows:

var str = MyStringFormat(StringTransform.Uppercase, format, time, name);

…and the result would be as we would expect:

[12:30 AM] HELLO, GUEST.

But now that we have two overloads (each accepting an indeterminate number of strings) the compiler cannot always properly determine which overload to invoke. Let’s see what happens when we supply only three arguments as we did in the first example.

var str = MyStringFormat(format, time, name);

Now the result is:

12:30 AM

What happened? The compiler recognized that there are multiple candidate overloads so it took a greedy approach, selecting the overload with the most explicit parameters. As a result, each of the arguments were essentially shifted to the left of what we expected. In other words, what should have been the format parameter was treated as the transformation parameter, the first of the token values was treated as the format parameter, and so on. To force the compiler to use the correct overload, we can just be explicit with the array as we saw in the second example above:

var str = MyStringFormat(format, new [] { time, name });

So what does all of this have to do with params IEnumerable? Params IEnumerable is an extension of the core idea; rather than treating the last arguments as an array within the method, we’d treat them as a sequence. That is, in our MyStringFormat method, we’d define the final parameter as params IEnumerable<string> tokens rather than params string[] tokens.

This change makes sense from a language perspective especially since params arrays were implemented long before IEnumerable<T> was a central concept within the framework. But, as the May 21st design notes state, when the method is used as a params method (rather than by passing an explicit sequence), the compiler will still generate an array at the call site. For all other cases, passing a sequence will behave no differently than if the parameter had been defined without the params modifier.

Given that:

  • it’s so easy to define sequences inline
  • using params arrays can easily confuse the overload resolver
  • params IEnumerable will only hide the sequence’s details from the method implementation but otherwise have no effect on the code

…I don’t see myself ever using this feature. I’ll likely continue designing methods to accept IEnumerable<T> without including the params modifier to avoid the problems and be more specific about what the method is expected to do.

Advertisements

One comment

Comments are closed.