How it works
An object cannot promise a stable snapshot while silently sharing mutable storage with an unrelated caller. Basket receives an array, but its constructor copies the array before keeping it. The caller still owns source and can edit it; those edits no longer rewrite the basket's private items.
The second boundary is just as important. Returning the private array would let another caller bypass the constructor's protection. CopyItems returns another copy, so modifying view also leaves the basket unchanged. The total remains eight after both external arrays have been edited. Each copy has a separate storage identity.
A readonly field prevents replacing the field's array reference after construction. It does not prevent writing to that array's elements. Encapsulation is what keeps outside code from accessing the private storage here. The implementation itself could still mutate items, so the team's internal conventions and methods must preserve the intended contract.
Copying an array of reference objects copies the references, not each referenced object. This example uses integers to isolate the ownership rule. If each item were a mutable Product object, callers might still change a product shared by two arrays. A real snapshot may require immutable item values or a deliberate deeper transformation.
Practice by adding Count and an index-based reading method that returns an integer. Compare that API with returning a copy for every read. Then add a second basket built from the same source and show that changing the source or a returned copy cannot alter either basket's established total.
There is a cost to defensive copying: allocation and time proportional to the copied data. A read-only interface can avoid copying on each read, but it does not by itself prove that the underlying collection has no other writer. Document who owns the storage and when mutation is allowed, then choose an API that enforces that ownership.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
var source = new[] { 3, 5 };
var basket = new Basket(source);
source[0] = 100;
var view = basket.CopyItems();
view[1] = 200;
Console.WriteLine(basket.Total);
class Basket {
private readonly int[] items;
public Basket(int[] source) { items = source.ToArray(); }
public int[] CopyItems() => items.ToArray();
public int Total => items.Sum();
}
Expected output
8
Copy both incoming storage and any mutable storage returned to callers.