Last Updated: May 22, 2026
Every array in C#, whether it's a string[], an int[,], or a jagged decimal[][], is actually an object of type System.Array internally. That base class ships with a long list of static helpers (sort, search, copy, resize, fill) and a handful of instance members (Length, Rank, Clone, CopyTo) that work on every array you ever declare. This lesson walks through the parts of System.Array you'll use the most often, with the catches that trip up people who treat them as black boxes.
When you write int[] stockCounts = new int[10];, the compiler doesn't invent a fresh type. It hands you an object whose runtime type is System.Int32[], and that type inherits from System.Array. Every member declared on System.Array is available on every array, regardless of element type or rank.
The cast to Array works because int[] derives from it. Length gives the total number of elements (5). Rank is the number of dimensions (1 for a one-dimensional array, 2 for a [,], and so on). GetType().Name shows the actual runtime type.
Members inherited from System.Array that you'll meet in this lesson:
| Member | What it does |
|---|---|
Length | Total elements across all dimensions. O(1) field read. |
Rank | Number of dimensions. 1 for int[], 2 for int[,]. |
GetLength(int dim) | Size of a specific dimension. Useful for multidim arrays. |
Clone() | Returns a shallow copy of the array. |
CopyTo(Array dest, int index) | Copies all elements into another array starting at index. |
GetValue / SetValue | Read/write by index without knowing the element type at compile time. |
The static methods on Array are what make this class genuinely useful day to day. Those are next.
Array.Sort reorders the elements of an array in place. The default comparison uses the element's natural ordering (int sorts numerically, string sorts by ordinal Unicode value, decimal by value, and so on).
The array is mutated. There's no separate "sorted copy" returned, and the original order is gone after the call. If you need to keep the original, clone first and sort the clone.
Array.Sort uses an introspective sort (quicksort that switches to heapsort on bad inputs and insertion sort on small ranges). The time complexity is O(n log n) on average and in the worst case. It's an in-place sort, so the space overhead is O(log n) for the recursion stack.
When you sort an array of custom objects, the default behavior won't know how to compare them. You have two ways to teach it:
Comparison<T> delegate (an inline lambda is usually fine).IComparer<T> implementation when you want the comparison logic to be reusable.A Product array sorted by price ascending:
The lambda (a, b) => a.Price.CompareTo(b.Price) returns a negative number when a should come before b, zero when they're equivalent, and a positive number when a should come after b. Flipping it to b.Price.CompareTo(a.Price) gives you descending order.
Array.Sort has an overload that takes a starting index and a count, sorting only that slice. Handy when you only want to reorder a window.
Indices 0 and 1 stay put (5, 2). Indices 2 through 5 (8, 1, 9, 3) are sorted to 1, 3, 8, 9. Index 6 (7) is left alone.
Array.Reverse flips the order of elements in place. The default overload reverses the whole array, and there's a range overload that reverses a slice.
A common pattern is to sort ascending, then reverse to get descending. It's slightly less efficient than sorting with a descending comparison directly, but it reads clearly.
Array.Reverse is O(n). It swaps elements pairwise from the outside in, no extra allocation.
Array.IndexOf finds the first position of a value. Array.LastIndexOf finds the last. Both return -1 when the value isn't present.
Both methods do a linear scan, so the cost is O(n) in the worst case. They also have overloads that take a starting index and a count, useful when you want to keep searching past an earlier hit:
For value-type arrays (like int[]), the comparison uses EqualityComparer<T>.Default, which delegates to the type's Equals. For reference-type arrays, it uses Object.Equals on the elements, so two strings with the same content match even if they're different string instances.
Array.BinarySearch does a divide-and-conquer search: it inspects the middle element, decides whether the target is in the left or right half, and recurses. That's O(log n), much faster than the linear IndexOf for large arrays.
There's one catch, and it's a strict one: the array must already be sorted in ascending order. If it isn't, the return value is meaningless.
The return value contract deserves attention. If the value is found, the return is the index. If it isn't found, the return is a negative number that encodes where the value would have been inserted to keep the array sorted. Specifically, it's the bitwise complement (~) of the insertion index.
1006 isn't present. The runtime tells you it belongs at index 3 (between 1005 and 1007), and returns ~3, which is -4. Applying ~ to that negative value flips it back to the insertion index. This is genuinely useful: you can use one call to either find an existing order or know exactly where a new one should go.
Array.BinarySearch is O(log n), but only because it can assume the input is sorted. Sorting first is O(n log n), so if you only need a single lookup on an unsorted array, IndexOf (O(n)) is cheaper. The win comes when you sort once and search many times.
These four methods take a Predicate<T> and let you ask "is there an element matching this rule?" without writing the loop yourself.
| Method | Returns | Use when... |
|---|---|---|
Array.Find(arr, pred) | First matching element, or default | You want the element itself |
Array.FindAll(arr, pred) | New array of all matches | You want every match |
Array.FindIndex(arr, pred) | Index of first match, or -1 | You need the position |
Array.Exists(arr, pred) | true if any match | You only care whether one exists |
A Product array filtered by price:
All four methods do a linear scan, so they're O(n). FindAll also allocates a new array for the matches, which adds a bit more cost when the array is large. LINQ's Where and FirstOrDefault generalize these to any IEnumerable<T>.
A subtle point on Find: when no element matches, it returns default(T). For reference types that's null. For value types, it's the type's default (0 for int, false for bool). When "no match" is indistinguishable from a legitimate zero, prefer FindIndex and compare to -1.
C# arrays have a fixed length once created. Array.Resize looks like it changes the size in place, but it doesn't. It allocates a new array of the new size, copies the elements over, and then updates the reference you passed in.
The ref keyword on the first parameter is what allows the method to swap your variable to point at the new array. If you resize to a smaller length, the trailing elements are dropped. If you resize to a larger length, the new slots are initialized to default(T) (null for reference types, 0 for numeric types).
Array.Resize is O(n) because it has to copy every element from the old array to the new one. It also allocates the entire new array up front. If you're growing an array repeatedly, the total cost adds up fast. For that pattern, List<T> is the standard tool; it grows by doubling its internal array, which keeps amortized append at O(1).
Three ways to duplicate an array, and they're not interchangeable.
Array.Copy copies a range from one array to another. You control the source range, destination range, and length, which makes it the most flexible option.
The arguments are: source array, source start index, destination array, destination start index, count. Two elements (Keyboard, Mouse) get copied into the first two slots of gift. The remaining two slots are still their default value (null, which renders as empty when joined).
CopyTo is the instance-method version. It copies the entire source array into a destination, starting at the index you specify.
The source elements land at indices 1, 2, 3. The destination must be at least as long as sourceIndex + sourceLength, otherwise you get an ArgumentException.
Clone returns a new array containing the same elements as the original. The return type is object, so you cast it back to the array type.
The clone is a separate array, so writing to copied[0] doesn't affect originalPrices. For value types like decimal, that's the whole story.
Clone, Copy, and CopyTo all perform a shallow copy. For value-type elements (int, decimal, bool, struct), that's fine: the values themselves are copied, and the two arrays are independent.
For reference-type elements (objects, strings, other arrays), the references are copied, not the objects they point to. Both arrays end up holding the same set of references, so mutating an object through one array changes what the other array sees too.
The discount applied through shallowCopy is visible through originalCart because both arrays point at the same Product instance at index 0. The diagram below shows what's happening in memory.
Two array objects, but only one set of Product instances. A write through either array's reference is visible through the other. A deep copy would also duplicate the Product objects so the two arrays are fully independent.
A shallow Clone() is O(n) and only copies the references. A deep copy is more expensive because every referenced object also has to be cloned, and there's no built-in deep-copy helper. You implement it yourself by walking the array and constructing new objects, often with a copy constructor or a with expression on records.
A deep copy by hand, for the same Product array:
The price change to deepCopy[0] doesn't bleed back into originalCart because each array now holds a different Product instance. The deep version is doing twice the allocation, but the two arrays are now fully independent.
Two methods for bulk-setting values.
Array.Fill writes a single value to every slot of an array. Useful for initializing a fresh stock-count array to zero, or seeding a row of placeholder names.
There's also a range overload that takes a start index and a count, if you only want to fill part of the array.
Array.Clear resets elements to default(T): 0 for numeric types, false for bool, null for reference types. It always takes a start index and a count.
A common end-of-day pattern: zero out a counter array before tomorrow's reads accumulate fresh values. Array.Clear is one call, no loop, and the runtime can use a vectorized clear instruction internally for large blocks.
Array.ConvertAll takes a source array and a Converter<TInput, TOutput> delegate, applies the converter to every element, and returns a new array of the output type. It's the array-flavored answer to "I want a transformed copy."
The source array isn't modified. The new array's length is the same, and its element type is whatever the converter returns. For more complex transformations and chaining (filter, group, project, aggregate), LINQ's Select is the more flexible tool.
A quick reference table for when to use each one. The complexity column assumes the typical case for an array of length n.
| Method | What it does | Cost | Mutates? |
|---|---|---|---|
Array.Sort | Sorts in place, ascending by default | O(n log n) | Yes |
Array.Reverse | Flips order in place | O(n) | Yes |
Array.IndexOf / LastIndexOf | Linear find by value | O(n) | No |
Array.BinarySearch | Find on a sorted array | O(log n) | No |
Array.Find / FindIndex / Exists | Linear find by predicate | O(n) | No |
Array.FindAll | Linear filter, returns new array | O(n), allocates | No |
Array.Resize | Allocates new array, copies elements, updates ref | O(n), allocates | Yes (via ref) |
Array.Copy | Copies a range to another array | O(count) | Destination yes |
array.CopyTo | Copies the whole array to a destination at offset | O(n) | Destination yes |
array.Clone() | Returns a shallow copy | O(n), allocates | No |
Array.Fill | Sets every slot to a value | O(n) | Yes |
Array.Clear | Resets a range to default(T) | O(count) | Yes |
Array.ConvertAll | Projects to a new element type, returns new array | O(n), allocates | No |
Pulling several of these methods together. The program maintains an array of Order objects, sorts them by total, finds a specific order with BinarySearch on a parallel ID array, resizes when a new order arrives, and clears stale data when a day rolls over.
A few details. The sort comparison lambda gives us an ascending-by-total order in one line. The parallel ids array is built with ConvertAll, then sorted independently so we can binary-search it. The ^1 index in orders[^1] is C#'s "from end" syntax for the last slot, handy after a resize. And Array.Clear resets the daily counters in one call without a loop.
Four things trip people up regularly with the Array class. They're all easy to spot once you've named them.
Array.BinarySearch returns a value even when the array isn't sorted, but the value is meaningless. The method has no way to detect that you're misusing it.
The value 8 is in the array, but the search might return -1 or a wrong positive index depending on which comparisons happen to land. Always sort first, or use IndexOf if sorting isn't worth it for a single lookup.
Array.Resize doesn't grow an array. It allocates a new array, copies the elements, and updates your reference. Other variables that still hold the old reference see the old size.
cart now points at the new four-element array. alias still points at the original two-element array. If you have multiple variables holding the array and you need them all to see the new size, you have to update them too. For frequently resized collections, switch to List<T>.
Clone is shallow. For arrays of value types it doesn't matter, but for arrays of reference types both arrays end up sharing the underlying objects. A change made through one array is visible through the other. Build a deep copy explicitly when independence matters.
The range-overload signatures for Sort, Copy, and similar methods follow a specific convention: (array, index, count, ...), not (array, startIndex, endIndex, ...). The third argument is how many elements, not the last index. Off-by-one errors here usually look like "the last element didn't get touched" or "an extra element got swept in."
The convention is "start at index 1, sort 3 elements", which is the same as sorting indices 1, 2, 3.