Welcome to MSDN Blogs Sign in | Join | Help

Creating an immutable value object in C# - Part III - Using a struct

Other posts:

In Part II I talked about the asymmetry created by using 'null' as the special value for our little DateSpan domain. We also noticed the boredom of having to implement Equals, GetHashCode, '==' and '!=' for our value objects. Let's see if structs solve our problem.

Well, to the untrained eye they do. Structs cannot be null and they implement Equals and GetHashCode by checking the state of the object, not its pointer in memory.

So, have we found the perfect tool to implement our value object?

Unfortunately, no. Here is why a struct is a less than optimal way to implement a value object:

  1. Convenience issues - it is not as convenient as it looks
    1. You still have to implement '==' and '!='.
    2. You still want to implement Equals() and GetHashCode(), if you want to avoid boxing/unboxing.
  2. Performance issues - it is not as fast as it looks
    1. Structs are allocated on the stack. Every time you pass them as arguments, the state is copied. If your struct has more than a few fields, performance might suffer
  3. Usability issues - it is not as useful as it looks.
    1. Structs always have a public default constructor that 'zeros' all the fields
    2. Structs cannot be abstract
    3. Structs cannot extend another structs

Don't get me wrong, structs are extremely useful as a way to represent small bundles of data. But if you use value objects extensively, their limitations start to show.

A case could be made that you should use struct to implement value objects if the issues exposed above don't apply to your case. When they do apply, you should use classes. I'm a forgetful and lazy programmer, I don't want to remember all these cases. I just want a pattern that I can use whenever I need a value object. It seems to me that structs don't fit the bill.

For the sake of completeness, here is the code for DateSpan using a struct. Note that I explicitly introduced a 'special value' instead of using null (which is not available for structs).

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


public struct DateSpan {

    public static DateSpan NoValueDateSpan { get { return noValueDateSpan; } }

    public DateSpan(DateTime pstart, DateTime pend) {

        if (pend < pstart)
            throw new ArgumentException(pstart.ToString() + " doesn't come before " + pend.ToString());
        start = pstart;
        end = pend;
        hasValue = true;
    }

    public DateSpan Union(DateSpan other) {

        if (!HasValue)
            return other;

        if (!other.HasValue)
            return this;

        if (IsOutside(other))
            return DateSpan.NoValueDateSpan;

        DateTime newStart = other.Start < Start ? other.Start : Start;
        DateTime newEnd = other.End > End ? other.End : End;

        return new DateSpan(newStart, newEnd);
    }

    public DateSpan Intersect(DateSpan other) {

        if (!HasValue)
            return DateSpan.NoValueDateSpan;

        if (!other.HasValue)
            return DateSpan.NoValueDateSpan;

        if (IsOutside(other))
            return DateSpan.NoValueDateSpan;

        DateTime newStart = other.Start > Start ? other.Start : Start;
        DateTime newEnd = other.End < End ? other.End : End;

        return new DateSpan(newStart, newEnd);
    }

    public DateTime Start { get { return start; } }
    public DateTime End { get { return end; } }
    public bool HasValue { get { return hasValue; } }

    // Making field explicitely readonly (but cannot use autoproperties)
    // BTW: If you want to use autoproperties, given that it is a struct,
    // you need to add :this() to the constructor
    private readonly DateTime start;
    private readonly DateTime end;
    private readonly bool hasValue;

    private bool IsOutside(DateSpan other) {

        return other.start > end || other.end < start;
    }

    // Changing the internal machinery so that hasValue default is false
    // This way the automatically generated empty constructor returns the right thing
    private static DateSpan noValueDateSpan = new DateSpan();

    #region Boilerplate Equals, ToString Implementation

    public override string ToString() {
        return string.Format("Start:{0} End:{1}", start, end);
    }

    public static Boolean operator ==(DateSpan v1, DateSpan v2) {

        return (v1.Equals(v2));
    }
    public static Boolean operator !=(DateSpan v1, DateSpan v2) {

        return !(v1 == v2);
    }

    //public override bool Equals(object obj) {

    //    if (this.GetType() != obj.GetType()) return false;

    //    DateSpan other = (DateSpan) obj;

    //    return other.end == end && other.start == start;
    //}

    //public override int GetHashCode() {

    //    return start.GetHashCode() | end.GetHashCode();
    //}

    #endregion
}
Published Monday, December 24, 2007 2:39 PM by lucabol
Filed under:

Attachment(s): TimeLineAsStruct.zip

Comments

# Luca Bolognese's WebLog : Creating an immutable value object in C# - Part II - Making the class better

# Creating an immutable value object in C# - Part IV - A class with a special value

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the

Friday, December 28, 2007 6:45 PM by Luca Bolognese's WebLog

# Creating an immutable value object in C# - Part IV - A class with a special value

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the

Friday, December 28, 2007 7:06 PM by Noticias externas

# Community Convergence XXXVIII

Welcome to the thirty-eighth Community Convergence. These posts are designed to keep you in touch with

Wednesday, January 02, 2008 4:13 PM by Charlie Calvert's Community Blog

# make static

Combine the union and intersect into a single private function.  Add a 1 line wrapper function for the existing union and intersect functions which calls the combined function.

This is to ensure that the checking of parameter arguments and HasValue are done the same for both union and intersect (i..e, only one set of code to maintain.)

Change exception message so that the message identifies the object datatype (DateSpan) that is invalid (simplifies support calls and enhances maintainability)

Change

pstart.ToString() + " doesn't come before " + pend.ToString());

to

"DateSpan invalid: " + pstart.ToString() + " doesn't come before " + pend.ToString());

Wednesday, January 09, 2008 11:45 AM by Bob

# Creating an immutable value object in C# - Part V - Using a library

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part

Friday, January 11, 2008 1:36 PM by Luca Bolognese's WebLog

# Creating an immutable value object in C# - Part V - Using a library

Other posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part

Friday, January 11, 2008 1:52 PM by Noticias externas

# Immutability in C#

For some reason, there's been a lot of buzz lately around immutability in C#. If you're interested in

Wednesday, January 16, 2008 6:36 PM by Tales from the Evil Empire

# re: Creating an immutable value object in C# - Part III - Using a struct

i have get thart to t you that

{

}

{

would you thjat

Friday, January 18, 2008 8:01 AM by akhayre2000@yahoo.co.ukl
New Comments to this post are disabled
 
Page view tracker