15

I'm toying with the idea of making primitive .NET value types more type-safe and more "self-documenting" by wrapping them in custom structs. However, I'm wondering if it's actually ever worth the effort in real-world software.

(That "effort" can be seen below: Having to apply the same code pattern again and again. We're declaring structs and so cannot use inheritance to remove code repetition; and since the overloaded operators must be declared static, they have to be defined for each type separately.)

Take this (admittedly trivial) example:

struct Area
{
    public static implicit operator Area(double x) { return new Area(x); }
    public static implicit operator double(Area area) { return area.x; }

    private Area(double x) { this.x = x; }
    private readonly double x;
}

struct Length
{
    public static implicit operator Length(double x) { return new Length(x); }
    public static implicit operator double(Length length) { return length.x; }

    private Length(double x) { this.x = x; }
    private readonly double x;
}

Both Area and Length are basically a double, but augment it with a specific meaning. If you defined a method such as…

    Area CalculateAreaOfRectangleWith(Length width, Length height)

…it would not be possible to directly pass in an Area by accident. So far so good.

BUT: You can easily sidestep this apparently improved type safety simply by casting a Area to double, or by temporarily storing an Area in a double variable, and then passing that into the method where a Length is expected:

    Area a = 10.0;

    double aWithEvilPowers = a;
    … = CalculateAreaOfRectangleWith( (double)a, aWithEvilPowers );

Question: Does anyone here have experience with extensive use of such custom struct types in real-world / production software? If so:

  • Has the wrapping of primitive value types in custom structs ever directly resulted in less bugs, or in more maintainable code, or given any other major advantage(s)?

  • Or are the benefits of custom structs too small for them to be used in practice?


P.S.: About 5 years have passed since I asked this question. I'm posting some of my experiences that I've made since then as a separate answer.

Community
  • 1
  • 1
stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
  • 1
    **P.S.:** I'm aware that one might be able to get rid of one of the two described sidestepping techniques by defining the `Area -> double` cast operator `explicit` instead of `implicit`. – stakx - no longer contributing Aug 07 '11 at 01:05
  • I'm probably not thinking big enough, but have you found that this kind of error comes up very often for you? That is - once you have your data from the external source (perhaps untyped) and into your program, it's going to be in a structure of some kind where it's accessed by some name that makes sense like "object.length". You don't usually end up passing around raw numbers, but rather you pass around structures. I would think this kind of error rare, I can't think of it coming up for me. – Jamie Treworgy Aug 07 '11 at 01:07
  • I suspect this type of error doesn't happen very often to me, @jamietre, but I haven't got any hard statistics to support or refute that assumption. However, there are other potential benefits; easier-to-understand code might be one of them. I'm just not sure up-front if that justifies having to implement lots of very similar `struct` types... – stakx - no longer contributing Aug 07 '11 at 01:19
  • One catch is all the best-practice recommendations for structs: http://stackoverflow.com/questions/1502451/c-what-needs-to-be-overriden-in-a-struct-to-ensure-equality-operates-properly – TrueWill Aug 07 '11 at 01:35
  • This is unlikely to go well in C#. You need more powerful features, then you can do something like this: http://www.boost.org/doc/libs/release/libs/units/ – me22 Aug 26 '13 at 00:23

5 Answers5

11

I did this in a project a couple of years ago, with some mixed results. In my case, it helped a lot to keep track of different kinds of IDs, and to have compile-time errors when the wrong type of IDs were being used. And I can recall a number of occasions where it prevented actual bugs-in-the-making. So, that was the plus side. On the negative side, it was not very intuitive for other developers -- this kind of approach is not very common, and I think other developers got confused with all these new types springing up. Also, I seem to recall we had some problems with serialization, but I can't remember the details (sorry).

So if you are going to go this route, I would recommend a couple of things:

1) Make sure you talk with the other folks on your team first, explain what you're trying to accomplish and see if you can get "buy-in" from everyone. If people don't understand the value, you're going to be constantly fighting against the mentality of "what's the point of all this extra code"?

2) Consider generate your boilerplate code using a tool like T4. It will make the maintenance of the code much easier. In my case, we had about a dozen of these types and going the code-generation route made changes much easier and much less error prone.

That's my experience. Good luck!

John

JohnD
  • 14,327
  • 4
  • 40
  • 53
3

In the aprox. 5 years since I've asked this question, I have often toyed with defining struct value types, but rarely done it. Here's some reasons why:

  • It's not so much that their benefit is too small, but that the cost of defining a struct value type is too high. There's lots of boilerplate code involved:

    • Override Equals and implement IEquatable<T>, and override GetHashCode too;
    • Implement operators == and !=;
    • Possibly implement IComparable<T> and operators <, <=, > and >= if a type supports ordering;
    • Override ToString and implement conversion methods and type-cast operators feom/to other related types.

    This is simply a lot of repetitive work that is often not necessary when using the underlying primitive types directly.

  • Often, there is no reasonable default value (e.g. for value types such as zip codes, dates, times, etc.). Since one can't prohibit the default constructor, defining such types as struct is a bad idea and defining them as class means some runtime overhead (more work for the GC, more memory dereferencing, etc.)

  • I haven't actually found that wrapping a primitive type in a semantic struct really offers significant benefits with regard to type safety; IntelliSense / code completion and good choices of variable / parameter names can achieve much of the same benefits as specialized value types. On the other hand, as another answer suggests, custom value types can be unintuitive for some developers, so there's an additional cognitive overhead in using them.

There have been some instances where I ended up wrapping a primitive type in a struct, normally when there are certain operations defined on them (e.g. a DecimalDegrees and Radians type with methods to convert between these).

Defining equality and comparison methods, on the other hand, does not necessarily mean that I'd define a custom value type. I might instead use primitive .NET types and provide well-named implementations of IEqualityComparer<T> and IComparer<T> instead.

stakx - no longer contributing
  • 83,039
  • 20
  • 168
  • 268
3

This is a logical next step to Hungarian Notation, see an excellent article from Joel here http://www.joelonsoftware.com/articles/Wrong.html

We did something similar in one project/API where esp. other developers needed to use some of our interfaces and it had siginificantly less "false alarm support cases" - because it made really obvious what was allowed/needed... I would suspect this means measurably less bugs though we never did the statistics because the resulting apps were not ours...

Yahia
  • 69,653
  • 9
  • 115
  • 144
1

I don't often use structs. One thing you may consider is to make your type require a dimension. Consider this:

public enum length_dim { meters, inches }
public enum area_dim { square_meters, square_inches }
public class Area {
    public Area(double a,area_dim dim) { this.area=a; this.dim=dim; }
    public Area(Area a) { this.area = a.area; this.dim = a.dim; }
    public Area(Length l1, Length l2) 
    {
        Debug.Assert(l1.Dim == l2.Dim); 
        this.area = l1.Distance * l1.Distance;
        switch(l1.Dim) { 
            case length_dim.meters: this.dim = square_meters;break;
            case length_dim.inches: this.dim = square_inches; break;
        }
    }
    private double area;
    public double Area { get { return this.area; } }
    private area_dim dim;
    public area_dim Dim { get { return this.dim; } }   
}
public class Length {
    public Length(double dist,length_dim dim)
    { this.distance = dist; this.dim = dim; }
    private length_dim dim;
    public length_dim Dim { get { return this.dim; } }
    private double distance;
    public double Distance { get { return this.distance; } }
}

Notice that nothing can be created from a double alone. The dimension must be specified. My objects are immutable. Requiring dimension and verifying it would have prevented the Mars Express disaster.

codecore
  • 61
  • 2
  • 1
    **1)** Keeping track of units would certainly be a reason for creating a custom type. Another more generic way of doing this would be: `class Scalar where TUnit : IUnit { public Scalar(double value) { … } }`, plus a unit type, e.g. `class Meters : IUnit { … }`; then use it like `new Scalar(2.5)`. **2)** The reason I used `struct` instead of `class` is that it seems to be more proper. There's one disadvantage: one always gets a parameter-less c'tor. There's also advantages; e.g. VB.NET's `Is` operator (`object.ReferenceEquals`, which makes no real sense for values) won't work. – stakx - no longer contributing Aug 07 '11 at 09:07
0

I can't say, from my experience, if this is a good idea or not. It certainly has it's pros and cons. A pro is that you get an extra dose of type safety. Why accept a double for an angle when you can accept a type that has true angle semantics (degrees to/from radians, constrained to degree values of 0-360, etc). Con, not common so could be confusing to some developers.

See this link for a real example of a real commercial product that has several types like you describe:

http://geoframework.codeplex.com/

Types include Angle, Latitude, Longitude, Speed, Distance.

wageoghe
  • 27,390
  • 13
  • 88
  • 116