Using conversion operators to create flexible types
February 20, 2012Follow along with me. Have you ever had a situation where you had to represent some conceptually complex or flexible value in a class? For instance, the the concept of “hours”? Maybe you would start off simple with something like:
public class Foo
{
public decimal Hours { get; set; }
}
//usage
foo.Hours = 1;
foo.Hours = 1.5m;
You were clever to note that hours may be used as whole numbers or fractions, so you made sure you could assign int and decimal values. But it turns out your data is more complex than that. You also must account for string representations of hours, such as “1:30” (one hour and thirty minutes).
Easy enough. It’s probably time to create a custom class or struct to encapsulate all that. If you’re not familiar with implicit or explicit conversion operators, you may end up with a simple class with overloaded constructors that accept int, decimal, or string, so that it could be used like this:
hours = new Hours(1);
hours = new Hours(1.5m);
hours = new Hours("1:30");
decimal hoursAsDecimal = hours.Value;
It works. But this is kinda lame. Wouldn’t it be cool if you could just do something like this:
hours = 1; hours = 1.5m; hours = "1:30"; decimal hoursAsDecimal = hours;
You can!
Understanding conversion operators
To quote MSDN: C# enables programmers to declare conversions on classes or structs so that classes or structs can be converted to and/or from other classes or structs, or basic types. Conversions are defined like operators and are named for the type to which they convert. Either the type of the argument to be converted, or the type of the result of the conversion, but not both, must be the containing type.
Here’s a basic Hours struct that has just one conversion operator:
public struct Hours
{
private readonly decimal _hours;
private Hours(decimal hours) { _hours = hours; }
//implicit decimal to Hours conversion operator
public static implicit operator Hours(decimal value)
{
return new Hours(value);
}
}
This implicit operator is specifying that types of decimal can be implicitly converted into types of Hours, and the method body is how the conversion is performed (in this case, decimal is the underlying type, so it just returns a new Hours instance).
To allow other values to be converted, just specify additional implicit operators:
//implicit int to Hours conversion operator
public static implicit operator Hours(int value)
{
return new Hours(value);
}
//implicit string to Hours conversion operator
public static implicit operator Hours(string value)
{
//Parse() is the private method of Hours responsible for
//converting the string to a decimal. It's omitted for brevity
return Parse(value);
}
Switching things around
Every operator above defines how a particular type can be converted into the custom type Hours, but what about the other way around? How can we make Hours directly assignable to a decimal type? All you have to do is flip it around.
//implicit Hours to decimal conversion operator
public static implicit operator decimal(Hours value)
{
return value._hours;
}
Again, this is defining that the type Hours can be implicitly converted into type decimal. The net result of this and the other operators defined above is that you can write super flexible and clean code:
hours = 1; hours = 1.5m; hours = "1:30"; decimal hoursAsDecimal = hours;
A quick note about explicit operators
You can also make operators explicit. They work the same way, but require an explicit cast to work.
//explicit decimal to Hours conversion operator
public static explicit operator Hours(decimal value)
{
return new Hours(value);
}
//usage
hours = (Hours) 1.5m; //explicit cast from decimal to Hours
hours = 1.5m; //compile error, "cannot convert decimal to Hours"
Because of this, implicit operators are generally preferred, but if you want callers of your code to be explicitly aware that they must cast properly you can do so.
Next time you find yourself creating a custom chameleon-like type that must be assignable to or from several different types, consider encapsulating all of the conversion code by putting it into the type and then creating implicit or explicit conversion operators to make the calling code clean, easy and flexible.
Tags: conversion operators, implicit, explicit
Categories: C#
