Creating an immutable value object in C# - Part III - Using a struct
Luca -
☕ 3 min. read
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:
- Convenience issues - it is not as convenient as it looks
- You still have to implement ‘==’ and ‘!=’.
- You still want to implement Equals() and GetHashCode(), if you want to avoid boxing/unboxing.
- You still have to implement ‘==’ and ‘!=’.
- Performance issues - it is not as fast as it looks
- 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
-
Usability issues - it is not as useful as it looks.
- Structs always have a public default constructor that ‘zeros’ all the fields
- Structs cannot be abstract
- Structs cannot extend another structs
- Structs cannot be abstract
- Structs always have a public default constructor that ‘zeros’ all the fields
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).
<pre class="code"><span style="color:rgb(0,0,255);">using</span> 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 }
[TimeLineAsStruct.zip](https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Components.PostAttachments/00/06/85/58/26/TimeLineAsStruct.zip)
0 Webmentions
These are webmentions via the IndieWeb and webmention.io.
11 Comments
Comments
Luca Bolognese's WebLog : Crea
2007-12-24T17:43:01ZPingBack from http://blogs.msdn.com/lucabol/archive/2007/12/06/creating-an-immutable-value-object-in-c-part-ii-making-the-class-better.aspx
Luca Bolognese's WebLog
2007-12-28T18:45:34ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the
Noticias externas
2007-12-28T19:06:21ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct In the
Charlie Calvert's Community Bl
2008-01-02T16:13:59ZWelcome to the thirty-eighth Community Convergence. These posts are designed to keep you in touch with
Bob
2008-01-09T11:45:58ZCombine 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());
Luca Bolognese's WebLog
2008-01-11T13:36:13ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part
Noticias externas
2008-01-11T13:52:09ZOther posts: Part I - Using a class Part II - Making the class better Part III - Using a struct Part
Tales from the Evil Empire
2008-01-16T18:36:54ZFor some reason, there's been a lot of buzz lately around immutability in C#. If you're interested in
akhayre2000@yahoo.co.ukl
2008-01-18T08:01:36Zi have get thart to t you that
{
}
{
would you thjat
adamjcooper.com/blog
2008-06-03T15:38:04ZThe Quest for Quick-and-Easy Class-Based Immutable Value Objects in C# - Part 1: Introduction
adamjcooper.com/blog
2008-06-03T16:57:49ZThe Quest for Quick-and-Easy Immutable Value Objects in C#