C# vs JavaScript: Arrays
This post belongs to the C# vs JavaScript series.by Nicklas Envall
An array is a collection of items (values) stored in a sequence of memory locations. It is an ordered list where each element has an index mapped to them. These indices get used for retrieving values from the array. Visually an array looks like: [1, 2, 3, 5, 10]
.
Arrays in JavaScript and C# are very different. We'll look at the differences, and we'll also look at other collection data types available in C# and compare them to what's available in JavaScript. Lastly, we'll cover common use cases and thoughts on JavaScript arrays versus C# arrays.
JavaScript Arrays
An array in JavaScript can contain different values like numbers, strings, objects, and even functions. It grows dynamically in size when you add elements, which means that you do not have to worry about the array's capacity. Yet, more importantly, there are two ways to create a new array, and the most common way is to use an array literal:
const characters = []; characters[0] = 'A'; characters[1] = 'B'; characters[2] = 'C';
Often we'll add our initial elements inside the square brackets:
const characters = ['A', 'B', 'C'];
But, an array in JavaScript is not really an array. In reality, it's an object with array-like characteristics. We only view it as an array, but it's most likely, implemented as a hash table (depending on the JS engine you're using) under the hood.
Essentially everything in JavaScript is an object except primitive data types like number
, string
, booleans
, null
, and undefined
. Nevertheless, what's an object or not can be confusing. Since number
, string
, and boolean
are object-like because they have methods available with the help of a primitive wrapper object.
Objects like these inherit methods from a prototype object, and since arrays are objects, so do they. Arrays inherit from Array.prototype
that provides instance methods and properties. In short, instance methods represent the behaviour or actions of an object. Additionally, arrays also have so-called static methods that exist directly on the Array
object like Array.from()
or Array.isArray()
.
// instance methods ['a'].concat(['b']); // ['a', 'b'] [1, 2].map(x => x * 2); // [2, 4] // static methods Array.isArray([1, 2]); // true Array.from({ length: 4 }, (_, i) => i); // [0, 1, 2, 3]
C# Arrays
An array in C# holds a fixed number of elements of a specific type. Once you've created one in C#, you may not change its size. So clearly, the approach in C# is much more strict than JavaScript. Subsequently, to create and populate an array, we may do the following:
char[] characters = new char[3]; characters[0] = 'A'; characters[1] = 'B'; characters[2] = 'C';
Additionally, you can declare and populate an array at once by using an array initialization expression (object and collection initializer):
char[] characters = new char[] { 'A', 'B', 'C' }; char[] characters = { 'A', 'B', 'C' };
Furthermore, all types inherit from System.Object
, so we can (doesn't mean we should) have an array containing different values, like strings, integers, and whatnot. We do that by creating an object array with the object
type.
object[] objectArr = new object[] { 1, "two", 3.5f };
But where from does the C# array get its methods? Well, the System.Array
base class gets inherited by all arrays. That class contains methods for things like searching, sorting, creating, and much more. It also provides plenty of static methods, which frequently get used by C# developers. Consequently, this may confuse your everyday JavaScript developer because static methods rarely get used when working with arrays in JavaScript. Instead, pre-built instance methods are often used.
// Static methods var floatArr = new float[] { 1.5f, 2.5f }; Array.Reverse(floatArr); String.Join(", ", floatArr); // 2.5, 1.5 int[] intArr = Array.ConvertAll(floatArr, r => Convert.ToInt32(r)); String.Join(", ", intArr); // 2, 2 // Instance methods int[] arrSource = new int[2] { 1, 2 }; int[] arrSourceCopy = new int[2]; arrSource.CopyTo(arrSourceCopy, 0); String.Join(", ", arrSourceCopy); // 1, 2 int[] intArr2 = intArr.Clone() as int[]; String.Join(", ", intArr2); // 2, 2
System.Collection
If you're used to JavaScript's dynamic nature, then you might think that arrays in C# are unnecessarily difficult to use. Luckily, there's a namespace called System.Collection
that contains higher-level data structures. It provides many things, like collections that are list-like or dictionary-like.
One example is the dynamic array List<T>
, which is more similar to JavaScript's array. It may dynamically change its size by keeping an internal array that they replace once the capacity gets reached. It has methods like:
// Add(T item); // Insert(int index, T item); // Remove(T item); // RemoveAt(int index); // RemoveRange(int index, int count); // ToArray(); // CopyTo(T[] array); // Reverse(); // Clear(); List<int> numbers = new List<int> { 2, 3, 4, 5 }; numbers.Add(6); // 2,3,4,5,6 numbers.Insert(0, 1); // 1,2,3,4,5,6 numbers.RemoveAt(2); // 1,2,4,5,6 numbers.Reverse(); // 6,5,4,2,1
But there are three interfaces that you should be aware of to understand System.Collection
, because each class implements one or more. These interfaces come as non-generic interfaces, but we'll be focusing solely on the generic ones. The hierarchy of the interfaces is as follows:
IEnumerable<T>
ICollection<T>
IList<T>/IDictionary<K, V>
Firstly, IEnumerable<T>
is the standard interface for making collections enumerable. Consequently, making it possible to get enumerated by using the foreach
statement. It also only has one method, GetEnumerator
, that returns an IEnumerator<T>
object.
Secondly, ICollection<T>
is the standard interface for collections. It contains properties and methods like Count
, Contains
, ToArray
, Add
, Remove
, and Clear
. It also extends IEnumerable<T>
, consequently making it traversable, with foreach
.
Thirdly, IList<T>
is the standard interface for collections indexable by position. It allows us to read, write, insert, and remove elements at positions (via an indexer). We also get methods such as IndexOf
(linear search), Insert
, and RemoveAt
. Lastly, it extends both ICollection<T>
and IEnumerable<T>
.
List-like Collections
- LinkedList
- Queue
- Stack
- BitArray
1. LinkedList
LinkedList<T>
is a doubly-linked list that implements IEnumerable<T>
and ICollection<T>
. It does not implement IList<T>
because it's not indexable by position. The linked list does not also use an internal array.
var linkedList = new LinkedList<string>(); linkedList.AddFirst("head"); linkedList.AddLast("tail"); linkedList.AddAfter(linkedList.First, "middle"); LinkedListNode<string> middleNode = linkedList.Find("middle"); linkedList.Remove(middleNode);
We do not have a linked list available by default in JavaScript. You can instead create your own or use a library like yallist (Yet Another Linked List).
2. Queue
Queue<T>
and Queue
do not implement IList<T>
or IList
since they are not indexable. But, they use an internal array that's resizeable. They contain the methods you'd expect from a queue:
Queue<int> numbers = new Queue<int>(); numbers.Enqueue(1); numbers.Enqueue(2); numbers.Peek(); // 1 numbers.Dequeue(); numbers.Peek(); // 2
We do not have a Queue data structure in JavaScript - but we have Array.prototype.push
and Array.prototype.shift
. Those methods achieve something similar to enqueue
and dequeue
. But sadly, shift
is O(n), so you might want to create your own queue for high inputs.
const queue = []; queue.push(1); queue.push(2); queue[0]; // 1 queue.shift(); queue[0]; // 2
3. Stack
Stack<T>
and Stack
do not implement IList<T>
or IList
since they are not indexable. But it uses an internal array that's resizeable. They contain the methods you'd expect from a stack:
Stack<int> numbers = new Stack<int>(); numbers.Push(1); numbers.Push(2); numbers.Peek(); // 2 numbers.Pop(); numbers.Peek(); // 1
We do not have a Stack data structure in JavaScript - but we have Array.prototype.push
and Array.prototype.pop
, which provides the core functionality of a stack. For the peek
method you'll do arrayStack[arrayStack.length-1]
:
const stack = []; stack.push(1); stack.push(2); stack[stack.length - 1]; // 2 stack.pop(); stack[stack.length - 1]; // 1
4. BitArray
BitArray
is dynamically sized and contains bool
values. You can use it instead of an array containing booleans. Because each value within a boolean array occupies 1 byte of memory, while a BitArray
only occupies 1 bit. By the way, let me know if you know of anything similar in JavaScript.
var bits = new BitArray(4); bits[0] = true;
C# vs JS: Array Examples
Let's end this article by looking at some code examples that compare C# arrays to JS arrays.
- Index out of bound
- Comparing Arrays
- Sorting Arrays
- Length property
- Creating Multidimensional Arrays
C# vs JS: Index out of bound
There is no "index out of bounds" exception in JavaScript, instead undefined
gets returned:
const arr = [1, 2]; arr[2]; // undefined
C#, on the other hand, throws an IndexOutOfRangeException
exception if you try to access an element with an index that is outside the bounds of the array:
var arr = new int[] { 1, 2 }; var val = arr[2]; // Throws: IndexOutOfRangeException
C# vs JS: Comparing Arrays
When we assign an array (object) to a variable in JavaScript, the variable will hold a reference to the array's location in memory - not the actual value. So when comparing two arrays, we compare the references, not the values:
[1, 2, 3] === [1, 2, 3]; // false
So if we would compare two similar arrays, then they would be deemed not to be equal. To combat this, we may use a different set of approaches. One of them is to JSON.stringify(array1) === JSON.stringify(array2)
, but that may lead to unexcepted results with some values. For instance, '1' === new String('1')
returns false
, while JSON.stringify(['1']) === JSON.stringify([new String('1')])
returns true
. Thus, a more viable option is to use a combination of Array.prototype.every()
and the length
property. But even that falls short for nested arrays in some cases.
const arraysAreEqual = (arr1, arr2) => ( arr1.length === arr2.length && arr1.every((v, i) => v === arr2[i]) ); const arr1 = [1, 2, 3]; const arr2 = [1, 2, 3]; arraysAreEqual([1, 2, 3], [1, 2, 3]); // true arraysAreEqual([1, 2, 3], [1, 2, 6]); // false arraysAreEqual([arr1], [arr1]); // true arraysAreEqual([arr1], [arr2]); // false
We have the same "problem" in C# where the equals operator only returns true if they are the same instance.
var array1 = new int[] { 1, 2, 3 }; var array2 = new int[] { 1, 2, 3 }; array1 == array2; // False
Luckily, the System.Linq
namespace (which we'll cover in detail in a future article) makes our lives easier by giving us SequenceEqual
:
var array1 = new int[] { 1, 2, 3 }; var array2 = new int[] { 1, 2, 3 }; array1.SequenceEqual(array2); // True
It does work for arrays of arrays (Jagged Array) in some cases, but we're back to the reference issue again:
// Example 1 int[] array = new int[] { 1, 2 }; int[][] nestedArray = new int[][] { array }; int[][] nestedArray2 = new int[][] { array }; nestedArray.SequenceEqual(nestedArray2); // True // Example 2 int[][] nestedArray = new int[][] { new int[] { 1, 2 } }; int[][] nestedArray2 = new int[][] { new int[] { 1, 2 } }; nestedArray.SequenceEqual(nestedArray2); // False
C# vs JS: Sorting Arrays
In C#, the elements in the array must implement IComparable
, which gets used for order comparisons. Most built-in types already implement it and can therefore be sorted. If they don't implement it or if you want to override it, then you may provide your custom comparison logic with two different approaches:
- Create a helper object that implements
IComparer/IComparer<T>
. - Create a comparison delegate.
Here's how it can look like with a comparison delegate:
int[] array = new int[] { 1, 2, 3, 4 }; Array.Sort(array, (x, y) => y - x); String.Join(", ", array); // 4, 3, 2, 1
Another approach is to use LINQ, but we'll cover that in later articles. Lastly, I would point out that both sort
in JavaScript and Sort
in C# sorts in place - which often is not desirable.
const arr = [1, 2, 3, 4]; arr.sort((x, y) => y - x); arr; // [ 4, 3, 2, 1 ]
C# vs JS: Length property
You might expect that JavaScript's arrays' length
property returns the number of elements it holds. But instead, it returns the biggest index plus one. I'll leave you with a couple of examples that perhaps will make you want to look up answers:
const arr = [1, 2]; arr.length; // 2 arr[1000] = 3; arr.length; // 1001 arr; // [ 1, 2, <8 empty slots>, … ] for (let element in arr) { console.log(element); } // 0 // 1 // 1000 for (let element of arr) { console.log(element); } // 1 // 2 // (998) undefined // 3 arr.forEach(el => console.log(el)); // 1 // 2 // 3
While in C#, it returns the number of elements in the Array as you'd expect. For example, all indexes not set are by default 0
with int arrays:
int[] arr = new int[1001]; arr[1000] = 1; Console.WriteLine(arr.Length); // 1001 foreach (var x in arr) Console.WriteLine(x); // (1000) 0 // 1
C# vs JS: Creating Multidimensional Arrays
A two-dimensional array is a matrix with rows and columns. C# lets you declare multidimensional arrays, while in JavaScript, we must build them. So, to imitate a two-dimensional array, in JavaScript, you create an array of arrays:
const matrix = [ [1, 4, 7], [2, 5, 8], [3, 6, 9] ]; const [row1, row2, row3] = matrix; row1[0]; // 1 row2[1]; // 5 row3[2]; // 9 const createMatrix = (m, n) => ( Array.from({ length: m }, (_, i) => Array.from({ length: n }, (_, j) => 1 + i + n * j)) );
To create a two-dimensional array in C#, you declare it from the start. Note that a two-dimensional array in C# is not the same as a Jagged Array:
int[,] matrix = new int[3,3] { { 1, 4, 7 }, { 2, 5, 8 }, { 3, 6, 9 } };