he Whidbey release of .NET will include a new Common Language Runtime (CLR) feature called generics. Generics allow you to use a variable to represent a desired data type, and thereby create very generic code (hence the name) that works with any data type.
You define the data type for the generic variable at run time and the CLR substitutes that data type for the variable everywhere in the code that it is used; basically providing you with strongly typed generic code.
Understanding Generics
Generics provide for multiple levels of abstraction, which can make them somewhat difficult to get your head around. So before I show you how to use generics in a more complex “real-world” application, it’s best to start with a simple example using structures.
Structures provide a straightforward mechanism for defining a set of related properties and methods. For example, a point consists of an x-value and a y-value, represented by two properties in a point structure.
You could use a structure to define a value with properties for the old value and the new value. If you wanted your value structure to support multiple data types, such as string, integers, and so on, you need to define a different structure for each data type.
In Visual Basic .NET:
' Without generics Public Structure StringValues Public sOldValue As String Public sNewValue As String End Structure Public Structure IntValues Public iOldValue As Int32 Public iNewValue As Int32 End Structure
In C#:
// Without generics public struct StringValues { public string sOldValue; public string sNewValue; } public struct IntValues { public int iOldValue; public int iNewValue; }
This can be unwieldy, especially if you have many different data types that you need to support.
With generics, you can create one set of code and use a variable to represent the desired type. You define the generic type using the Of keyword in Visual Basic .NET and the angle bracket (< >) symbols in C#:
|
In Visual Basic .NET “Whidbey”:
' With generics Public Structure Values(Of T) Public OldValue As T Public NewValue As T End Structure
In C# .NET “Whidbey”:
// With generics public struct Values { public T OldValue; public T NewValue; }
One structure then supports any type of data. You define the desired data type when you create the structure:
In Visual Basic .NET “Whidbey”:
Dim a As New Values(Of String) a.OldValue = "" a.NewValue = "Generics Test" Dim b As New Values(Of Int32) b.OldValue = 0 b.NewValue = 10
In C# .NET “Whidbey”:
Values a = new Values(); a.OldValue = ""; a.NewValue = "Generics Test"; Values b = new Values(); b.OldValue = 0; b.NewValue = 10;
The first three lines in both the Visual Basic .NET and C# examples define a structure of type String and the last three lines define a structure of type Integer. The CLR replaces each reference to the generic variable T with the defined data type?providing a strongly typed structure.
Strongly typed implies that it enforces the data type at compile time. So if you attempt to set the OldValue or NewValue to anything but a String in the first example or an Integer in the second example, you will get a compile error.
Using strong typing improves the quality of your application because it minimizes the possibility of data type errors. And, since it does not have to perform boxing or data type casting, generic code performs better.
Note that in Visual Basic .NET, assigning a value of the wrong type only generates a compile-time error if you use Option Strict On in your project. Otherwise the error occurs at run time.
Building a Generic List
When your application needs to retain a set of data or objects, you can use one of the many collection-type classes provided in the .NET Framework such as Stack, Queue, and Dictionary. These classes allow you to store any type of object in the collection. In some cases this may be a good thing, such as when you want to keep a list of the last set of changed objects. In many cases, however, you want to ensure that a collection only contains objects of a particular type.
|
In Visual Studio .NET 2003 you can only create a strongly-typed collection class by building a collection class for each type. For example, if you want a strongly-typed collection for strings and a strongly-typed collection for integers, you would create two collection classes, one for strings and one for integers. This makes it laborious to create strongly-typed collections for many different types.
With Whidbey and generics, you can create a single collection class and define its type at run time, just like in the prior structure.
In Visual Basic .NET “Whidbey”:
Public Class List(Of ItemType) Private elements() As ItemType End Class
In C# “Whidbey”:
public class List { private T[] elements; }
The Visual Basic .NET example defines a List class with an elements array that will only hold items of a specific type, generically represented in the example as ItemType. Likewise, the C# example defines a List class with an elements array that will only hold items of a specific type, generically represented in the example as T. In both examples, you can use any variable name to represent the generic variables shown as ItemType and T.
When you use the List class, you define the desired data type for the list.
In Visual Basic .NET “Whidbey”:
Dim intList as New List(Of Integer) Dim strList as New List(Of String)
In C# .NET “Whidbey”:
List intList = new List(); List strList = new List();
The first line of each example creates a list of integers and the second line creates a list of strings.
You can then add code to the List class to manage the list. Every List class needs a count of the number of elements, a way to add elements, and a way to get and set element contents.
In Visual Basic .NET “Whidbey”:
Private iIndex As Integer = -1 Public Sub Add(ByVal element As ItemType) ' Start the array out at 10 If iIndex = -1 Then ReDim elements(10) End If If (iIndex = elements.Length) Then ReDim Preserve elements(iIndex + 10) End If iIndex += 1 elements(iIndex) = element End Sub Default Public Property _ Item(ByVal index _ As Integer) As ItemType Get Return elements(index) End Get Set(ByVal Value As ItemType) elements(index) = Value End Set End Property Public ReadOnly Property Count() As Integer Get ' The index starts at 0, the count at 1 ' So adjust the index to the count Return iIndex + 1 End Get End Property
In C# .NET “Whidbey”:
private int count = -1; public void Add(T element) { if (count == -1) { // Start with 10 elements elements = new T[10]; } // Increment by 10 more as needed if (count == elements.Length) { elements = new T[count + 10]; } elements[++count] = element; } public T this[int index] { get { return elements[index]; } set { elements[index] = value; } } public int Count { get { return count; } }
You can then add items and retrieve their contents in a strongly-typed manner. This means that value types, such as integers, won’t be boxed and won’t require casting. It also means that no one can accidentally put anything but an item of the defined type into the list.
You can use the List class to manage a list of integers:
In Visual Basic .NET “Whidbey”:
Dim intList as New List(Of Integer) intList.Add(1) ' No boxing intList.Add(2) ' No boxing intList.Add("Three") ' Compile-time error Dim i As Int32 = intList(0) ' No cast required For j As Int32 = 0 To intList.Count Debug.WriteLine(intList(j).ToString) Next
In C# .NET “Whidbey”:
List intList = new List(); intList.Add(1); // No boxing intList.Add(2); // No boxing //intList.Add("Three"); // Compile-time error int i = intList[0]; // No cast required for (int j = 0; j <=intList.Count; j++) { Debug.WriteLine(intList[j]); }
Since the List class is generic, you can also use it to manage a collection of strings:
In Visual Basic .NET "Whidbey":
Dim strList as New List(Of String) strList.Add("This") strList.Add("tests") strList.Add("generics") For j As Int32 = 0 To strList.Count Debug.WriteLine(strList(j)) ' No cast required Next
In C# .NET "Whidbey":
List strList = new List(); strList.Add("This"); strList.Add("tests"); strList.Add("generics"); for (int j = 0; j<=strList.Count; j++) { Debug.WriteLine(strList[j]); // No cast required }
Using generics you can create one set of code that works with any data type and yet is strongly-typed at run time. And, since there are no boxing or casting operations, you also get improved performance.
Leaving an Audit Trail
Applications with critical business data often require an audit trail. Take invoicing for example. In a perfect world, an application should generate invoices for a business using input data such as time sheets, job reports, or purchase orders. There should be no need for the users to update these documents. But in the real world, there are data input errors, special customer requests, last minute discounts or uplifts, and so on. So users occasionally need to update some invoices. But do you just want any user to be able to update any invoice? (Probably not.)
In addition to building code that controls which users have access to invoicing, you can build features into the application to track which users updated which fields. This provides an auditing mechanism to answer any future questions regarding changes to the invoice.
Though these examples use invoicing, you can apply the concepts similarly to any other business entity where you want to audit changes to the data.
Here I've created an Audit class to collect the set of auditing data. It tracks what field was changed, the old value and new value, the user that made the change, and the date and time that the user made the change.
In Visual Basic .NET "Whidbey":
Public Class Audit Private m_sPropertyName As String Private m_sOriginalValue As String Private m_sNewValue As String Private m_sUserName As String _ = "Deborah" Private m_dtAuditDate As Date End Class
In C# .NET "Whidbey":
public class Audit { string m_sPropertyName=""; string m_sOriginalValue=""; string m_sNewValue =""; string m_sUserName= "Deborah"; DateTime m_dtAuditDate =DateTime.Now; }
Notice that the user name in both examples is hard-coded to my name. In a real application you would want to set the appropriate user's identification information. This may be the username that was used to log into the system or some ID entered into your application.
The constructor for this Audit class sets the properties based on the passed in parameters.
In Visual Basic .NET "Whidbey":
Public Sub New(ByVal sPropertyName As String, _ ByVal sOriginalValue As String, _ ByVal sNewValue As String) m_sPropertyName = sPropertyName m_sOriginalValue = sOriginalValue m_sNewValue = sNewValue m_dtAuditDate = Now End Sub
In C# .NET "Whidbey":
public Audit(string sPropertyName, string sOriginalValue, string sNewValue) { m_sPropertyName = sPropertyName; m_sOriginalValue = sOriginalValue; m_sNewValue = sNewValue; m_dtAuditDate = DateTime.Now; }
You can expose any of the values associated with the Audit object using properties. You may want to consider making the properties read-only so they can be retrieved, but not updated.
In Visual Basic.NET "Whidbey":
Public ReadOnly Property PropertyName() _ As String Get Return m_sPropertyName End Get End Property Public ReadOnly Property AuditDate() _ As Date Get Return m_dtAuditDate End Get End Property
In C# .NET "Whidbey":
public string PropertyName { get {return m_sPropertyName;} } public DateTime AuditDate { get { return m_dtAuditDate ;} }
Each business object could include code that creates Audit objects, but by using a class factory to build the Audit objects you keep that code encapsulated. The class factory generates the Audit objects as needed. In this example, the AuditFactory class builds Audit objects only if the data changed.
In Visual Basic .NET "Whidbey":
Public Class AuditFactory(Of BOType) Public Function Add(ByVal bo As BOType, _ ByVal sPropertyName As String, _ ByVal sNewValue As String) As Audit Dim boPropertyInfo As _ System.Reflection.PropertyInfo = _ bo.GetType.GetProperty(sPropertyName) Dim sOriginalValue As String = _ boPropertyInfo.GetValue(bo, Nothing).ToString If sOriginalValue <> sNewValue Then ' Create an audit entry Dim oAudit As New Audit(sPropertyName, _ sOriginalValue, sNewValue) Return oAudit Else Return Nothing End If End Function End Class
In C# .NET "Whidbey":
public class AuditFactory { public Audit Add(BOType bo, string sPropertyName, string sNewValue) { System.Reflection.PropertyInfo boPropertyInfo = typeof(BOType).GetProperty(sPropertyName); string sOriginalValue= (string)boPropertyInfo.GetValue(bo, null); if (sOriginalValue != sNewValue) { Audit oAudit = new Audit(sPropertyName, sOriginalValue, sNewValue); return oAudit; } else { return null; } } }
The class factory uses generics to define the type of business object that will be audited at run time. The Add method in the class factory uses reflection to get the current value of a particular property. It then compares the current value with the new value that was passed in to this method. If the value is changed then an Audit object is created and returned.
Code in the business object keeps the list of audit records. This example uses an Invoice class, though you can use any class.
In Visual Basic .NET "Whidbey":
Public Class Invoice Dim oInvoiceAudit As New List(Of Audit) Dim oAuditFactory As New AuditFactory(Of Invoice) Dim oAudit As Audit Dim m_sInvoiceDescription As String = "" Public Property InvoiceDescription() As String Get Return m_sInvoiceDescription End Get Set(ByVal Value As String) oAudit = oAuditFactory.Add(Me, _ "InvoiceDescription", Value) If oAudit IsNot Nothing Then oInvoiceAudit.Add(oAudit) End If m_sInvoiceDescription = Value End Set End Property End Class
In C# .NET "Whidbey":
public class Invoice { List oInvoiceAudit = new List(); AuditFactory oAuditFactory = new AuditFactory(); Audit oAudit; string m_sInvoiceDescription = ""; public string InvoiceDescription { get { return m_sInvoiceDescription; } set { oAudit = oAuditFactory.Add(this, "InvoiceDescription", value); if (oAudit != null) { oInvoiceAudit.Add(oAudit); } m_sInvoiceDescription = value; } } }
This code first creates a list of Audit objects. It uses the generic List class to manage the list. It then creates an instance of the AuditFactory, defining that it will create audit records for the Invoice class. An Audit object is declared but not created because the AuditFactory is responsible for creating the Audit objects.
The code for each property in the class then calls the AuditFactory, which compares the original property value and new property value to determine whether to create an Audit object. The property code shown in the examples provide a pattern that you can use to define any other properties of the class.
Generics provide a way to build general code that is specific at run time. Generics give you the best of both worlds: the efficiency of building generic code and the type safety and performance of building strongly-typed code.
The techniques shown in this article only show a small fraction of the power of generics. You can use them in interfaces and in delegates. You can specify constraints to limit the valid data types that your users can use in the generic code. You can define multiple generic types in one class. Plus, the .NET Framework provides a pre-defined set of generic collections.