VB.NET: Essential Tuples
Introduction
This article provides simple examples for working with Tuples. In the Microsoft documentation for the new tuple the code samples are written towards experienced developer while the novice developer will generally not grasp possibilities for using the new tuples.
Public Function CouldHaveBeenAnonymousResults(pChar As Char) As
(Item As Char, Occurrences As Integer, Code As Integer)
Dim results =
(
From T In
(
From c In "T0*A1?0*23aTA3 4T4\+a4 ?407#?A*6T+".ToCharArray()
Group c By c Into Group Select New With
{
.Item = c,
.Occurrences = Group.Count,
.Code = Asc(c)
}
).ToList.OrderBy(Function(x) x.Item)
).FirstOrDefault(Function(x) x.Item = pChar)
Return (results.Item, results.Occurrences, results.Code)
End Function
Definition of Tuple prior to Visual Studio 2017
A tuple is a data structure that has a specific number and sequence of elements. An example of a tuple is a data structure with three elements (known as a 3-tuple or triple) that is used to store an identifier such as a person's name in the first element, a year in the second element, and the person's income for that year in the third element. The .NET Framework directly supports tuples with one to seven elements. In addition, you can create tuples of eight or more elements by nesting tuple objects in the Rest property of a Tuple(Of T1, T2, T3, T4, T5, T6, T7, TRest) object.
Although tuples had a place in writing code the results sent back from a method returning a tuple were not readily understood as each tuple returns as (where "result" is the tuple) result.ItemN where N is a value such as first name. So if a method using a tuple returned first and last name we would have result.Item1 for first name and result.Item2 for last name.
An alternate would be to create a simple class with two properties, FirstName and LastName, create an instance of the class and return the values so we would have result.FirstName and result.LastName.
Visual Studio 2017/VB.NET 15.4 Tuples
Visual Studio 2017, VB.NET 15.3 changed this by allowing a developer to create named values for tuples returning from a method call. When you instantiate the tuple, you define the number and the data type of each value (or element). For example, a 2-tuple (or pair) has two elements. For example, you have a strong typed list of Person class and want to return only first and last name you can write code as shown below.
Basic Example
Public Function FindPersonByIdentifierAsTuple(pIdentifier As Integer) As (FirstName As String, LastName As String)
Dim personData = People.FirstOrDefault(Function(person) person.Identifier = pIdentifier)
If personData Is Nothing Then
Return ("", "")
Else
Return (personData.FirstName, personData.LastName)
End If
End Function
Which would be called as follows
Dim identifier As Integer = 3
Dim ops = New Operations
Dim results = ops.FindPersonByIdentifierAsTuple(identifier)
If Not String.IsNullOrWhiteSpace(results.FirstName) Then
MessageBox.Show($"{results.FirstName} {results.LastName} for id of {identifier}")
Else
MessageBox.Show($"Failed to locate a person with the id of {identifier}")
End If
The quark here for some is checking to see if results.FirstName is an empty string which indicates the passed identifier was not located. Some might consider simple returning a class instance as shown below which is perfectly okay to do yet what about if the class Person has many properties and moving parts were one example might be a Entity Framework entity such as Person with Account members? In this case the returning container of the instance Person is heavy rather than light weight as in the tuple example shown above.
Public Function FindPersonByIdentifierAsPerson(pIdentifier As Integer) As Person
Return People.FirstOrDefault(Function(person) person.Identifier = pIdentifier)
End Function
Anonymous type replacement
What is Anonymous type in VB.NET? An anonymous type is a class that contains one or more named values. Provides a quick and easy way to define simple classes for holding multiple values. Supports both mutable and immutable anonymous types.
A perfect example for using the new tuple is writing a LINQ or Lambda query which returns an anonymous type, using named tuples a developer can return two or more values from a method as shown below where a character is passed in, a Lambda statement finds the character and returns the character, Occurrences of that character and the code for the character.
Public Function CouldHaveBeenAnonymousResults(pChar As Char) As (Item As Char, Occurrences As Integer, Code As Integer)
Dim results =
(
From T In
(
From c In "T0*A1?0*23aTA3 4T4\+a4 ?407#?A*6T+".ToCharArray()
Group c By c Into Group Select New With
{
.Item = c,
.Occurrences = Group.Count,
.Code = Asc(c)
}
).ToList.OrderBy(Function(x) x.Item)
).FirstOrDefault(Function(x) x.Item = pChar)
Return (results.Item, results.Occurrences, results.Code)
End Function
Anonymous/Iterator/Yield example
An Anonymous function can be an iterator function and can return a tuple. In the following example a DataTable is loaded with mocked data which would represent data returned from reading data from a database table. The task is to find duplicates by name and return the primary key. In GetDuplicatesByIdentifier a Lambda statement goes a GroupBy to get the duplicates where the variable duplicates is IEnumberable(Of 'a') which is an anonymous type with three properties, in this case we will return name and identifier in the iterator function via Yield statement. In the calling method the name and identifier are placed into a second DataGridView to see the results. In a application the task might show this to the user and allow them to delete or not delete the duplicate rows.
Public Class Form1
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Dim dt = New DataTable With {.TableName = "MyTable"}
dt.Columns.Add(New DataColumn With {.ColumnName = "Identifier",
.DataType = GetType(Int32),
.AutoIncrement = True, .AutoIncrementSeed = 1,
.ColumnMapping = MappingType.Hidden})
dt.Columns.Add(New DataColumn With {.ColumnName = "Name",
.DataType = GetType(String)})
dt.Columns.Add(New DataColumn With {.ColumnName = "Status",
.DataType = GetType(Boolean)})
dt.Rows.Add(New Object() {Nothing, "Karen", False})
dt.Rows.Add(New Object() {Nothing, "Karen", True})
dt.Rows.Add(New Object() {Nothing, "Bill", True})
dt.Rows.Add(New Object() {Nothing, "Karen", False})
dt.Rows.Add(New Object() {Nothing, "Bill", True})
DataGridView1.DataSource = dt
End Sub
Private Sub cmdExecute_Click(sender As Object, e As EventArgs) Handles cmdExecute.Click
DataGridView2.Rows.Clear()
For Each item In GetDuplicatesByIdendifier()
DataGridView2.Rows.Add(item.Identifer, item.Name)
Next
End Sub
Public Iterator Function GetDuplicatesByIdendifier() As IEnumerable(Of (Name As String, Identifer As Integer))
Dim dt = CType(DataGridView1.DataSource, DataTable)
Dim duplicates = dt.AsEnumerable().
GroupBy(Function(r) New With
{
Key .Name = CStr(r("Name")),
Key .Status = r("Status"),
.Identifier = CInt(r("Identifier"))
}).
Where(Function(gr) gr.Count() > 1).Select(Function(g) g.Key)
For Each d In duplicates
Yield (d.Name, d.Identifier)
Next
End Function
End Class
Another example using a iterator method using Entity Framework where a subset of data is needed. In this case the task is to return all countries with an identifier of 4 which is Brazil with the fields CustomerIdentifier (the primary key), CompanyName and ContactName where each field will have a different name then in the Entity model.
Public Class Form1
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
Dim ops As New Operations
For Each customer In ops.CustomersByCountryIdentifier(4)
Console.WriteLine($"Id: {customer.Identifer} Name: {customer.Name} Contact: {customer.ContactName}")
Next
End Sub
End Class
Public Class Operations
Public Iterator Function CustomersByCountryIdentifier(
pCountryIdentifier As Integer) As IEnumerable(Of
(Identifer As Integer, Name As String, ContactName As String))
Using context As New NorthWindEntities
Dim results = context.Customers.
Where(Function(cust) cust.CountryIdentfier.HasValue And
cust.CountryIdentfier.Value = pCountryIdentifier).
Select(Function(cust) New With
{
.Id = cust.CustomerIdentifier,
.Company = cust.CompanyName,
.Contact = cust.ContactName
})
For Each customer In results
Yield (customer.Id, customer.Company, customer.Contact)
Next
End Using
End Function
End Class
Caveats
VB.NET tuples do not support discards as in C# 7.
Summary
Using the new style tuple provides additional options for returning values from a function when more than one value needs to be returned and on the caller side the members of the tuple are easily understandable and strong typed.
Other resources
Source code
https://github.com/karenpayneoregon/VisualBasicNewTuples
In the source code there are three projects, one for demonstrating using a class instance to return data, the second for performing the same operation as the first project using named tuples while the third project demonstrates the alternate to working with anonymous types for dealing with returning information from a method. Note there are several classes that are there to show what could be returned if in the class example would be returned rather than using a light weight solution as with tuples.
Requires
From NuGet package manager in your solution, add System.ValueTuple from the "Browse tab" or from NuGet console PM>Install-Package System.ValueTuple -Version 4.5.0