According to wikipedia, an immutable object is thusly defined:
An immutable object (unchangeable object) is an object whose state cannot be modified after it is created. This is in contrast to a mutable object (changeable object), which can be modified after it is created. In some cases, an object is considered immutable even if some internally used attributes change but the object’s state appears to be unchanging from an external point of view.
In simple terms, this means once an instance of the immutable object has been created no parts of it can be modified and it becomes effectively locked/readonly, this is inherantly useful when working with multi-threaded applications as it creates implicit thread safety in-so-much as because the objects cannot be mutated/altered there is no risk of multiple threads trying to alter the same object and locking, it also has the side effect of creating less bug prone and safer code especially when multiple developers are involved who may be enticed to alter properties of objects at random places in code introducing bugs elsewhere.
So, how do records help... well, actually, consider the following code:
class Program
{
static void Main(string[] args)
{
var c1 = new Candidate() { FirstName = "Donald", Surname = "Trump" };
var c2 = new Candidate() { FirstName = "Donald", Surname = "Trump" };
//this will return true as both records match even though they are different objects.
Console.WriteLine($"c1 and c2 are {(c1.Equals(c2) ? "the same people" : "different")} ");
c1.Surname = "BOB";
//Now they won't match
Console.WriteLine($"c1 and c2 are {(c1.Equals(c2) ? "the same people" : "different")} ");
Console.ReadLine();
}
}
public record Candidate
{
public string FirstName { get; set; }
public string Surname { get; set; }
}
This code is fully functional and compiles but clearly here, a record has been created and mutated... so what's all the fuss about, are records not a new feature explicitly for writing immutable code, how is this even any different from using a class?
Good question, let me answer...
Firstly, pay attention to the above line of code repeated below:
Console.WriteLine($"c1 and c2 are {(c1.Equals(c2) ? "the same people" : "different")} ");
"What's that I see", you might say, "an equality check, but the record doesn't implement the IEquatable<T> interface, what black magic is going on here"... well my friend, welcome to record types.
When a record type is defined, behind the scenes at compile time the compiler creates a lot of boiler plate code for you, implementing the IEquatable interface which allows the above equality check to take place, furthermore it overrides the ToString method so as to print out the object name and methods as shown below:
Candidate { FirstName = Donald, Surname = BOB }
It also creates a "Copy constructor" which when used with the "with" keyword creates a copy of the existing object which will be covered in more detail.
Altering our original code slightly we can now start to leverage some immutability:
class Program
{
static void Main(string[] args)
{
var c1 = new Candidate("Donald", "Trump");
var c2 = new Candidate("Donald", "Trump");
//Can't alter the surname any more, because
//c1.Surname = "BOB";
Console.ReadLine();
}
}
record Candidate
{
string FirstName { get; init; }
string Surname { get; init; }
public Candidate(string Fname, String Sname) => (Fname, Sname) = (FirstName, Surname);
}
Note that now the record is using the init keyword, this means the records properties can only be set at the time it is created, this code can be made even simpler by using positional records which automatically generate the backing fields and constructor shown above, this is acomplished like so:
class Program
{
static void Main(string[] args)
{
var c1 = new Candidate("Donald", "Trump");
Console.ReadLine();
}
}
record Candidate(string FirstName, string Surname);
Currently though this shorter positional syntax is bugged and requires you to add in the following class somewhere in your program:
namespace System.Runtime.CompilerServices
{
public class IsExternalInit { }
}
and if you don't you will see the follwing error:
predefined type 'System.Runtime.compilerServices.IsexternalInit is not defined or imported'
Incedently, init only references can also be used on classes as shown below in the snippet:
namespace CSharp9
{
class Program
{
static void Main(string[] args)
{
var c1 = new Candidate("Donald","Trump");
Console.ReadLine();
}
}
public class Candidate
{
public string FirstName { get; init; }
public string Surname { get; init; }
public Candidate(string Fname, String Sname) => (Fname, Sname) = (FirstName, Surname);
}
}
namespace System.Runtime.CompilerServices
{
public class IsExternalInit { }
}
But while this allows you to create an immutable class, what you won't get is the other good stuff that comes with records and will have to write your own boilerplate code for equality checks etc.
Obviously there will come a time when writing code you need to change the object you are working with but this is not possible due to the immutable nature of init only properties, enter "With" keyword, consider the below code snippet if you will:
class Program
{
static void Main(string[] args)
{
var c1 = new Candidate("Donald","Trump");
var c2 = new Candidate("Donald", "Trump");
var c3 = c1 with { Surname = "Duck" };
}
}
public record Candidate
{
public string FirstName { get; init; }
public string Surname { get; init; }
public Candidate(string Fname, String Sname) => (Fname, Sname) = (FirstName, Surname);
}
Here the with keyword is used to leverage the automatically created clone method in the record to create a copy of the c2 record object but altering the surname.
Finally, as a quick aside, if you create a class without an access modifier it defaults to private with records however, the default state is public, one to watch out for if you like to keep your members private!
Happy coding!
Comments
Comments are closed