Table of Contents

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] or x[idx] returns a copy - writing into that copy does nothing to x. But assigning x[mask] = v / x[idx] = v scatters back into x through the setter. So var picked = x[mask]; picked[0] = 9; leaves x unchanged, while x[mask] = 9 changes 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 null for a boolean test, written with the is/is not pattern - not arr.@base != null, because != on NDArray is the elementwise-comparison operator, not a null check. @base differs from NumPy in one way: it builds a fresh wrapper each call, so ReferenceEquals(b.@base, a) is false even though they share memory (confirm the sharing with np.shares_memory). (NumSharp also has an internal Storage.IsView that says the same thing, but Storage is 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