Creating and using a generic interface

Generic interfaces work in much the same way as the previous examples in generics. Let's assume that we want to find the properties of certain classes in our code, but we can't be sure how many classes we will need to inspect. A generic interface could come in very handy here.

Getting ready

We need to inspect several classes for their properties. To do this, we will create a generic interface that will return a list of all the properties found for a class as a list of strings.

How to do it…

Let's take a look at the following implementation of the generic interface as follows:

  1. Go ahead and create a generic interface called IListClassProperties<T>. The interface will define a method that needs to be used called GetPropertyList() that simply uses a LINQ query to return a List<string> object:
    interface IListClassProperties<T>
    {
        List<string> GetPropertyList();
    }
  2. Next, create a generic class called InspectClass<T>. Let the generic class implement the IListClassProperties<T> interface created in the previous step:
    public class InspectClass<T> : IListClassProperties<T>
    {
    
    }
  3. As usual, Visual Studio will highlight that the interface member GetPropertyList() has not been implemented in the InspectClass<T> generic class:
    How to do it…
  4. To show any potential fixes, type Ctrl + . (period) and implement the interface implicitly:
    How to do it…
  5. This will create the GetPropertyList() method in your InspectClass<T> class without any implementation. You will add the implementation in a moment. If you try to run your code without adding any implementation to the GetpropertyList() method, the compiler will throw NotImplementedException:
    public class InspectClass<T> : IListClassProperties<T>
    {
        public List<string> GetPropertyList()
        {
            throw new NotImplementedException();
        }
    }
  6. Next, add a constructor to your InspectClass<T> class that takes a generic type parameter and sets it equal to the private variable _classToInspect that you also need to create. This is setting up the code that we will use to instantiate the InspectClass<T> object. We will pass to the object we need a list of properties from the constructor, and the constructor will set the private variable _classToInspect so that we can use it in our GetPropertyList() method implementation:
    public class InspectClass<T> : IListClassProperties<T>
    {
        T _classToInspect;
        public InspectClass(T classToInspect)
        {
            _classToInspect = classToInspect;
        }
    
        public List<string> GetPropertyList()
        {
            throw new NotImplementedException();
        }
    }
  7. To finish off our class, we need to add some implementation to the GetPropertyList() method. It is here that the LINQ query will be used to return a List<string> object of all the properties contained in the class supplied to the constructor:
    public List<string> GetPropertyList()
    {
        return _classToInspect.GetType().GetProperties().Select(p => p.Name).ToList();
    }
  8. Moving to our console application, go ahead and create a simple class called Invoice. This is one of several classes that can be used in the system, and the Invoice class is one of the smaller classes. It usually just holds invoice data specific to a record in the invoices records of the data store you connect to. We need to find a list of the properties in this class:
    public class Invoice
    {
        public int ID { get; set; }
        public decimal TotalValue { get; set; }
        public int LineNumber { get; set; }
        public string StockItem { get; set; }
        public decimal ItemPrice { get; set; }
        public int Qty { get; set; }
    }
  9. We can now make use of our InspectClass<T> generic class that implements the IListClassProperties<T> generic interface. To do this, we will create a new instance of the Invoice class. We will then instantiate the InspectClass<T> class, passing the type in the angle brackets and the oInvoice object to the constructor. We are now ready to call the GetPropertyList() method. The result is returned to a List<string> object called lstProps. We can then run foreach on the list, writing the value of each property variable to the console window:
    Invoice oInvoice = new Invoice();
    InspectClass<Invoice> oClassInspector = new InspectClass<Invoice>(oInvoice);
    List<string> lstProps = oClassInspector.GetPropertyList();
    
    foreach(string property in lstProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
  10. Go ahead and run the code to see the output generated by inspecting the properties of the Invoice class:
    How to do it…

    As you can see, the properties are listed as they exist in the Invoice class. The IListClassProperties<T> generic interface and the InspectClass<T> class don't care what type of class they need to inspect. They will take any class, run the code on it, and produce a result.

But the preceding implementation still poses a slight problem. Let's have a look at one of the variation of this problem:

  1. Consider the following code in the console application:
    InspectClass<int> oClassInspector = new InspectClass<int>(10);
    List<string> lstProps = oClassInspector.GetPropertyList();
    foreach (string property in lstProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();

    You can see that we have easily passed an integer value and type to the InspectClass<T> class, and the code does not show any warnings at all. In fact, if you ran this code, nothing would be returned and nothing outputs to the console window. What we need to do is implement the constraints on our generic class and interface.

  2. At the end of the interface implementation after the class, add the where T : class clause. The code now needs to look like this:
    public class InspectClass<T> : IListClassProperties<T> where T : class
    {
        T _classToInspect;
        public InspectClass(T classToInspect)
        {
            _classToInspect = classToInspect;
        }
    
        public List<string> GetPropertyList()
        {
            return _classToInspect.GetType().GetProperties().Select(p => p.Name).ToList();
        }
    }
  3. If we returned to our console application code, you will see that Visual Studio has underlined the int type passed to the InspectClass<T> class:
    How to do it…

    The reason for this is because we have defined a constraint against our generic class and interface. We have told the compiler that we only accept reference types. Therefore, this applies to any class, interface array, type, or delegate. Our Invoice class will therefore be a valid type, and the constraint will not apply to it.

We can also be more specific in our type parameter constraints. The reason for this is that we perhaps do not want to constrain the parameters to reference types. If we, for example, wanted to button down the generic class and interface to only accept classes created inside our current system, we can implement a constraint that the argument for T needs to be derived from a specific object. Here, we can use abstract classes again:

  1. Create an abstract class called AcmeObject and specify that all classes that inherit from AcmeObject implement a property called ID:
    public abstract class AcmeObject
    {
        public abstract int ID { get; set; }
    }
  2. We can now ensure that objects we create in our code for which we need to read the properties from are derived from AcmeObject. To apply the constraint, modify the generic class and place the where T : AcmeObject constraint after the interface implementation. Your code should now look like this:
    public class InspectClass<T> : IListClassProperties<T> where T : AcmeObject
    {
        T _classToInspect;
        public InspectClass(T classToInspect)
        {
            _classToInspect = classToInspect;
        }
    
        public List<string> GetPropertyList()
        {
            return _classToInspect.GetType().GetProperties().Select(p => p.Name).ToList();
        }
    }
  3. In the console application, modify the Invoice class to inherit from the AcmeObject abstract class. Implement the ID property as defined in the abstract class:
    public class Invoice : AcmeObject
    {
        public override int ID { get; set; }
        public decimal TotalValue { get; set; }
        public int LineNumber { get; set; }
        public string StockItem { get; set; }
        public decimal ItemPrice { get; set; }
        public int Qty { get; set; }            
    }
  4. Create two more classes called SalesOrder and CreditNote. This time, however, only make the SalesOrder class inherit from AcmeObject. Leave the CreditNote object as is. This is so that we can clearly see how the constraint can be applied:
    public class SalesOrder : AcmeObject
    {
        public override int ID { get; set; }
        public decimal TotalValue { get; set; }
        public int LineNumber { get; set; }
        public string StockItem { get; set; }
        public decimal ItemPrice { get; set; }
        public int Qty { get; set; }
    }
    
    public class CreditNote
    {
        public int ID { get; set; }
        public decimal TotalValue { get; set; }
        public int LineNumber { get; set; }
        public string StockItem { get; set; }
        public decimal ItemPrice { get; set; }
        public int Qty { get; set; }
    }
  5. Create the code needed to get the property list for the Invoice and SalesOrder classes. The code is straightforward, and we can see that Visual Studio does not complain about either of these two classes:
    Invoice oInvoice = new Invoice();
    InspectClass<Invoice> oInvClassInspector = new InspectClass<Invoice>(oInvoice);
    List<string> invProps = oInvClassInspector.GetPropertyList();
    
    foreach (string property in invProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
    SalesOrder oSalesOrder = new SalesOrder();
    InspectClass<SalesOrder> oSoClassInspector = new InspectClass<SalesOrder>(oSalesOrder);
    List<string> soProps = oSoClassInspector.GetPropertyList();
    
    foreach (string property in soProps)
    {
        Console.WriteLine(property);
    }
    Console.ReadLine();
  6. If, however, we had to try do the same for our CreditNote class, we will see that Visual Studio will warn us that we can't pass the CreditNote class to the InspectClass<T> class because the constraint we implemented only accepts objects that derive from our AcmeObject abstract class. By doing this, we have effectively taken control over exactly what we allow to be passed to our generic class and interface by means of constraints:
    How to do it…

How it works…

Speaking of generic interfaces, we have seen that we can implement behavior on a generic class by implementing a generic interface. The power of using the generic class and generic interface is well illustrated earlier.

Having said that, we do believe that knowing when to use constraints is also important so that you can close down your generic classes to only accept the specific types that you want. This ensures that you don't get any surprises when someone accidently passes an integer to your generic class.

Finally, the constraints that you can use are as follows:

  • where T: struct: The type argument must be any value types
  • where T: class: The type argument must be any reference types
  • where T: new(): The type argument needs to have a parameterless constructor
  • where T: <base class name>: The type argument must derive from the given base class
  • where T: <T must derive from object>: T The type argument must derive from the object after the colon
  • where T: <interface>: The type argument must implement the interface specified
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.12.186