Just this morning I wrote about choosing a class over a struct to take advantage of inheritance and abstractness. It turns out, I was wrong.
Well, not that wrong. It's just that this particular design doesn't actually derive that many benefits from inheritance.
First, there will still be a lot of redundant code. The IEquatable<T> and IComparable<T> interfaces both require implementation in concrete classes, so all of the measurement classes would have nearly identical CompareTo and Equals methods. Also, all of the arithmetic operators come in three flavors:
public static bool operator ==(double a, ConcreteClass b)
public static bool operator ==(ConcreteClass a, double b)
public static bool operator ==(ConcreteClass a, ConcreteClass b)
...so all of those need to be in each of the concrete implementations (even if they call the same code in the base class).
And, of course, the Explicit and Implicit operators have to be customized for each class.
Redundancy. I hate it. But it's necessary for this kind of design.
Second, as it turns out, the implementation differs significantly enough between the measurement classes in one key respect that they really can't use a common base class. The problem is, some measurements are actually aggregations of other measurements.
Area, for example, is a length times a length. Volume is either three lengths or a length times an area. Don't even get me started on how to represent velocity.
In my first implementation of representing measurements (still visible as of this writing), I finessed the problem of multi-dimensional values by creating multi-dimensional units, like MeterSquare and PoundSquareInch. That created a real headache in the implementation of conversions, because while converting meters to feet is a simple mulitplication, converting liters to cubic inches is only linear if you proliferate your inch class into square and cubic dimensions.
In other words, despite my goal of having a simple conversion mechanism, I had to write specific conversions for multidimensional measurements anyway, which I managed by putting the knowledge of how to convert units into the units themselves rather than in the measurements where (I think) they belong.
The only way to do that and retain some extensibility was to require each unit to know how to convert itself to a base unit: square meters for area, for example. This, in turn, introduced rounding errors for broad conversions within measurement systems that prevent round-trip conversions from working. In other words, if you convert a very small value to a very big value by using an intermediary, you can't convert the big value back to the same small value. For example, there are exactly 4,014,489,600 square inches in a square mile, but if you convert to square meters first you get 4,014,489,340.464 instead. That's just embarrassing.
In sum, despite what I wrote this morning, I'm throwing out the work that resulted from it and going back to structs for my measurement classes.