So far, we have discussed a lot of important concepts about .NET development with Visual Basic. Just to mention some key topics, we have covered class fundamentals, objects’ lifetime, types’ organization, and exception handling. We applied all these concepts to reference types (classes). But in your developer life, you will often work with value types, both built-in and custom ones. In Chapter 4, “Data Types and Expressions,” you started with the most important built-in value types in the .NET Framework. To complete your skills, though, you now need to know how to implement your own value types. In this chapter you get this information. You first understand structures and how to create them. Then you learn how to extend structures with custom versions of operators. Finally, you learn about enumerations, another kind of value type in the .NET Framework. This is not just a simple illustration because you also gain information on memory allocation to get a complete overview of this development area.
Structures in .NET development are the way to create custom value types. You find a lot of similarities between classes and structures, although this section explains some important differences. You create structures using a Structure..End Structure
block. The following code provides an example of a structure representing a fictitious order received by your company:
Public Structure Order
Public Property OrderID As Integer
Public Property OrderDate As Date
Public Property ShippedDate As Date
Public Property CustomerID As Integer
Public Property EmployeeID As Integer
End Sub
End Structure
Structure
can expose several members, such as fields, properties, and methods, as it happens for classes. The first important difference has to do with constructors. First, to create an instance of a structure, you aren’t required to use the New
keyword as you are for classes. The following code shows how you can instantiate a structure:
Dim o As Order
Dim o1 As New Order
Both syntaxes are legal. The only difference is that if you don’t use the New
keyword, the variable will be marked as unused until you assign a value and the VB compiler will show a warning message about this. Then you can assign (or invoke) members of the structure by typing the name followed by a dot:
o.OrderDate = Date.Now
o.OrderID = 1
'Other assignments..
Notice that, when you declare a structure without assigning its members, the compiler assigns the structure members a default value (which is usually zero for value types and Nothing
for reference types). You can also utilize the object initializers feature discussed in Chapter 7, “Class Fundamentals,” to initialize members of a structure. The following syntax is allowed but requires the specification of the New
keyword:
Dim o As New Order With {.OrderID = 1, .OrderDate = Date.Now,
.ShippedDate = Date.Now.AddDays(1), .CustomerID = 1,
.EmployeeID = 1}
The second difference is that Visual Basic automatically provides an implicit constructor with no parameters, and you are not enabled to explicitly provide a constructor that receives no parameters; if you try, the Visual Basic compiler throws an error. You are instead enabled to provide a constructor that receives arguments, as follows:
Public Sub New(ByVal Id As Integer,
ByVal OrderDate As Date,
ByVal ShippedDate As Date,
ByVal CustomerId As Integer,
ByVal EmployeeId As Integer)
Me.OrderID = Id
Me.OrderDate = OrderDate
Me.ShippedDate = ShippedDate
Me.CustomerID = CustomerId
Me.EmployeeID = EmployeeId
End Sub
As previously stated, structures can expose methods and fields but also shared members. Consider the following new implementation of the structure:
Public Structure Order
Private Shared orderCount As Integer
Public Property OrderID As Integer
Public Property OrderDate As Date
Public Property ShippedDate As Date
Public Property CustomerID As Integer
Public Property EmployeeID As Integer
Public Sub New(ByVal Id As Integer,
ByVal OrderDate As Date,
ByVal ShippedDate As Date,
ByVal CustomerId As Integer,
ByVal EmployeeId As Integer)
Me.OrderID = Id
Me.OrderDate = OrderDate
Me.ShippedDate = ShippedDate
Me.CustomerID = CustomerId
Me.EmployeeID = EmployeeId
orderCount += 1
End Sub
Public Shared Function Count() As Integer
Return orderCount
End Function
End Structure
As you can see, now a private shared field exists that provides a counter for the instances of the type. Moreover, a shared method named Count
returns the number of instances of the structure. The following code snippet demonstrates how the method works:
Dim firstOrder As New Order(1, Date.Now, Date.Now, 1, 1)
Dim secondOrder As New Order(2, Date.Now, Date.Now, 1, 1)
'Returns 2
Console.WriteLine(Order.Count)
The preceding code returns 2 because there are two active instances of the structure. Notice that the code would not work if you initialized the structure using object initializers because the orderCount
field is incremented in the parameterized constructor.
Because structures are value types, assigning an instance of a structure to a variable declared as of that type creates a full copy of the data. The following brief code demonstrates this:
'Creates a real copy of firstOrder
Dim thirdOrder As Order
thirdOrder = firstOrder
In the preceding code, thirdOrder
is a full copy of firstOrder
. You can easily check this by using the data tips feature of the Visual Studio Debugger or adding the variable to a Watch window.
Structures can be passed to methods as arguments. For example, consider the following method that simulates an order process taking an instance of the previously shown Order
structure:
Private Sub ShowOrderInfo(ByVal orderInstance As Order)
Console.WriteLine("Order info:")
Console.WriteLine("ID: {0}, Date received: {1}",
orderInstance.OrderID,
orderInstance.OrderDate)
Console.ReadLine()
End Sub
You can then invoke the method, passing the desired instance as follows:
Dim firstOrder As New Order(1, Date.Now, Date.Now, 1, 1)
ShowOrderInfo(firstOrder)
The preceding code produces an output that looks like this:
ID: 1, Date received: 05/30/2012 22:44:11
Structures’ members require you to specify a scope qualifier. Structures accept only the Private
, Public
, and Friend
qualifiers. If no qualifier is specified, Public
is provided by default. Only fields can be declared using the Dim
keyword, and they are equivalent to Public
. The following line demonstrates this:
Public Structure Order
'Means Public:
Dim orderCount As Integer
As for other built-in value types, structures implicitly inherit from the System.ValueType
type that inherits from System.Object
. This is the only inheritance level allowed for structures. This means that, different from reference types (classes), structures can neither inherit nor derive from other structures. Therefore, the Inherits
keyword is not allowed within structures. Because structures derive from System.Object
, they inherit only from classes such as the Equals
, GetHashCode
, and ToString
methods that can also be overridden within structures. Chapter 12, “Inheritance,” provides detailed information on inheritance and overriding. Structures can instead implement interfaces; therefore, the Implements
keyword is enabled. Chapter 13, “Interfaces,” discusses interfaces.
Structures are value types. This means they are allocated in the stack. Such behavior provides great efficiency to structures because when they are no longer necessary, the Common Language Runtime (CLR) removes them from the Stack and avoids the need of invoking the garbage collector as happens for reference types. But this is just a general rule. Structures’ members can expose any kind of .NET type and therefore reference types, too. The following revisited implementation of the Order
structure provides an example, exposing an OrderDescription
property of type String
that is a reference type:
Public Structure Order
Public Property OrderID As Integer
Public Property OrderDate As Date
Public Property ShippedDate As Date
Public Property CustomerID As Integer
Public Property EmployeeID As Integer
Public Property OrderDescription As String
End Structure
In this scenario, the garbage collection process is invoked to free up memory space when the OrderDescription
is released and causes an increase in performance overhead. With that said, value types are faster and more efficient than reference types only if they do not expose members that are reference types. Another important consideration is that when you pass or assign a structure to a method or variable, the actual value and data of the structure are passed (or copied in assignments) unless you pass the structure to a method by reference. If the data you want to represent is large, you should consider reference types.
You can optimize structures’ efficiency with a little bit of work. This work is related to the order in which you implement members within a structure. For example, consider the following code:
Public Structure VariousMembers
Public Property anInteger As Integer
Public Property aByte As Byte
Public Property aShort As Short
End Structure
Notice in which order the members are exposed. Because of their memory allocation in bytes, it’s preferable to expose members in the order of bytes they require. This is a revisited, more efficient version of the structure:
Public Structure VariousMembers
Public Property aByte As Byte
Public Property aShort As Short
Public Property anInteger As Integer
End Structure
If you are in doubt, don’t worry. The .NET Framework offers an interesting attribute named StructLayout
, exposed by the System.Runtime.InteropServices
namespaces, which tells the compiler to organize a structure in the most appropriate way and that you can use as follows:
'Requires
'Imports System.Runtime.InteropServices
<StructLayout(LayoutKind.Auto)>
Public Structure VariousMembers
Public Property aByte As Byte
Public Property aShort As Short
Public Property anInteger As Integer
End Structure
Important Note
Remember that if you use the StructLayout
attribute, you cannot pass your structure to unmanaged code such as Windows APIs. If you try, the compiler throws an exception. This is because the Windows APIs expect members to be presented in a predefined order and not reorganized at compile time.
In Chapter 4 you learned about operators offered by the Visual Basic grammar. Although Visual Basic does not enable creating new custom operators, it offers the possibility of overloading existing operators. In other words, you have the ability to extend existing operators with custom versions. You might wonder when and why this could be necessary. You get an answer to this question with the following code. Consider the simple structure that represents a three-dimensional coordinate:
Public Structure ThreePoint
Public Property X As Integer
Public Property Y As Integer
Public Property Z As Integer
Public Sub New(ByVal valueX As Integer, ByVal valueY As Integer,
ByVal valueZ As Integer)
Me.X = valueX
Me.Y = valueY
Me.Z = valueZ
End Sub
End Structure
Now imagine that, for any reason, you want to sum two instances of the structure using the +
operator. If you try to write the following code
'Won't compile, throws an error
Dim result As ThreePoint = t1 + t2
the Visual Basic compiler throws an error saying that the +
operator is not defined for the ThreePoint
structure. You should begin understanding why operator overloading can be a good friend. The same situation is for other operators. In Visual Basic, you overload operators using a Public Shared Operator
statement within your type definition. The following code overloads the +
and -
operators:
Public Shared Operator +(ByVal firstValue As ThreePoint,
ByVal secondValue As ThreePoint) As ThreePoint
Return New ThreePoint With {.X = firstValue.X + secondValue.X,
.Y = firstValue.Y + secondValue.Y,
.Z = firstValue.Z + secondValue.Z}
End Operator
Public Shared Operator -(ByVal firstValue As ThreePoint,
ByVal secondValue As ThreePoint) As ThreePoint
Return New ThreePoint With {.X = firstValue.X - secondValue.X,
.Y = firstValue.Y - secondValue.Y,
.Z = firstValue.Z - secondValue.Z}
End Operator
Of course, this is just an example, and you might want to perform different calculations. Both overloads return a ThreePoint
structure whose members have been populated with the sum and the difference between the X
, Y
, and Z
properties, respectively, from both initial instances. When overloading operators, you need to remember that some of them require you to also overload the negation counterpart. For example, the equality =
operator cannot be overloaded alone but requires the overloading of the inequality <>
operator. You will be informed by the Visual Basic background compiler when an operator can’t be overloaded alone. The following code shows an overloading example of equality and inequality operators for the ThreePoint
structure:
Public Shared Operator =(ByVal firstValue As ThreePoint,
ByVal secondValue As ThreePoint) As Boolean
Return (firstValue.X = secondValue.X) _
AndAlso (firstValue.Y = secondValue.Y) _
AndAlso (firstValue.Z = secondValue.Z)
End Operator
Public Shared Operator <>(ByVal firstValue As ThreePoint,
ByVal secondValue As ThreePoint) As Boolean
Return (firstValue.X <> secondValue.X) _
OrElse (firstValue.Y <> secondValue.Y) _
OrElse (firstValue.Z <> secondValue.Z)
End Operator
IntelliSense can help you understand which operators can be overloaded. For your convenience, a list of operators that can be overloaded is provided in Table 11.1.
Overloading operators is discussed in this chapter because operators such as sum and subtraction make more sense with value types. However, this technique is also allowed with classes—for example, for comparison operators. You can certainly overload operators within reference types, too.
The CType
operator also can be overloaded to provide appropriate mechanisms for converting to and from a custom type. The interesting thing in overloading CType
is that you have to consider both situations studied in Chapter 4, known as widening and narrowing conversions (see that topic for further details). Continuing the previous example of the ThreePoint
structure, the following code snippet offers a special implementation of CType
, enabling conversions to and from an array of integers:
'From ThreePoint to Array of Integer
Public Shared Narrowing Operator CType(ByVal instance As ThreePoint) _
As Integer()
Return New Integer() {instance.X,
instance.Y,
instance.Z}
End Operator
'From Integer() to ThreePoint
Public Shared Widening Operator CType(ByVal instance As Integer()) _
As ThreePoint
If instance.Count < 3 Then
Throw New ArgumentException("Array is out of bounds",
"instance")
Else
Return New ThreePoint With {.X = instance(0),
.Y = instance(1),
.Z = instance(2)}
End If
End Operator
The code is quite simple. Notice how you must specify a keyword corresponding to the effective kind of conversion (Widening
and Narrowing
) and how, within the Widening
definition, the code performs a basic validation ensuring that the array of integers contains at least three items.
CType Conventions
As a convention, your type should implement an overload of CType
that converts from a String
into the custom type. Such conversion should also be offered implementing two methods conventionally named as Parse
and TryParse
that you saw in action in Chapter 4 with several primitive types.
The Common Language Specification (CLS) has established specific rules for structures. If you want your structure to be CLS-compliant, you need to overload the equality and inequality operators and redefine the behavior of the Equals
and GetHashCode
methods inherited from Object
. Listing 11.1 shows an example of a CLS-compliant structure.
<CLSCompliant(True)>
Public Structure CLSCompliantStructure
Public Shared Operator =(ByVal obj1 As CLSCompliantStructure,
ByVal obj2 As CLSCompliantStructure) As Boolean
Return obj1.Equals(obj2)
End Operator
Public Shared Operator <>(ByVal obj1 As CLSCompliantStructure,
ByVal obj2 As CLSCompliantStructure) As Boolean
Return Not obj1.Equals(obj2)
End Operator
Public Overrides Function Equals(ByVal obj As Object) As Boolean
Return Object.Equals(Me, obj)
End Function
Public Overrides Function GetHashCode() As Integer
Return Me.GetHashCode
End Function
End Structure
If you are not already familiar with overriding, you can read the next chapter and then take a look back at the preceding code.
Enumerations are another kind of value type available in the .NET Framework. They represent a group of constants enclosed within an Enum..End Enum
code block. An enumeration derives from System.Enum
, which derives from System.ValueType
. The following is an example of enumeration:
'These are all Integers
Public Enum Sports
Biking '0
Climbing '1
Swimming '2
Running '3
Skiing '4
End Enum
By default, enumerations are sets of integer values. The preceding code defines a Sports
enumeration of type Integer
, which stores a set of integer constants. The Visual Basic compiler can also automatically assign an integer value to each member within an enumeration, starting from zero, as indicated in comments. You can eventually manually assign custom values, but you should avoid this when possible because the standard behavior ensures that other types can use your enumeration with no errors. The following code shows how you can change the result type of an enumeration instead:
Public Enum LongSports As Long
Biking
Climbing
'and so on...
End Enum
IntelliSense can help you understand that enumerations support only numeric types, such as Byte
, Short
, Integer
, Long
, UShort
, UInteger
, ULong
, and SByte
. Notice that enumerations can be made of mixed numeric data types if assigned with values of different numeric types.
Writing Enumerations
Enumerations are easy to use and fast to implement. They are essentially read-only groups of read-only constants. Because of this, use them when you are sure that those values need no modifications; otherwise, consider implementing a structure instead.
You use enumerations as any other .NET type. For example, consider the following method that receives the Sports
enumeration as an argument and returns a response depending on which value has been passed:
Private Sub AnalyzeSports(ByVal sportsList As Sports)
Select Case sportsList
Case Is = Sports.Biking
Console.WriteLine("So, do you really like biking my friend?")
Case Is = Sports.Climbing
Console.WriteLine("I do not like climbing like you!")
Case Else
Console.WriteLine("Every sport is good!")
End Select
End Sub
The following code snippet then declares a variable of type Sports
, assigns a value, and then invokes the method by passing the variable:
Dim mySport As Sports = Sports.Climbing
AnalyzeSports(mySport)
Notice how IntelliSense comes in when you need to specify a value whose type is an enumeration. Figure 11.1 shows the IntelliSense pop-up window related to our custom enumeration.
As mentioned at the beginning of this section, all enumerations derive from the System.Enum
class. Such a type exposes some shared methods that enable performing operations on enumerations. This subsection explains how you can take advantage of methods for working on enumerations.
The first two methods described are GetValues
and GetNames
. Both enable retrieving an array of items stored within an enumeration, but GetValues
gets an array of integers corresponding to the numeric values of enumerations’ items, whereas GetNames
retrieves an array of strings storing the names of the enumerations’ items. Continuing the example of the Sports
enumeration, consider the following code:
For Each item As Integer In System.Enum.GetValues(GetType(Sports))
Console.WriteLine(item)
Next
This code’s output is the following list:
0
1
2
3
4
GetNames
works similarly except that it returns an array of strings:
For Each item As String In System.Enum.GetNames(GetType(Sports))
Console.WriteLine(item)
Next
And this code produces the following output:
Biking
Climbing
Swimming
Running
Skiing
Notice how both methods require a System.Type
argument instead of a System.Enum
; therefore, you must invoke the GetType
operator. Another interesting thing about the syntax is that the System.Enum
class full name for invoking its methods is used here because the Enum
class is exposed by the System
namespace that is always imported at project level. Technically, you could just write invocations as follows: Enum.GetNames
. But this is not allowed in Visual Basic because of conflicts with the Enum
reserved keyword. To use the simplified syntax, you can enclose the Enum
work within square brackets as follows:
[Enum].GetNames(GetType(Sports))
The Visual Basic compiler enables this syntax perfectly equivalent to the previous one. Notice that the IDE will add the square brackets for you when typing Enum
. Now, let’s discover other useful methods.
GetName
works similarly to GetNames
, except that it returns just a single name for a constant. Consider the following code:
'Returns Climbing
Console.WriteLine(System.Enum.GetName(GetType(Sports), 1))
You need to pass the type instance and the value in the enumeration whose name you want to retrieve.
IsDefined
checks whether the specified constant exists within an enumeration and returns a Boolean value. The following code looks first for an existing value and then for a nonexisting one:
'Returns True
Console.WriteLine(System.Enum.IsDefined(GetType(Sports), "Climbing"))
'Returns False
Console.WriteLine(System.Enum.IsDefined(GetType(Sports), "Soccer"))
System.Enum
also provides two methods for converting to and from string. The ToString
method is inherited from System.Object
and is redefined so that it can provide a string representation of the specified value. Consider the following code snippet:
'Sports.Climbing
Dim mySport As Sports = CType(1, Sports)
Console.WriteLine(mySport.ToString)
Such code returns Climbing
, which is the string representation of the specified constant value. Also notice how, if Option Strict
is On
, you must explicitly convert the value into a Sports
enumeration using CType
. Parse
is the opposite of ToString
and gets the corresponding numeric value within an enumeration depending on the specified string. The following code provides an example:
Console.WriteLine("Enter your favorite sport:")
Dim sport As String = Console.ReadLine
Dim result As Sports = CType(System.Enum.Parse(GetType(Sports),
sport, True), Sports)
'Returns 2
Console.WriteLine("The constant in the enumeration for {0} is {1}",
sport.ToString, CInt(result))
Console.ReadLine()
The previous code requires the input from the user, who has to enter a sport name. Using Parse
, the code obtains the element in the enumeration corresponding to the entered string. For example, if you enter Swimming
, the code produces the following output:
The constant in the enumeration for Swimming is 2
Notice how Parse
can receive a third argument of type Boolean that enables specifying if the string comparison must ignore casing.
You can assign an enumeration variable to an Integer
type without a conversion operator.
A common usage of enumerations is representing different results from methods that return a numeric value, as often happens for methods that return a number for communicating the result of the code. Consider the following code, which defines an enumeration that a method uses to communicate the result of a simple elaboration on a file:
Public Enum Result
Success = 0
Failed = 1
FileNotFound = 2
End Enum
Public Function ElaborateFile(ByVal fileName As String) As Result
Try
Dim text As String = My.Computer.FileSystem.ReadAllText(fileName)
'Do some work here on your string
Return Result.Success
Catch ex As IO.FileNotFoundException
Return Result.FileNotFound
Catch ex As Exception
Return Result.Failed
End Try
End Function
Each Return
statement returns an Integer
value from 0 to 2 depending on the method result, but using an enumeration provides a more convenient way for understanding the result, as demonstrated in the following code:
Sub OpenFile()
Dim res As Result = ElaborateFile("myfile.txt")
'Success = 0
If res = Result.Success Then
Console.WriteLine("Success")
'FileNotFound = 2
ElseIf res = Result.FileNotFound Then
Console.WriteLine("File not found")
'Failed = 1
ElseIf res = Result.Failed Then
Console.WriteLine("The elaboration failed")
End If
End Sub
Enumerations can be designed for supporting bitwise operations by marking them with the Flags
attribute. This allows combining enumeration values with bitwise operators such as OR
. Consider the following implementation of the Sports
enumeration that was described previously:
<Flags>
Public Enum Sports
Biking
Climbing
Swimming
Running
Skiing
End Enum
By applying the Flags
attribute, the values for each enumeration value now become bitflag
patterns that have a binary representation such as the following:
00000000
00000001
00000010
00000100
00001000
Combining all values with the OR
operator will result in a 11111111
binary value. For example, you could perform an evaluation like the following:
'sportsTest is 0000010
Dim sportsTest As Sports =
Sports.Biking And Sports.Climbing Or Sports.Swimming
This kind of approach is useful when you want to be able to perform bitwise operations and comparisons.
When introducing enumerations, you learned that they support only numeric types. There is another limitation if you plan to implement CLS-compliant enumerations. Only CLS-compliant types can characterize CLS-compliant enumerations; therefore, the SByte
, UShort
, UInteger,
and ULong
types cannot be used within CLS-compliant enumerations. Another important consideration is that CLS-compliant enumerations must be decorated with the Flag
attribute. The following is an example of a CLS-compliant enumeration:
<Flags()> Public Enum ClsCompliantEnum As Byte
FirstValue = 0
SecondValue = 1
ThirdValue = 2
End Enum
Note
The Flag
attribute indicates to the compiler that an enumeration has to be considered as a bit field.
In this chapter you saw another important part of .NET development with Visual Basic, which is related to creating custom value types. Structures are the .NET way of building custom value types and can expose methods, properties, and fields. There are several similarities with classes, but structures are value types allocated in the Stack and cannot inherit or derive from other structures but can implement interfaces. Because of their nature, structures are susceptible to operations. This requires, in certain situations, the need for specific operators. The .NET Framework enables overloading operators to provide custom implementations of unary, binary, and logical operators—a technique that is allowed for classes. Another kind of value types is enumeration, which represent a group of read-only constants and that are optimized for Integer
values, offering several shared methods for performing operations on constants composing the enumeration. An overview of how Common Language Specification rules the implementation of structures and enumerations completed the chapter.
3.15.137.59