If you’ve spent time in the C# world, you’ve probably encountered (and possibly asked) the question: “Should I use .ToList() or .ToArray()?”
The answer, of course, is that it’s a false dichotomy: I’ve written before about how you should pick the right data structure for the job. But what if you really don’t care? What if you’d prefer to just roll the dice and let the computer pick for you? Is that possible?
Of course it is. First, we need an extension method:
public static class Extensions { public static IEnumerable<T> ToRandomCollectionType<T>(this IEnumerable<T> collection) { throw new NotImplementedException(); } }
The first thing that’s going to have to do is pick a collection type. Let’s load all of the types we can find which implement IEnumerable:
var types = AppDomain.CurrentDomain.GetAssemblies() .SelectMany(assembly => assembly.GetTypes()) .Where(t => typeof(IEnumerable).IsAssignableFrom(t))
We actually want to be pickier than that – because we need to work out how to actually instantiate one of these types. So let’s make sure all of our candidate types have a constructor which accepts a single IEnumerable of the passed-in collection type:
.Select(t => { try { return t.MakeGenericType(typeof(T)); } catch { return null; }}) .Where(t => t != null && t.GetConstructor(constructorParamTypeArray) != null)
The try/catch block there is to deal with classes like ConcurrentDictionary, which require multiple generic parameters even though they implement IEnumerable. Is there a better way to check? Probably! But we’re not writing production-grade code here, because .ToRandomCollectionType<>() is fundamentally production-unsuitable anyway.
The next step is to randomly select one of our candidate types and grab a reference to the right constructor.
var type = types[_rnd.Next(0, types.Count-1)]; var constructor = type.GetConstructor(constructorParamTypeArray);
Now all that’s left is to invoke the constructor and return the result!
return (IEnumerable<T>)constructor.Invoke(new [] {collection});
Does it work? Absolutely! Most of the time, anyway. Here’s the outcome of feeding { 1, 2, 3 } into it a few times:
Much like the FileChangeMonitor hack I blogged about, this is unsuitable for anything production-like. Please treat it as the humourous hack it is and don’t use it for anything important!
Hang on though, we’re not done. You should always test your code. Like, with automated tests.
public class ItShouldUsuallyWork { [Test] public void TryingAFewTimes_SometimesWorks() { var inputSet = new List<int> {1, 2, 3}; int successCount = 0; for (int i = 0; i < 10; i++) { var result = inputSet.ToRandomCollectionType(); try { inputSet.Should().BeEquivalentTo(result); Console.WriteLine($"Got a {result.GetType()}"); successCount++; } catch { Console.WriteLine("Looks like that one failed."); } } successCount.Should().BeGreaterOrEqualTo(5); } }
The code is available on GitHub, and there is a NuGet package so this lovely little extension method is never more than a quick “Install-Package ToRandomCollectionType” away.
If you enjoy a little C# whimsy, you might like to read about my Object.Extend(…) implementation in C#.
Looks like a candidate for property testing https://www.codit.eu/blog/property-based-testing-with-c/
The idea behind ToArray() and ToList() is to memorize/finalize/”cool down” otherwise “hot” enumerable. Correct? Means you need to pick up only those type that implement IReadOnlyCollection. If I remember correctly now days this is the lowers denominator and all BCL types do implement it. Otherwise non-generic ICollection.
I think there is better way to find the suiting types than try/catch. Why don’t inspect each ctor’s parameters?