Поделиться через


Simulating "Extension Interfaces" with Structs and Generics

While many still debate the value and appropriateness of Extension Methods in C# 3.0, I have always felt that there was still something missing. Extension methods allow me to add individual methods to classes; however, there are several cases where I have always wanted to add a new interface to a class that is sealed and outside my control. For lack of a better term, I would consider these an "Extension Interface."

Take for example the TextWriter and StringBuilder classes. Both of these classes serve the function of "sending text" to another object. In fact, these classes have similar methods ( .Append() and .Write() ) that support a wide-range of data types. I've always wished that they supported a common interface as follows. (For the purposes of this example, I only show two signatures, but you could imagine support for other common data types and options. )

public interface ITextSender {     void Write(char c);       void Write(string s); }

If I has such as interface, then I would be able to write algorithms that generate text output that could efficiently write to strings, files, etc.

One option, of course, would be to write a wrapper class (such as TextSender) that could be subclassed for various output scenarios. The downside is that this is an extra memory allocation on the heap. While .NET is optimized for small, short-lived objects, the reality is that there is still a very real cost to instantiating and destroying objects. Particularly if you are creating methods in libraries that are going to be called often, it's worth the effort to make them efficient both in terms of speed and memory.

Creating Wrappers Using Structs

The trick to simulating "Extension Interfaces" while maximizing performance is to use structs in conjunction with generics.

For example, using the ITextSender interface that I defined above, I can create two structs that implement the behaviors for both StringBuilder and TextWriter.

public struct StringBuilderTextSender : ITextSender {     private readonly StringBuilder output;       public StringBuilderTextSender(StringBuilder output)     {         this.output = output;     }       public void Write(char c)     {         output.Append(c);     }       public void Write(string s)     {         output.Append(s);     } }     public struct TextWriterTextSender: ITextSender {     private readonly TextWriter output;       public TextWriterTextSender(TextWriter output)     {         this.output = output;     }       public void Write(char c)     {         output.Write(c);     }       public void Write(string s)     {         output.Write(s);     } }

Example: Using the "Extension Interface"

Let's imagine that I want to create a simple encoder that converts new lines within a string to a "<br />". (To be flexible, let's consider a new line to be "\r", "\n" or "\n\r".)

We'll start by creating the base method that will take a generic "T" of that implements the interface ITextSender.

public static void ConvertLineFeeds<T>(string source, T output)     where T : ITextSender {     char currChar;     char lastChar = '\0';       for (int i = 0; i < source.Length; i++)     {         currChar = source[i];           if (currChar == '\r' || (currChar == '\n' && lastChar != '\r'))             output.Write("<br />");         else if(currChar != '\n')             output.Write(currChar);           lastChar = currChar;     } }

It is extremely important that the base method take a generic as opposed to a parameter of type ITextSender! If our parameter was an interface, than our struct would be boxed into an object and it would have defeated the performance objective.

Now that we have the base method, we can then quickly create three overloads to support output to String, StringBuilder and TextWriter.

public static string ConvertLineFeeds(string source) {     StringBuilder output = new StringBuilder(source.Length);       ConvertLineFeeds(source, output);       return output.ToString(); }   public static void ConvertLineFeeds(string source, StringBuilder output) {     ConvertLineFeeds<StringBuilderTextSender>(source, new StringBuilderTextSender(output)); }   public static void ConvertLineFeeds(string source, TextWriter output) {     ConvertLineFeeds<TextWriterTextSender>(source, new TextWriterTextSender(output)); }

We can test our methods using a simple bit of test code. Each of the following variations will return "Line 1<br />Line 2".

public void Test() {     //     // Test as String     //       Console.WriteLine(ConvertLineFeeds("Line 1\r\nLine 2"));       //     // Test as StringBuilder     //       StringBuilder sb = new StringBuilder();     ConvertLineFeeds("Line 1\r\nLine 2", sb);     Console.WriteLine(sb.ToString());       //     // Test as TextWriter     //       ConvertLineFeeds("Line 1\r\nLine 2", Console.Out);     Console.WriteLine();   }

Performance

There are several benefits to this approach:

  • There is no object instantiation require to implement the interface.
  • Methods that implement generics are compiled for each combination of types passed when calling the method. This provides a unique opportunity for the method to be optimized for each type. Members of the type parameter could be potentially inlined in ways that may not be possible if you did not use generics.
  • structs can never be null and member calls are very efficient.

In .NET Framework 3.5 and earlier, there are a number of optimizations that do not apply to structs; however, we are told that we can expect improvements in the area in the next Framework release.

If you found this useful, drop me a line!

Happy Coding!

Comments

  • Anonymous
    July 21, 2008
    Isn't a StringWriter a TextWriter that writes to a StringBuilder? :-)

  • Anonymous
    July 21, 2008
    Hi, Tom.  Yes, you are absolutely correct.  However, my goal is to avoid object instantiations. In the actual scenario that I have, I have written a series of "encoders" for numerous scenarios and I want these encoders to support numerous combinations of input and output formats.  If the consumer passes me a StringBuilder, I would need to wrap it with a TextWriter (or force the caller to do it).  That may be acceptable if it's called occassionally, but I'm less fond of the idea if it's called thousands of times in a row. Using this technique, my encoders support very complex scenarios, but cause no object instantiations as a side effect.