To aid in the development of my main application work, I often find myself writing lots of little C# apps to analyze diagnostic bread crumbs left behind by in-development code. Yeah, I said C# and not Ruby or Python or whatever. This is mainly because of the Linq facilities and what they offer for analyzing and projecting lists of data. Because these are C# apps and the bread crumbs typically take the form of files, I would constantly write code to this effect:
var file = @"C:\logs\diag.log";
if (File.Exists(file))
{
using (StreamReader reader = File.OpenText(file))
{
while (reader.Peek() > -1)
{
doSomething(reader.ReadLine());
}
}
}
This really grows tiresome when all you really want to do is act on the content of the file. Ruby has a nice shortcut for this using the foreach class method on File (or IO).
File.foreach("C:\\logs\\diag.log") { |line| doSomething(line) }
As it turns out, this sort of behavior is easy to add to C#:
public static void ForEachLine(string file, Action action )
{
if (File.Exists(file))
{
using (StreamReader reader = File.OpenText(file))
{
while (reader.Peek() > -1)
{
action(reader.ReadLine());
}
}
}
}
which opens the door to calls like
ForEachLine( @"C:\logs\diag.log", line => doSomething(line) );
Thats nice and everything, but I wanted to make an extension method out of this and put it on the File class (which would be the intuitive thing to do). The problem is that extension methods in C# as of 3.0 are only allowed to be instance methods. So I’m denied the File.ForEachLine() invocation. I don’t like the prospect of having to construct a FileInfo instance to call this since in order to do that I would have to write all the boilerplate code this call is intending to avoid in the first place.
There is, however, a lightweight object that is already being created in every one of these cases that actually winds up making sense as the target of this call.
the string.
If we stick our call from before in an extension class and change the first param to be the call receiver, we get this:
public static class Extensions
{
public static void ForEachLine(this string file, Action action )
{
if (File.Exists(file))
{
using (StreamReader reader = File.OpenText(file))
{
while (reader.Peek() > -1)
{
action(reader.ReadLine());
}
}
}
}
}
This might not look too different, but allows us the following syntax:
var file = @"C:\logs\diag.log";
file.ForEachLine( line => doSomething(line) );
or even
@"C:\logs\diag.log".ForEachLine( line => doSomething(line) );
This is succinct, intuitive, and makes those little C# apps feel a little more Rubyish. That might even be a good thing.
Yea, I get to be the first to comment in your blog!
This is cool and everything, but one thing that feels a little unsettling is extending “string” with something that treats the string as a file path (not generally applicable to all strings), yet does not convey that restriction in its name. In other words, ForEachLine applied to string would suggest that I could have a string with a bunch of newline-separated text, and the action would be taken on each of the new-line-separated substrings. That would actually be a nice and useful extension to string.
But the idea is awesome. The final syntax is much more declarative than imperative, which I love.
Thanks Steve. You’re right, I hadn’t considered operating on the lines of the string itself. Maybe a new name like ForEachFileLine() or something similar would be less misleading, but I don’t like it.
Maybe a better strategy would be a ToFileInfo() method on string which would allow us to relocate the ForEachLine() method to the FileInfoClass.
Then we’d be able to call
@”C:\logs\diag.log”.ToFileInfo().ForEachLine(line => doSomething(line));
I could live with that