Sometimes you want 2 returned values: playing around with Tuples
When writing code in various languages, you’ll write functions from which you get a return value.
Sometimes you’ll want to get 2 return values. A common way to handle this is to use parameters to pass a variable by reference that will get one of the return values. Alternatively, you could use a Structure, Dictionary or Map or List, and pass that around but those are heavyweight.
A lighter way is to use the std::pair class (C++: example Use a Custom Allocator for your STL container ) or the KeyValuePair Generic Structure in VB or C#.
See also the Tuple class , new to CLR4, which can hold multiple values. You can even have a Dictionary with the Value being a Tuple.
Dim resBoth = GetTwoResults()
Dim theFirst = resBoth.Key
Dim theSecond = resBoth.Value
Function GetTwoResultsOneByref(ByRef theSecond As String) As String
' caller must have already declared theSecond
theSecond = "two"
Return "One"
End Function
Sub GetTwoResultsTwoByref(ByRef theFirst As String, ByRef theSecond As String)
' caller must have already declared both theFirst and theSecond
theFirst = "One"
theSecond = "two"
End Sub
Function GetTwoResults() As KeyValuePair(Of String, String)
Dim FirstResult = "One"
Dim SecondRes = "two"
Dim dp As New KeyValuePair(Of String, String)(FirstResult, SecondRes)
Return dp
End Function
More things to play with:
Function Get2Values() As Tuple(Of Integer, String)
Return New Tuple(Of Integer, String)(11, "#of inches light travels in 1 nanosecond")
End Function
Function GetManyManyValues() As Dictionary(Of String, Tuple(Of Integer, Integer))
Return New Dictionary(Of String, Tuple(Of Integer, Integer)) From
{
{"one", New Tuple(Of Integer, Integer)(1, 2)},
{"two", New Tuple(Of Integer, Integer)(1, 2)}
}
End Function
I was writing some code using a Dictionary with an Enum as the key, and Tuple as the value.
However, sometimes not every Enum had a dictionary entry, so the code would throw an exception. So I wrote my own wrapper class of a Dictionary.
A sample is below: it uses the processes running on your machine as data, gets the number of threads per process, puts them arbitrarily into buckets. (Note: the “\” operator is integer division with truncation).
Then a summary of the buckets is created: count and size. However, not every bucket will have a value, so the dictionary will throw an exception.
A way to get around that is to create your own dictionary, handling the case where the key is absent in the overloaded default item property.
My first attempt is the MyBTDictNonGeneric below. It’s just a non-generic wrapper. Then I tried a generic approach.
Start Visual Studio 2010->File-New->Project->VB->WPF Application.
Switch to the MainWindow.xaml.vb file and replace the contents with the sample.
A Default Property is needed to create own dictionary which from which values can be returned
The “As New” is needed to constrain the generic to types that can have a New with no parameters.
'New' cannot be used on a type parameter that does not have a 'New' constraint
See also Use DataTemplates and WPF in code to create a general purpose LINQ Query results display
<code>
Class MainWindow
Enum Buckets
wee
tiny
small
medium
large
larger
grand
super
extralarge
biggest
End Enum
Private Sub Window_Loaded(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles MyBase.Loaded
Me.Width = 1200
Me.Height = 800
Dim numBuckets = [Enum].GetNames(GetType(Buckets)).Length
Dim queryProc = From proc In System.Diagnostics.Process.GetProcesses
Let ThrdCnt = proc.Threads.Count
Let Bucket = ThrdCnt \ numBuckets
Where Bucket < numBuckets
Select proc.ProcessName,
proc.WorkingSet64,
ThrdCnt,
BucketName = CType(Bucket, Buckets).ToString
' now accum results Count and Size per bucket
Dim tots = From data In queryProc
Group By data.BucketName
Into Cnt = Count(), size = Sum(data.WorkingSet64)
'put results into a dictionary keyed on bucket name
Dim dict1 = New Dictionary(Of Buckets, Tuple(Of Integer, Long)) ' this won't work if values missing
Dim dict2 = New MyBTDict(Of Buckets, MyTuple(Of Integer, Long))
For Each datum In tots
dict1.Add([Enum].Parse(GetType(Buckets), datum.BucketName), New Tuple(Of Integer, Long)(datum.Cnt, datum.size))
dict2.Add([Enum].Parse(GetType(Buckets), datum.BucketName), New MyTuple(Of Integer, Long)(datum.Cnt, datum.size))
Next
Dim nTotCnt = 0
Dim nTotSize As Long = 0
Try ' enumerating all buckets: if there's no dict entry, will throw
For Each bucket In [Enum].GetNames(GetType(Buckets))
Dim tup = dict1([Enum].Parse(GetType(Buckets), bucket))
nTotCnt += tup.Item1
nTotSize += tup.Item2
Next
Catch ex As Exception
MsgBox("Dict1 doesn't work " + ex.Message)
End Try
' with our version of dict, no problem
For Each bucket In [Enum].GetNames(GetType(Buckets))
Dim tup = dict2([Enum].Parse(GetType(Buckets), bucket))
nTotCnt += tup.Item1
nTotSize += tup.Item2
Next
Dim sp As New StackPanel With {.Orientation = Orientation.Horizontal}
sp.Children.Add(New Browse(queryProc))
Dim q2 = From a In dict2
Select Bucket = a.Key.ToString, a.Value.Item1, a.Value.Item2
sp.Children.Add(New Browse(q2))
Me.Content = sp
End Sub
Class MyTuple(Of t1 As New, t2 As New) 'need MyTuple, which has a Public New with no params
Inherits Tuple(Of t1, t2)
Sub New()
MyBase.New(New t1, New t2)
End Sub
Sub New(ByVal p1 As t1, ByVal p2 As t2)
MyBase.New(p1, p2)
End Sub
End Class
Private Class MyBTDict(Of key, value As New) ' value must be constructable
Inherits Dictionary(Of key, value)
Default Overloads ReadOnly Property item(ByVal bt As key) As value
Get
Dim val As value = Nothing
If Not Me.TryGetValue(bt, val) Then
val = New value
End If
Return val
End Get
End Property
End Class
Private Class MyBTDictNonGeneric ' or you could use this non-generic wrapper version
Private _dict As New Dictionary(Of Buckets, Tuple(Of Integer, Long))
Sub add(ByVal bt As Buckets, ByVal nCnt As Integer, ByVal nSize As Long)
_dict.Add(bt, New Tuple(Of Integer, Long)(nCnt, nSize))
End Sub
Default ReadOnly Property MyDefault(ByVal bt As Buckets) As Tuple(Of Integer, Long)
Get
Dim tp As Tuple(Of Integer, Long) = Nothing
If Not _dict.TryGetValue(bt, tp) Then
tp = New Tuple(Of Integer, Long)(0, 0)
End If
Return tp
End Get
End Property
End Class
End Class
'The Browse class: https://blogs.msdn.com/calvin\_hsia/archive/2007/11/15/6275106.aspx
Class Browse
Inherits ListView
Sub New(ByVal Query As Object, Optional ByVal Parent As Object = Nothing)
Dim gv As New GridView
Me.View = gv
Me.ItemsSource = Query
If Not Parent Is Nothing Then
If Parent.GetType.BaseType Is GetType(Window) Then
CType(Parent, Window).Title = "# items = " + Me.Items.Count.ToString
End If
End If
Me.AddHandler(GridViewColumnHeader.ClickEvent, New RoutedEventHandler(AddressOf HandleHeaderClick))
For Each mem In From mbr In _
Query.GetType().GetInterface(GetType(IEnumerable(Of )).FullName) _
.GetGenericArguments()(0).GetMembers _
Where mbr.MemberType = Reflection.MemberTypes.Property
Dim coltype = CType(mem, Reflection.PropertyInfo).PropertyType.Name
Select Case coltype
Case "Int32", "String", "Int64"
Dim gvc As New GridViewColumn
gvc.Header = mem.Name
gv.Columns.Add(gvc)
If coltype = "Int32" Then
gvc.Width = 80
Dim dt As New DataTemplate
Dim factSP = New FrameworkElementFactory(GetType(StackPanel))
dt.VisualTree = factSP
factSP.SetValue(StackPanel.OrientationProperty, Orientation.Horizontal)
Dim factTb = New FrameworkElementFactory(GetType(TextBlock))
factTb.SetBinding(TextBlock.TextProperty, New Binding(mem.Name))
factTb.SetValue(TextBlock.FontWeightProperty, FontWeights.Bold)
' factTb.SetValue(TextBlock.BackgroundProperty, Brushes.SpringGreen)
factSP.AppendChild(factTb)
'factTb = New FrameworkElementFactory(GetType(Button))
'factTb.SetValue(Button.ContentProperty, "hit me")
'factSP.AppendChild(factTb)
gvc.CellTemplate = dt
Else
gvc.DisplayMemberBinding = New Binding(mem.Name)
gvc.Width = 180
End If
End Select
Next
Dim MyListboxStyle As New Style(GetType(ListBoxItem))
Dim tr = New Trigger
tr.Property = ListBoxItem.IsSelectedProperty ' when Selected
tr.Value = True ' is true
tr.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.Bisque))
MyListboxStyle.Triggers.Add(tr)
tr = New Trigger
tr.Property = ListBoxItem.IsMouseOverProperty ' when mouseover
tr.Value = True ' is true
tr.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.Red))
tr.Setters.Add(New Setter(ListBoxItem.BackgroundProperty, Brushes.Aquamarine)) ' this trigger sets both fore and back properties
MyListboxStyle.Setters.Add(New Setter(ListBoxItem.ForegroundProperty, Brushes.RoyalBlue)) ' set style for all items
MyListboxStyle.Triggers.Add(tr)
Me.ItemContainerStyle = MyListboxStyle
End Sub
Dim _Lastdir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending
Dim _LastHeaderClicked As GridViewColumnHeader = Nothing
Sub HandleHeaderClick(ByVal sender As Object, ByVal e As RoutedEventArgs)
If e.OriginalSource.GetType Is GetType(GridViewColumnHeader) Then
Dim gvh = CType(e.OriginalSource, GridViewColumnHeader)
Dim dir As System.ComponentModel.ListSortDirection = ComponentModel.ListSortDirection.Ascending
If Not gvh Is Nothing AndAlso Not gvh.Column Is Nothing Then
Dim hdr = gvh.Column.Header
If gvh Is _LastHeaderClicked Then
If _Lastdir = ComponentModel.ListSortDirection.Ascending Then
dir = ComponentModel.ListSortDirection.Descending
End If
End If
Sort(hdr, dir)
_LastHeaderClicked = gvh
_Lastdir = dir
End If
End If
End Sub
Sub Sort(ByVal sortby As String, ByVal dir As System.ComponentModel.ListSortDirection)
Me.Items.SortDescriptions.Clear()
Dim sd = New System.ComponentModel.SortDescription(sortby, dir)
Me.Items.SortDescriptions.Add(sd)
Me.Items.Refresh()
End Sub
End Class
</code>
Comments
Anonymous
April 29, 2010
You can also use it in place of anonymous classes: http://weblogs.asp.net/nmarun/archive/2010/04/01/tuple-net-4-0-new-feature.aspx ArunAnonymous
May 02, 2010
The comment has been removed