Leaning on the compiler with intentional compilation errors in refactoring
Recently I had to make a major and subtle change in our codebase. We have a highly used object that does something (allows us to read structured data from a buffer). That object is created at least once per document load, and it is very widely used. It also has a very short lifetime, and it is passed around a lot between various methods. The problem we have is that we don’t want to pay the GC cycles for this object, since that take away from real performance.
So we want to make it a structure, but at the same time, we don’t want to pass it to methods, because it isn’t a small struct. The solution was to make sure that it is a struct that is always passed by reference. But I did mention that it is widely used, right? Just changing it to struct is going to have a high cost of copying it, and we want to make sure that we don’t switch one cost with another.
The problem is that there is no difference in the code between passing a class argument to a function and passing a struct argument. So the idea is that we’ll lean on the compiler. We’ll change the object name to by ObjStruct, and then start going over each usage scenario, fixing it in turn. At the same time, because of things like “var” automatic variables and the lack of ref returns in the current version of C#, we make sure that we change a method like this:
TableValueReader GetCurrentReaderFor(IIterator it);
void GetCurrentReaderFor(IIterator it, out TableValueReader result);
And that means that the callers of this method will break as well, and so on and so forth until we reach all usage locations. That isn’t a particularly fun activity, but it is pretty straightforward to do, and it allows you to make large scale changes easily and safely.
Note that this requires two very important features:
- Reasonable compilation timeframes – you probably wouldn’t use this approach in C++ if your build time was measured in multiple minutes
- An actual compiler – I don’t know how you do large scale refactoring in dynamic languages. Tests can help, but the feedback cycle is much faster and more straightforward when you deliberately let the compiler know what you want it to do.