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.
- They can be used as keys in a hash table.
- They simplify state comparison.
- 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.
The next image shows the intersection of the yellow and the green rectangle.
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.