Share via


Overview of Visual Basic 9.0

 

Erik Meijer, Amanda Silver, and Paul Vick
Microsoft Corporation

February 2007

Summary: Provides an overview of new Visual Basic language features and new language extensions that support data-intensive programming. (17 printed pages)

Contents

Introduction
Getting Started With Visual Basic 9.0
Implicitly Typed Local Variables
Object and Array Initializers
Anonymous Types
Deep XML Support
Query Comprehensions
Extension Methods and Lambda Expressions
Nullable Types
Relaxed Delegates
Conclusion

Introduction

Visual Basic has always centered on building pragmatic, data-oriented, line of business applications. While the move to .NET brought the power of a unified framework and a managed platform to the application developer, the next release of Visual Basic includes a set of features that result in a profound effect on a developer's productivity when building data-oriented applications. These language extensions introduce general-purpose query facilities that apply to all sources of data be it relational, hierarchical object graphs, or XML documents

This document is an informal overview of these new features. More information, including updates to the Visual Basic language definition and compiler previews, is available on the Visual Basic Developer Center (https://msdn.microsoft.com/vbasic/default.aspx).

Getting Started With Visual Basic 9.0

To see the power of these language features at work, let's start with a real world example—the CIA World Factbook database. The database contains a variety of geographic, economic, social, and political information about the world's countries. For the sake of our example, we begin with a schema for the name of each country and its capital, total area, and population. We represent this schema in Visual Basic 9.0 by using the following class (pseudo code used for brevity):

Class Country
  Public Property Name As String
  Public Property Area As Long 
  Public Property Population As Integer
End Class

Here is a small subset of the country database that we will use as our running example:

Dim countries = { 
  New Country With { .Name = "Palau", .Area = 458, .Population = 16952 }, _
  New Country With { .Name = "Monaco", .Area = 1.9, .Population = 31719 }, _
  New Country With { .Name = "Belize", .Area = 22960, .Population = 219296 }, _
  New Country With { .Name = "Madagascar", .Area = 587040, .Population =
 13670507}}

Given this list, we can query for all countries whose population is less than one million by using the following query expression:

Dim smallCountries = From country In countries _
                     Where country.Population < 1000000 _
Select country

For Each country In SmallCountries
  Console.WriteLine(country.Name)
Next

Because only Madagascar has more than one million inhabitants, the above program would print the following list of country names when compiled and run:

Palau
Monaco
Belize 

Let's examine the program to understand the features of Visual Basic 9.0 that made this so simple to write. First of all, the declaration of the each of the expressions that represents the Countries uses the new object-initializer syntax New Country With {..., .Area = 458, ...} to create a complex object instance through a concise, expression-based syntax similar to the existing With statement.

The declaration also illustrates implicitly typed local-variable declarations, where the compiler infers the type of the local variable Countries from the initializer expression on the right side of the declaration. The declaration above is precisely equivalent to an explicitly typed local-variable declaration of type Country().

Dim countries As Country() = {...}

To repeat, this is still a strongly typed declaration; the compiler has automatically inferred the type of the right side of the local declaration, and there is no need for the programmer to enter that type into the program manually.

The local-variable declaration SmallCountries is initialized with a SQL-style query expression to filter out all countries that have fewer than one million inhabitants. The resemblance to SQL is intentional, allowing programmers who already know SQL to get started with Visual Basic query syntax all the more quickly.

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
   Select country

Note that this code example presents another application of implicit typing: the compiler infers the type of SmallCountries as IEnumerable(Of Country) based on the result type of the query expression. The compiler translates the query expression itself into calls to the LINQ-enabled API, which implements the query operators for all types that implement IEnumerable(Of T). In this case, the translation is as simple as the following:

Dim smallCountries As IEnumerable(Of Country) = _
   Countries.Where(Function(country) country.Population < 1000000). _
             Select(Function(country) country)

The expanded syntax depends on lambda expressions, which represent inline functions that return the result of an expression. The lambda expression is converted to a delegate and passed to the extension function Where, which is defined in the standard query operator library as an extension of the IEnumerable(Of T) interface.

Now that we have seen a few of the new features in Visual Basic 9.0, let us drill down into a more detailed overview.

Implicitly Typed Local Variables

In an implicitly typed local-variable declaration, the type of the local variable is inferred from the initializer expression on the right-hand side of a local declaration statement. For example, the compiler infers the types of all the following variable declarations:

Dim population = 31719
Dim name = "Belize"
Dim area = 1.9
Dim country = New Country With { .Name = "Palau", ...}

Hence they are precisely equivalent to the following, explicitly typed declarations:

Dim population As Integer = 31719
Dim name As String = "Belize"
Dim area As Float = 1.9
Dim country As Country = New Country With { .Name = "Palau", ...}

Because types of local-variable declarations are inferred with the new Option Infer On (the default for new projects) no matter what the setting of Option Strict is, access to such variables is always early-bound. The programmer must explicitly specify late binding in Visual Basic 9.0, by explicitly declaring variables as of type Object, as follows:

Dim country As Object = New Country With { .Name = "Palau", ... }

Inferring types prevents accidentally using late binding and, more importantly, it allows powerful extensions to binding for new data types such as XML, as we will see below.

The loop-control variable in a For...Next or a For Each...Next statement can also be an implicitly typed variable. When the loop-control variable is specified, as in For I = 0 To SmallCountries.Count, or as in For Each country In smallCountries, the identifier defines a new, implicitly typed local variable, whose type is inferred from the initializer or collection expression and is scoped to the entire loop. With this application of type inference, we can rewrite the loop that prints all small countries as follows:

For Each country In smallCountries
  Console.WriteLine(country.Name)
Next

The type of country is inferred to be Country, the element type of SmallCountries.

Object and Array Initializers

In Visual Basic, the With statement simplifies access to multiple members of an aggregate value without specifying the target expression multiple times. Inside the With statement block, a member-access expression starting with a period is evaluated as if the period were preceded by the target expression of the With statement. For example, the following statements initialize a new Country instance and subsequently initialize its fields to the required values:

Dim palau As New Country()
With palau
  .Name = "Palau"  
  .Area = 458
  .Population = 16952
End With

The new object initializers in Visual Basic 9.0 are an expression-based form of With for creating complex object instances concisely. Using object initializers, we can capture the above two statements into a single (implicitly typed) local declaration, as follows:

Dim palau = New Country With { _
  .Name = "Palau", _
  .Area = 458, _
  .Population = 16952 _
}

This style of object initialization from expressions is important for queries. Typically, a query looks like an object declaration initialized by a Select clause on the right side of the equals sign. Because the Select clause returns an expression, we must be able to initialize the entire object with a single expression.

As we have seen, object initializers are also convenient for creating collections of complex objects. Arrays can be initialized and the element types inferred by using an array initializer expression. For instance, given the declaration for cities as the class,

Class City
  Public Property Name As String
  Public Property Country As String
  Public Property Longitude As Long 
  Public Property Latitude As Long
End Class

we can create an array of capital cities for our example countries as follows:

Dim Capitals = { _
  New City With { _
    .Name = "Antanarivo", _
    .Country = "Madagascar", _
    .Longitude = 47.4, _
    .Latitude = -18.6 }, _
  New City With { _
    .Name = "Belmopan", _
    .Country = "Belize", _
    .Longitude = -88.5, _
    .Latitude = 17.1 }, _
  New City With { _
    .Name = "Monaco", _
    .Country = "Monaco", _
    .Longitude = 7.2, _
    .Latitude = 43.7 }, _
  New City With { _
    .Country = "Palau",
    .Name = "Koror", _
    .Longitude = 135, _
    .Latitude = 8 } _
}

Anonymous Types

Often, we just want to remove, or to project out, certain members of a type as the result of a query. For example, we might want to know just the Name and Country of all tropical capital cities, using the Latitude and Longitude columns in the source data to identify the tropics, but projecting away those columns in the result. In Visual Basic 9.0, we do this by creating a new object instance—without naming the type—for each city C whose latitude is between the tropic of Cancer and the tropic of Capricorn:

Const TropicOfCancer = 23.5
Const TropicOfCapricorn = -23.5

Dim tropical = From city In Capitals _
               Where TropicOfCancer <= city.Latitude _
                   AndAlso city.Latitude >= TropicOfCapricorn _
               Select New With {city.Name, city.Country}

The inferred type of the local variable Tropical is a collection of instances of an anonymous type, that is (using pseudo-code) IEnumerable(Of { Name As String, Country As String }). The Visual Basic compiler will create an implicit class, for example, _Name_As_String_Country_As_String_, whose member names and types are inferred from the object initializer, as follows:

Class _Name_As_String_Country_As_String_ 
    Public Property Name As String
    Public Property Country As String
    ...
End Class

Within the same program, the compiler will merge identical anonymous types. Two anonymous object initializers that specify a sequence of properties of the same names and types in the same order will produce instances of the same anonymous type. Externally, Visual Basic-generated anonymous types are erased to Object, which allows the compiler to uniformly pass anonymous types as arguments and results of functions.

Because anonymous types are typically used to project members of an existing type, Visual Basic 9.0 allows the shorthand projection notation New With { city.Name, city.Country } to abbreviate the long-form New With { .Name = city.Name, .Country = city.Country }. When used in the result expression of a query comprehension, we may further abbreviate projection initializers even further, as follows:

Dim Tropical = From city In Capitals _
               Where TropicOfCancer <= city.Latitude _
                   AndAlso city.Latitude >= TropicOfCapricorn _
               Select city.Name, city.Country

Note that both of these abbreviated forms are identical in meaning to the long form above.

Deep XML Support

LINQ to XML is a new, in-memory XML programming API designed specifically to leverage the latest .NET Framework capabilities such as the Language-Integrated Query framework. Just as query comprehensions add familiar, convenient syntax over the underlying standard .NET Framework query operators, Visual Basic 9.0 provides deep support for LINQ to XML through XML literals and XML properties.

To illustrate XML literals, let us query over the essentially flat relational data sources Countries and Capitals to construct a hierarchical XML model that nests the capital of each country as a child element and calculates the population density as an attribute.

To find the capital for a given country, we do a join on the name-member of each country with the country-member of each city. Given a country and its capital, we can then easily construct the XML fragment by filling in embedded expression holes with computed values. We would write a "hole" for a Visual Basic expression with syntax reminiscent of ASP, as in Name=<%= country.Name %> or <Name><%= city.Name %></Name>. Here is our query that combines XML literals and query comprehensions:

Dim countriesWithCapital As XElement = _
    <Countries>
    <%= From country In Countries, city In Capitals _
        Where country.Name = city.Country _
        Select <Country Name=<%= country.Name %>
                        Density=<%= country.Population / country.Area %>>
                  <Capital>
                   <Name><%= city.Name %></Name>
                   <Longitude><%= city.Longitude %></Longitude>
                   <Latitude><%= city.Latitude %></Latitude>
                  </Capital>
               </Country> _
    %>
    </Countries>

Note that the type XElement could be omitted from the declaration, in which case it will be inferred, just like any other local declaration.

In this declaration, the result of the Select query is to be substituted inside the <Countries> element. Thus, the Select query is the content of the first 'hole', demarcated by the familiar ASP style tags <%= and %> inside <Countries>. Because the result of a Select query is an expression, and XML literals are expressions, it is natural, then, to nest another XML literal in the Select itself. This nested literal itself contains nested attribute "holes " for Country.Name and the computed population density ratio Country.Population/Country.Area, and nested element "holes" for the name and coordinates of the capital city.

When compiled and run, the above query will return the following XML document (reformatted slightly from the standard IDE pretty print to save space)

<Countries>
 <Country Name="Palau" Density="0.037117903930131008">
   <Capital>
     <Name>Koror</Name><Longitude>135</Longitude><Latitude>8</Latitude>
   </Capital>
 </Country>
 <Country Name="Monaco" Density="16694.21052631579">
   <Capital>
     <Name>Monaco</Name><Longitude>7.2</Longitude><Latitude>3.7</Latitude>
   </Capital>
 </Country>
 <Country Name="Belize" Density="9.5512195121951216">
   <Capital>
     <Name>Belmopan</Name><Longitude>-88.5</Longitude><Latitude>17.1</Latitude>
   </Capital>
 </Country>
 <Country Name="Madagascar" Density="23.287181452711909">
   <Capital>
     <Name>Antananarivo</Name>
     <Longitude>47.4</Longitude><Latitude>-18.6</Latitude>
   </Capital>
  </Country>
</Countries>

Visual Basic 9.0 compiles XML literals into normal System.Xml.Linq objects, ensuring full interoperability between Visual Basic and other languages that use LINQ to XML. For our example query, the code produced by the compiler (if we could see it) would be:

Dim countriesWithCapital As XElement = _ 
  New XElement("Countries", _
        From country In Countries, city In Capitals _
        Where country.Name = city.Country _
  Select New XElement("Country", _
             New XAttribute("Name", country.Name), _
             New XAttribute("Density", country.Population/country.Area), _
             New XElement("Capital", _
               New XElement("Name", city.Name), _
               New XElement("Longitude", city.Longitude), _
               New XElement("Latitude", city.Latitude))))

Besides constructing XML, Visual Basic 9.0 also simplifies accessing XML structures via XML properties; that is, identifiers in Visual Basic code are bound at run time to corresponding XML attributes and elements. For example, we can print the population density of all example countries as follows:

  • Use the child axis countriesWithCapital.<Country> to get all "Country " elements from the countriesWithCapital XML structure.
  • Use the attribute axis country.@Density to get the "Density" attribute of the Country element.
  • Use the descendants axis country...<Latitude>—written literally as three dots in the source code—to get all "Latitude" children of the Country element, no matter how deeply in the hierarchy they occur.
  • Use the extension property .Value on IEnumerable(Of XElement) to select the value of the first element of the resulting sequence, or the extension indexer (i) to select the i-th element.

Putting all these features together, the code can be dramatically condensed and simplified:

For Each country In countriesWithCapital.<Country>
  Console.WriteLine("Density = " & country.@Density)
  Console.WriteLine("Latitude = " & country...<Latitude>.Value)
Next

The compiler knows to use late binding over normal objects when the target expression of a declaration, assignment, or initialization is of type Object rather than of some more specific type. Likewise, the compiler knows to use binding over XML when the target expression is of type, or collection of, XElement, XDocument, or XAttribute.

As a result of late binding over XML, the compiler translates as follows:

  • The child-axis expression countriesWithCapital.<Country> translates into the raw LINQ to XML call countriesWithCapital.Elements("Country"), which returns the collection of all child elements named "Country" of the Country element.
  • The attribute axis expression country.@Density translates into Country.Attribute("Density").Value, which returns the single child attribute named "Density" of Country;.
  • The descendant axis expression country...<Latitude> translates into the raw LINQ to XML call country.Descendants(“Latitude”), which returns the collection of all elements named at any depth below country.

Query Comprehensions

A query operator is an operator such as Select, Order By, or Where that can be applied to a collection of values across the entire collection at once. A query expression is an expression that applies a series of query operators to a particular collection. For example, the following query expression takes a collection of countries and returns the names of all of the countries with fewer than a million inhabitants:

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
                     Select country

The query expression syntax is designed to be reasonably close to standard relational SQL syntax, with the intent that anyone familiar with SQL will be able to use query expressions with very little instruction. The syntax is not constrained, however, by SQL, nor are query expressions intended to be a translation of SQL into Visual Basic. Because SQL was designed around a purely relational model, some of its idioms do not work as well in a type system that allows, and even embraces, hierarchy. Additionally, some of the SQL syntactic and semantics elements conflict or do not mesh well with existing Visual Basic syntax or semantics. Thus, while query expressions should be familiar to anyone familiar with SQL, there will be differences that will need to be learned.

The query expressions are translated into calls to the underlying sequence operators on queryable types specified as the source type in the From clause. Given that the sequence operators are generally defined as extension methods on the source type, they are bound to whatever sequence operators are in scope. This implies that by importing a particular implementation, the query-expression syntax can be bound again to different LINQ-enabled APIs. This is how query expressions can be bound again to an implementation that uses LINQ to SQL or LINQ to objects (a local query execution engine that executes the query in-memory.)

Some query operators such as From, Select, and Group By, introduce a special kind of local variable called a range variable. By default, a range variable is scoped from the introducing operator to an operator that hides the range variable and represents a property or column of the individual row in a collection as the query evaluates. For example, in the following query:

Dim smallCountries = From country In Countries _
                     Where country.Population < 1000000 _
                     Select country

The From operator introduces a range variable country typed as "Country." The following Where query operator then refers to the range variable country to represent each individual customer in the filter expression country.Population < 1000000.

Some query operators, such as Distinct, do not use or change the control variable. Other query operators such as Select hide the current range variables in scope and introduce new range variables. For example, in the query:

Dim smallCountries = From country In Countries _
                     Select country.Name, Pop = country.Population
                     Order By Pop

The Order By query operator only has access to the Name and Pop range variables introduced by the Select operator; if the Order By operator had tried to reference Country, a compile-time error would occur.

If a query ends without a Select operator, the resulting element type of the collection is as though a Select projection was present with all of the control variables in scope:

Dim countriesWithCapital = From country In Countries, city In Capitals _
                           Where country.Name = city.Country
The inferred type for this local declaration is (in pseudo-code to represent an anonymous type) IEnumerable(Of { Country As Country, City As City }).

How query expressions are unlike SQL: Compositionality and hierarchical data

Query expressions in Visual Basic 9.0 are fully compositional, meaning that query comprehensions can be arbitrarily nested or built up by appending a query with additional query operators. Compositionality makes it easy to understand a large query by simply understanding each individual subexpression in isolation and makes it easy to follow the semantics and types that flow through each query operator. However, compositionality as a design principle yields a rather different experience for writing a query from SQL where queries are analyzed as a monolithic block.

Additionally, the LINQ-enabled APIs tend to implement the sequence operators with deferred execution. Deferred execution means that the query is not evaluated until the results are enumerated. For LINQ to SQL, this means that the query is not remoted to SQL until the results are requested. This means that separating the queries into multiple statements does not mean that the database is hit multiple times. As a result, what would usually be a nested query in SQL becomes a compositional query in LINQ.

One reason why SQL lacks compositionality is that the underlying relational data model is itself not compositional. For instance, tables may not contain subtables; in other words, all tables must be flat. As a result, instead of breaking up complex expressions into smaller units, SQL programmers write monolithic expressions whose results are flat tables, fitting to the SQL data model. Because Visual Basic is based on the CLR type system, there are no restrictions on what types can appear as components of other types. Aside from static typing rules, there are no restrictions on the kind of expressions that can appear as components of other expressions. As a result, not only rows, objects, and XML, but also Active Directory, files, registry entries, and so on, are all first-class citizens in query sources and query results.

Query operators

Those familiar with the implementation of SQL will recognize, in the underlying .NET sequence operators, many of the compositional relational-algebra operators such as projection, selection, cross-product, grouping, and sorting, that represent query plans inside the query processor.

  • The From operator introduces one or more range variables and either specifies a collection to query or computes a value for the range variable.
  • The Select operator specifies the shape of the output collection.
  • The Where and Distinct operators restrict the values of the collection.
  • The Order By operator imposes an ordering on the collection.
  • The Skip, Skip While, Take and Take While operators return a subset of a collection based on order or a condition.
  • The Union, Union All, Except, and Intersect operators take two collections and produce a single collection.
  • The Group By operator groups the collection based on one or more keys.
  • The Avg, Sum, Count, Min, and Max operators aggregate a collection and produce a value.
  • The Any and All operators aggregate a collection and return a Boolean value based on a condition.
  • The Join operator takes two collections and produces a single collection based on matching keys derived from the elements.
  • The Group Join operator performs a grouped join of two collections based on matching keys extracted from the elements.

The full syntax for all of the operators can be found in the full language specification. However, for illustration purposes, the following finds the capitals of each country and orders the country names by the latitude of the capital:

Dim countriesWithCapital = _
  From country In Countries _
  Join city In Capitals On country.Name Equals city.Country _
  Order By city.Latitude _
  Select country.Name

For queries that compute a scalar value based on a collection, the Aggregate operator works over the collection. The following query finds the number of small countries and computes their average density in one statement:

Dim popInfo = _
  Aggregate country In Countries _
  Where country.Population < 1000000 _
  Into Total = Count(), Density = Average(country.Population/country.Area)

Aggregate functions appear most often in combination with partitioning the source collection. For example, we can group all countries by whether they are tropical and then aggregate the count of each group. To do so, the aggregate operators can be used in conjunction with the Group By and Group Join clauses. In the example below, the helper function, IsTropical encapsulates the test whether a City has a tropical climate:

  Function IsTropical() As Boolean
    Return TropicOfCancer =< Me.Latitude AndAlso Me.Latitude >= TropicOfCapricorn
  End Function

Given this helper function, we use exactly the same aggregation as above, but first partition the input collection of Country and Capital pairs into groups for which Country.IsTropical is the same. In this case there are two such groups: one that contains the tropical countries Palau, Belize, and Madagascar; and another that contains the non-tropical country Monaco.

Key Country City
Country.IsTropical() = True Palau Koror
  Belize Belmopan
  Madagascar Antanarivo
Country.IsTropical() = False Monaco Monaco

Then, we aggregate the values in these groups by computing the total count and average density. The result type is now a collection of pairs of Total As Integer and Density As Double:

Dim countriesByClimate = _
  From country In Countries _
 Join city In Capitals On country.Name Equals city.Country _
 Group By country.IsTropical()
 Into Total = Count(), Density = Average(country.Population/country.Area)

The above query hides considerable complexity. The query below produces the same results using lambda expressions and extension methods to express the query with method call syntax.

Dim countriesByClimate7 = _
  countries. _
    SelectMany( _
      Function(country) Capitals, _ 
      Function(country, city) New With {country, city}). _
    Where(Function(it) it.country.Name = it.city.Country). _ 
    GroupBy( _
      Function(it) it.city.IsTropical(), _
      Function(IsTropical, Group) _
        New With { _
          IsTropical, _
          .Total = Group.Count(), _
          .Density = Group.Average( _
             Function(it) it.country.Population / it.country.Area _
          ) _
        } _
    )

Extension Methods and Lambda Expressions

Much of the underlying power of the .NET Framework standard query infrastructure comes from extension methods and lambda expressions. Extension methods are shared methods marked with custom attributes that allow them to be invoked with instance-method syntax. Most extension methods have similar signatures. The first argument is the instance against which method is applied, and the second argument is the predicate to apply. For example, the Where method, to which the Where clause is translated, has the signature:

Module IEnumerableExtensions
  <Extension> _
  Function Where (Of TSource) _
    (Source As IEnumerable(Of TSource), _
     predicate As Func(Of TSource, Boolean)) As IEnumerable(Of TSource)
    ...
  End Function
End Module

As many of the standard query operators such as Where, Select, SelectMany, and so on are defined as extension methods that take delegates of type Func(Of S,T) as arguments the compiler abstracts away the requirement of producing the delegates that represent the predicate. The compiler creates closures, delegates that capture their surrounding context, and passes them to the underlying method call. For example, with the following query, the compiler generates lambda expressions that represent the delegates to be passed to the Select and Where functions:

Dim smallCountries = From country In countries _
                     Where country.Population < 1000000 _
                     Select country.Name

The compiler generates two lambda expressions for the projection and the predicate respectively:

Function(Country As Country) country.Name
Function (Country As Country) country.Population < 1000000

And the query above is translated to method calls to the standard query framework, passing the source and the lambda expression to apply as arguments:

Dim smallCountries = _
  Enumerable.Select( _
      Enumerable.Where(countries, _
        Function (country As Country) country.Population < 1000000), _
        Function(country As Country) country.Name)

Nullable Types

Relational databases present semantics for nullable values that are often inconsistent with ordinary programming languages and often unfamiliar to programmers. In data-intensive applications, it is critical for programs to handle these semantics clearly and correctly. Recognizing this necessity, with .NET Frameworks 2.0 the CLR has added run-time support for nullability using the generic type Nullable(Of T As Structure). Using this type we can declare nullable versions of value types such as Integer, Boolean, Date, and so on. For reasons that will become apparent, the Visual Basic syntax for nullable types is T?.

For example, because not all countries are independent, we can add a new member to the class Country that represents their independence date, if applicable:

Partial Class Country
  Public Property Independence As Date?
End Class 

The independence date for Palau is #10/1/1994#, but the British Virgin Islands are a dependent territory of the United Kingdom, and hence its independence date is Nothing.

Dim palau = _
  New Country With { _
    .Name = "Palau", _
    .Area = 458, _
    .Population = 16952, _
    .Independence = #10/1/1994# }

Dim virginIslands = _
  New Country With { _
    .Name = "Virgin Islands", _
    .Area = 150, _
    .Population = 13195, _
    .Independence = Nothing }

Visual Basic 9.0 will support three-valued logic and null propagation arithmetic on nullable values, which means that if one of the operands of an arithmetic, comparison, logical or bitwise, shift, string, or type operation is Nothing, the result will be Nothing. If both operands are proper values, the operation is performed on the underlying values of the operands and the result is converted to nullable.

Because both Palau.Independence and VirginIslands.Independence have type Date?, the compiler will use null-propagating arithmetic for the subtractions below, and hence the inferred type for the local declaration PLength and VILength will both be TimeSpan?.

Dim pLength = #8/24/2005# - Palau.Independence          ‘ 3980.00:00:00

The value of PLength is 3980.00:00:00 because neither of the operands is Nothing. On the other hand, because the value of VirginIslands.Independence is Nothing, the result is again of type TimeSpan?, but the value of VILength will be Nothing because of null-propagation.

Dim vILength = #8/24/2005# - virginIslands.Independence ‘ Nothing

As in SQL, comparison operators will do null propagation, and logical operators will use three-valued logic. In If and While statements, Nothing is interpreted as False; hence in the following code snippet, the Else branch is taken:

If vILength < TimeSpan.FromDays(10000)
  ...
Else
  ...
End If

Note that under three-valued logic, the equality checks X = Nothing, and Nothing = X always evaluates to Nothing; in order to check if X is Nothing, we should use the two-valued logic comparison X Is Nothing or Nothing Is X.

Relaxed Delegates

When creating a delegate using AddressOf or Handles in Visual Basic 8.0, one of the methods targeted for binding to the delegate identifier must exactly match the signature of the delegate's type. In the example below, the signature of the OnClick subroutine must exactly match the signature of the event handler delegate Delegate Sub EventHandler(sender As Object, e As EventArgs), which is declared behind the scenes in the Button type:

Dim WithEvents btn As New Button()

Sub OnClick(sender As Object, e As EventArgs) Handles B.Click
  MessageBox.Show("Hello World from" & btn.Text)
End Sub

However, when invoking non-delegate functions and subroutines, Visual Basic does not require the actual arguments to exactly match one of the methods we are trying to invoke. As the following fragment shows, we can actually invoke the OnClick subroutine using an actual argument of type Button and of type MouseEventArgs, which are subtypes of the formal parameters Object and EventArgs, respectively:

Dim m As New MouseEventArgs(MouseButtons.Left, 2, 47, 11,0)
OnClick(btn, m)

Conversely, suppose that we could define a subroutine RelaxedOnClick that takes two Object parameters, and then we are allowed to call it with actual arguments of type Object and EventArgs:

Sub RelaxedOnClick(sender As Object, e As Object) Handles btn.Click
  MessageBox.Show("Hello World from" & btn.Text))
End Sub
Dim e As EventArgs = m
Dim s As Object = btn
RelaxedOnClick(btn,e)

In Visual Basic 9.0, binding to delegates is relaxed to be consistent with method invocation. That is, if it is possible to invoke a function or subroutine with actual arguments that exactly match the formal-parameter and return types of a delegate, we can bind that function or subroutine to the delegate. In other words, delegate binding and definition will follow the same overload-resolution logic that method invocation follows.

This implies that in Visual Basic 9.0 we can now bind a subroutine RelaxedOnClick that takes two Object parameters to the Click event of a Button:

Sub RelaxedOnClick(sender As Object, e As Object) Handles btn.Click
  MessageBox.Show(("Hello World from" & btn.Text)
End Sub

The two arguments to the event handler, sender and EventArgs, very rarely matter. Instead, the handler accesses the state of the control on which the event is registered directly and ignores its two arguments. To support this common case, delegates can be relaxed to take no arguments, if no ambiguities result. In other words, we can simply write the following:

Sub RelaxedOnClick Handles btn.Click
  MessageBox.Show("Hello World from" & btn.Text)
End Sub

It is understood that delegate relaxation also applies when constructing delegates using an AddressOf or delegate-creation expression, even when the method group is a late-bound call:

Dim F As EventHandler = AddressOf RelaxedOnClick
Dim G As New EventHandler(AddressOf btn.Click)

Conclusion

Visual Basic 9.0 unifies access to data independently of its source in relational databases, XML documents, or arbitrary object graphs, however persisted or stored in memory. The unification consists of styles, techniques, tools, and programming patterns. The especially flexible syntax of Visual Basic makes it easy to add extensions like XML literals and SQL-like query expressions deeply into the language. This greatly reduces the "surface area" of the new .NET Language-Integrated Query APIs, increases the discoverability of data-access features through IntelliSense and Smart Tags, and vastly improves debugging and compile-time checking by lifting foreign syntaxes out of string data into Visual Basic.

Additionally, features such as type inference, object initializers, and relaxed delegates greatly reduce code redundancy and the number of exceptions to the rules that programmers need to learn and remember or look up, with no impact on performance.

Although it may seem that the list of new features in Visual Basic 9.0 is long, we hope that the above themes will convince you that it is coherent, timely, and dedicated to making Visual Basic the world's finest programming language. We hope your imagination will be stimulated, too, and that you will join us in realizing that this is really just the beginning of even greater things to come.

© Microsoft Corporation. All rights reserved.