views:

55

answers:

2

I have a namespace of structs which represent various units of measure (Meters, Feet, Inches, etc.) ... anout 12 in total, generated courtesy of T4 templates :) .

Each struct carries implicit casting operators to support casting the value to any other measurement value-type, so the following sytax is legal:

var oneThousandMeters = new Meters(1000);    
Kilometers aKilo = oneThousandMeters ;     // implicit cast OK. Value = 1 Km

To add to the joy, there's a catch-all class called Distance which can hold any unit of measure, and can also be implicitly cast to and from and measurement value...

var magnum = new Distance(12, DistanceUnits.Inches); 
Feet wifesDelight = magnum;               // implicit cast OK. Value = 1 foot.

Following the .NET framework standard, all string formatting and parsing is handled by external a FormatProvider, which implements ICustomFormatter. Sadly, this means that the value is boxed when it is passed to the Format method, and the format method needs to test the object against every known measurement type before it can act upon it. Internally, the Format method just casts the measurement to a Distance value anyway, so here comes the question....

Question:

public string Format(string format, object arg, IFormatProvider formatProvider)
{
    Distance distance;           

    // The following line is desired, but fails if arg != typeof(Distance)   
    distance = (Distance)arg;    

    // But the following tedious code works:
    if(arg is Distance)
       distance = (Distance)arg;
    else if(arg is Meters)
       distance = (Distance)(Meters)arg;     // OK. compile uses implicit cast. 
    else if(arg is Feet)
       distance = (Distance)(Feet)arg;       // OK. compile uses implicit cast. 
    else if(arg is Inches)
       distance = (Distance)(Inches)arg;     // OK. compile uses implicit cast. 
    else
        ... // tear you hair out for all 12 measurement types
}

Are there any solutions for this, or is this just one of those unsolvable drawbacks of value types?

PS: I've checked this post, and though the question is similar, it's not what I'm looking for.

+1  A: 

Yeah it's just one of those things you have to live with.

You run into the same thing if you shove an integer into an object:

int a = 0;
object b = a;
int c = (int)b; // this works
short d = (short)b; // this fails
short e = (short)(int)b; // this works
Dismissile
+3  A: 

Well, it's a matter of separating the unboxing conversion from the user-defined conversion. You want both to occur - and you have to specify the type to unbox, as well as letting the compiler know when you want a user-defined conversion. The user-defined conversion has to be picked at compile time unless you're using dynamic typing, so the compiler needs to know which type it's trying to convert from.

One option is to have an IDistance interface which all the structs implement. Then you could just use:

IDistance distanceArg = arg as IDistance;
if (distanceArg != null)
{
    Distance distance = distanceArg.ToDistance();
}

As you've got a boxed value already, using an interface won't cause extra boxing or anything like that. Each ToDistance implementation can probably just use the implicit conversion:

public Distance ToDistance()
{
    return this;
}

... or you could make the conversion use ToDistance.

Jon Skeet
Jon to the rescue again... thanks a ton for providing yet another brilliant answer here on SO! Much appreciated.
Mark
@Jon - as a side-bar - would you consider it bad form to generate these various value types in the first place, given that it's possible to encapsulate all their functionalities in the Distance struct?
Mark
@Mark: Unclear... I'm facing a similar dilemma in NodaTime at the moment. There are lots of different ways of representing units, and I don't know enough about them to judge, I'm afraid. I recommend that you look at what F# does though, for possible inspiration.
Jon Skeet
@Jon - I've got a library now which has value types to support distances and volumes (with weights on their way)... it's completely localizable, offers FormatProviders and TypeConverters, and (thans to templates) allows new measurements to be quickly added. I'd like to get it into open-source... would you care to collaborate?
Mark
@Mark: Whilst I'm sure it would be fun, I'm afraid it would be irresponsible of me to take anything else on at the moment. I really should concentrate on Noda Time when I get any spare cycles...
Jon Skeet