domingo, 24 de julio de 2011

Tipos anulables, inicialización de objetos y tipos anónimos

En este post me gustaría casi completar mi check-list previo, antes de meterme de lleno con Linq.

Después de haber visto clases y métodos parciales, métodos extensores, colecciones, colecciones genéricas e interfaces must-know, me queda comentar los tipos anulables, inicialización de objetos, tipos anónimos, delegados y las expresiones lambda (estos dos últimos puntos lo veremos en un post siguiente porque son lo que más tela tienen que cortar, ya están disponibles los post Delegados en VB.NET (I) y Delegados y Expresiones lambda en VB.NET (II)).

Todo esto es porque quiero aprender Linq y no quiero que los arboles no me dejen ver el bosque.

Los tipos anulables me gustan especialmente porque vienen a llenar un espacio que, en mi opinión, siempre había estado vacío. Permiten que los tipos de datos básicos (Int32, Boolean, etc.) puedan almacenar un valor vacío o nulo además del propio tipo de datos que definen.

Los tipos anulables utilizan la clase System.Nullable(Of T). Esta clase define métodos como HasValue (para saber si estamos guardando un valor distinto de nulo), Value As T (para acceder al valor almacenado – ya sea nulo o cualquier otro), GetValueOrDefault (que permite recuperar el valor almacenado o el valor por defecto en caso de que sea nulo), etc.

Imagina que quieres recibir un parámetro que indica si el usuario quiere buscar los pedidos enviados, no enviados o todos. Antes de los tipos anulables, lo más probable es que hubieras escrito esto:

    Public Function GetPedidos(ByVal enviados As Object) As IEnumerable

        If enviados Is Nothing Then

            ' No se ha especificado un valor, luego devolvemos todos los pedidos

        ElseIf Convert.ToBoolean(enviados) Then

            ' Devolvemos los pedidos enviados

        Else

            ' Devolvemos los pedidos NO enviados

        End If

    End Function


Si te fijas, recibimos un parámetro de tipo Object cuando realmente queríamos recibir un parámetro de tipo Boolean, pero claro ¿Cómo guardar la ausencia de filtro? Además, este acercamiento incurre en una operación de boxing y unboxing, también podría haber errores de conversión ya que el parámetro no está tipado correctamente y podríamos recibido cualquier cosa, etc.

Ahora que tenemos los tipos anulables, el código es mucho más intuitivo y menos propenso a errores:

    Public Function GetPedidos(ByVal enviados As Nullable(Of Boolean)) As IEnumerable

        If Not enviados.HasValue Then

            ' No se ha especificado un valor, luego devolvemos todos los pedidos

        ElseIf enviados.Value Then

            ' Devolvemos los pedidos enviados

        Else

            ' Devolvemos los pedidos NO enviados

        End If

    End Function


Cabe mencionar que además de utilizar el método HasValue (mi opción preferida), sigue siendo válido comparar la variable con Nothing o la función IsNothing.

Otra situación donde los tipos anulables son de gran ayuda es al leer valores de una base de datos. Por ejemplo, si un campo de tipo Integer en nuestra tabla de base también permite almacenar nulos, la única forma digna de recuperar este valor será con un Nullable(Of Integer) porque, recuerda que una asignación como la siguiente no guarda el valor Nothing (o nulo) sino que guarda 0 (ya que estamos trabajando con tipos por valor y estos no pueden guardar una referencia vacía, sino que siempre guardan el valor predeterminado o de inicialización).

clip_image002[1]

La inicialización de objetos y de colecciones es otra característica que últimamente está presente en casi todos los códigos de ejemplo que caen en mis manos.

Esta característica permite declarar, inicializar y asignar valores a un nuevo objeto en una sola línea.

Hace años un código como este hubiera sido válido

        Dim yo As Persona ' Crear una variable de tipo Persona

        yo = New Persona ' Crear una instancia de tipo Persona

        yo.Nombre = "Sergio" ' Asignar valor a una propiedad

        yo.Apellido = "León" ' Asignar valor a una propiedad


Lógicamente, hoy por hoy no escribiríamos eso sino que quizás utilizaríamos este otro código:

        ' Crear, tanto la variable de tipo Persona

        ' como una nueva instancia en una sola sentencia

        Dim yo As New Persona

        With yo

            .Nombre = "Sergio" ' Asignar valor a una propiedad

            .Apellido = "León" ' Asignar valor a una propiedad

        End With


Como vemos, hemos incluido en una sola sentencia tanto la declaración de la variable como la asignación a una nueva instancia del tipo. Además, después hemos utilizado un bloque With para agilizar la asignación de valores a las propiedades del objeto. Parece un buen código (de hecho lo es), pero ¿Te gusta más este otro?

Dim yo As New Persona With {.Nombre = "Sergio", .Apellido = "León"}


Está claro que hemos reducido drásticamente el número de líneas y además seguimos teniendo un código legible e intuitivo (en mi opinión…). Pues bien, este último código es justamente lo que permite la nueva inicialización de objetos: declarar, instanciar y asignar valores en una sola línea.

La inicialización de objetos también se hace extensible a las colecciones (arrays, listas, etc.).

' Crear un lista de personas y añadirla 1 persona

Dim gente As New List(Of Persona) From {New Persona With {.Nombre = "Sergio"}}


' Crear un array e inicializarlo

Dim gente() As Persona = _

{ _

New Persona(), _

New Persona With {.Nombre = "Sergio"}, _

            New Persona With {.Apellido = "León"} _

}

Los tipos anónimos permiten crear objetos sin tener una definición de clase.

 

Si eres programador de Javascript, la sintaxis es muy parecida a como se crean los objetos a través de JSON.

 

Cuando creamos un tipo anónimo se crea automáticamente una clase sin nombre y un nuevo objeto del tipo de esa clase. Un objeto anónimo sólo puede tener propiedades y además el tipo de las mismas se infiere de los valores que asignamos a sus propiedades. Por ejemplo:

 

        ' Tipo anónimo

        ' Nombre es de tipo String (se infiere de Sergio)

        ' Edad es de tipo Integer (se infiere de 35)

        Dim yo = New With {.Nombre = "Sergio", .Edad = 35}

 

clip_image003[1]

 

Como podemos observar, para crear un tipo anónimo hemos hecho lo siguiente:

  • No especificar un tipo para la variable “yo” (se inferirá a partir de una clase automática creada por el tipo anónimo).
  • Utilizar la inicialización de objetos para crear 2 propiedades que además, han inferido su tipo a partir de los valores que le hemos asignado.

Te adelanto que los tipos anónimos son muy importantes en Linq porque muchas veces recibiremos una lista de objetos de un tipo anónimo como resultado de una consulta Linq.

 

Respecto al tipo que se crea automáticamente para el tipo anónimo, cabe mencionar que el compilador creará siempre un nuevo tipo cada vez que declaremos una variable de tipo anónimo si no existe un tipo anónimo a nivel de ensamblado que puede reutilizar. Por ello, si queremos crear varias instancias de un tipo anónimo y que compartan el mismo tipo, la única solución que tenemos es declarar ambas variables con la misma definición (esto es, mismo orden, número, nombre y tipo de propiedades – tipo de la propiedad y además también si es de lectura o de lectura-escritura.). De este modo, el compilador sabrá que ya existe un tipo anónimo que encaja en la definición que le estamos dando y en vez de crear un nuevo tipo, reutilizará el existente.

 

Por ejemplo, el siguiente código genera 2 tipos anónimos distintos:

 

        ' yo es diferente de tu, porque yo tiene la propiedad Apellidos

        Dim yo = New With {.Nombre = "Sergio", .Edad = 35, .Apellidos = "León"}

        Dim tu = New With {.Nombre = "John Doe", .Edad = 35}

        Console.WriteLine(yo.GetType.ToString)

        Console.WriteLine(tu.GetType.ToString)

 

 

clip_image005[1]

 

Sin embargo, este otro código reutiliza el mismo tipo anónimo:

 

        Dim yo = New With {.Nombre = "Sergio", .Edad = 35}

        Dim tu = New With {.Nombre = "John Doe", .Edad = 35}

        Console.WriteLine(yo.GetType.ToString)

        Console.WriteLine(tu.GetType.ToString)

 

 

clip_image006[1]

 

En un tipo anónimo también podemos definir propiedades de sólo-lectura con la palabra Key anteponiéndola al nombre de la propiedad. Que una propiedad sea de sólo-lectura es más relevante de lo que podemos pensar puesto que, además de tener su comportamiento predeterminado de sólo-lectura, será una propiedad que intervendrá en la comparación de 2 instancias del tipo anónimo si se comparan con el método Equals (que hereda de Object, puesto que los tipos anónimos hereden siempre de Object). Es decir, la comparación de objetos de un tipo anónimo se realiza sólo a través de las propiedades de sólo-lectura.

 

Dim yo = New With {.Nombre = "Sergio", Key .Edad = 35}

 

clip_image007[1]

 

        Dim yo = New With {.Nombre = "Sergio", Key .Edad = 35}

        Dim yo2 = New With {.Nombre = "Sergio"}

        Dim yo3 = New With {.Nombre = "Sergio", Key .Edad = 1}

        Dim yo4 = New With {.Nombre = "Sergio", Key .Edad = 35}

 

        Console.WriteLine(yo.Equals(yo2)) ' False

        Console.WriteLine(yo.Equals(yo3)) ' False

        Console.WriteLine(yo.Equals(yo4)) ' True

 

Por último, un aspecto muy importante sobre los tipos anónimos es donde podemos utilizarlos y cuál es su ámbito de visibilidad.

 

En términos generales, un tipo anónimo sólo puede ser utilizado como un tipo local al procedimiento donde se crea. Es decir, no se puede utilizar un tipo anónimo como parámetro en un procedimiento o función, ni puede ser devuelto por una función, ni ser declarada una variable de tipo anónimo a nivel de clase, etc. Insisto en que un tipo anónimo sólo puede ser utilizado como un tipo para una variable local a un procedimiento o función (ya veremos después, cuando lo sepa porque ahora no lo sé, como Linq puede devolver tipos anónimos…)

 

Aunque todo lo dicho respecto a su visibilidad es cierto (todo ello derivado de que no conocemos el tipo anónimo fuera del procedimiento donde fue declarado), se pueden saltar estas reglas pasando el objeto como tipo Object. Esta solución, está claro, que utilizará late-binding y además perderemos intellisense, pero al menos tenemos la posibilidad de jugar un poco más con los tipos anónimos. Veamos un ejemplo:

 

    Sub Main()

        Dim args = New With {.Operador1 = 5, .Operador2 = 10}

        Dim retorno As Object = Sumar(args)

        Console.WriteLine(retorno.Valor) ' 15

        Console.ReadLine()

    End Sub

 

    Public Function Sumar(ByVal args As Object) As Object

        ' Se recibe un argumento que es un tipo anónimo, pero su tipo tiene que ser Object (late-binding)

        Dim suma As Integer = args.Operador1 + args.Operador2

        ' Se devuelve un tipo anónimo, pero de nuevo su tipo también es Object

        Dim retorno = New With {.Valor = suma}

        Return retorno

    End Function

 

A partir de aquí, sólo nos quedaría ver las expresiones lambda, pero como dije al principio, eso mejor lo vemos en un siguiente post.

 

Un saludo!

1 comentario: