Using generics to manage a collection of objects
Introduction
Generics refer to classes and methods that work uniformly on values of different types . They offer some rich capabilities that enhance the coding experience through better performance and easier maintenance. Generics enable you to define classes that use a generic type, and define the type at the time of instantiation or method calls. This makes your code strongly typed, but makes maintenance easier.
Generics enable you to define classes, interfaces, delegates or methods with placeholders for parameterized types used within. At the time of instantiation or method calls, these placeholders are replaced with concrete types.
Let’s say that you need to manipulate a collection of strings. You can use a collection object that is not strongly typed – that is, it manipulates a collection of object instances – or you can create a strongly typed collection to handle a collection of strings.
The first approach will be prone to error because your code will not be restrictive enough nor strongly typed. In the second approach, defining a class that derives from collection for each strongly typed collection you require becomes a maintenance nightmare, since you need to manage many collection classes.
Using generics, a single collection is defined and the type of the objects contained inside is marked with a placeholder. When creating a collection, you specify the type the placeholder denotes.
Generic collections
The .NET Framework 2.0 provides generic collections and interfaces that make it easier to manipulate a collection or define your own. These classes are contained within the namespace System.Collections.Generics.
Below is a simple example that uses a generic List<T> class. Let’s see how we can use it to manipulate a collection of objects, but let’s first define a simple Employee class that we will use inside our list:
C# Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class Employee { private string name; private double salary; public string Name { get { return name; } set { name = value; } } public double Salary { get { return salary; } set { salary = value; } } public Employee(string name, double salary) { { this.name=name; this.salary=salary; } } |
Defining and using a collection of employees is simplified when using the List<T>. The <T> denotes the placeholder for the type the list contains. We can define our strongly typed collection by replacing the placeholder with the actual type – “Employee” in our example.
C# Code
1 2 3 4 5 |
System.Collections.Generic.List<Employee> col = new List<Employee>(); col.Add(new Employee("John",25500)); col.Add(new Employee("Smith", 32000)); col.Add("This will be a compile error"); //can only add Employees Employee emp = col [0]; //no typecasting required |
Sorting a list
Sorting collections is also simplified using the generic list. The System.Collections.Generic.List has a sort method that accepts a generic Comparison<T>. Comparison<T> is a delegate that takes two parameters of type T, where T is the type (Employee) used to instantiate our collection.
Since we are handling a collection of employees in our sample code, Comparison<Employee> will be a delegate that points to a method that accepts two employee objects. An integer result is returned after the comparison is performed between the two employee objects passed to it.
C# Code
1 2 3 4 5 |
private int CompareBySalary(Employee emp1, Employee emp2) { return emp1.Salary.CompareTo(emp2.Salary); } |
The method above compares two employees by salary and returns the comparison result. To sort our generic employee list, we simple plug the sort method into the Comparison<Employee> delegate.
C# Code
1 2 3 |
Comparison<Employee> compareBySalary; compareBySalary = new Comparison< Employee>(CompareBySalary); col.Sort(compareBySalary); |
Searching a list
Searching collections is a matter of using a delegate that does the filtering for us. In our collection example, the Find and FindAll methods in List<T> use a Predicate<T> delegate that accepts a parameter of type T. T is the type used to instantiate our collection that returns a boolean indicating whether or not a match is found.
The following class wraps a method that accepts an employee and indicates in the return whether the salary of the employee is greater than the amount.
C# Code
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class SalaryFilter { private int amount; public bool FilterBySalary(Employee emp) { return emp.Salary > amount; } public SalaryFilter(int value) { amount = value; } } |
The above class encapsulates the function of searching by the salary of an employee.
To search our previous list and return all the employees who have a salary greater than 1,000, we need to point to our method defined inside SalaryFilter using a Predicate<Employee> delegate, and pass it to the Find method.
C# Code
1 2 3 4 |
Predicate<Employee> filterBySalary; SalaryFilter filter = new SalaryFilter(1000); filterBySalary = new Predicate< Employee>(filter.FilterBySalary); List<Employee> lstEmps= col.FindAll(filterBySalary); |
The difference between the Find and FindAll methods is that Find returns a single object and FindAll returns a collection of objects using a list of the type we are handling.
Iterating a list
Iterating objects and performing simple operations on them in the list has been simplified in .NET 2.0. In .NET 1.1, you would use a ForEach loop to loop through a collection of objects and perform actions. In .NET 2.0, you can use the Action<T> delegate to perform operations on objects inside a list.
Let’s create a class that encapsultes functionality to add the salaries of the employee objects.
C# Code
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class SalaryCalculator { private double amount; public double Amount { get { return amount; } set { amount = value; } } public void CalculateSalary(Employee emp) { amount += emp.Salary; } } |
We point to the CalculateSalary method using an Action<Employee> delegate, and pass it to the ForEach method of the list.
C# Code
1 2 3 4 |
SalaryCalculator calc = new SalaryCalculator(); Action<Employee> action; action = new Action<Employee>(calc.CalculateSalary); col.ForEach(action); |
List conversion
Converting a list is useful when you want a subset of the data in the collection. If you have a collection of employee objects, for example, you might need to retrieve a collection of string objects of the employee names.
In .NET 1.1, you would have to loop through the employee objects, add one to a new collection and return it. In .NET 2.0, you can simply point to a method using a Converter<TInput,TOutput> delegate that accepts a parameter of type TInput (in our case Employee) and it returns an object of type TOutput.
C# Code
1 2 3 4 |
private string ConvertEmpToStr(Employee emp) { return emp.Name; } |
The method above shows how an employee object should be converted to a string object. To convert the entire list, we use the method above with a Converter<Employee,string> delegate.
C# Code
1 2 3 4 |
Converter<Employee, string> conv; conv = new Converter<Employee, string>(ConvertEmpToStr); System.Collections.Generic. List<string> empNames = col.ConvertAll<string>(conv); |
Conclusion
This article reviewed the enhancements in .NET 2.0 that enable generic List<T> to better handle collections. Some special features implemented using generics were also introduced.
Load comments