
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:
- Go ahead and create a generic interface called
IListClassProperties<T>
. The interface will define a method that needs to be used calledGetPropertyList()
that simply uses a LINQ query to return aList<string>
object:interface IListClassProperties<T> { List<string> GetPropertyList(); }
- Next, create a generic class called
InspectClass<T>
. Let the generic class implement theIListClassProperties<T>
interface created in the previous step:public class InspectClass<T> : IListClassProperties<T> { }
- As usual, Visual Studio will highlight that the interface member
GetPropertyList()
has not been implemented in theInspectClass<T>
generic class: - To show any potential fixes, type Ctrl + . (period) and implement the interface implicitly:
- This will create the
GetPropertyList()
method in yourInspectClass<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 theGetpropertyList()
method, the compiler will throwNotImplementedException
:public class InspectClass<T> : IListClassProperties<T> { public List<string> GetPropertyList() { throw new NotImplementedException(); } }
- 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 theInspectClass<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 ourGetPropertyList()
method implementation:public class InspectClass<T> : IListClassProperties<T> { T _classToInspect; public InspectClass(T classToInspect) { _classToInspect = classToInspect; } public List<string> GetPropertyList() { throw new NotImplementedException(); } }
- 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 aList<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(); }
- 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 theInvoice
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; } }
- We can now make use of our
InspectClass<T>
generic class that implements theIListClassProperties<T>
generic interface. To do this, we will create a new instance of theInvoice
class. We will then instantiate theInspectClass<T>
class, passing the type in the angle brackets and theoInvoice
object to the constructor. We are now ready to call theGetPropertyList()
method. The result is returned to aList<string>
object calledlstProps
. We can then runforeach
on the list, writing the value of eachproperty
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();
- Go ahead and run the code to see the output generated by inspecting the properties of the
Invoice
class:As you can see, the properties are listed as they exist in the
Invoice
class. TheIListClassProperties<T>
generic interface and theInspectClass<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:
- 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. - 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(); } }
- If we returned to our console application code, you will see that Visual Studio has underlined the
int
type passed to theInspectClass<T>
class: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:
- Create an abstract class called
AcmeObject
and specify that all classes that inherit fromAcmeObject
implement a property calledID
:public abstract class AcmeObject { public abstract int ID { get; set; } }
- 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 thewhere 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(); } }
- In the console application, modify the
Invoice
class to inherit from theAcmeObject
abstract class. Implement theID
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; } }
- Create two more classes called
SalesOrder
andCreditNote
. This time, however, only make theSalesOrder
class inherit fromAcmeObject
. Leave theCreditNote
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; } }
- Create the code needed to get the property list for the
Invoice
andSalesOrder
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();
- 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 theCreditNote
class to theInspectClass<T>
class because the constraint we implemented only accepts objects that derive from ourAcmeObject
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 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 typeswhere T: class
: The type argument must be any reference typeswhere T: new()
: The type argument needs to have a parameterless constructorwhere T: <base class name>
: The type argument must derive from the given base classwhere T: <T must derive from object>
:T
The type argument must derive from the object after the colonwhere T: <interface>
: The type argument must implement the interface specified