Chapter 14. Generics and Nullable Types

When you organize your home, you probably place things according to their type. For example, you have a place for food that is different from the place where you put clothes. But foods are also different. You do not treat fish like candy or pizza and so on. So you need safe places for each kind of food, avoiding the risks derived from treating all foods the same way; the same is true for clothes and any other item in your home. The .NET development would be similar without Generics. Consider groups of .NET objects of different types, all grouped into a collection of Object. How can you be sure to treat an item in the collection as you should if the collection stores different types that you identify as Object? What if you want to create just a collection of strings? Generics solve this problem. In this chapter you learn what Generics are. In Chapter 16, “Working with Collections and Iterators,” you learn about generic collections and see why Generics are so useful. This chapter also introduces nullable types, which are generic on their own.

Introducing Generics

Generic types are .NET types that can adapt their behavior to different types of objects without the need of defining a separate version of the type. In other words, you can implement a generic type to work with integers, strings, custom reference types, and any other .NET type with a single implementation. Generics offer several advantages:

Strongly typed programming techniques—Generic objects can hold only the specified type and avoid accidents of handling objects of different types within the same group.

Better performances—Because Generics enable you to handle only the specified type, they avoid the need of boxing and unboxing from and to System.Object, and this retains for performance overhead.

Code reuse—As you will see in a few moments, Generics enable you to create objects that behave the same way and that have the same infrastructure whatever kind of .NET type you pass them.

The ability of writing better code—Avoiding working with nongeneric System.Object, you not only get all IntelliSense features for the specified type, but also can take advantages from not using late-binding techniques.

Generally, the use of Generics is related to the creation of strongly typed collections for storing groups of items of the same type. Because of this, there are two considerations: the first one is that you should check whether the .NET Framework provides a built-in generic collection suiting your needs before creating custom ones (read Chapter 16); the second consideration is that code examples shown in this chapter will be related to creating a generic collection so you know how to create one if .NET built-in Generics are not sufficient for you.

Creating and Consuming Generics

Creating a generic type is accomplished by providing a parameterized type definition. The following is an example:

Class CustomType(Of T)

End Class

The Of keyword is followed by the type that the new object can handle. T is the type parameter and represents the .NET type you want to be held by your generic type. The type parameter’s name is left to your own choosing, but T is often used as a common name in the .NET Framework Base Class Library (BCL). At this point, you must write code to manipulate the T type in a way that will be convenient for possibly every .NET type. Now imagine you want to build a custom collection that you want to reuse with any .NET type. Listing 14.1 shows how to accomplish this.

Listing 14.1. Building a Custom Generic Type


Public Class CustomType(Of T)

    Private items() As T

    Public Sub New(ByVal upperBound As Integer)
        ReDim items(upperBound - 1)
    End Sub

    Private _count As Integer = 0
    'Cannot provide auto-implemented properties when read-only
    Public ReadOnly Property Count As Integer
        Get
            Return _count
        End Get
    End Property

    Public Sub Add(ByVal newItem As T)
        If newItem IsNot Nothing Then
            Me.items(Me._count) = newItem
            Me._count += 1
        End If
    End Sub

    Default Public ReadOnly Property Item(ByVal index As Integer) As T
        Get
            If index < 0 OrElse index >= Me.
               Count Then Throw New IndexOutOfRangeException
            Return items(index)
        End Get
    End Property
End Class



Why Arrays?

You notice that the code in Listing 14.1 uses arrays to store objects. Arrays do not support removing objects or, at least, this cannot be done easily. This is the reason you only find an Add method. By the way, in this particular case you do not need to focus on how to add and remove items (Chapter 16 covers this), but you need to understand how to handle the generic type parameter.


The code shows how simple it is to manage the type parameter. It can represent any .NET type but, as in the previous example, an array can be of that type and store objects of that type. Because arrays cannot be empty, the constructor receives the upper bound that is then used by ReDim. The Add method equally receives an argument of type T whose value is pushed into the array. This introduces another important concept: generic methods. In generic methods, methods can accept generic parameters (named type argument). Notice how a Count property returns the number of items in the array. In this particular scenario, auto-implemented properties cannot be used because a read-only property needs an explicit Get block. Finally, the Item property enables retrieving the specified object in the array at the given index. The new class therefore can handle different types with the same infrastructure.


What Can Be Generics?

You can define as generic the following types: classes, interfaces, delegates, structures, and methods.


Consuming Generic Types

To instantiate and consume generic types, you pass to the constructor the type you want to be handled. For example, the following code creates a new instance of the CustomType class, enabling it to handle only integers or types that are converted to Integer via a widening conversion:

Dim integerCollection As New CustomType(Of Integer)(2)
integerCollection.Add(0)
integerCollection.Add(1)
'Writes 1
Console.WriteLine(integerCollection(1).ToString)

You pass the desired type to the constructor after the Of keyword. When invoking the Add method, you can notice how IntelliSense tells you that the method can receive only Integer (see Figure 14.1).

Image

Figure 14.1. IntelliSense shows what type is accepted by the generic method.

If you pass a different type, you get an error message. But this does not work only with .NET common types. You can use this technique with custom types, too. For example, you can create a generic collection of Person objects (supposing you have defined a Person class in your code) as follows:

Dim onePerson As New Person
onePerson.FirstName = "Alessandro"
onePerson.LastName = "Del Sole"

Dim secondPerson As New Person
secondPerson.FirstName = "Robert"
secondPerson.LastName = "White"

Dim personCollection As New CustomType(Of Person)(2)
personCollection.Add(onePerson)
personCollection.Add(secondPerson)
'Returns 2

Console.WriteLine("How many people are there?")
Console.WriteLine("The answer is {0}", personCollection.Count)

This code produces the result shown in Figure 14.2.

Image

Figure 14.2. Demonstrating the usage of custom generic types.

Generics’ purpose should be clearer now. Their purpose is to provide reusable infrastructures for different types, avoiding mixed groups of objects in favor of strongly typed objects.

Implementing Generic Methods

In Listing 14.1 you saw how to implement a method that receives a generic type parameter. Generic methods are something more. You can add the Of keyword to a generic method to parameterize the method, other than getting generic-type parameters. The following code provides an example, where two arrays of integers are swapped:

'Arrays are passed by reference in this case
Public Sub Swap(Of T1)(ByRef array1() As T1, ByRef array2() As T1)
    Dim temp() As T1
    temp = array1
    array1 = array2
    array2 = temp

End Sub

Continuing the executive code shown in the “Consuming Generic Types” section, the following snippet shows how you can invoke the previous generic method to swap the content of two arrays of integers:

Dim arr1() As Integer = {1, 2, 3}
Dim arr2() As Integer = {4, 5, 6}
integerCollection.Swap(Of Integer)(arr1, arr2)

'Demonstrates that arr2 now
'contains values previously
'stored in arr1
For Each item In arr2
    Console.WriteLine(item)
Next

Similarly, you could have arrays of Person objects and invoke the Swap method on the personCollection class, demonstrating how generic methods can work with different types. For instance, the following code creates a new instance of the Person class continuing the previous example and defines two arrays of Person. Next, it invokes Swap(Of Person) on the personCollection class:

Dim thirdPerson As New Person
thirdPerson.FirstName = "Neil"
thirdPerson.LastName = "Rowe"

Dim personArray1() As Person = {onePerson, secondPerson}
Dim personArray2() As Person = {thirdPerson}
personCollection.Swap(Of Person)(personArray1,
                                 personArray2)

This demonstrates how different types can be targeted with generic methods.

Understanding Constraints

With constraints, you can control Generics’ behavior, and both provide additional functionalities and limit the implementation to specific data types. Let’s begin by understanding constraints on methods.

Methods Constraints

Imagine you want the ability to compare two items within an array. To accomplish this, you need to take advantage of the IComparable interface, and because of this, you want to require that the type argument implements the IComparable interface. The following code demonstrates this:

Public Function CompareItems(Of T As IComparable)(ByVal sourceArray() As T,
                                                  ByVal index1 As Integer,
                                                  ByVal index2 As Integer)
                                                 As Integer

    Dim result As Integer = _
                  sourceArray(index1).CompareTo(sourceArray(index2))

    Return result
End Function

Notice how the As clause in the method argument requires the type to implement the IComparable interface. If the type does not implement the interface, the generic method cannot be used. This simplifies how objects can be compared, in that you can directly invoke the CompareTo method on the first item in the array. This approach is useful for another reason: If you did not specify the IComparable constraint, you could attempt a conversion from T to IComparable at runtime, but this would throw an InvalidCastException if the object does not implement the interface. Therefore, by using constraints you can ensure that your objects suit your needs.

Type Constraints

At a higher level, you can apply constraints to generic objects’ definitions. For example, you can require the type parameter to implement the IComparable interface as follows:

Public Class CustomType(Of T As IComparable)

End Class

You can specify which interfaces the object must implement or which class it has to inherit from. This is an example that accepts types deriving from System.IO.Stream:

Public Class CustomType(Of T As System.IO.Stream)

End Class

In this example, acceptable types would be StreamWriter, StreamReader, BinaryWriter, and BinaryReader objects.

New Constraints

You can combine the As New keywords to require the type argument to expose an explicit parameterless constructor. This is accomplished by the following definition:

Public Class CustomType(Of T As New)
    Public Sub TestInstance()
        Dim instance As New T
    End Sub
End Class

The TestInstance method is an example of how you can instantiate the T type. This approach gives you the ability to create new instances of the type and prevents the type from being an interface or an abstract (MustInherit) class.

Providing Multiple Constraints

You can combine multiple constraints to provide a series of requirements in your generic types. Multiple constraints are enclosed in curly braces and separated by commas. The following code defines a generic type accepting only reference types that implement the ICloneable interface and an explicit parameterless constructor:

Public Class CustomType(Of T As {Class, ICloneable, New})

End Class


Inheritance Constraint

You can also provide inheritance constraints other than interfaces constraint, as described at the beginning of this section. You provide the name of the abstract or base class you require to be inherited in the type parameter.


The Class keyword in the constraint indicates that only reference types are accepted. You use the Structure keyword if you want to accept only value types, keeping in mind that in such a scenario you cannot combine it with the New keyword. The following code demonstrates how you can directly access the Clone method because of the ICloneable implementation constraint:

Public Sub TestConstraint()
    Dim newObj As New T
    Dim clonedObj As Object = newObj.Clone()
End Sub


Nested Types

The type parameter can be used only within the body of the generic type. Nested types can still take advantage of the type parameter as well, so you can create complex infrastructures in your generic types.


Overloading Type Parameters

You can overload generic definitions providing different signatures for the type parameter, similar to what happens in method overloads. The following code provides an example:

Public Class CustomType

End Class

Public Class CustomType(Of T1, T2)

End Class

Public Class CustomType(Of T As {Class, ICloneable, New})

End Class

It is worth mentioning that providing a nongeneric version of your class is not necessary. You can provide different implementations for your generic types. Now consider the following overloading attempt:

Class CustomType(Of T1, T2)

End Class

'Fails at compile time

Class CustomType(Of T1 As IComparable, T2 As ICloneable)

End Class

This code is not compiled because, although in the second definition some constraints are defined, the type implementation is considered by the compiler with the same, identical signature. Similarly, you can provide overloaded methods using techniques learned in Chapter 7, “Class Fundamentals,” but against generic methods as demonstrated in the following code:

Sub DoSomething(Of T1, T2)(ByVal argument1 As T1, ByVal argument2 As T2)

End Sub

Sub DoSomething(Of T)(ByVal argument As T)

End Sub

Overloading provides great granularity over Generics implementation, and you will often see examples in built-in generic collections.

Introducing Nullable Types

As we mentioned when talking about value types and reference types, value types have a default value that is zero, whereas reference types have a default value that is Nothing. This is because a reference type can store null values, but value types cannot. Attempting to assign a null value to a value type would result in resetting to the default value for that type. This is a limitation because there are situations in which you need to store null values in value types, such as when fetching data from a SQL Server database. You can have a hypothetical Orders table where the Ship date column enables null values. SQL Server has its own data types, one of which is the DBNull that enables null values. Because Visual Basic 2012 enables mapping SQL Server data types to .NET data types, as you see in Part IV, “Data Access with ADO.NET and LINQ,” it could be a problem trying to map a NULL type in SQL Server into a DateTime type in VB. To avoid such problems, starting from .NET Framework 2.0, Microsoft introduced the Nullable types.


Why Nullable Types in This Chapter?

Nullable types are covered in this chapter because they are generic types and are required in the next chapters.


Nullable types were first introduced with Visual Basic 2005 and differ from other types because they can have both a value and have a null value. Nullable types are generic types, and variables are declared as Nullable(Of T) or by adding a question mark just after the type name. You declare a nullable value type as follows:

Dim nullInt As Nullable(Of Integer)

The following syntax is also supported:

Dim nullInt as Integer?

You can also add inline initialization:

Dim nullInt As Nullable(Of Integer) = Nothing

Nullable types expose two properties, HasValue and Value. The first one is of type Boolean and allows understanding if a variable stores a value so that you can avoid using it if it is null. The second one returns the actual value of the type. For example, the following code checks whether the preceding declared nullInt variable has a value and shows its value if it has one:

'Has no value, so WriteLine is not executed
If nullInt.HasValue Then
    Console.WriteLine(nullInt.Value)
End If

Because we assigned Nothing, HasValue is False. The next example declares a Boolean nullable and demonstrates how you can use the value (the alternative syntax with the question mark is used):

Dim nullBool As Boolean? = False
If nullBool.HasValue Then
    Console.WriteLine(nullBool.Value)
End If

Nullable types also expose a method called GetValueOrDefault, which returns the current value for the type instance if the HasValue property is True. If HasValue is False, the method returns the default value for the type or the specified default value. The following code describes how you use GetValueOrDefault:

Dim anInt As Integer? = 10
'HasValue is True, so returns 10
Dim anotherInt As Integer = anInt.GetValueOrDefault

Dim anInt As Integer?
'HasValue is False, so returns 0
'which is the default Value for Integer
Dim anotherInt As Integer = anInt.GetValueOrDefault

Dim anInt As Integer?
'HasValue is False, so returns the default value
'specified as the method argument
Dim anotherInt As Integer = anInt.GetValueOrDefault(10)

Dim anInt As Integer? = 5
'HasValue is True, so returns the current value
'while the method argument is ignored
Dim anotherInt As Integer = anInt.GetValueOrDefault(10)

Using nullables will make your life easier in lots of scenarios.

Summary

Generics are a great benefit in the .NET development with Visual Basic 2012. Generics are .NET types that can adapt their behavior to different kinds of objects without the need of defining a separate version of the type. So, you can implement a generic type to work with integers, strings, custom reference types, and any other .NET type with a single implementation. They provide several benefits, including strongly typed programming, IntelliSense support, and better performance. Generics require the Of keyword to specify the type parameter that you can manipulate in your object body.

Within Generics definition, you can implement your own custom methods both in the usual fashion and as generic methods, which still require the Of keyword followed by the type specification. Generics are also very flexible thanks to the constraints feature. It allows them to accept only types that adhere to the specified requirements, such as interfaces implementation, inheritance, or the presence of an explicit constructor. Nullable types are special generic types that allow null values for value types. They work like other types but expose a HasValue property for checking whether the object is null or populated. During the rest of the book, you will find hundreds of Generics usages, especially after Chapter 16 where collections and generic collections will be covered.

..................Content has been hidden....................

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