A logbook for curious waters
← Software Engineering Field Guide

Mutable Shared State

Before you begin: Objects, lists, references, and assignment.

Mutable shared state is data that more than one part of a program can change. Two variables may refer to the same list or object, so a change through one variable is visible through the other. This can surprise even a single-threaded program; threads introduce additional risks. 1

Suppose a party roster receives a list of player names. Does the party keep that exact list, or keep the names as they were when the party was created? Those are different promises.

Original version: both owners have the same list

public sealed class Party
{
    public List<string> Names { get; }

    public Party(List<string> names)
    {
        Names = names;
    }
}
List<string> names = new List<string> { "Mira", "Sol" };
Party party = new Party(names);
names.Clear();
Console.WriteLine(party.Names.Count); // 0

Names = names copies a reference, not the list contents. Clearing names therefore empties the party’s roster. A get-only property prevents assigning a different list through that property; it does not make the existing list immutable.

class Party:
    def __init__(self, names):
        self.names = names
names = ["Mira", "Sol"]
party = Party(names)
names.clear()
print(len(party.names))  # 0

self.names = names stores a reference to the existing list. It does not copy the contents. Clearing names therefore empties the party’s roster too.

Revised version: keep an independent roster

For this example, the intended rule is that constructing a party captures its initial names. Later edits to the input list must not change that roster.

public sealed class Party
{
    private readonly string[] names;

    public Party(IEnumerable<string> names)
    {
        this.names = names.ToArray();
    }

    public string[] Names
    {
        get { return (string[])names.Clone(); }
    }
}
List<string> names = new List<string> { "Mira", "Sol" };
Party party = new Party(names);
names.Clear();
Console.WriteLine(party.Names.Length); // 2

The constructor copies the names into an array. The property also returns a copy, so callers cannot edit the stored array. Both directions matter: copying on input alone would still leave a problem if the property exposed the internal array.

class Party:
    def __init__(self, names):
        self._names = tuple(names)

    @property
    def names(self):
        return self._names
names = ["Mira", "Sol"]
party = Party(names)
names.clear()
print(len(party.names))  # 2

The constructor captures the names in a tuple. Callers cannot append to that tuple or replace its elements. Returning it does not expose a mutable list. Deliberately reassigning the private-by-convention _names attribute remains possible in Python.

Copies are not a universal solution

These are strings, which cannot themselves be mutated. If the collection held mutable player objects, copying the collection would still share the player objects. That is a shallow copy, not an independent copy of an entire object graph.

The revision also intentionally changes the sharing behavior. If callers depend on a live roster, use controlled add/remove operations or an explicitly shared model rather than silently substituting a snapshot.

For concurrent access, assigning a clear owner, using immutable messages, or protecting a complete operation with synchronization may be appropriate. A copy does not make unrelated shared state thread-safe. First decide who owns each change; then choose a mechanism that enforces that decision.

Sources

  1. Python — Assignment and copies, explaining shared references and shallow versus deep copying.
  2. Microsoft — Reference types, explaining multiple references to the same object.