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 thecountriesWithCapital
XML structure. - Use the attribute axis
country.@Density
to get the"
Density"
attribute of theCountry
element. - Use the descendants axis
country...<Latitude>
—written literally as three dots in the source code—to get all"
Latitude"
children of theCountry
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 callcountriesWithCapital.Elements("Country")
, which returns the collection of all child elements named"
Country"
of theCountry
element. - The attribute axis expression
country.@Density
translates intoCountry.Attribute("Density").Value
, which returns the single child attribute named"
Density"
ofCountry;
. - The descendant axis expression
country...<Latitude>
translates into the raw LINQ to XML callcountry.Descendants(“Latitude”)
, which returns the collection of all elements named at any depth belowcountry
.
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
andDistinct
operators restrict the values of the collection. - The
Order By
operator imposes an ordering on the collection. - The
Skip
,Skip While
,Take
andTake While
operators return a subset of a collection based on order or a condition. - The
Union
,Union All
,Except
, andIntersect
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
, andMax
operators aggregate a collection and produce a value. - The
Any
andAll
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.