;

C# Generic & Non-generic Collections


Collections are essential in any programming language for organizing, managing, and manipulating groups of objects. In C#, collections are divided into two types: generic collections and non-generic collections. Both types serve to hold multiple items, but they differ in functionality, performance, and type safety. Understanding these differences helps in making the right choice for optimal performance and maintainable code.

Introduction to Collections in C#

Collections in C# store and manage groups of related items, making data handling more convenient. Collections can be resized automatically, offer different ways to access data, and enable actions such as sorting, filtering, and searching.

Collections are primarily split into two main categories in C#:

  • Non-Generic Collections: Work with object types, allowing mixed data types but lacking type safety.
  • Generic Collections: Type-safe collections that allow only specific data types.

What Are Non-Generic Collections?

Non-generic collections are part of the System.Collections namespace in C#. They are flexible but lack type safety, which can lead to runtime errors. These collections work with objects, meaning they can store any data type. However, every item is treated as an object, and you need to cast types when accessing elements.

Advantages of Non-Generic Collections

  • Flexibility: Can store any type of object.
  • Backward Compatibility: Useful when dealing with older .NET code.

Drawbacks of Non-Generic Collections

  • Lack of Type Safety: No compile-time checking, making it easy to introduce runtime errors.
  • Performance Overhead: Due to boxing/unboxing with value types.

Examples of Non-Generic Collections

ArrayList

ArrayList is a resizable array. However, since it is non-generic, any type of object can be added.

using System;
using System.Collections;

ArrayList myList = new ArrayList();
myList.Add(10);                 // int
myList.Add("Hello");             // string
myList.Add(DateTime.Now);        // DateTime

foreach (var item in myList)
{
    Console.WriteLine(item);
}

Hashtable

A hashtable stores key-value pairs, where keys are unique. It’s similar to a dictionary, but it's non-generic.

Hashtable myTable = new Hashtable();
myTable.Add("ID", 1);
myTable.Add("Name", "Alice");
myTable.Add("Country", "USA");

Console.WriteLine(myTable["Name"]);  // Output: Alice

Use Cases for Non-Generic Collections

  • Working with legacy code
  • When flexibility is prioritized over type safety
  • Quick prototyping without enforcing data types

What Are Generic Collections?

Generic collections, found in the System.Collections.Generic namespace, allow for type safety at compile time. They prevent runtime errors related to type mismatches, enhance performance, and are easier to maintain.

Advantages of Generic Collections

  • Type Safety: You define the type of data the collection will hold, reducing errors.
  • Performance: Avoids boxing/unboxing, which is beneficial for performance.

Drawbacks of Generic Collections

  • Type Restriction: Requires specifying a type, which reduces flexibility.

Examples of Generic Collections

List<T>

List<T> is a strongly-typed, dynamic array that only stores elements of the specified type T.

List<int> numbers = new List<int> { 1, 2, 3, 4, 5 };
numbers.Add(6);

foreach (int number in numbers)
{
    Console.WriteLine(number);
}

Dictionary<TKey, TValue>

A dictionary stores key-value pairs, where each key is unique and strongly typed.

Dictionary<int, string> employeeNames = new Dictionary<int, string>();
employeeNames.Add(1, "Alice");
employeeNames.Add(2, "Bob");

Console.WriteLine(employeeNames[1]);  // Output: Alice

Queue<T>

Queues are useful for processing items in a first-in, first-out (FIFO) order. They’re commonly used in scenarios like task scheduling.

Queue<string> tasks = new Queue<string>();
tasks.Enqueue("Task1");
tasks.Enqueue("Task2");
tasks.Enqueue("Task3");

Console.WriteLine(tasks.Dequeue());  // Output: Task1

Use Cases for Generic Collections

  • List<T>: Dynamic arrays where you need type safety.
  • Dictionary<TKey, TValue>: Fast lookups with key-value pairs.
  • Queue<T> and Stack<T>: Order-sensitive operations.

Real-World Example: Managing a Shopping Cart

Consider an e-commerce application where users add items to their shopping carts. Using generic collections such as List<T> for a cart and Dictionary<TKey, TValue> for inventory is ideal.

Example

using System;
using System.Collections.Generic;

public class Product
{
    public int ProductId { get; set; }
    public string Name { get; set; }
    public double Price { get; set; }
}

public class ShoppingCart
{
    private List<Product> cartItems = new List<Product>();

    public void AddProduct(Product product)
    {
        cartItems.Add(product);
        Console.WriteLine($"{product.Name} added to the cart.");
    }

    public void RemoveProduct(int productId)
    {
        var product = cartItems.Find(p => p.ProductId == productId);
        if (product != null)
        {
            cartItems.Remove(product);
            Console.WriteLine($"{product.Name} removed from the cart.");
        }
    }

    public void DisplayCart()
    {
        Console.WriteLine("Shopping Cart:");
        foreach (var item in cartItems)
        {
            Console.WriteLine($"{item.Name} - ${item.Price}");
        }
    }
}

public class Inventory
{
    private Dictionary<int, Product> productInventory = new Dictionary<int, Product>();

    public void AddToInventory(Product product)
    {
        productInventory[product.ProductId] = product;
    }

    public Product GetProductById(int id)
    {
        return productInventory.ContainsKey(id) ? productInventory[id] : null;
    }
}

public class Program
{
    public static void Main()
    {
        Inventory inventory = new Inventory();
        inventory.AddToInventory(new Product { ProductId = 1, Name = "Laptop", Price = 1500.00 });
        inventory.AddToInventory(new Product { ProductId = 2, Name = "Smartphone", Price = 800.00 });

        ShoppingCart cart = new ShoppingCart();
        Product laptop = inventory.GetProductById(1);
        Product phone = inventory.GetProductById(2);

        if (laptop != null) cart.AddProduct(laptop);
        if (phone != null) cart.AddProduct(phone);

        cart.DisplayCart();
    }
}
Explanation
  • Inventory uses a Dictionary<int, Product> to store and retrieve products by their ID.
  • ShoppingCart uses a List<Product> to keep track of products added by a user.
  • The Inventory dictionary enables quick product lookup by ProductId, while the ShoppingCart list allows adding and removing items easily.

Key Takeaways

  • Generic vs. Non-Generic: Generic collections provide type safety and performance benefits, while non-generic collections offer flexibility.
  • Common Non-Generic Collections: ArrayList and Hashtable, suitable for backward compatibility and flexibility.
  • Common Generic Collections: List<T>, Dictionary<TKey, TValue>, Queue<T>, and Stack<T> are widely used for type safety and performance.
  • Real-World Applications: Use generic collections to enforce data consistency and non-generic collections when flexibility is needed.

Summary

In C#, generic and non-generic collections provide diverse options for managing groups of objects. Non-generic collections offer flexibility, but their lack of type safety can lead to errors. Generic collections, on the other hand, enforce type safety, improve performance, and simplify code maintenance. By understanding the differences and appropriate use cases for each, you can write more efficient and reliable code. Whether you’re implementing a shopping cart, handling inventory, or processing tasks in order, C# collections are essential tools that contribute to clean, structured, and high-performing applications.