Movatterモバイル変換


[0]ホーム

URL:


SLaks.Blog

Making the world a better place, one line of code at a time

Code Snippets: Variadic Generics in C#

Posted on Tuesday, June 16, 2015

This post is part of aseries of blog posts called code snippets. These blog posts will explore successively more interesting ways to do simple tasks or abuse language features.

C++ introducedVariadic Templates – template classes or functions that can take an arbitrary number of template parameters (like varargs/paramarray function parameters). This feature has a number of uses. It’s the simplest way arbitrary tuple or function types. It’s also useful when making a function that can take an arbitrary number of objects or delegates.

C# does not support this feature. For variadic types, there is no direct workaround; this is why the BCL has 16 overloads of the genericFunc andAction delegates, and 8 overloads ofTuple, to cover all common uses.

However, when designing something that needs to take an arbitrary number of objects of different generic parameters, there are workarounds.

This simplest workaround is to use repeated method calls. You can make a generic function that returnsthis, and call it separately for each parameter with inferred type parameters. For example:

classContainer1{publicContainer1WithStuff<T>(Func<T>func){// Do interesting things...returnthis;}}newContainer1().WithStuff(()=>newList<byte>()).WithStuff(()=>newMemoryStream()).WithStuff(()=>newCredentialCache());

The disadvantage of this approach is that the repeated method calls don’t look nice, especially if you’re just trying to accept a number of parameters for a single thing.

A more interesting approach is to abusecollection initializers. Collection initializers let you write a list of expressions (or argument lists) in braces (just like an array initializer), and compile them into a series ofAdd() calls. The nice part of this feature is that each item compiles to its ownAdd() call (with its own generic parameters and type inference), giving you a way to hide the repeated calls from the syntax.

The previous example can thus be adapted to look like this:

classContainer2:System.Collections.IEnumerable{publicvoidAdd<T>(Func<T>func){// Do interesting things...}System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){thrownewNotSupportedException();}}newContainer2(){()=>newList<byte>(),()=>newMemoryStream(),()=>newCredentialCache()};

The class must implement the non-genericIEnumerable interface in order to allow collection initializers (since the feature is meant, as its name implies, to work with collections).

Unlike normal method calls, collection initializers don’t expose any way to explicitly specify generic type parameters; they can only be called using type inference. For methods that accept instances ofT (or lambdas that return such instances), this is not a problem; the compiler can infer the type parameter from the compile-time type of the expression used. If your method only uses the generic type parameter as the parameter type of a lambda, you must instead explicitly specify the parameter type in the lambda (eg,(string x) => blah), telling the compiler whatT is.

Note that this only works at construction time (syntactically, this is an optional part of thenew keyword). If you’re trying to make a variadic method on an existing instance (or a static method), you can make a new class to serve as the method’s parameter, and put the collection initializer in that class. For example:

classValidationManager{readonlyList<Tuple<object,string>>errors;publicvoidValidateItems(ItemValidatorsparameter){errors.AddRange(parameter.Errors);}}classItemValidators:System.Collections.IEnumerable{readonlyList<Tuple<object,string>>errors;publicIReadOnlyCollection<Tuple<object,string>>Errors=>errors.AsReadOnly();publicvoidAdd<T>(stringerror,Tinstance,Func<T,bool>rule){if(!rule(instance))errors.Add(Tuple.Create((object)instance,error);}System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){thrownewNotSupportedException();}}varvalidation=newValidationManager();validation.ValidateItems(newItemValidators{{"Stream cannot be empty",newMemoryStream(),s=>s.Length==0},{"Email address must have a display name",newMailAddress(),m=>string.IsNullOrWhiteSpace(m.DisplayName)}});

As you may have noticed from this example, these workarounds don’t help youstore variadically-typed items. What you do with the parameters you receive depends on what you’re ultimately trying to accomplish. If you’re dealing with reflection (but want strongly typed lambdas), you can simply storetypeof(T). If you just need instances (like the above example), you can cast the items toobject. However, the most powerful way to deal with this is to collect non-generic delegates that close over the generic parameter (creating a generic closure class). As long as you’re able to do useful things from a non-generic entry-point (eg, cast an object toT), you can retain the generic parameter from within the delegate. For example:

classItemReporter:System.Collections.IEnumerable{readonlyDictionary<Type,Func<object,string>>reporters=newDictionary<Type,Func<object,string>>();publicvoidAdd<T>(Func<T,string>reporter){reporters.Add(typeof(T),obj=>typeof(T)+"\t"+reporter((T)obj));}publicstringReport(paramsobject[]items){returnstring.Join(Environment.NewLine,items.Select(obj=>reporters[obj.GetType()](obj)));}System.Collections.IEnumeratorSystem.Collections.IEnumerable.GetEnumerator(){thrownewNotSupportedException();}}varreporter=newItemReporter{(FileStreamfs)=>fs.Name+"; "+fs.Length+" bytes",(DateTimed)=>d.ToString("U"),(DirectoryInfod)=>d.Name+": "+d.EnumerateFiles().Count()+" files"};Console.WriteLine(reporter.Report(DateTime.Now,DateTime.UtcNow,newDirectoryInfo(Environment.CurrentDirectory)));

As mentioned above, you must explicitly specify the lambda parameters to allow the compiler to inferT for each generatedAdd() call.

This demonstrates one more limitation of this workaround: It is impossible to store type information. If you implement this class in C++, you’d be able to enforce – at compile time – that the arguments passed toReport() match the types of the reporters passed to the constructor (since those types would be encoded in the template parameters of the instance’s compile-time type). In C#, however, the only way to implementReport() is to accept an array ofobject, which obviously has no type safety.

Note that this approach does not work for base classes or interfaces; if the reporter doesn’t have a delegate for an object’s exact runtime type, the dictionary lookup will fail. It is impossible to allow that with a dictionary (since assignment compatibility is not an equivalence relation). If you want to support that, you could either loop through every reporter until you find one with a compatible type (which would be O(n) in the number of reporters, but would be simpler code), or recursively loop through the base classes and interfaces ofobj.GetType() until you find a type in the dictionary (which would be O(n) in the depth of the type hierarchy).

This technique is particularly useful for visitor patterns (exampleusage andimplementation); more details coming soon in a separate blog post.

Categories:C#,generics,code-snippetsTweet this post

PreviousNext

comments powered byDisqus