Sobre la implementación interna de las tuplas-valor

“… ¡Azúcar!”
Exclamación que popularizó la cantante cubana Celia Cruz (la Reina de la Salsa)

Algunas entradas atrás prometimos hablar sobre los mecanismos en los que se apoyan C# 7.0 y posteriores (note el lector que ya está disponible C# 7.2) para dar soporte al nuevo “azúcar sintáctico” que permite hacer uso de las tuplas-valor, y eso es precisamente sobre lo que trataremos hoy aquí.

Ya en aquella ocasión indicábamos que la base fundamental para la característica lo aporta el tipo (o familia de tipos) System.ValueTuple, que se diferencia de la ahora cuasi-obsoleta System.Tuple introducida en .NET Framework 4.0 en varias aspectos fundamentales:

  • ValueTuple es una estructura en vez de una clase, lo que lo hace en principio más ligero y eficiente. En particular, esto hace posible que el compilador aloje las variables locales de ese tipo, así como las tuplas-valor que sean argumentos de entrada y valores de retorno, en la pila (stack) en vez de la memoria dinámica.
  •  Los miembros de una estructura ValueTuple son campos públicos en vez de propiedades de solo lectura, lo que nuevamente permite generar un código más rápido para el acceso a ellos.
  • Como consecuencia de lo anterior, a diferencia de lo que ocurría en el caso de Tuple, los elementos de las tuplas-valor pueden ser modificados (son mutables).
  • Si bien por defecto el convenio de nombres para los elementos de una tupla es el mismo que utiliza Tuple (Item1, Item2, etc.), para las tuplas-valor el lenguaje soporta el uso de nombres alternativos para los campos.

Cuando hablamos de ValueTuple, en realidad nos estamos refiriendo a una familia de ocho estructuras genéricas ValueTuple<T1>, ValueTuple<T1, T2>, ValueTuple<T1, T2, T3>, etc. que se utilizan para trabajar con tuplas-valor de 1, 2, 3, etc. componentes. Para el remoto caso en que se necesite definir tuplas de 8 o más elementos, el “octavo pasajero” en el tipo con ocho parámetros es en realidad otra tupla, lo que nos permitiría extendernos hasta 14 componentes (o más, si se aplicara nuevamente el truco). Por simplicidad, en el código a continuación nos ceñiremos a duplas (o sea, tuplas con dos componentes).  Existe además una versión no genérica de ValueTuple, que ofrece algunos métodos auxiliares de propósito general, y en particular métodos fábrica estáticos Create<T1>, Create<T1, T2>, etc. para la creación de tuplas-valor de los tipos genéricos.

Si se desensamblan los tipos ValueTuple<T1, T2> y ValueTuple utilizando una herramienta adecuada (yo estoy utilizando el Assembly Browser de Visual Studio for Mac, que me ha sorprendido muy favorablemente) se verá algo similar al código a continuación, en el que se muestra prácticamente todo lo que es necesario conocer sobre la implementación interna de las tuplas-valor. Nótese que las tuplas-valor implementan las interfaces de comparación estructural, que hacen posible la comparación y ordenación de entidades de esos tipos basadas en los valores de sus campos.

namespace System
{
    [Serializable]
    [StructLayout (LayoutKind.Auto)]
    public struct ValueTuple<T1, T2> : IValueTupleInternal, ITuple,
        IEquatable<ValueTuple<T1, T2>>, 
        IComparable, IComparable<ValueTuple<T1, T2>>,
        IStructuralEquatable, IStructuralComparable
    {
        // Campos
        public T1 Item1;
        public T2 Item2;
 
        // Constructor
        public ValueTuple (T1 item1, T2 item2) { Item1 = item1; Item2 = item2; }
 
        // Implementación de las interfaces, Equals y GetHashCode
    }

    public struct ValueTuple
    {
        // Fábrica de tuplas de dos elementos
        public static ValueTuple<T1, T2> Create<T1, T2> (T1 item1, T2 item2)
        {
            return new ValueTuple<T1, T2> (item1, item2);
        }

        // ...
    }
}

El código de ejemplo que he preparado para hoy se apoya en la clase Person que hemos venido utilizando anteriormente, y es el siguiente:

01    using System;
02    class MainClass
03    {
04        static Person dh = new Person("Denis"new DateTime(19851227));
05 
06        static void Main()
07        {
08            var d = GetPersonData();
09            // Imprime 'Denis is 31'
10            Console.WriteLine($"{d.Name} is {d.Age}");
11        }
12 
13        static (string Name, int Age) GetPersonData()
14        {
15            int years = Years(dh.BirthDate, DateTime.Now);
16            return (dh.Name, years);
17        }
18 
19        static int Years(DateTime d1, DateTime d2)
20        {
21            return (d2.Year - d1.Year - 1) +
22                (((d2.Month > d1.Month) ||
23                 ((d2.Month == d1.Month) && (d2.Day >= d1.Day))) ? 1 : 0);
24         }
25     }

El siguiente es el código que muestra el desensamblador para dicha clase (solo se presentan las porciones relevantes):

01        private static void Main()
02        {
03            ValueTuple<stringint> d = GetPersonData();
04            Console.WriteLine(string.Format ("{0} is {1}", d.Item1, d.Item2));
05        }
06
07        [return: TupleElementNames (new string[] { "Name", "Age" })]
08        private static ValueTuple<stringint> GetPersonData()
09        {
10            int years = Years(dh.BirthDate, DateTime.Now);
11            return new ValueTuple<stringint>(dh.Name, years);
12        }

Observe cómo el compilador transforma las líneas 08, 10, 13 y 16 del código original en las líneas 03, 04, 08 y 11 del código compilado utilizando los conceptos explicados anteriormente. ¡Azúcar! La única duda que podría quedar es de qué va el atributo generado para el método GetPersonData en la línea 07, pero el propio texto de esa línea y el de la línea 04 seguramente le habrán dado una buena pista. Con ello continuaremos en la próxima entrega, porque ésta ya se ha hecho demasiado larga.


Referencia musical: Celia Cruz fue una excelsa cantante que llevó la música cubana y su amor por Cuba a todos los rincones del planeta y que tuve la gran suerte de conocer personalmente. Su extensa obra ha quedado ampliamente plasmada en múltiples fuentes audiovisuales disponibles. Ahora bien, hace un tiempo que está en Netflix una serie sobre su vida que a mi modo de ver no le hace ninguna justicia: además de incorporar muy pocas de sus canciones (¡y son 80 capítulos!), se basa en un guión verdaderamente atroz que exhuma cursilería, maniqueísmo y frecuentes faltas a la verdad e incorrecciones históricas, entre otros defectos. Yo sólo me la “fumé” por motivos nostálgicos: oír hablar a los personajes interpretados por actores cubanos (especialmente los femeninos, que tanto me recordaban a mi madre). Si usted no se encuentra en mi situación, le recomiendo que se abstenga de verla y aproveche su tiempo de una mejor manera.

Octavio Hernandez

Desarrollador y consultor en tecnologías .NET. Microsoft C# MVP entre 2004 y 2010.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *