TextWriter is an abstract class that has heavily-overloaded Write() and WriteLine() methods for all of the more basic types such as Int32, Boolean, and String. The idea is that it takes these values, converts them to character representations (given a specified Encoding), and then outputs the results (usually to a StringBuilder or a Stream). It’s equivalent to Java’s PrintWriter. However, unlike PrintWriter, there’s no underlying output source / OutputStream: you’re free to do whatever you like with the resulting characters.
The stupidity lies in the interface and it’s accompanying documentation. TextWriter is an abstract class, but the only abstract member is the Encoding property’s get accessor; you get to decide how and when you want the Encoding to be specified. All of the Write() and WriteLine() methods, however, are not abstract: they’re simply overrideable. That’s not necessarily bad, but there’s little to no documentation on what any of these 36 methods actually do (specifically, whether they call any of the other Write() methods and thus don’t need to be overridden in any subclass). For example:
- Write(char[]):
- “This method does not search the specified String for individual newline characters (hexadecimal 0x000a) and replace them with NewLine.” (Gee thanks. Does it also not cure cancer?)
- “This default method calls Write and passes the entire character array.” (If it’s passing the entire character array to Write(), doesn’t that mean it’s recursive? I can guess that they mean it passes each member of the array to Write(char), but that’s not what the documentation actually says.)
- Write(Decimal): “dd” (And that’s all it says.)
- Write(Double): “The text representation of the specified value is produced by calling ToString.” (But once you do that conversion from Double to String, where does the String go? Is it used anywhere or simply dropped?)
- Write(String): “This version of Write is equivalent to Write .” (Yes, the URLs are identical. I’m glad to see they understand the Reflexive Property.)
- WriteLine(char) states that it (effectively) calls Write(char) and then WriteLine(), which is exactly what it should be doing. Unfortunately, none of the other WriteLine() overloads state this; all they say is that they write the converted characters plus the newline (not necessarily calling the appropriate Write() methods). Thus you should be overriding these methods too.
- Write(char): “This default method does nothing, but derived classes can override the method to provide the appropriate functionality.” (If it does nothing, then why not make it an abstract method and communicate the intent explicitly?)
The documentation for the TextWriter class itself is no help either:
A derived class must minimally implement the Write method to make a useful instance of TextWriter.
(The link points to the list of all the Write() overloads, not one specific method.)
All Microsoft needed to do was:
- Make Write(char) abstract, to show that derived classes need to do something with a character
- Document that Write(char[]) calls Write(char) for every element in the array
- Document that Write(String) calls Write(char[]) after converting the String to a char[]
- Document that all of the other overloads call Write(String) after calling .ToString() on the target
- Document that all of the WriteLine() methods call their equivalent Write() method and then call WriteLine() to write the newline character.
- Leave all the Write() and WriteLine() methods as overrideable, to allow for changes in behavior / optimizations.
Then, a TextWriter implementation would only have to override one method (ie: Write(char)) and reliably get all of the conversion behavior for free. That’s almost exactly what PrintWriter already does (except that it defers to an aggregated Writer/OutputStream to write its characters rather than deferring to its own abstract method). Instead, we get this mess.
Bonus Points: Write(Object) simply does a .ToString() on the given object, which is reasonable. Most of the other overloads (for Boolean, Double, Int32, Int64, Single, UInt32, UInt64, and probably Decimal) state that they do exactly the same thing… thus making them unnecessary. It’s clear that the .NET API designers were trying to emulate Java’s PrintWriter, which is forced to create the equivalent overloads due to the separation between primitive and Object types. Of course, if you’re implementing your own TextWriter, you still have to override all of the superfluous methods (and don’t forget the WriteLine()s!).
Extra credit: There’s Write(String, Object) to format the Object argument given the formatting String argument (which is a very common way to write out dates and numbers in varying ways). There’s also a Write(String, Object, Object) method that does the exact same thing but with two Object arguments, and then a Write(String, Object, Object, Object) that does the same thing but with three Object arguments. There’s no method that takes four arguments though; I guess they considered that excessive.