Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Power of C# Generics - Constraints

0.00/5 (No votes)
26 May 2010 1  
The idea is to make the best use of Generics when we look for code reusability and performance.

Table of Contents

  1. Introduction
  2. Prerequisites
  3. Case Study: Problem Statement
  4. Create Generic Interface IBaseComparer
  5. Create Class Employee and Implement IBaseComparer
  6. Create Generic BaseCompare
  7. Utility Class
  8. Entry Point: Program
  9. Reverse Engineering: Sequence Diagram
  10. Conclusion
  11. Reference

Introduction

The purpose of this article is to showcase the power of Generics that we have in C#. Initially, I was working on IComparable and IComparer for a custom sorting operation; there were scenarios when I wanted to generalize certain parts of my code and I failed to achieve the same. The only reason I was not able to do that was due to lack of knowledge of Generics. I explored the basic concepts of C# Generics that encompasses constraints, covariance and contravariance, and nullable types. I did lots of reading online during my leisure time, and finally I was able to crack the solution.

Prerequisites

  • VS 2010
  • .NET 4.0

Case Study: Problem Statement

While doing a sort operation, we use the Interface IComparer and implement this IComparer in nested class results in a lot of duplicate code. The aim was to replace the duplications with a more generalized construct for all other domain classes that I had in my application class library.

public class Country
{
    private int m_CountryID = int.MinValue;
    private string m_CountryName = string.Empty;
    private int? m_CountryTradeCode = null;

    public Country()
    {

    }
    public Country(int countryID)
    {
        m_CountryID = countryID;

    }
    public Country(int countryID, string countryName, int? countryTradeCode)
        : this(countryID)
    {
        m_CountryName = countryName;
        m_CountryTradeCode = countryTradeCode;
    }
    public int CountryID
    {
        get { return m_CountryID; }
        set { m_CountryID = value; }
    }
    public int? CountryTradeCode
    {
        get { return m_CountryTradeCode; }
        set { m_CountryTradeCode = value; }
    }
    public string CountryName
    {
        get { return m_CountryName; }
        set { m_CountryName = value; }
    }
    public int CompareTo(Country rhs, 
           Country.CountryComparer.CountryComparisonType which)
    {
        switch (which)
        {
            case CountryComparer.CountryComparisonType.CountryID:
                return this.m_CountryID.CompareTo(rhs.m_CountryID);
            case CountryComparer.CountryComparisonType.CountryName:
                return this.m_CountryName.CompareTo(rhs.m_CountryName);
            case CountryComparer.CountryComparisonType.CountryTradeCode:
                return this.m_CountryTradeCode.GetValueOrDefault().CompareTo(
                            rhs.m_CountryTradeCode.GetValueOrDefault());

        }
        return 0;
    }
    public class CountryComparer : IComparer< Country >
    {
        public enum CountryComparisonType
        {
            CountryID,
            CountryName,
            CountryTradeCode,
            NULL
        }
        private CountryComparisonType _whichComparison;
        private Utility.SortOrder _sortDirection;
        public CountryComparisonType WhichComparison
        {
            get { return _whichComparison; }
            set { _whichComparison = value; }
        }
        public int Compare(Country lhs, Country rhs)
        {
            if (SortDirection == Utility.SortOrder.Asc)
                return lhs.CompareTo(rhs, WhichComparison);
            else
                return rhs.CompareTo(lhs, WhichComparison);
        }
        public bool Equals(Country lhs, Country rhs)
        {
            return this.Compare(lhs, rhs) == 0;
        }
        public int GetHashCode(Country e)
        {
            return e.GetHashCode();
        }
        public Utility.SortOrder SortDirection
        {
            get { return _sortDirection; }
            set { _sortDirection = value; }
        }
    }
}

In the above code snippet, the nested class CountryComparer is very much tied to the country class, and it implements IComparer< Country >. My idea was to decouple this CountryComparer class into a more reusable piece of code.

In the above code, if I've to create another domain class, then I need to implement IComparer. In the process, I ended up creating a nested class for each such domain class. For example:

public class Employee
{
    public int CompareTo(Employee obj, 
           EmployeeComparisonType sortexpression)
    {

    }
    class EmployeeComparer: IComparer < Employee>


    //Step 1 Create WhichComparison property

    //Step 2 Implement IComparer.Compare()
    public int IComparer.Compare()
    {
        //Invoke Employee class method Compare()
        return rhs.CompareTo(lhs,WhichComparison);
    }
}

My objective is to decouple the nested class CountryComparer into the BaseComparer class such that it is independent of any class entity. The challenge I faced was to create a generic method CompareTo() in Country which can take any class object and sort expression. The only problem here is that the sorting occurs within objects of its own class and the CompareTo() method is in the main class construct called Country.

Create Generic Interface IBaseComparer

In order to segregate the Compare method from the generic object and sort expression, we need to create a generalised Compare() method in the BaseComparer class. For this to happen, we need to create a contract method CompareTo in the Interface IBaseComparer.

T: Object{Employee,Country,Location....Domain Class Intance}

N: Enum {WhichComparer-Sort On}

Solution: Create an Interface and reference it in BaseComparer using a Constraint.

namespace Domain
{
    public interface IBaseComparer < T,N >
    {
        int CompareTo(T obj,N whichComparer);
        
    }
}

Create Class Employee and Implement IBaseComparer

With the help of the interface, we made CompareTo() available to all classes so as to implement it. Now, compareTo() is generic to all class entities. In the below case, all subscriber classes must declare its respective object type and comparison type.

IBaseComparer < T,N > :IBaseComparer <Employee, Domain.Utility.EmployeeComparisonType>

IBaseComparer < T,N > :IBaseComparer <Country, Domain.Utility.CountryComparisonType>

namespace Domain
{
    public class Employee : IBaseComparer <Employee, Domain.Utility.EmployeeComparisonType>
    {
        private int m_EmployeeID = int.MinValue;
        private string m_EmployeeName = string.Empty;
        private int? m_EmployeeTradeCode = null;

        public Employee(){}
        public Employee(int employeeID)
        {
            m_EmployeeID = employeeID;
        }
        public Employee(int employeeID, string employeeName, 
                        int? employeeTradeCode): this(employeeID)
        {
            m_EmployeeName = employeeName;
            m_EmployeeTradeCode = employeeTradeCode;
        }
        public int employeeID
        {
            get { return m_EmployeeID; }
            set { m_EmployeeID = value; }
        }
        public int? EmployeeTradeCode
        {
            get { return m_EmployeeTradeCode; }
            set { m_EmployeeTradeCode = value; }
        }
        public string EmployeeName
        {
            get { return m_EmployeeName; }
            set { m_EmployeeName = value; }
        }          
        public int CompareTo(Employee rhs,Utility.EmployeeComparisonType WhichComparer)
        {
            switch (WhichComparer)
            {
                case Utility.EmployeeComparisonType.EmployeeID:
                    return this.m_EmployeeID.CompareTo(rhs.m_EmployeeID);
                case Utility.EmployeeComparisonType.EmployeeName:
                    return this.m_EmployeeName.CompareTo(rhs.m_EmployeeName);
                case Utility.EmployeeComparisonType.EmployeeTradeCode:
                    return this.m_EmployeeTradeCode.GetValueOrDefault().CompareTo(
                           rhs.m_EmployeeTradeCode.GetValueOrDefault());
            }
            return 0;
        }             
    }
}

Create Generic BaseCompare

Now the challenge is to get this tied to the Comparer nested class which we planned to separate out. For this, we create the BaseComparer class which takes a generic parameter of Class T object and the N WhichComparer field.

Now, to invoke CompareTo of the respective class, like Employee, Customer, Country etc., we need to assign BaseCompare with the constraint, as where T : IBaseComparer<T,N>. Using the constraint, we instruct the compiler to use the specification of the Contract methods and properties of the interface. The interesting point here is that we have not implemented the Interface here. We have not defined the method CompareTo in the code below. The constraint helped us to invoke the contract method from the class that has implemented it.

The best part is using the constraint to setup a relationship with two different classes. If you look thoroughly, the Employee class and the BaseCompare class have a relationship due to the Interface IBaseComparer. Unlike the problem statement code, we see that the class Country is tightly coupled with the nested class CountryComparer.

To my rescue, I got constraints in Generics, which helped me to surpass all these hurdles. Trust me, it took a week to get to the workaround. The only reason behind is not having touched base on Generics for a long time.

namespace Domain
{
    public class BaseCompare <T,N> : IComparer<T> where T : 
                 IBaseComparer<T,N>
    {
        private Utility.SortOrder _sortDirection;
        private N _whichComparison;        
        public N WhichComparer
        {
            get
            {
                return _whichComparison;
            }
            set
            {
                _whichComparison = value;
            }
        }
        public int Compare(T lhs, T rhs)
        {           
            if (SortDirection == Utility.SortOrder.Asc)
                return lhs.CompareTo(rhs, WhichComparer);
            else
                return rhs.CompareTo(lhs,WhichComparer);
        }        
        public bool Equals(T lhs, T rhs)
        {
            return this.Compare(lhs, rhs) == 0;
        }
        public int GetHashCode(object e)
        {
            return e.GetHashCode();
        }
        public Utility.SortOrder SortDirection
        {
            get { return _sortDirection; }
            set { _sortDirection = value; }
        }
    }

Utility Class

The utility class will have all sort expression definitions and the sort order.

namespace Domain
{
    public class Utility
    {
        public enum EmployeeComparisonType
        {
            EmployeeID,
            EmployeeName,
            EmployeeTradeCode,
            NULL
        }
        public enum SortOrder
        {
            Asc,
            Desc
        }
    }
}

Entry Point: Program

In the code snippet below, we consumed the sorting logic for the IList collection of Employee objects.

namespace GenricsConsoleApp
{
    class Program
    {
        private Utility.SortOrder m_SortOrder;
        private Utility.EmployeeComparisonType m_SortColumn;
        private IList <Employee> m_EmployeeDataSource;
    
        public Utility.EmployeeComparisonType SortColumn
        {
            get { return m_SortColumn; }
            set { m_SortColumn = value; }
        }
        public Utility.SortOrder SortDirection
        {
            get { return m_SortOrder; }
            set { m_SortOrder = value; }
        }
        public IList <Employee> EmployeeDataSource
        {
            get { return m_EmployeeDataSource; }
            set { m_EmployeeDataSource = value; }
        }

        static void Main(string[] args)
        {
            Program obj = new Program();            
            obj.SortDirection = Utility.SortOrder.Desc;
            obj.SortColumn = Utility.EmployeeComparisonType.EmployeeName;
            obj.SortEmployeeList(Utility.EmployeeComparisonType.EmployeeName);

            foreach (Employee emp in obj.EmployeeDataSource)
            {
                Console.WriteLine(emp.EmployeeName);
            }
        }      
        public  void LoadEmployeeList()
        {
            SortEmployeeList(Utility.EmployeeComparisonType.NULL);
        }

        private void SortEmployeeList(Utility.EmployeeComparisonType sortExpression)
        {
            List <Employee> employeeList = (List <Employee>)GetEmployeeList();
            BaseCompare <Employee, Utility.EmployeeComparisonType> 
                        employeeComparer = new BaseCompare <Employee, 
                                                 Utility.EmployeeComparisonType>();
            if (Utility.EmployeeComparisonType.NULL != sortExpression)
            {
                if (sortExpression == SortColumn)
                {
                    if (SortDirection == Utility.SortOrder.Asc)
                    {
                        SortDirection = Utility.SortOrder.Desc;
                    }
                    else
                    {
                        SortDirection = Utility.SortOrder.Asc;
                    }
                }
                else
                {
                    SortDirection = Utility.SortOrder.Asc;
                }
                SortColumn = sortExpression;
                employeeComparer.WhichComparer = SortColumn;
                employeeComparer.SortDirection = SortDirection;
                employeeList.Sort(employeeComparer);
                 m_EmployeeDataSource = employeeList;
            }

            private IList <Employee> GetEmployeeList()
            {
                IList <Employee> employeeList = new List <Employee>();
                employeeList.Add(new Employee(1, "Santosh Poojari", 100001));
                employeeList.Add(new Employee(2, "Sandhya", null));
                employeeList.Add(new Employee(3, "Darsh", null));
                employeeList.Add(new Employee(4, "Gautam Sharma", 100004));
                employeeList.Add(new Employee(5, "Shawn Miranda", 100005));
                employeeList.Add(new Employee(6, "Karan", 100006));
                employeeList.Add(new Employee(7, "Rajan Golambade", 100007));
                employeeList.Add(new Employee(8, "Swati Lakshminarayan", 100008));
                employeeList.Add(new Employee(9, "Vaibhav", null));
                employeeList.Add(new Employee(10, "Rahul Mahurkar", 100011));
                employeeList.Add(new Employee(11, "Abdul Qabiz", 100009));
                employeeList.Add(new Employee(12, "Bibeka Pahi", null));
                return employeeList;
            }            
        }
    }

Reverse Engineering: Sequence Diagram

Here is the complete flow of sorting for a given collection objects for a better understanding. You can get this sequence diagram using the VS2010 Ultimate edition. In VS2010, there are options to create this diagram under the 'Architecture' tab in the main menu.

Conclusion

Any ideas, corrections, and suggestions are most welcome.

References

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here