I think that this question is a specific case of a more general question about creating type safety across a C# application. My example here is with 2 types of data: Prices and Weights. They have different units of measure so one should never try an assign a price to a weight or vice versa. Both under the covers are really decimal values. (I am ignoring the fact that there could be conversions like pounds to kg etc.) This same idea could be applied to strings with specific types like EmailAddress and UserLastName.
With some fairly boiler plate code one can make either explicit conversion or implicit conversions back and forth between the specific types: Price and Weight, and the underlying type Decimal.
public class Weight
{
private readonly Decimal _value;
public Weight(Decimal value)
{
_value = value;
}
public static explicit operator Weight(Decimal value)
{
return new Weight(value);
}
public static explicit operator Decimal(Weight value)
{
return value._value;
}
};
public class Price {
private readonly Decimal _value;
public Price(Decimal value) {
_value = value;
}
public static explicit operator Price(Decimal value) {
return new Price(value);
}
public static explicit operator Decimal(Price value)
{
return value._value;
}
};
With the "explicit" operator overrides, one gets a more restrictive set of things one can do with these classes. You have to be manually case every time you change from one type to another. For example:
public void NeedsPrice(Price aPrice)
{
}
public void NeedsWeight(Weight aWeight)
{
}
public void NeedsDecimal(Decimal aDecimal)
{
}
public void ExplicitTest()
{
Price aPrice = (Price)1.23m;
Decimal aDecimal = 3.4m;
Weight aWeight = (Weight)132.0m;
// ok
aPrice = (Price)aDecimal;
aDecimal = (Decimal)aPrice;
// Errors need explicit case
aPrice = aDecimal;
aDecimal = aPrice;
//ok
aWeight = (Weight)aDecimal;
aDecimal = (Decimal) aWeight;
// Errors need explicit cast
aWeight = aDecimal;
aDecimal = aWeight;
// Errors (no such conversion exists)
aPrice = (Price)aWeight;
aWeight = (Weight)aPrice;
// Ok, but why would you ever do this.
aPrice = (Price)(Decimal)aWeight;
aWeight = (Weight)(Decimal)aPrice;
NeedsPrice(aPrice); //ok
NeedsDecimal(aPrice); //error
NeedsWeight(aPrice); //error
NeedsPrice(aDecimal); //error
NeedsDecimal(aDecimal); //ok
NeedsWeight(aDecimal); //error
NeedsPrice(aWeight); //error
NeedsDecimal(aWeight); //error
NeedsWeight(aWeight); //ok
}
Just changing the "explicit" operators to "implicit" operators by replacing the words "explicit" with "implicit" in the code, one can convert back and forth to the underlying Decimal class without any extra work. This makes Price and Weight behave more like a Decimal, but you still cannot change a Price to a Weight. This is usually the level of type safety I am looking for.
public void ImplicitTest()
{
Price aPrice = 1.23m;
Decimal aDecimal = 3.4m;
Weight aWeight = 132.0m;
// ok implicit cast
aPrice = aDecimal;
aDecimal = aPrice;
// ok implicit cast
aWeight = aDecimal;
aDecimal = aWeight;
// Errors
aPrice = aWeight;
aWeight = aPrice;
NeedsPrice(aPrice); //ok
NeedsDecimal(aPrice); //ok
NeedsWeight(aPrice); //error
NeedsPrice(aDecimal); //ok
NeedsDecimal(aDecimal); //ok
NeedsWeight(aDecimal); //ok
NeedsPrice(aWeight); //error
NeedsDecimal(aWeight); //ok
NeedsWeight(aWeight); //ok
}
When doing this for String instead of Decimal. I like the idea Thorarin's answer about checking for null and passing null back in the conversion. e.g.
public static implicit operator EMailAddress(string address)
{
// Make
// EmailAddress myvar=null
// and
// string aNullString = null;
// EmailAddress myvar = aNullString;
// give the same result.
if (address == null)
return null;
return new EMailAddress(address);
}
To get these classes working as keys to Dictionary collections, you will also need to implement Equals, GetHashCode, operator ==, and operator !=
To make all of this easier, I made a class ValueType which I can extend, The ValueType class calls the base type for everything but the conversion operators.
type MyType = type string;which allows this directly. I miss this in C#. I understand the question, and that creating a new class in such cases could be avoided.