C# Collections & Generics

A Comprehensive Guide to Non-Generic and Generic Collections

This guide covers both the older non-generic collections and the modern generic collections in C#, with detailed explanations, examples, and comparisons.

Non-Generic Collections

Non-generic collections store elements as object types, which means they can store any type of data but require casting when retrieving items. They are found in the System.Collections namespace.

Advantages

  • Can store different types in the same collection
  • Available in older .NET versions
  • Useful for compatibility with legacy code

Disadvantages

  • Performance overhead due to boxing/unboxing
  • No type safety at compile time
  • Casting exceptions at runtime
  • Less readable code

ArrayList

Dynamic array that can grow and shrink automatically.

using System.Collections;

ArrayList list = new ArrayList();
list.Add("Hello");
list.Add(42);
list.Add(DateTime.Now);

// Must cast when retrieving
string first = (string)list[0];
int second = (int)list[1];

// This will compile but cause runtime error
// int invalid = (int)list[0];

Hashtable

Collection of key-value pairs organized by hash code.

Hashtable table = new Hashtable();
table.Add("key1", "value1");
table.Add(123, "value2");
table.Add(45.6, "value3");

// Retrieving values
string val1 = (string)table["key1"];
string val2 = (string)table[123];

// Iterating through hashtable
foreach (DictionaryEntry entry in table) {
    Console.WriteLine($"{entry.Key}: {entry.Value}");
}

Queue & Stack

First-In-First-Out (FIFO) and Last-In-First-Out (LIFO) collections.

// Queue example
Queue queue = new Queue();
queue.Enqueue("first");
queue.Enqueue("second");
queue.Enqueue(123);

string firstItem = (string)queue.Dequeue();

// Stack example
Stack stack = new Stack();
stack.Push("bottom");
stack.Push("middle");
stack.Push(456);

string topItem = (string)stack.Pop();

Generic Collections

Generic collections are type-safe collections that specify the type of elements they contain. They are found in the System.Collections.Generic namespace and provide better performance and type safety.

Advantages

  • Type safety at compile time
  • Better performance (no boxing/unboxing)
  • Cleaner code with no casting needed
  • IntelliSense support for specific types

Disadvantages

  • Can only store one type of element
  • Not available in very old .NET versions

List<T>

Strongly typed list of objects that can be accessed by index.

using System.Collections.Generic;

List stringList = new List();
stringList.Add("first");
stringList.Add("second");
// stringList.Add(123); // Compile error!

string first = stringList[0]; // No casting needed

List intList = new List { 1, 2, 3, 4, 5 };
int sum = 0;
foreach (int num in intList) {
    sum += num; // No unboxing overhead
}

Dictionary<TKey, TValue>

Collection of key-value pairs with fast lookups by key.

Dictionary ages = new Dictionary();
ages.Add("John", 25);
ages.Add("Jane", 30);
// ages.Add(123, 45); // Compile error - wrong key type

int johnAge = ages["John"]; // No casting needed

// Iterating with type safety
foreach (KeyValuePair entry in ages) {
    Console.WriteLine($"{entry.Key} is {entry.Value} years old");
}

Queue<T> & Stack<T>

Generic versions of FIFO and LIFO collections.

// Generic Queue
Queue stringQueue = new Queue();
stringQueue.Enqueue("first");
stringQueue.Enqueue("second");
// stringQueue.Enqueue(123); // Compile error

string first = stringQueue.Dequeue(); // No casting

// Generic Stack
Stack intStack = new Stack();
intStack.Push(1);
intStack.Push(2);
// intStack.Push("three"); // Compile error

int top = intStack.Pop(); // No casting

HashSet<T> & SortedSet<T>

Collections that store unique elements with high-performance set operations.

HashSet set1 = new HashSet { 1, 2, 3, 4, 5 };
HashSet set2 = new HashSet { 4, 5, 6, 7, 8 };

// Set operations
set1.UnionWith(set2);        // set1 becomes {1,2,3,4,5,6,7,8}
set1.IntersectWith(set2);    // set1 becomes {4,5}
set1.ExceptWith(set2);       // set1 becomes {1,2,3}

// SortedSet maintains order
SortedSet sortedSet = new SortedSet { "zebra", "apple", "banana" };
// sortedSet is { "apple", "banana", "zebra" }

Comparison & Recommendations

Collection Type Namespace Type Safety Performance When to Use
Non-Generic ArrayList System.Collections ❌ No ❌ Slow (boxing) Legacy code, mixed types
Generic List<T> System.Collections.Generic ✅ Yes ✅ Fast Most cases needing a list
Non-Generic Hashtable System.Collections ❌ No ❌ Slow (boxing) Legacy code, mixed types
Generic Dictionary<TKey, TValue> System.Collections.Generic ✅ Yes ✅ Fast Key-value lookups
Non-Generic Queue/Stack System.Collections ❌ No ❌ Slow (boxing) Legacy code
Generic Queue<T>/Stack<T> System.Collections.Generic ✅ Yes ✅ Fast FIFO/LIFO collections

Recommendation: For new development, always prefer generic collections over non-generic ones for better performance, type safety, and code clarity.

Specialized Collections

.NET provides several specialized collections for specific scenarios:

// ObservableCollection - notifies when changes occur
ObservableCollection observable = new ObservableCollection();
observable.CollectionChanged += (s, e) => {
    Console.WriteLine("Collection changed!");
};

// Concurrent Collections - thread-safe
ConcurrentDictionary concurrentDict = new ConcurrentDictionary();
ConcurrentQueue concurrentQueue = new ConcurrentQueue();
ConcurrentStack concurrentStack = new ConcurrentStack();

// ReadOnlyCollection - provides a read-only wrapper
List originalList = new List { 1, 2, 3 };
ReadOnlyCollection readOnly = new ReadOnlyCollection(originalList);
// readOnly.Add(4); // Compile error - collection is read-only