Copies and views
An NDArray is two parts: a data buffer (the elements, in unmanaged memory) and metadata (dtype, shape, strides, offset). Two arrays can share the same buffer while presenting it differently through their metadata - that shared-buffer array is a view. An array with its own duplicated buffer is a copy.
Knowing which operations return which is the single most important thing to internalize about NumSharp: a write through a view mutates the original; a write through a copy does not. This is identical to NumPy, and it is what makes slicing cheap.
View
A view reuses the original data buffer and only changes metadata - a different Shape (strides, offset) or dtype. Because the buffer is shared, writing through a view changes the original:
var x = np.arange(10); // [0 1 2 3 4 5 6 7 8 9]
var y = x["1:3"]; // a view
x["1:3"] = np.array([10, 11]);
// x → [0 10 11 3 4 5 6 7 8 9]
// y → [10 11] ← the view sees the change
Views are how NumSharp avoids copying: a slice of a gigabyte array costs a few bytes of metadata.
Copy
A copy duplicates both the buffer and the metadata. Changes to a copy never touch the original. Copying is slower and uses memory, but is sometimes exactly what you want:
var y = x["1:3"].copy(); // independent buffer
y[0] = 999; // x is unaffected
Force a copy with .copy() (or np.copy(arr)).
Indexing operations
The rule is mechanical:
- Basic indexing (integers, slices, ellipsis, newaxis) → always a view. The selected elements can be addressed with an offset and strides into the original, so no data moves.
- Advanced indexing (integer arrays, boolean masks) → always a copy. The selected elements are gathered into a fresh buffer.
var x = np.arange(10);
var v = x["1:3"]; // basic → view (v.@base is x's storage)
var m = np.arange(9).reshape(3, 3);
var c = m[np.array([1, 2])]; // advanced → copy (c.@base is null)
The assignment asymmetry. Reading
x[mask]orx[idx]returns a copy - writing into that copy does nothing tox. But assigningx[mask] = v/x[idx] = vscatters back intoxthrough the setter. Sovar picked = x[mask]; picked[0] = 9;leavesxunchanged, whilex[mask] = 9changes it. This matches NumPy exactly. See Indexing → Views vs copies.
Other operations
reshape returns a view where possible, a copy otherwise. It can reshape by adjusting strides for a contiguous array, but a layout that cannot be expressed with strides (e.g. after a transpose) forces a copy:
var x = np.ones((2, 3));
var y = x.T; // transpose → non-contiguous view
var z = y.reshape(6); // must copy (strides can't express it)
ravel returns a contiguous flattened view where possible; flatten always copies. When you specifically want a view most of the time, x.reshape(-1) is preferable:
x.ravel(); // view if contiguous, else copy
x.flatten(); // always a copy
x.reshape(-1); // view where possible
.T / transpose, swapaxes, flip/fliplr/flipud, rot90, broadcast_to, and field-free np.diag on a 2-D input are all views (O(1) stride tricks). astype copies (it changes the dtype and therefore the buffer).
Broadcast views are read-only
A broadcast view has a stride of 0 on the stretched axis - one stored element is read for many logical positions. Writing to it would corrupt every position that aliases that element, so NumSharp makes broadcast views non-writeable:
var small = np.array([1, 2, 3]);
var big = np.broadcast_to(small, (1000000, 3)); // read-only view, ~3 elements of memory
big.Shape.IsBroadcasted; // true
big.Shape.IsWriteable; // false
big[0, 0] = 9; // throws: assignment destination is read-only
var writable = big.copy(); // materialize all 3M elements to write
See Broadcasting → Memory behavior.
How to tell a view from a copy
NumSharp mirrors NumPy's ndarray.base and adds explicit checks:
var x = np.arange(9);
var v = x.reshape(3, 3); // view
var c = x[np.array([0, 2])]; // copy
(v.@base is not null); // true ← the simple boolean check (pattern match)
(c.@base is not null); // false
arr.@base is NumPy's ndarray.base - null when the array owns its data, otherwise an NDArray wrapping the base storage; views chain to the ultimate owner, not intermediate views:
var a = np.arange(10); // a.@base is null (owns data)
var b = a["2:5"]; // b.@base is not null (view of a)
var d = a.copy(); // d.@base is null (copy owns data)
np.shares_memory(a, b); // true - b really does share a's buffer
Use
arr.@base is not nullfor a boolean test, written with theis/is notpattern - notarr.@base != null, because!=onNDArrayis the elementwise-comparison operator, not a null check.@basediffers from NumPy in one way: it builds a fresh wrapper each call, soReferenceEquals(b.@base, a)isfalseeven though they share memory (confirm the sharing withnp.shares_memory). (NumSharp also has an internalStorage.IsViewthat says the same thing, butStorageis not part of the public API.)
To ask whether two arrays could share memory, use np.shares_memory / np.may_share_memory:
np.shares_memory(x, v); // true - exact overlap solver
np.may_share_memory(x, c); // false - fast bounds check
The Shape flags expose the layout that decides all of this: IsContiguous, IsSliced, IsBroadcasted, IsWriteable, IsFContiguous. See NDArray.
In-place modification
Writing through a view mutates shared memory. But C#'s compound-assignment operators are not in-place - the compiler expands x += 1 into x = x + 1, allocating a new array and rebinding the variable. Other references keep seeing the old data:
var x = np.array([1, 2, 3]);
var alias = x;
x += 10; // x → NEW array [11, 12, 13]
// alias is still [1, 2, 3] ← differs from NumPy, where += is in-place!
To mutate the buffer in place (so aliases and views observe the change), assign through a slice - the setter path:
x[":"] = x + 1; // rewrites x's buffer; alias sees it
data["1:3"] += 5; // expands to data["1:3"] = data["1:3"] + 5, but the LHS
// is a view, so the result is written back into the parent
This is a deliberate consequence of C# operator semantics, and one of the few places ported NumPy code behaves differently. See Getting & Setting Values → in-place vs reassignment.
Common patterns
Detach a small slice from a large parent
var window = huge["1000:1010"].copy(); // parent can now be collected
A view keeps the whole parent buffer alive. Copy the piece you need if the parent is large and about to go out of scope.
Guarantee a writeable, contiguous array
var w = np.ascontiguousarray(maybeView); // copies only if needed
Modify in place safely
arr[":"] = arr * 2; // in-place double; views/aliases observe it
Troubleshooting
"I modified a slice and the original array changed"
Basic slices are views - that is by design. Use .copy() for independence.
"assignment destination is read-only"
You are writing to a broadcast view (stride 0). Copy first: var w = b.copy();. See Broadcasting.
"x += 1 didn't update another reference"
C# compound assignment rebinds; it does not mutate. Use x[":"] = x + 1.
"arr.@base != null gave a weird array instead of true/false"
!= on NDArray is elementwise. Use arr.@base is not null (the is not pattern) for a boolean.
API reference
| Member | Returns | Meaning |
|---|---|---|
arr.copy() / np.copy(arr) |
NDArray |
force an independent copy |
arr.@base |
NDArray? |
the owning array (null if it owns its data); chains to the ultimate owner |
arr.@base is not null |
bool |
simple public view check (use the is not pattern, not !=) |
np.shares_memory(a, b, max_work) |
bool |
exact overlap solver |
np.may_share_memory(a, b, max_work) |
bool |
fast bounds-based check |
arr.Shape.IsContiguous / .IsSliced / .IsBroadcasted / .IsWriteable / .IsFContiguous |
bool |
layout flags |
| Operation | View or copy |
|---|---|
Basic index / slice, .T, transpose, swapaxes, flip, rot90, broadcast_to, np.diag (2-D input) |
view |
| Advanced index (integer array, boolean mask) | copy |
reshape, ravel |
view if possible, else copy |
flatten, astype, .copy() |
copy |
Related reading
- Indexing on NDArray - which index forms view and which copy.
- Getting & Setting Values - the write-through rules in full.
- Broadcasting - why broadcast views are read-only.
- NDArray - storage, shape, and the anatomy behind views.
- NumPy copies-and-views guide - the upstream article.