Professional Visual Studio 2005 (2006) [eng]
.pdfChapter 22
‘Creation - custom collection
Dim myList As New IntegerCollection
‘Adding - type checking, so can only add integers
myList.Add(1)
‘Retrieving - type checking, so no casting required output = myList.Item(0)
Clearly, the second approach is preferable because it ensures that you put only integers into the collection. However, the downside of this approach is that you have to create collection classes for each type you want to put in a collection. You can rewrite this example using the Generic List class:
‘Creation - generic list, specifying the type of objects it contains Dim genericList As New List(Of Integer)
‘Adding - type checking
genericList.Add(1) ‘Retrieving - type checking output = genericList.Item(0)
This example has the benefits of the strongly typed collection without the overhead of having to rewrite the collection for each type. To create a collection that holds strings, all you have to do is change the type argument of the List — for example, List(Of String).
In summary, generic types have one or more type parameters that will be defined when an instance of the type is declared. From the example you just saw, the class List has a type parameter, T, which, when specified, determines the type of items in the collection. The following sections describe in more detail how to consume, create, and constrain generic types.
Consumption
You have just seen a VB.NET example of how to consume the Generic List to provide either a collection of integers or a collection of strings. You can accomplish this by supplying the type parameter as part of the declaration. The following code snippets illustrate the consumption of generic types for both VB.NET and C#:
C#
Dictionary<String,double> scores = new Dictionary<String,double>();
VB.NET
Dim scores As New Dictionary(Of String, Double)
There are also generic methods, which also have a type parameter that must be supplied when the method is invoked. This is illustrated in calling the Choose method, which randomly picks one of the two arguments passed in:
C#
newValue=Chooser.Choose<int>(5, 6);
newValue=Chooser.Choose(7, 8);
VB.NET
newValue = Chooser.Choose(of Integer)(5,6)
newValue = Chooser.Choose(7,8)
286
Generics, Nullable Types, and Partial Types
In these examples, you can see that a type argument has been supplied in the first line but omitted in the second line. You’re able to do this because the type inference process kicks in to determine what the type argument should be.
Creation
To create a generic type, you need to define the type parameters that must be provided when the type is constructed, performed as part of the type signature. In the following example, the ObjectMapper class defines two type parameters, TSource and TDestination, that need to be supplied when an instance of this class is declared:
C#
public class ObjectMapper<TSource, TDestination>
{
private TSource source;
private TDestination destination;
public ObjectMapper(TSource src , TDestination dest )
{
source = src; destination = dest;
}
}
VB.NET
Public Class ObjectMapper(Of TSource, TDestination)
Private source As TSource
Private destination As TDestination
Public Sub New(ByVal src As TSource, ByVal dest As TDestination) source = src
destination = dest End Sub
End Class
A naming convention for type parameters is to begin them with the letter T, followed by some sort of descriptive name if there is more than one type parameter. In this case, the two parameters define the type of source and destination objects to be provided in the mapping.
Generic methods are defined using a similar syntax as part of the method signature. Although generic methods may often be placed within a generic type, that is not a requirement; in fact, they can exist anywhere a non-generic method can be written. The following CreateObjectMapper method takes two objects of different types and returns a new ObjectMapper object, passing the type arguments for the method through to the constructor:
C#
public static ObjectMapper<TCreateSrc, TCreateDest>
CreateObjectMapper<TCreateSrc, TCreateDest> (TCreateSrc src, TCreateDest dest)
{
return new ObjectMapper<TCreateSrc, TCreateDest>(src, dest);
}
287
Chapter 22
VB.NET
Public Shared Function CreateObjectMapper(Of TCreateSrc, TCreateDest) _
(ByVal src As TCreateSrc, ByVal dest As TCreateDest) _
As ObjectMapper(Of TCreateSrc, TCreateDest)
Return New ObjectMapper(Of TCreateSrc, TCreateDest)(src, dest)
End Function
Constraints
So far, you have seen how to create and consume generic types and methods. However, having type parameters limits what you can do with the parameter because you only have access to the basic object methods such as GetType, Equals, and ToString. Without more information about the type parameter, you are limited to building simple lists and collections. To make generics more useful, you can place constraints on the type parameters to ensure that they have a basic set of functionality. The following example places constraints on both parameters:
C#
public class ObjectMapper<TSource, TDestination>
: IComparable<ObjectMapper<TSource,TDestination>> where TSource: IComparable<TSource>
where TDestination: new()
{
private TSource source;
private TDestination destination;
public ObjectMapper(TSource src)
{
source = src;
destination = new TDestination();
}
public int CompareTo(ObjectMapper<TSource,TDestination> mapper)
{
return source.CompareTo(mapper.source);
}
}
VB.NET
Public Class ObjectMapper(Of TSource As IComparable(Of TSource), _
TDestination As New)
Implements IComparable(Of ObjectMapper(Of TSource, TDestination))
Private source As TSource
Private destination As TDestination
Public Sub New(ByVal src As TSource) source = src
destination = new TDestination End Sub
Public Function CompareTo _
(ByVal other As ObjectMapper(Of TSource, TDestination)) As Integer _ Implements System.IComparable(Of ObjectMapper _
(Of TSource, TDestination)).CompareTo
Return source.CompareTo(other.source) End Function
End Class
288
Generics, Nullable Types, and Partial Types
The TSource parameter is required to implement the IComparable interface so that an object of that type can be compared to another object of the same type. This is used in the CompareTo, which implements the IComparable interface for the ObjectMapper class, to compare the two source objects. The TDestination parameter requires a constructor that takes no arguments. The constructor is changed so that instead of a destination object being provided, it is created as part of the constructor.
This example covered interface and constructor constraints. The full list of constraints is as follows:
Base class: Constrains the type parameter to be, or be derived from, the class specified
Class or Structure: Constrains the type parameter to be a class or a structure (a struct in C#)
Interface: Constrains the type parameter to implement the interface specified
Constructor: Constrains the type parameter to expose a no-parameter constructor
Multiple constraints can be supplied by separating the constraints with a comma, as shown in these snippets:
C#
public class MultipleConstraintClass<T>
where IComparable, new()
{...}
VB.NET
Public Class MultipleConstraintClass(Of T As {IComparable,new})
...
End Class
Nullable Types
Any developer who has worked with a database understands some of the pain that goes into aligning business objects with database schemas. One of the difficulties has been that the default value for a database column could be nothing (as in not specified), even if the column was an integer. In .NET, value types, such as integers, always have a value. When pulling information from the database, it was necessary to add additional logic that would maintain state for the database columns to indicate whether a value had been set. Two of the most prominent solutions to this problem were to either adjust the database schema to prevent nothing values, which can be an issue where a field is optional, or to add a Boolean flag for every field that could be nothing, which added considerable amounts of code to even a simple application.
Generic types provide a mechanism to bridge this divide in quite an efficient manner, using the generic Nullable type. The Nullable type is a generic structure that has a single type parameter, which is the type it will be wrapping. It also contains a flag indicating whether a value exists, as shown in the following snippet:
Public Structure Nullable(Of T As Structure)
Private m_hasValue As Boolean
Private m_value As T
Public Sub New(ByVal value As T)
Me.m_value = value
289
Chapter 22
Me.m_hasValue = True
End Sub
Public ReadOnly Property HasValue() As Boolean
Get
Return Me.m_hasValue
End Get
End Property
Public ReadOnly Property Value() As T
Get
If Not Me.HasValue Then
Throw new Exception(“...”)
End If
Return Me.m_value
End Get
End Property
Public Function GetValueOrDefault() As T
Return Me.m_value
End Function
Public Function GetValueOrDefault(ByVal defaultValue As T) As T
If Not Me.HasValue Then
Return defaultValue
End If
Return Me.m_value
End Function
Public Shared Narrowing Operator CType(ByVal value As Nullable(Of T)) As T Return value.get_Value
End Operator
Public Shared Widening Operator CType(ByVal value As T) As Nullable(Of T) Return New Nullable(Of T)(value)
End Operator End Structure
This code indicates how you can create a new Nullable type by specifying a type argument and calling the constructor. However, the last two methods in this structure are operators that allow conversion between the Nullable type and the type argument provided. Conversion operators are covered later in this chapter, but for now it is sufficient to understand that conversion from the type argument to a Nullable type is allowed using implicit conversion, whereas the reverse requires explicit casting. You can also see that the type parameter, T, is constrained to be a structure. Because class variables are object references, they are implicitly nullable.
The following example creates and uses a Nullable type. You can see that C# has additional support for Nullable types with an abbreviated syntax:
C#
Nullable<int>x=5; int? y,z;
if (x.HasValue) y=x.Value;
290
Generics, Nullable Types, and Partial Types
else
y=8;
z=x?? + y??7; int? w = x + y;
VB.NET
Dim x, y, z As Nullable(Of Integer) x = 5
If x.HasValue Then y = x.Value
Else
y = 8 End If
z = x.GetValueOrDefault + y.GetValueOrDefault(7)
In these examples, both languages can use the HasValue property to determine whether a value has been assigned to the Nullable type. If it has, the Value property can be used to retrieve the underlying value. The Value property throws an exception if no value has been specified. Having to test before you access the value property is rather tedious, so the GetValueOrDefault function was added. This retrieves the value if one has been supplied; otherwise, it returns the default value. There are two overloads to this method, with and without an alternative value. If an alternative value is supplied, this is the default value that is returned if no value has been supplied. Alternatively, the default value is defined as the zero-initialized underlying type. For example, if the underlying type were a Point, made up of two double values, the default value would be a Point with both values set to zero.
C# uses two abbreviations to make working with Nullable types easier. First, Nullable<int> can be abbreviated as int?, which defines a Nullable integer variable. The second abbreviation is the null coalescing operator, ??. This is used to abbreviate the GetValueOrDefault function. The last line of the C# snippet shows an interesting feature, which is that C# supports null propagation. If either x or y are null, the null value propagates to w. This is the equivalent of the following:
int? w = x.HasValue && y.HasValue ? x.Value + y.Value : (int?)null;
Par tial Types
Partial types are a simple concept that enable a single type to be split across multiple files. The files are combined at compile time into a single type. As such, Partial types cannot be used to add or modify functionality in existing types. The most common reason to use Partial types is to separate generated code. In the past, elaborate class hierarchies had to be created to add additional functionality to a generated class due to fear of that code being overwritten when the class was regenerated. Using Partial types, the generated code can be partitioned into a separate file, and additional code added to a file where it will not be overwritten by the generator.
Partial classes are defined by using the Partial keyword in the type definition. The following example defines a Person class across two files:
‘File 1 – fields and constructor Partial Public Class Person
Private m_Name As String
Private m_Age As Integer
291
Chapter 22
Public Sub New(ByVal name As String, ByVal age As Integer) Me.m_Name = name
Me.m_Age = age End Sub
End Class
‘File 2 – public properties Public Class Person
Public ReadOnly Property Age() As Integer Get
Return Me.m_Age
End Get
End Property
Public ReadOnly Property Name() As String
Get
Return Me.m_Name
End Get
End Property
End Class
You will notice that the Partial keyword is used only in one of the files. This is specific to VB.NET, as C# requires all partial classes to use this keyword. The disadvantage there is that the Partial keyword needs to be added to the generated file. The other difference in C# is that the Partial keyword appears after the class accessibility keyword (in this case, Public).
Form Designers
Both the Windows and Web Forms designer make use of Partial types to separate the designer code from event handlers and other code written by the developer. The Windows Forms designer generates code into an associated designer file. For example, for Form1.vb there would also be Form1.designer.vb. In addition to protecting your code so that it isn’t overwritten by the generated code, having the designer code in a separate file also trims down the code files for each form. Typically, the code file would only contain event handlers and other custom code.
In the previous version of Visual Studio, web forms were split across two files where controls had to be defined in both the designer file and the code-behind files so event handlers could be wired up. The designer file inherited from the code-behind file, which introduced another level of complexity. With Partial types, this has been simplified, with controls being defined in the designer file and only event handlers being defined in the code file. The code file is now a code-beside file, as both the code and designer information belong to the same class.
Operator Overloading
Both VB.NET and C# now support operator overloading, which means that you can define the behavior for standard operators such as +, -, / and *. You can also define type conversion operators that control how casting is handled between different types.
Operators
The syntax for operator overloading is very similar to a static method except that it includes the Operator keyword, as shown in the following example:
292
Generics, Nullable Types, and Partial Types
C#
public class OperatorBaseClass{ private int m_value;
public static OperatorBaseClass operator +(OperatorBaseClass op1 , OperatorBaseClass op2 )
{
OperatorBaseClass obc =new OperatorBaseClass(); obc.m_value = op1.m_value + op2.m_value; return obc;
}
}
VB.NET
Public Class OperatorBaseClass
Private m_value As Integer
Public Shared Operator +(ByVal op1 As OperatorBaseClass, _
ByVal op2 As OperatorBaseClass) As OperatorBaseClass
Dim obc As New OperatorBaseClass obc.m_value = op1.m_value + op2.m_value Return obc
End Operator End Class
In both languages, a binary operator overload requires two parameters and a return value. The first value, op1, appears to the left of the operator, with the second on the right side. Clearly, the return value is substituted into the equation in place of all three input symbols. Although it makes more sense to make both input parameters and the return value the same type, this is not necessarily the case, and this syntax can be used to define the effect of the operator on any pair of types. The one condition is that one of the input parameters must be of the same type that contains the overloaded operator.
Type Conversions
A type conversion is the process of converting a value of one type to another type. These can be broadly categorized into widening and narrowing conversions. In a widening conversion, the original type has all the necessary information to produce the new type. As such, this conversion can be done implicitly and should never fail. An example would be casting a derived type to its base type. Conversely, in a narrowing conversion, the original type may not have all the necessary information to produce the new type.
An example would be casting a base type to a derived type. This conversion cannot be guaranteed, and needs to be done via an explicit cast.
The following example illustrates conversions between two classes, Person and Employee. Converting from a Person to an Employee is a well-known conversion, as an employee’s initial wage can be defined as a multiple of their age (for example, when they are employed). However, converting an Employee to a Person is not necessarily correct, as an employee’s current wage may no longer be a reflection on their age:
C#
public class Employee
{
...
293
Chapter 22
static public implicit operator Employee(Person p)
{
Employee emp=new Employee(); emp.m_Name=p.Name; emp.m_Wage = p.Age * 1000; return emp;
}
static public explicit operator Person(Employee emp)
{
Person p = new Person(); p.Name = emp.m_Name; p.Age=(int)emp.m_Wage/1000; return p;
}
}
VB.NET
Public Class Employee
...
Public Shared Widening Operator CType(ByVal p As Person) As Employee Dim emp As New Employee
emp.m_Name = p.Name emp.m_Wage = p.Age * 1000 Return emp
End Operator
Public Shared Narrowing Operator CType(ByVal emp As Employee) As Person Dim p As New Person
p.Name = emp.m_Name
p.Age = CInt(emp.m_Wage / 1000) Return p
End Operator End Class
Why Static Methods Are Bad
Now that you know how to overload operators and create your own type conversions, this section serves as a disclaimer stating that static methods should be avoided at all costs. Because both type conversions and operator overloads are static methods, they are only relevant for the type for which they are defined. This can cause all manner of grief and unexpected results when you have complex inheritance trees. To illustrate how you can get unexpected results, consider the following example:
C#
Public Class FirstTier Public Value As Integer
Public Shared Widening Operator CType(ByVal obj As FirstTier) As String Return “First Tier: “ & obj.Value.ToString
End Operator
Public Overrides Function ToString() As String Return “First Tier: “ & Me.Value.ToString
End Function End Class
Public Class SecondTier
294
Generics, Nullable Types, and Partial Types
Inherits FirstTier
Public Overloads Shared Widening Operator CType(ByVal obj As SecondTier) _
As String
Return “Second Tier: “ & obj.Value.ToString End Operator
Public Overrides Function ToString() As String
Return “Second Tier: “ & Me.Value.ToString
End Function
End Class
VB.NET
‘Sample code to call conversion and tostring functions Public Class Form1
Private Sub BtnLanguageClick(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles BtnLanguage.Click
Dim foo As New SecondTier foo.Value = 5
Dim bar As FirstTier = foo
Console.WriteLine(“<SecondTier> ToString “ & vbTab & foo.ToString)
Console.WriteLine(“<SecondTier> CStr “ & vbTab & CStr(foo))
Console.WriteLine(“<FirstTier> ToString “ & vbTab & bar.ToString) Console.WriteLine(“<FirstTier> CStr “ & vbTab & CStr(bar))
End Sub End Class
The output from this sample is as follows:
<SecondTier> ToString |
Second Tier: 5 |
<SecondTier> CStr |
Second Tier: 5 |
<FirstTier> ToString |
Second Tier: 5 |
<FirstTier> CStr |
First Tier: 5 |
|
|
As you can see from the sample, the last cast gives an unusual response. In the first two casts, you are dealing with a SecondTier variable, so both ToString and CStr operations are called from the SecondTier class. When you cast the object to a FirstTier variable, the ToString operation is still routed to the SecondTier class, as this overrides the functionality in the FirstTier. However, because the CStr operation is a static function, it is routed to the FirstTier class, as this is the type of variable. Clearly, the safest option here is to ensure that you implement and call the ToString method on the instance variable. This rule holds for other operators such as equals, which can be overridden instead of defining the = operator. In cases where you need a +, -, / or * operator, consider using non-static Add, Subtract, Divide, and Multiply operators that can be run on an instance.
Predefined Delegates
The event-driven model that forms an integral part of the .NET Framework is built around the concept of a delegate, or function pointer. In fact, an event, as you will see later, is no more than a multicast delegate, or a delegate that when invoked triggers multiple functions (such as listeners or event handlers).
295