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

-

Other posts:

Well, to the un­trained eye they do. Structs can­not be null and they im­ple­ment Equals and GetHashCode by check­ing the state of the ob­ject, not its pointer in mem­ory.

So, have we found the per­fect tool to im­ple­ment our value ob­ject?

Unfortunately, no. Here is why a struct is a less than op­ti­mal way to im­ple­ment a value ob­ject:

  1. Convenience issues - it is not as convenient as it looks
    1. You still have to implement ‘==’ and ‘!=’.
      • 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
    • Usability is­sues - it is not as use­ful as it looks.

      1. Structs always have a public default constructor that ‘zeros’ all the fields
        • Structs cannot be abstract
          • 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).

<pre class="code"><span style="color:rgb(0,0,255);">using</span> System;

us­ing System.Collections.Generic; us­ing System.Linq; us­ing System.Text; pub­lic struct DateSpan { pub­lic sta­tic DateSpan NoValueDateSpan { get { re­turn no­Val­ue­DateS­pan; } } pub­lic DateSpan(DateTime pstart, DateTime pend) { if (pend < pstart) throw new ArgumentException(pstart.ToString() + ″ does­n’t come be­fore + pend.ToString()); start = pstart; end = pend; has­Value = true; } pub­lic DateSpan Union(DateSpan other) { if (!HasValue) re­turn other; if (!other.HasValue) re­turn this; if (IsOutside(other)) re­turn DateSpan.NoValueDateSpan; DateTime new­Start = other.Start < Start ? other.Start : Start; DateTime newEnd = other.End > End ? other.End : End; re­turn new DateSpan(newStart, newEnd); } pub­lic DateSpan Intersect(DateSpan other) { if (!HasValue) re­turn DateSpan.NoValueDateSpan; if (!other.HasValue) re­turn DateSpan.NoValueDateSpan; if (IsOutside(other)) re­turn DateSpan.NoValueDateSpan; DateTime new­Start = other.Start > Start ? other.Start : Start; DateTime newEnd = other.End < End ? other.End : End; re­turn new DateSpan(newStart, newEnd); } pub­lic DateTime Start { get { re­turn start; } } pub­lic DateTime End { get { re­turn end; } } pub­lic bool HasValue { get { re­turn has­Value; } } // Making field ex­plicitely read­only (but can­not use au­to­prop­er­ties) // BTW: If you want to use au­to­prop­er­ties, given that it is a struct, // you need to add :this() to the con­struc­tor pri­vate read­only DateTime start; pri­vate read­only DateTime end; pri­vate read­only bool has­Value; pri­vate bool IsOutside(DateSpan other) { re­turn other.start > end || other.end < start; } // Changing the in­ter­nal ma­chin­ery so that has­Value de­fault is false // This way the au­to­mat­i­cally gen­er­ated empty con­struc­tor re­turns the right thing pri­vate sta­tic DateSpan no­Val­ue­DateS­pan = new DateSpan(); #region Boilerplate Equals, ToString Implementation pub­lic over­ride string ToString() { re­turn string.Format(Start:{0} End:{1}”, start, end); } pub­lic sta­tic Boolean op­er­a­tor ==(DateSpan v1, DateSpan v2) { re­turn (v1.Equals(v2)); } pub­lic sta­tic Boolean op­er­a­tor !=(DateSpan v1, DateSpan v2) { re­turn !(v1 == v2); } //public over­ride bool Equals(object obj) { // if (this.GetType() != obj.Get­Type()) re­turn false; // DateSpan other = (DateSpan) obj; // re­turn other.end == end && other.start == start; //} //public over­ride int GetHashCode() { // re­turn start.GetH­ash­Code() | end.GetH­ash­Code(); //} #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)

Tags

11 Comments

Comments

Luca Bolognese's WebLog

2007-12-28T18:45:34Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct In the

Noticias externas

2007-12-28T19:06:21Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct In the

Charlie Calvert's Community Bl

2008-01-02T16:13:59Z

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

Combine the union and in­ter­sect into a sin­gle pri­vate func­tion.  Add a 1 line wrap­per func­tion for the ex­ist­ing union and in­ter­sect func­tions which calls the com­bined func­tion.
This is to en­sure that the check­ing of pa­ra­me­ter ar­gu­ments and HasValue are done the same for both union and in­ter­sect (i..e, only one set of code to main­tain.)
Change ex­cep­tion mes­sage so that the mes­sage iden­ti­fies the ob­ject datatype (DateSpan) that is in­valid (simplifies sup­port calls and en­hances main­tain­abil­ity)
Change
pstart.ToString() + does­n’t come be­fore + pend.ToString());
to
DateSpan in­valid: + pstart.ToString() + does­n’t come be­fore + pend.ToString());

Luca Bolognese's WebLog

2008-01-11T13:36:13Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct Part

Noticias externas

2008-01-11T13:52:09Z

Other posts: Part I - Using a class Part II - Making the class bet­ter Part III - Using a struct Part

Tales from the Evil Empire

2008-01-16T18:36:54Z

For some rea­son, there’s been a lot of buzz lately around im­mutabil­ity in C#. If you’re in­ter­ested in

akhayre2000@yahoo.co.ukl

2008-01-18T08:01:36Z

i have get thart to t you that
{
}
{
would you th­jat

adamjcooper.com/blog

2008-06-03T15:38:04Z

The Quest for Quick-and-Easy Class-Based Immutable Value Objects in C# - Part 1: Introduction

adamjcooper.com/blog

2008-06-03T16:57:49Z

The Quest for Quick-and-Easy Immutable Value Objects in C#