CHAPTER 6
You’ve seen arrays in previous chapters and they can be useful in scenarios where you need a fixed size, strongly typed list of objects. However, there are many times when you need to organize objects into different types of data structures like lists, queues, stacks, and dictionaries. These capabilities are available to C# developers via collection classes in the .NET Framework.
A core part of working with collections is the use of generics, which allow you to use parameterized code. This allows you to strongly type your collections. You can even write your own code that uses generics, allowing you to create strongly typed reusable libraries.
Note: The first version of .NET offered a collection library based on Object which isn’t strongly typed. Since all .NET types are assignable to Object, this worked. However, you had to write a lot of code that used cast operators to convert from Object back to the type you added to the collection. Generics solves this problem, and using generic collections is standard practice in .NET today.
.NET collection classes let you work with data in many different ways. Instead of an array, you can use a List. If you need a first-in first-out set of items, you can use a Queue. If you need to work with items that have unique IDs, you can use a Dictionary. With generics, you can build your own collection to manage data any way that you need.
Tip: Check out the System.Collections.Generic namespace before writing your own collection; you might find that what you need is already written.
using System; using System.Collections.Generic; public class Company { public string Name { get; set; } } public class Program { public static void Main() { List<string> names = new List<string>(); names.Add("Joe"); names.Insert(0, "Car"); names.Add("Jill"); names[0] = "Building"; names.RemoveAt(0); Console.WriteLine($"First name: {names[0]}"); IList<Company> companies = new List<Company> { new Company { Name = "Syncfusion" }, new Company { Name = "Microsoft" }, new Company { Name = "Acme" } }; foreach (Company cmp in companies) Console.WriteLine(cmp.Name); Console.ReadKey(); } } |
The previous program demonstrates the versatility of collections. You have a List of string, which only holds objects of type string. This is a generic collection, meaning that its type parameters, inside < and >, specify the type of object the collection works with. Each item added is appended to the list and the list grows dynamically. The Insert operation adds a new string at the first position of the list and pushes down the first, "Joe", into the second position at index 1. The second Add puts "Jill" at index 2. Notice how you can use indexer (array-like) syntax to access elements of the list. The RemoveAt deleted the string at the first index of the collection, moving "Joe" to 0 and "Jill" to 1.
The second List in Main shows how to use custom types. Since List derives from IList, you can assign the instance to that interface. This is convenient because it means you can create code that operates on an IList; whether the caller passes in a List<T> or any other collection type that derives from IList, your code will still work.
The example also uses collection initialization syntax, where you can instantiate a comma-separated list of the collection type that populates the List. The foreach statement iterates through the collection, printing each item.
The previous example uses a foreach loop, but you could also use the ForEach method of List, as shown in the following example.
List<Company> companyList = companies as List<Company>; companyList.ForEach(cmp => Console.WriteLine(cmp.Name)); |
The first line uses the as operator to convert the IList<Company> to List<Company>. With an instance of List<T>, you can call the ForEach method, which takes a lambda parameter. This lambda executes for each of the items in the List<T> and the lambda parameter, cmp, contains the current item.
This should give you an idea of how List works. There are more methods available that you can learn about by reading the documentation for the List class.
Another useful collection is a Dictionary. It works like a hash table where you store and retrieve objects by index as shown in the following sample.
using System; using System.Collections.Generic; public class Customer { public int ID { get; set; } public string Name { get; set; } } public class Program { public static void Main() { Dictionary<int, Customer> customers = new Dictionary<int, Customer>(); Customer jane = new Customer { ID = 0, Name = "Jane" }; Customer joe = new Customer { ID = 1, Name = "Joe" }; customers.Add(jane.ID, jane); customers[joe.ID] = joe; foreach (int key in customers.Keys) Console.WriteLine(customers[key].Name); Dictionary<int, Customer> customers2 = new Dictionary<int, Customer> { [0] = new Customer { ID = 0, Name = "Chris" }, [1] = new Customer { ID = 1, Name = "Alex" } }; Console.ReadKey(); } } |
A Dictionary in the previous example takes two type parameters for the key and value, respectively. The first example instantiates the dictionary to take an int key and Customer value. The Customer class has two properties, where the ID will be used as a key for the dictionary. Notice the two different ways you can add values to a dictionary, via the Add method or indexer assignment. The first parameter to Add is the index and the second is the value. When using the indexer, put the index in brackets and assign the value. Just as in other collections, there are many methods available and you should review the documentation of that collection.
The foreach loop shows how to iterate through Dictionary items. A Dictionary has a Keys property, which is a collection of keys, and a Values property, which is a collection of values (the Customer instances in the previous example). Notice how the loop uses the indexer customers[key] to access the value associated with each key.
The second Dictionary in the example shows how to use the dictionary initializer syntax. Just assign the value to the index that matches the key for that value.
One of the primary applications of generics is to support collections. In the previous section, you saw how to use collections. You could also write your own collection class. If you wrote a generic linked list, you would need a Node class to hold an object and reference the next in the list, and a LinkedList collection class that performed list operations. The Node class in the following listing contains an object instance.
class Node<T> { public T Item { get; set; } public Node<T> Next; public Node(T item) { Item = item; } } |
The <T> syntax makes the Node<T> class generic. Whenever code instantiates a Node, it specifies a type that replaces T. Anywhere you’re using an object of that type, specify T. Node<T> doesn’t have an access modifier because it’s only used with the code inside this assembly and the default internal accessibility is appropriate. The following sample shows how to instantiate a Node<T>.
Node<string> name = new Node<string>(“May”); |
Here, you see Node<string> as the type, meaning that all of the places you see T inside of the Node class are now string. You’re protected from passing an int, decimal, or any other type to the constructor of this class because it will only hold a string. It is strongly typed.
Next, you need a collection that holds Node<T> instances as a linked list, as shown in the following listing.
Using System; using System.Collections; using System.Collections.Generic; public class LinkedList<T> : IList<T> { Node<T> head; Node<T> tail; public void Add(T item) { var node = new Node<T>(item); if (head == null) head = node; else tail.Next = node; tail = node; } // Other IList members… } |
The LinkedList class is generic and holds items of the type it’s instantiated as. The IList<T> interface belongs to the FCL and facilitates creating collections. As you would expect with interfaces, developers who write code to the IList interface can use this collection too. The LinkedList class implements all the members of the IList<T> interface, as it must.
Add is a minimal implementation, but illustrates some concepts of working with generics. Even though the code instantiates a new Node<T>, the actual type will be the same as the type that LinkedList is defined as. The same concept applies to the interface where IList<T> becomes the same type as LinkedList. The following example instantiates a LinkedList<T>.
public class Program { public static void Main() { var llist = new LinkedList<string>(); llist.Add("Jamie"); llist.Add("Ron"); //... Node<string> name = new Node<string>("May"); } } |
This shows that you instantiate and use your generic collection like any other collection. Just supply the type during instantiation and the collection will work with objects of that type.
Any place you see the object type being used is a potential candidate for creating a generic type. All types inherit the object type, which is why you’ll see types in the FCL and elsewhere work with object type values.
You can also create generic methods. The following example shows a couple factory methods where one is type object and the other is generic.
using System; public class CustomerReport { public DateTime Date { get; set; } } public class OrdersReport { public DateTime Date { get; set; } } public class ReportFactory { public static object Create(Type reportType) { switch (reportType.ToString()) { case "CustomerReport": var custRpt = new CustomerReport(); custRpt.Date = DateTime.Now; return custRpt; default: case "OrdersReport": var ordsRpt = new OrdersReport(); ordsRpt.Date = DateTime.Now; return ordsRpt; } } } public class Program { public static void Main() { var rpt = (CustomerReport)ReportFactory.Create(typeof(CustomerReport)); Console.ReadKey(); } } |
What you should get out of the previous ReportFactory implementation is that there’s a lot of duplication in the code and the use of cast and typeof operators in the Main method includes more syntax than necessary. You can probably see where this code might become less maintainable with more complexity. The following example shows how to refactor the Create method into a generic method.
using System; public abstract class Report { } public class CustomerReport : Report { public DateTime Date { get; set; } } public class OrdersReport : Report { public DateTime Date { get; set; } } public class ReportFactory { public static TReport Create<TReport>() where TReport : Report { switch (typeof(TReport).Name) { case "CustomerReport": var custRpt = new CustomerReport(); custRpt.Date = DateTime.Now; return (TReport)(Report)custRpt; default: case "OrdersReport": var ordsRpt = new OrdersReport(); ordsRpt.Date = DateTime.Now; return (TReport)(Report)ordsRpt; } } } public class Program { public static void Main() { var rpt2 = ReportFactory.Create<CustomerReport>(); Console.ReadKey(); } } |
The Create method has a new type parameter, TReport. You’ve seen the use of just T in previous examples, but sometimes—as in Dictionary<TKey and TValue>—you have to differentiate between multiple type parameters or make the code more self-documenting. The return type is now strongly typed too. The code is able to cast from the derived type to Report, and then to TReport to return the proper type. This is allowed because of the generic constraint, where TReport : Report says that TReport must derive from Report. The calling code is much simpler.
The Create<TReport> method is still longer than it has to be and contains too much duplication. We can solve that problem with generic constraints. A constraint does what the name implies: it limits how generic a type can be. You saw the base class constraint on Report in the previous code. The following table describes all available constraints.
Table 3: Generic Type Constraints
Constraint | Description |
|---|---|
interface | Type must implement specified interfaces. |
base class | Type must derive from specified base class. |
class | Type must be a reference type. |
struct | Type must be a value type. |
new | Type must have a default (no parameter) constructor. |
We need two constraints to simplify our code: interface and new. The following example shows how they can be used.
using System; public interface IReport { DateTime Date { get; set; } } public class CustomerReport : IReport { public DateTime Date { get; set; } } public class OrdersReport : Report, IReport { public DateTime Date { get; set; } } public class ReportFactory { public static TReport Create<TReport>() where TReport : IReport, new() { return new TReport() { Date = DateTime.Now }; } } public class Program { public static void Main() { var rpt2 = ReportFactory.Create<CustomerReport>(); Console.ReadKey(); } } |
In this demo, there’s a new interface, IReport, which CustomerReport and OrdersReport derive from. Since we know the classes we expect are IReport, we can make assumptions about the type and write code that operates on any IReport.
The Create<TReport> method has additional syntax following the method signature. To specify a constraint, follow the where keyword with the type being constrained, append a semicolon, and then add a comma-separated list of constraints from the previous table. This example uses an interface and new() constraint. The new() constraint means we can create a new instance of a type, new TReport(). Further, since the type is an IReport, we know it has a Date property and can populate its Date property. Gone are the duplication and excessive code, simplified by generic code in both implementation and use.
Tip: You can also create generic delegates. As usual, you should seek to reuse types already present in the FCL. A popular reusable delegate in the .NET Framework is EventHandler<TEventArgs>. In fact, you can replace all the references to ClickHandler in Chapter 5 with EventHandler<ClickEventArgs> and your code will still work.
You’ve seen how to use generics and that they let you write reusable code. The .NET collection classes are more versatile than arrays and allow you to manage your objects in ways that better help the design of your application.