There’s probably only one good reason for a type to keep its state
unchangeable (apart from using it as a key in a hash table).
This reason is multithreading.
Unchangeable state means the type methods designed to update the instance will instead
return a new-minted one.
By and large, this is immutability: you manacle yourself in terms of
performance but gain from ease of multithreading development. Quite
straightforward, isn’t it – all multithreading issues appear because you have
to make sure only one thread at a time can update an object, but immutable
objects don’t need synchronization at all, huh![1]
So, the overall idea is somewhat familiar to most of us but very soon everybody will need to understand the
basics of immutability – because Eric Lippert said: "Immutable data
structures are the way of the future in C#", and Eric Lippert does
understand what he is talking about[2].
So, let's peel off the tasteless wrappings and see what we already knew.
Immutability
roots.
The most popular immutable type is good old System.String. We write
"foo".Replace("f", "b");
but nothing displeasing for "foo", the method creates and
returns a new string. As time
went by, we understood the need for StringBuilder, but also we learned the
captivating benefits:
- No need for string synchronization in multithreaded environment
- Possibility
to use strings as keys in hashtables, because the keys should not
change (in fact, hashtable keys can have just an immutable part so that
the hash would be calculated only from it)
Recently (?), a couple
of other immutable approaches have appeared in C#.
Anonymous classes.
We write
var foo = new { Name = "foo" };
and behind the scenes
the C#3 compiler generates a class with read-only properties that can’t be
changed. Anonymous types are heavily used in hashtables inside LINQ, so that’s
why they are immutable. Interestingly enough, anonymous types of VB9 are fully
mutable and the following code will compile and run
Dim foo
= New With { .Name = "foo" }
foo.Name = "boo"
Good news is that poor VB devs are not completely bereft of the
possibility of using anonymous types as hashtable keys, although they need a
new keyword trickery for that. Hash value will
be calculated from the (read-only) fields marked with the "Key"
keyword:
Dim foo = New With { Key .Name = "foo", .Index = 1 }
foo.Name = "boo" 'won’t compile.
Closures.
Second example of
immutability is closures, introduced in C#2 and enhanced with lambda
expressions of C#3. If you’re not
familiar with the idea, I really recommend learning it. One can write very
pithy and nice code, but still need to understand what’s going on behind the
curtain. So, take Reflector and Ildasm and dive into a morass of
autogenerated code (alternatively, read some articles on the topic[3]).
Perhaps, only lazybones haven’t
written about the inevitable pitfalls you’d face in this field:
internal static List<Action> GetActions(List<string> strings)
{
var
result = new List<Action>();
foreach(string s in
strings) { result.Add(() => Console.WriteLine(s));
}
return
result;
}
static void Main()
{
var
strings = new List<string> {"one",
"two", "three"};
GetActions(strings).ForEach(a =>
a());
}
Smell a rat? Three times
"three"? Calm down, it’s not that bad yet. Firstly, this can be
easily fixed (at least two possibilities). Secondly, Resharper eeks an
"access to modified closure" warning for the lucky guys who have it
installed.
So, allow me to
replenish your glass and let’s finish off this milk tooth post with the following admonitory code
sample:
class Developer
{
public string Name { get; set; }
public
Developer(string name) { Name = name; }
}
static void Main()
{
var
team = new List<Developer> {
new Developer("Кастусь"),
new Developer("Пятрусь")
};
var
writeNames = new List<Action>();
team.ForEach(i => writeNames.Add(()
=> Console.WriteLine(i.Name)));
foreach
(var action in
writeNames) { action(); }
Console.WriteLine("\nOriginal team was changed\n");
team[0] = new
Developer("Foo");
team[1].Name = "Boo";
foreach
(var action in
writeNames) { action(); }
}
Кастусь was left intact
but Пятрусь wasn’t: while changing the team we altered the second object.
If we used mere strings
instead of a custom type, everything would have been ok. Thus, if you want to
rely on the immutability of closures, you might want to make sure they operate with immutable
data structures.
Footnotes.
[1] – Well, we'd need some at the
system level. At least to make sure memory regions
allocated by
different threads would not overlap. But it's another day's post.
[2] – Fabulous adventures in immutability from Eric Lippert.
[3]
– A nice write-up about anonymous methods from Patrick Smacchia.