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)).
Text arrays and chararrays
S / U and variable-width StringDType use the same view/copy rules. A T view shares both cells and their allocator; .copy() duplicates the text into its own allocator. It is not safe to implement that copy with raw tobytes() cells, which may contain offsets or pointers valid only in the original allocation. Reinterpreting a T array as another dtype is refused.
NDCharArray is a subclass with one important element-access exception: c[0] is a read-only, stripped scalar stand-in, not an alias through which to update the original. c[0] = value still writes to c. Its ordinary slices are chararray views. Also, np.@char.asarray is not a blanket no-copy promise: NumPy's U chararray path copies even with copy: false, while its S path can share. See The chararray class.
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.