The Untouchables – Creating Immutable objects with VB

There are many articles available on the Internet about immutable objects in .Net. Unfortunately almost all of them are addressed toward C# developers. In this article I’m going to rectify that by providing some information on the subject aimed toward the VB developer.


Immutable objects are a very simple yet powerful concept within programming. An object is immutable if its state cannot change after it has been created, which means that all its members need to be read-only. The integer number 1, for example, is an immutable number. There is nothing you can do that changes it. You can’t make it into an even number, you can’t bribe it, and you can’t make it jump of joy. Yes, you can add 5 to it, and yes you can multiply it with 2, but that doesn’t change 1, instead you end up with another immutable number.


System.String


In .Net the most well-known immutable object is the String. Once you have created a string you can’t change it, you can only create other strings. It’s easy to forget that fact and sometimes you might write code like this:


Dim str As String = "Foo"
str.Replace("F", "f")

The above doesn’t change the str value, the Replace method returns a new string object containing the new text.


Dim str As String = "Foo"
str = str.Replace("F", "f")


Now, some might argue that we above have changed str, and we have, we have changed it’s reference to point to a new string, stored in another place on the heap. But we have not changed the original string “Foo”, we simply just don’t have any reference to the original string anymore, which means that the garbage collector will kick in sooner or later and clean up that memory for us.


Advantages of immutable objects


So what are the advantages of creating our own immutable objects? Well, first of all, since its state can’t be changed, the hash code of the object never changes either. That means we can safely use them as keys in hash tables. Another advantage is that we can compare two objects with equivalence. You can compare two different string objects in the same manner as you would with value types.


Public Function CheckPassword(ByVal password As String) As Boolean
  Dim mySecretPassword = "verySecret"
  If password = mySecretPassword Then
    Return True
  Else
    Return False
  End If
End Function

Public Sub Main()
  Console.Write("Enter password: ")
  Dim pass As String = Console.ReadLine
  If CheckPassword(pass) Then
    Console.WriteLine("Access allowed")
  Else
    Console.WriteLine("Access denied")
  End If
End Sub

Even though password and mySecretPassword are two different instances of string, we can still compare them in the same manner as you would do with value types (such as Integers or Doubles). This is possible because we can assume that the identity of an immutable object is its state. If the state could be changed we could not make that assumption.


But the killer argument to use immutable objects is because they simplify multithreading. Why is it that writing proper multithreaded applications are so hard? As Patrick Smacchia points out in this blog post, it’s because it’s hard to synchronize threads accesses to resources (objects or other OS things). The reason that this is so hard is because it’s hard to guarantee that there won’t be any race conditions between the multiple write and read accesses done by multiple threads on multiple objects.


If you had a hard time understanding the previous paragraph then think about it like this: You want to buy a new computer, but you’re not sure you have enough cash to get one. So you check your bank account over the Internet, and sure enough you have just enough to get the latest and best computer on the market. So you run down to the local computer store, but when you’re trying to pay using your Visa (or whatever card you use), your bank rejects the transaction. Why? Well, after the time you checked your balance but before you tried to pay for the computer, your wife (or significant other), which whom you share the bank account with, has bought a new coat (or something other less important thing compared to the computer you wanted). This is an example of a race condition that could happen between two separate threads. One thread checks a resource and then want to change it, or make some other assumption based on the current state, but between the check and the change another thread have already changed the state of the resource. But if the resource was immutable its state cannot be changed and are therefore more secure to use in a multithreaded environment.


So that gives us three great advantages of using immutable objects.


  1. They can be used as keys in a hash table.
  2. They simplify state comparison.
  3. They simplify multithreaded programming.

Creating your own immutable class


Just like the number 1 is immutable, the .Net framework provides two different keywords to create our own immutable values, Const and ReadOnly. Why do we have two keywords and what’s the difference between them? A field that is declared as a Const must be given a value when we create it, that means that there is no way for us to the let the user provide a value. The ReadOnly keyword on the other hand allows us to provide a value for the field in a constructor.


Public Class Person
  Private Const _age As Integer = 21
  Private ReadOnly _name As String

  Public Sub New(ByVal name As String, ByVal age As Integer)
    _name = name 'This is OK
    _age = age 'This will not compile
  End Sub
End Class

The true immutable value is the Const, but the ReadOnly keyword provides us with something that can be initialized when the object is created and then never change again which provides us with some flexibility.


In the rest of this article I will create an immutable Rectangle class. A rectangle has a top-left corner and a bottom-right corner. These four value (Top, Left, Right, and Bottom) are the only things we really need to describe a rectangle. Since the class is immutable we can only set these values, or properties, when we create a new rectangle object.


Private ReadOnly _left As Integer
Public ReadOnly Property Left() As Integer
  Get
    Return _left
  End Get
End Property

So the class have the above property, and three more for the Top, Right, and Bottom values, which are all declared in the same manner. As you see I didn’t only make the property read-only but the backing field as well. This is important! If the class should be treated as immutable, the class itself should not be able to change the value either. I also created a Width and a Height property, but you will never assign any values to these properties since they can be calculated.


Public ReadOnly Property Width() As Integer
  Get
    Return _right - _left
  End Get
End Property

Public ReadOnly Property Height() As Integer
  Get
    Return _bottom - _top
  End Get
End Property

The values are provided to the constructor.


Public Sub New(ByVal left As Integer, _
               ByVal top As Integer, _
               ByVal right As Integer, _
               ByVal bottom As Integer)
  If bottom < top OrElse right < left Then
    Throw New Exception( _
      "Left cannot be larger than Right and Top cannot be larger than Bottom.")
  Else
    _left = left
    _top = top
    _right = right
    _bottom = bottom
  End If
End Sub


The class also provides two different methods, Intersect and Union. Both of these creates a new rectangle object. The dashed red rectangle in the following image shows what a union between the yellow and the green rectangle would result in.


image


The next image shows the intersection of the yellow and the green rectangle.


image


If a union or an intersection of the two rectangles can’t be created the methods return Nothing (null). Since there are an IntersectRect and an UnionRect function provided by the Win32 API, I didn’t bother about writing the logic for these methods myself.


Private Declare Function IntersectRect Lib "user32" ( _
  ByRef lpDestRect As RECT, _
  ByRef lpSrc1Rect As RECT, _
  ByRef lpSrc2Rect As RECT) As Integer

Private Declare Function UnionRect Lib "user32" ( _
  ByRef lpDestRect As RECT, _
  ByRef lpSrc1Rect As RECT, _
  ByRef lpSrc2Rect As RECT) As Integer

<StructLayout(LayoutKind.Sequential)> _
Private Structure RECT
  Public Left As Integer
  Public Top As Integer
  Public Right As Integer
  Public Bottom As Integer
End Structure

I however needed to be able to convert my object into the RECT structure that these functions uses, so I wrote a quick little private helper method. I made this method shared (static) so I could use it on any rectangle object.


Private Shared Function ToRect(ByVal r As Rectangle) As RECT
  ToRect.Left = r.Left
  ToRect.Top = r.Top
  ToRect.Bottom = r.Bottom
  ToRect.Right = r.Right
End Function

With this in place, creating the methods was a breeze.


Public Function Intersect(ByVal other As Rectangle) As Rectangle
  If other Is Nothing Then
    Return Nothing
  Else
    Dim r1, r2, r3 As RECT
    r1 = Rectangle.ToRect(Me)
    r2 = Rectangle.ToRect(other)
    If IntersectRect(r3, r1, r2) <> 0 Then
      Return New Rectangle(r3.Left, r3.Top, r3.Right, r3.Bottom)
    Else
      Return Nothing
    End If
  End If
End Function

Public Function Union(ByVal other As Rectangle) As Rectangle
  If other Is Nothing Then
    Return Nothing
  Else
    Dim r1, r2, r3 As RECT
    r1 = Rectangle.ToRect(Me)
    r2 = Rectangle.ToRect(other)
    If UnionRect(r3, r1, r2) <> 0 Then
      Return New Rectangle(r3.Left, r3.Top, r3.Right, r3.Bottom)
    Else
      Return Nothing
    End If
  End If
End Function

OK, so that’s all the logic in this class. But since we want this class to act as a value type we need to override the Equals() method. However whenever you override that method you also need to override the GetHashCode() method. The hash code of one Rectangle must be equal to another identical Rectangle. That is two rectangles with the same top, left, bottom, and right values.


Public Overrides Function GetHashCode() As Integer
  Return (_top.GetHashCode Or _left.GetHashCode) Or _
         (_bottom.GetHashCode Or _right.GetHashCode)
End Function

Public Overrides Function Equals(ByVal obj As Object) As Boolean
  If obj IsNot Nothing AndAlso obj.GetType() Is Me.GetType() Then
    Dim other As Rectangle = CType(obj, Rectangle)
    Return (other.Left = Me.Left AndAlso other.Right = Me.Right) AndAlso _
           (other.Top = Me.Top AndAlso other.Bottom = Me.Bottom)
  Else
    Return False
  End If
End Function

We also want to be able to compare one Rectangle object to another using the = (equal) operator, please note that this is not the assignment operator which you can’t overload using VB since that is really a statement and not an operator. If we overload the equal operator we also need to overload its opposite, the not equal <> operator. Operator overloads are always shared (static).


Public Shared Operator =(ByVal r1 As Rectangle, _
                         ByVal r2 As Rectangle) As Boolean
  If r1 Is Nothing Then
    Return (r2 Is Nothing)
  Else
    Return r1.Equals(r2)
  End If
End Operator

Public Shared Operator <>(ByVal r1 As Rectangle, _
                          ByVal r2 As Rectangle) As Boolean
  Return Not (r1 = r2)
End Operator


So that’s it, put it all together and you’ve just created your own immutable class. OK, for your convenient I’ll show you the complete class below.


Imports System.Runtime.InteropServices
Public Class Rectangle
  Private Declare Function IntersectRect Lib "user32" ( _
    ByRef lpDestRect As RECT, _
    ByRef lpSrc1Rect As RECT, _
    ByRef lpSrc2Rect As RECT) As Integer

  Private Declare Function UnionRect Lib "user32" ( _
    ByRef lpDestRect As RECT, _
    ByRef lpSrc1Rect As RECT, _
    ByRef lpSrc2Rect As RECT) As Integer

  <StructLayout(LayoutKind.Sequential)> _
  Private Structure RECT
    Public Left As Integer
    Public Top As Integer
    Public Right As Integer
    Public Bottom As Integer
  End Structure

  Private ReadOnly _left As Integer
  Public ReadOnly Property Left() As Integer
    Get
      Return _left
    End Get
  End Property

  Private ReadOnly _top As Integer
  Public ReadOnly Property Top() As Integer
    Get
      Return _top
    End Get
  End Property

  Private ReadOnly _right As Integer
  Public ReadOnly Property Right() As Integer
    Get
      Return _right
    End Get
  End Property

  Private ReadOnly _bottom As Integer
  Public ReadOnly Property Bottom() As Integer
    Get
      Return _bottom
    End Get
  End Property

  Public ReadOnly Property Width() As Integer
    Get
      Return _right - _left
    End Get
  End Property

  Public ReadOnly Property Height() As Integer
    Get
      Return _bottom - _top
    End Get
  End Property

  Public Sub New(ByVal left As Integer, _
                 ByVal top As Integer, _
                 ByVal right As Integer, _
                 ByVal bottom As Integer)
    If bottom < top OrElse right < left Then
      Throw New Exception( _
        "Left cannot be larger than Right and Top cannot be larger than Bottom")
    Else
      _left = left
      _top = top
      _right = right
      _bottom = bottom
    End If
  End Sub

  Private Shared Function ToRect(ByVal r As Rectangle) As RECT
    ToRect.Left = r.Left
    ToRect.Top = r.Top
    ToRect.Bottom = r.Bottom
    ToRect.Right = r.Right
  End Function

  Public Function Intersect(ByVal other As Rectangle) As Rectangle
    If other Is Nothing Then
      Return Nothing
    Else
      Dim r1, r2, r3 As RECT
      r1 = Rectangle.ToRect(Me)
      r2 = Rectangle.ToRect(other)
      If IntersectRect(r3, r1, r2) <> 0 Then
        Return New Rectangle(r3.Left, r3.Top, r3.Right, r3.Bottom)
      Else
        Return Nothing
      End If
    End If
  End Function

  Public Function Union(ByVal other As Rectangle) As Rectangle
    If other Is Nothing Then
      Return Nothing
    Else
      Dim r1, r2, r3 As RECT
      r1 = Rectangle.ToRect(Me)
      r2 = Rectangle.ToRect(other)
      If UnionRect(r3, r1, r2) <> 0 Then
        Return New Rectangle(r3.Left, r3.Top, r3.Right, r3.Bottom)
      Else
        Return Nothing
      End If
    End If
  End Function

  Public Overrides Function GetHashCode() As Integer
    Return (_top.GetHashCode Or _left.GetHashCode) Or _
           (_bottom.GetHashCode Or _right.GetHashCode)
  End Function

  Public Overrides Function Equals(ByVal obj As Object) As Boolean
    If obj IsNot Nothing AndAlso obj.GetType() Is Me.GetType() Then
      Dim other As Rectangle = CType(obj, Rectangle)
      Return (other.Left = Me.Left AndAlso other.Right = Me.Right) AndAlso _
             (other.Top = Me.Top AndAlso other.Bottom = Me.Bottom)
    Else
      Return False
    End If
  End Function

  Public Shared Operator =(ByVal r1 As Rectangle, _
                           ByVal r2 As Rectangle) As Boolean
    If r1 Is Nothing Then
      Return (r2 Is Nothing)
    Else
      Return r1.Equals(r2)
    End If
  End Operator

  Public Shared Operator <>(ByVal r1 As Rectangle, _
                            ByVal r2 As Rectangle) As Boolean
    Return Not (r1 = r2)
  End Operator
End Class


Have fun.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>