Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Professional Visual Studio 2005 (2006) [eng]

.pdf
Скачиваний:
117
Добавлен:
16.08.2013
Размер:
21.9 Mб
Скачать

Chapter 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