Tuplas (Visual Basic)

A partir do Visual Basic 2017, a linguagem do Visual Basic oferece suporte interno para tuplas que facilitam a criação de tuplas e o acesso aos elementos de tuplas. Tupla é uma estrutura de dados leve que possui um número específico e uma sequência de valores. Ao instanciar a tupla, você define o número e o tipo de dados de cada valor (ou elemento). Por exemplo, uma tupla de 2 (ou par) tem dois elementos. O primeiro pode ser um valor Boolean, enquanto o segundo é um String. Como as tuplas facilitam o armazenamento de vários valores em um único objeto, elas geralmente são usadas como uma maneira leve de retornar vários valores de um método.

Importante

O suporte à tupla requer o tipo ValueTuple. Se o .NET Framework 4.7 não estiver instalado, você deverá adicionar o pacote System.ValueTupledo et, que está disponível na Galeria do NuGet. Sem esse pacote, você pode obter um erro de compilação semelhante a "O tipo predefinido 'ValueTuple(Of,,,)' não é definido ou importado."

Instanciar e usar uma tupla

Você cria uma instância de uma tupla colocando seus valores delimitados por vírgula entre parênteses. Cada um desses valores, em seguida, torna-se um campo da tupla. Por exemplo, o código a seguir define um triplo (ou 3 tuplas) com um Date como seu primeiro valor, um String como segundo valor e um Boolean como terceiro.

Dim holiday = (#07/04/2017#, "Independence Day", True)

Por padrão, o nome de cada campo em uma tupla consiste na cadeia de caracteres Item junto com a posição baseada em um campo na tupla. Para esta tupla de 3, o campo Date é Item1, o campo String é Item2 e o campo Boolean é Item3. O exemplo a seguir exibe os valores dos campos da tupla para qual foi criada uma instância na linha de código anterior

Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' Output: 7/4/2017 12:00:00 AM Is Independence Day, a national holiday

Os campos de uma tupla do Visual Basic são leitura-gravação. Depois de instanciar uma tupla, você pode modificar seus valores. O exemplo a seguir modifica dois dos três campos da tupla criada no exemplo anterior e exibe o resultado.

holiday.Item1 = #01/01/2018#
holiday.Item2 = "New Year's Day"
Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' Output: 1/1/2018 12:00:00 AM Is New Year's Day, a national holiday

Criar uma instância e usar uma tupla nomeada

Em vez de usar nomes padrão para os campos de uma tupla, você pode criar uma instância para uma tupla nomeada atribuindo seus próprios nomes aos elementos da tupla. Os campos da tupla podem ser acessados por seus nomes atribuídos ou por seus nomes padrão. O exemplo a seguir cria a mesma tupla de 3 como antes, exceto que ele nomeia explicitamente o primeiro campo EventDate, o segundo Name e o terceiro IsHoliday. Em seguida, ele exibe os valores de campo, modifica-os e exibe os valores do campo novamente.

Dim holiday = (EventDate:=#07/04/2017#, Name:="Independence Day", IsHoliday:=True)
Console.WriteLine($"{holiday.EventDate} Is {holiday.Name}" +
                  $"{If(holiday.IsHoliday, ", a national holiday", String.Empty)}")
holiday.Item1 = #01/01/2018#
holiday.Item2 = "New Year's Day"
Console.WriteLine($"{holiday.Item1} is {holiday.Item2}" +
                  $"{If(holiday.Item3, ", a national holiday", String.Empty)}")
' The example displays the following output:
'   7/4/2017 12:00:00 AM Is Independence Day, a national holiday
'   1/1/2018 12:00:00 AM Is New Year's Day, a national holiday

Você também pode especificar os nomes de tupla como parte da declaração de tipo de uma variável, campo ou parâmetro:

Dim holiday As (EventDate As Date, Name As String, IsHoliday As Boolean) =
    (#07/04/2017#, "Independence Day", True)
Console.WriteLine(holiday.Name)
' Output: Independence Day

ou no tipo de retorno de um método.

Isso é particularmente útil ao fornecer tuplas para um inicializador de coleção; os nomes de tupla podem ser fornecidos como parte da declaração de tipo da coleção:

Dim events As New List(Of (EventDate As Date, Name As String, IsHoliday As Boolean)) From {
    (#07/04/2017#, "Independence Day", True),
    (#04/22/2017#, "Earth Day", False)
}
Console.WriteLine(events(1).IsHoliday)
' Output: False

Nomes de elementos de tupla inferidos

A partir do Visual Basic 15.3, o Visual Basic pode inferir os nomes dos elementos de tupla – você não precisa atribuí-los explicitamente. Os nomes de tupla inferidos são úteis quando você inicializa uma tupla de um conjunto de variáveis e deseja que o nome do elemento tupla seja o mesmo que o nome da variável.

O exemplo a seguir cria uma tupla stateInfo que contém três elementos explicitamente nomeados, state, stateName e capital. Observe que, ao nomear os elementos, a instrução de inicialização de tupla simplesmente atribui aos elementos nomeados os valores das variáveis nomeadas de forma idêntica.

Const state As String = "MI"
Const stateName As String = "Michigan"
Const capital As String = "Lansing"
Dim stateInfo = (state:=state, stateName:=stateName, capital:=capital)
Console.WriteLine($"{stateInfo.stateName}: 2-letter code: {stateInfo.state}, Capital {stateInfo.capital}")
' The example displays the following output:
'      Michigan: 2-letter code: MI, Capital Lansing

Como elementos e variáveis têm o mesmo nome, o compilador do Visual Basic pode inferir os nomes dos campos, como mostra o exemplo a seguir.

Const state As String = "MI"
Const stateName As String = "Michigan"
Const capital As String = "Lansing"
Dim stateInfo = (state, stateName, capital)
Console.WriteLine($"{stateInfo.stateName}: 2-letter code: {stateInfo.State}, Capital {stateInfo.capital}")
' The example displays the following output:
'      Michigan: 2-letter code: MI, Capital Lansing

Para habilitar nomes de elementos de tupla inferidos, você deve definir a versão do compilador do Visual Basic a ser usada no arquivo do projeto do Visual Basic (*.vbproj):

<PropertyGroup>
  <LangVersion>15.3</LangVersion>
</PropertyGroup>

O número de versão pode ser qualquer versão do compilador do Visual Basic começando com 15.3. Em vez de codificar uma versão específica do compilador, você também pode especificar "Latest" como o valor de LangVersion para compilar com a versão mais recente do compilador do Visual Basic instalado em seu sistema.

Para obter mais informações, confira Definir da versão de linguagem do Visual Basic.

Em alguns casos, o compilador do Visual Basic não pode inferir o nome do elemento de tupla do nome do candidato e o campo de tupla só pode ser referenciado usando seu nome padrão, como Item1, Item2 etc. Eles incluem:

  • O nome do candidato é o mesmo que o nome de um membro de tupla, como Item3, Rest ou ToString.

  • O nome do candidato é duplicado na tupla.

Quando a inferência do nome do campo falha, o Visual Basic não gera um erro do compilador nem é uma exceção gerada no tempo de execução. Em vez disso, os campos de tupla devem ser referenciados por seus nomes predefinidos, como Item1 e Item2.

Tuplas versus estruturas

Uma tupla do Visual Basic é um tipo de valor que é uma instância de um dos tipos genéricos System.ValueTuple. Por exemplo, a tupla holiday definida no exemplo anterior é uma instância da estrutura ValueTuple<T1,T2,T3>. Ela foi projetada para ser um contêiner leve para dados. Como a tupla tem como objetivo facilitar a criação de um objeto com vários itens de dados, ela não tem alguns dos recursos que uma estrutura personalizada pode ter. Estão incluídos:

  • Membros personalizados. Você não pode definir suas próprias propriedades, métodos ou eventos para uma tupla.

  • Validação. Não é possível validar os dados atribuídos aos campos.

  • Imutabilidade. As tuplas do Visual Basic são mutáveis. Por outro lado, uma estrutura personalizada permite que você controle se uma instância é mutável ou imutável.

Se membros personalizados, validação de propriedade e campo ou imutabilidade forem importantes, você deverá usar a instrução Estrutura do Visual Basic para definir um tipo de valor personalizado.

Uma tupla do Visual Basic herda os membros de seu tipo ValueTuple. Além de seus campos, eles incluem os seguintes métodos:

Método Descrição
CompareTo Compara a tupla atual com outra tupla com o mesmo número de elementos.
É igual a Determina se a tupla atual é igual a outra tupla ou objeto.
GetHashCode Calcula o código hash para a instância atual.
ToString Retorna a representação de cadeia de caracteres dessa tupla, que usa o formulário (Item1, Item2...), onde Item1 e Item2 representam os valores dos campos da tupla.

Além disso, os tipos ValueTuple implementam as interfaces IStructuralComparable e IStructuralEquatable, que permitem definir comparadores personalizados.

Atribuição e tuplas

O Visual Basic dá suporte à atribuição entre tipos de tupla que têm o mesmo número de campos. Os tipos de campo poderão ser convertidos se um dos seguintes for verdadeiro:

  • O campo de origem e de destino é do mesmo tipo.

  • Uma conversão de ampliação (ou implícita) do tipo de origem para o tipo de destino é definida.

  • Option Strict é On, e uma conversão de redução (ou explícita) do tipo de origem para o tipo de destino é definida. Essa conversão poderá gerar uma exceção se o valor de origem estiver fora do intervalo do tipo de destino.

Outras conversões não são consideradas para atribuições. Vamos examinar os tipos de atribuições que são permitidos entre tipos de tupla.

Considere estas variáveis usadas nos exemplos a seguir:

' The number and field types of all these tuples are compatible. 
' The only difference Is the field names being used.
Dim unnamed = (42, "The meaning of life")
Dim anonymous = (16, "a perfect square")
Dim named = (Answer:=42, Message:="The meaning of life")
Dim differentNamed = (SecretConstant:=42, Label:="The meaning of life")

As primeiras duas variáveis, unnamed e anonymous, não têm nomes semânticos fornecidos para os campos. Seus nomes de campo são o padrão Item1 e Item2. As duas últimas variáveis, named e differentName, têm nomes semânticos de campos. Observe que essas duas tuplas têm nomes diferentes para os campos.

Todas essas quatro tuplas têm o mesmo número de campos (chamados de ‘arity’) e os tipos desses campos são idênticos. Portanto, todas essas atribuições funcionam:

' Assign named to unnamed.
named = unnamed

' Despite the assignment, named still has fields that can be referred to as 'answer' and 'message'.
Console.WriteLine($"{named.Answer}, {named.Message}")
' Output:  42, The meaning of life

' Assign unnamed to anonymous.
anonymous = unnamed
' Because of the assignment, the value of the elements of anonymous changed.
Console.WriteLine($"{anonymous.Item1}, {anonymous.Item2}")
' Output:   42, The meaning of life

' Assign one named tuple to the other.
named = differentNamed
' The field names are Not assigned. 'named' still has 'answer' and 'message' fields.
Console.WriteLine($"{named.Answer}, {named.Message}")
' Output:   42, The meaning of life

Observe que os nomes das tuplas não são atribuídos. Os valores dos campos são atribuídos na ordem dos campos na tupla.

Por fim, observe que podemos atribuir a tupla à named tupla conversion, mesmo que o primeiro campo de named seja um Integer, e o primeiro campo de conversion seja um Long. Essa atribuição é bem-sucedida porque converter um Integer em um Long é uma conversão de expansão.

' Assign an (Integer, String) tuple to a (Long, String) tuple (using implicit conversion).
Dim conversion As (Long, String) = named
Console.WriteLine($"{conversion.Item1} ({conversion.Item1.GetType().Name}), " +
                  $"{conversion.Item2} ({conversion.Item2.GetType().Name})")
' Output:      42 (Int64), The meaning of life (String)

As tuplas com diferentes números de campos não são atribuíveis:

' Does not compile.
' VB30311: Value of type '(Integer, Integer, Integer)' cannot be converted
'          to '(Answer As Integer, Message As String)'
var differentShape = (1, 2, 3)
named = differentShape

Tuplas como valores retornados do método

Um método pode retornar apenas um único valor. Com frequência, porém, você gostaria que uma chamada de método retornasse vários valores. Para solucionar essa limitação, há algumas alternativas:

  • Você pode criar uma classe ou estrutura personalizada cujas propriedades ou campos representam valores retornados pelo método. Esta é uma solução pesada e requer que você defina um tipo personalizado cuja única finalidade é recuperar valores de uma chamada de método.

  • Você pode retornar um único valor do método e retornar os valores restantes passando-os por referência ao método. Isso envolve a sobrecarga da criação de instância para uma variável e corre o risco de substituir inadvertidamente o valor da variável que você passa por referência.

  • Você pode usar uma tupla, que fornece uma solução leve para recuperar vários valores retornados.

Por exemplo, os métodos TryParse no .NET retornam um valor Boolean que indica se a operação de análise foi bem-sucedida. O resultado da operação de análise é retornado em uma variável passada por referência ao método. Normalmente, uma chamada para um método de análise, como Int32.TryParse se parece com o seguinte:

Dim numericString As String = "123456"
Dim number As Integer
Dim result = Integer.TryParse(numericString, number)
Console.WriteLine($"{If(result, $"Success: {number:N0}", "Failure")}")
'      Output: Success: 123,456

Podemos retornar uma tupla da operação de análise se encapsularmos a chamada para o método Int32.TryParse em nosso próprio método. No exemplo a seguir, NumericLibrary.ParseInteger chama o método Int32.TryParse e retorna uma tupla nomeada com dois elementos.

Imports System.Globalization

Public Module NumericLibrary
    Public Function ParseInteger(value As String) As (Success As Boolean, Number As Integer)
        Dim number As Integer
        Return (Integer.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, number), number)
    End Function
End Module

O chamador pode então chamar o método com o código semelhante ao seguinte:

Dim numericString As String = "123,456"
Dim result = ParseInteger(numericString)
Console.WriteLine($"{If(result.Success, $"Success: {result.Number:N0}", "Failure")}")
Console.ReadLine()
'      Output: Success: 123,456

Tuplas do Visual Basic e tuplas no .NET Framework

Uma tupla do Visual Basic é uma instância de um dos tipos genéricos System.ValueTuple, que foram introduzidos no .NET Framework 4.7. O .NET Framework também inclui um conjunto de classes genéricas System.Tuple. Essas classes, no entanto, diferem das tuplas do Visual Basic e dos tipos genéricos System.ValueTuple de várias maneiras:

  • Os elementos das classes Tupla são propriedades nomeadas Item1, Item2 e assim por diante. Nas tuplas do Visual Basic e nos tipos ValueTuple, os elementos de tupla são campos.

  • Você não pode atribuir nomes significativos aos elementos de uma instância de Tupla ou de uma instância do ValueTuple. O Visual Basic permite atribuir nomes que comunicam o significado dos campos.

  • As propriedades de uma instância de Tupla são somente leitura; as tuplas são imutáveis. Nas tuplas do Visual Basic e nos tipos ValueTuple, os campos de tupla são leitura-gravação e as tuplas são mutáveis.

  • Os tipos genéricos de Tupla são tipos de referência. Usar esses tipos de Tupla significa alocar objetos. Em caminhos de acesso, isso pode ter um impacto mensurável no desempenho do aplicativo. Tuplas do Visual Basic e os tipos ValueTuple são tipos de valor.

Os métodos de extensão na classe TupleExtensions facilitam a conversão entre tuplas do Visual Basic e objetos Tuple do .NET. O método ToTuple converte uma tupla do Visual Basic em um objeto Tupledo .NET e o método ToValueTuple converte um objeto Tuple do .NET em uma tupla do Visual Basic.

O exemplo a seguir cria uma tupla, converte-a em um objeto Tuple do .NET e converte-a, novamente, em uma tupla do Visual Basic. Em seguida, o exemplo compara essa tupla com a original para garantir que elas sejam iguais.

Dim cityInfo = (name:="New York", area:=468.5, population:=8_550_405)
Console.WriteLine($"{cityInfo}, type {cityInfo.GetType().Name}")

' Convert the Visual Basic tuple to a .NET tuple.
Dim cityInfoT = TupleExtensions.ToTuple(cityInfo)
Console.WriteLine($"{cityInfoT}, type {cityInfoT.GetType().Name}")

' Convert the .NET tuple back to a Visual Basic tuple and ensure they are the same.
Dim cityInfo2 = TupleExtensions.ToValueTuple(cityInfoT)
Console.WriteLine($"{cityInfo2}, type {cityInfo2.GetType().Name}")
Console.WriteLine($"{NameOf(cityInfo)} = {NameOf(cityInfo2)}: {cityInfo.Equals(cityInfo2)}")

' The example displays the following output:
'       (New York, 468.5, 8550405), type ValueTuple`3
'       (New York, 468.5, 8550405), type Tuple`3
'       (New York, 468.5, 8550405), type ValueTuple`3
'       cityInfo = cityInfo2 :  True

Confira também