Update October 10: v 1.1: Added performance benchmark and support for other types
Introduction
Instead of writing:
int distance = 10;
int period = 1;
double temperature = 37.1;
String email = "john.smith@company.com";
Wouldn’t it be better if we could write:
Integer<meter> distance = 10;
Integer<Milliseconds> period = 1;
Double<Celsius> temperature = 37.1;
String<Email> = "john.smith@company.com";
In this way, we can specify the meaning ("semantics") and the unit of measurements. The compiler can also prevent us to write stupid code like this:
distance = period
This is exactly what this article presents a solution for that is
- Easy to use
- Memory efficient (using value-typed
struct
s) - Versatile, since it can be used just as ordinary
int
and double
fields in WPF binding and serialization - Extendable, since customized validation also can be added
Background
Several solutions for semantic types have been presented here at Code Project this year. In January, Matt Perdeck presented the idea and motivation for semantic types in the excellent article, Introducing Semantic Types in .NET. More recently, Marc Clifton also presented his solutions for this in the article Strong Type-checking with Semantic Types. In this article, I will shortly present an alternative solution making use of value-types struct
s to save memory and time. This solution also supports custom validation rules and importantly, in-built support for value-conversion to support direct binding in WPF.
For introduction to why semantic types is a good idea, please read the referenced articles above.
Using the Code
To create a new semantic type, you first must declare it by creating a class for it:
class Celsius : SemanticType { }
Then you can start to use in one of the generic semantic value-types, just like any other type, with the difference that you have in-build documentation of the unit of measurement:
class Example {
public Double<Celsius> CurrentTemperature { get; set; }
public void IncreaseTemperatureBy(Double<Celsius> increment) {
CurrentTemperature += increment;
}
public Integer<Celsius>? TargetTemperature { get; set; }
}
Validation
If you want, you can customize your semantic type even more with validation. If you want to ensure that the values are within range, you can override the IsValid
function:
public class Celsius : SemanticType
{
public const double MinValue = -273.15;
public override bool IsValid(ref double doubleValue) {
return doubleValue >= MinValue;
}
public override bool IsValid(ref int value) {
return value >= MinValue;
}
public override string GetInvalidMessage(object invalidValue) {
return "Temperature in Celsius must always be higher than or equal to -273.15 degrees";
}
public static String HelpText {
get { return "Temperature in degress Kelvin."; }
}
}
If you want to use the same semantic type for different base types, e.g., doubles and integers, you must override the corresponding IsValid
function. The value is sent is a ref
-parameter to the IsValid
function to allow this function to adjust the value.
As you see, you can also provide a customized error message. This is used as the message returned in exceptions and can be accessed in code. Other members can be added to be accessed from the Semantic
property of the Double<T>
or Integer<T>
which returns an instance of the SemanticType
.
Data-Binding in WPF
To support direct use as binding sources in WPF binding sources, the semantic types have custom type converters. This means that you can use properties of Double<TSemantic>
just like double
in bindings. Additionally, you can bind to the other properties of the SemanticType
through the Semantic
property. Perhaps, you want to provide Maximum
and Minimum
values to be used in a slider control? Or a default value?
Performance
Obviously, semantic types cannot be as performant as ordinary built-in primary types, but remember that support for validation is included and you are fault-proofing you code. I included a simple benchmark test creating and manipulating array items. On my computer, a typical result was the following in Release mode without debugging:
Result for 1000000 iterations:
Double: 4 ms
Semantic: 15 ms
ByRef: 166 ms
As you see, the raw manipulating of semantic type (Double<T>
) takes about 4-5 times more time than an ordinary double
in this particular case where you do not much else. This must, of course, be considered if you have heavy number-crunching applications. However, in ordinary application where you do a lot other stuff, this should be insignificant. Also remember that the memory consumption of the data does not increase a single bit by using the semantic alternative.
For comparison, I also included a test with a simple "semantic" ByRef
type, i.e., a class that contains a value. As you see, initiating and manipulating data in such way is much slower and of course consumes a lot more memory space.
Support for Other Data Types
I provided support for Integer
, Double
and String
here which should be sufficient for many ordinary applications. To support other data types, such as long
, there is also a generic Data<TData,TSemantic>
type included. To use this, you must simply implement a TSemantic
class that implements the ISemanticType<TData>
interface. See examples in the provided code.
Points of Interest
Low Memory Consumption Using Value-Types Structs
All semantic types are value-type struct
s with a single instance variable storing the value. This means that they do not consume more bytes in memory than the encapsulated base type. Compared to solutions which use class-types, this reduces memory consumption and pressure on the garbage collector. The semantic value-types are immutable in the same sense as ordinary primitive types (passed by value).
Custom Type Converters for Generic Types
To support direct data binding, I had to provide custom TypeConverter
for the semantic types. For generic types, this was a bit challenging, but after reading a valuable StackOverflow post answer, it was easy to create a generic solution that can be reused for any scenario where you have a generic type that needs to have corresponding generic type converter (with the same type parameters). To use it, you just have to decorate the type with attributes, first the ordinary TypeConverter
attribute specifying the GenericTypeConverter
type as the converter type and then an additional attribute to specify the actual generic type converter type to use. When WPF needs a type converter for the type, a new instance of the GenericTypeConverter
will be created. In its constructor, a new instance of the generic typed converter is created. All requests are then forwarded to the generic type converter.
Conclusions
Now we have another tiny solution for semantic typing in .NET. What do you think? Is it useful? Maybe the .NET platform could include support generic versions of all primitive types similar to this? This could possibly be implemented even more efficiently by throwing away the semantic checking during Just in time compilation.
History
- October 8th, 2015 - First version published
- October 10th, 2015 - Added performance benchmark and support for other types. (v.1.1)
- October 13th, 2015 - Minor corrections to article text (no change in code)