miércoles, 13 de julio de 2011

Interfaces must-know

Hoy vamos a ver cuales son, en mi opinión, algunas de las interfaces de .NET que todo programador debería conocer.

Las interfaces que vamos a ver en este post son:

  • IComparable
  • IComparer
  • ICloneable
  • IDisposable
  • IEnumerable
  • IEnumerator.

Está claro que hay muchas más que serán relevantes según las necesidades de nuestro código, pero lo que está claro es que si no conocemos estas tenemos un problema en el contexto de una conversación con nuestro colegas programadores ;-)

Comenzaremos por IComparable.

Para este y siguientes ejemplos, trabajaremos con una clase Persona como la siguiente.

    Public Class Persona

 

        ' Propiedades autoimplementadas

        Public Property Nombre As String

        Public Property Edad As Integer

 

        ' Constructor con parámetros

        Public Sub New(ByVal nombre As String, ByVal edad As Integer)

            Me.Nombre = nombre

            Me.Edad = edad

        End Sub

 

        ' Sobreescribimos el método ToString

        Public Overrides Function ToString() As String

            Return String.Format("{0}, {1}", Nombre, Edad)

        End Function

 

    End Class


Con esta clase, si creamos una lista de personas e intentamos ordenarla, recibiremos el siguiente error:

image

Como vemos, no es posible ordenar la lista porque no se pueden comparar los objetos de tipo Persona. Para resolver esto tenemos que implementar la interfaz IComparable, y a partir de ese momento, el framework sabrá como comparar 2 instancias de tipo Persona.

La interfaz IComparable sólo declara un método, CompareTo, que recibe el objeto con el que se quiere comparar el objeto actual. El método tiene que devolver 1 (si el objeto actual es mayor que el suministrado), 0 (sin son iguales) o -1 (si el objeto actual es menor que el suministrado).

    Public Class Persona

        Implements IComparable

        Public Function CompareTo(obj As Object) As Integer Implements System.IComparable.CompareTo

            ' obj es el objeto con el que se quiere comparar el objeto actual

            ' Si el método devuelve -1, el objeto actual es MENOR que el objeto recibido

            ' Si el método devuelve 0, ambos objetos son iguales

            ' Si el método devuelve 1, el objeto actual es MAYOR que el objeto recibido

            Dim compare As Persona = TryCast(obj, Persona)

            If compare Is Nothing Then

                Throw New Exception("obj no es del tipo Persona.")

            End If

            ' Se compara la Edad

            If Edad > compare.Edad Then

                Return 1

            ElseIf Edad < compare.Edad Then

                Return -1

            Else

                Return 0

            End If

        End Function

    End Class

 

¡Ahora la salida ordena correctamente por Edad!

Además de implementar IComparable para que métodos como Sort comparen nuestros objetos automáticamente, también podemos nosotros mismos utilizar el método CompareTo en cualquier momento:

        Dim comparacion As Integer

        comparacion = sergio.CompareTo(antonio)

        If comparacion = 1 Then

            Console.WriteLine("Sergio es mayor que Antonio")

        ElseIf comparacion = -1 Then

            Console.WriteLine("Sergio es menor que Antonio")

        Else

            Console.WriteLine("Sergio y Antonio son iguales")

        End If


Si nos fijamos, no es lo mismo Ordenar (que implícitamente Compara) que simplemente Comparar. Sé que un poco lioso pero imagina que lo queremos ahora es encontrar el índice de una persona concreta en nuestra lista. Escribiríamos el siguiente código:

        Dim gente As New List(Of Persona)

        Dim antonio As New Persona("Antonio", 35)

        gente.Add(antonio)

        Dim antonio2 As New Persona("Antonio", 35)

        If gente.IndexOf(antonio2) = -1 Then

            ' Sólo queremos agregar a este otro Antonio si no existe ya un mismo Antonio en la lista

            gente.Add(antonio2)

        End If

        Console.WriteLine(gente.Count)


Que devolvería 2 elementos, es decir, se comparan Antonio y Antonio2 y el resultado es que son distintos, aunque en realidad son iguales, ambos se llaman Antonio y tienen 35 años.

Sin embargo, si sobreescribimos el método Equals, ahora nuestra lista sólo tendrá 1 elemento, esto es 1 Antonio. Equals determina si 2 instancias son iguales. El comportamiento predeterminado (en caso de no sobreescribir) es que son iguales sin ambas variables apuntan a la misma instancia.

        Public Overrides Function Equals(obj As Object) As Boolean

            If obj Is Nothing OrElse Not Me.GetType() Is obj.GetType() Then

                ' obj es nulo o no es del tipo Persona

                Return False

            End If

            With CType(obj, Persona)

                If .Nombre <> Nombre Then

                    ' obj y el objeto actual son distintos

                    Return False

                End If

                If .Edad <> Edad Then

                    ' obj y el objeto actual son distintos

                    Return False

                End If

            End With

            ' obj y el objeto actual son iguales

            Return True

        End Function


Ahora podemos escribir esto y funcionará como esperamos:

        If Not antonio2.Equals(antonio) Then

            ' Sólo queremos agregar a este otro Antonio si no existe ya un mismo Antonio en la lista

            gente.Add(antonio2)

        End If


La segunda interfaz que veremos será IComparer.

No hay que confundir IComparable con IComparer.

IComparer permite comparar objetos que no implementan IComparable. Además, con IComparer también podemos dotar a nuestras clases de múltiples métodos de ordenación. Aunque no es el propósito de este post, con IComparer también podemos comparar objetos de distinto tipo.

Imagina que la ordenación por defecto para instancias de tipo Persona es por Edad pero también queremos implementar una nueva ordenación que ordena por Edad y Nombre. Puesto que sólo podemos implementar una vez IComparable, tendremos que realizar esta operación a través de IComparer.

Para implementar IComparer, hay que crear una nueva clase (cuyo nombre ya puede estar lleno de significado, por ejemplo, ComparadorPersonasPorEdadYNombre) e implementar en la misma la interfaz IComparer. Después, llamaremos al método Sort pasándole una instancia de esta nueva clase, con lo que sabremos exactamente por qué estamos ordenando. Además, IComparer no utiliza IComparable por lo que el tipo Persona podría no implementar IComparable y seguiría siendo perfectamente comparable a través de IComparer.

En este ejemplo, tenemos que implementar System.Collections.Generic.IComparer(Of Persona) porque lo que queremos comparar son Personas. A partir de ahí, funciona igual que IComparable, es decir, devolvemos -1, 0 o 1.

    Public Class ComparadorPersonasPorEdadYNombre

        Implements System.Collections.Generic.IComparer(Of Persona)

 

        Public Function Compare(x As Persona, y As Persona) As Integer Implements System.Collections.Generic.IComparer(Of Persona).Compare

            ' Primero se compara la Edad y después el Nombre

            If x.Edad > y.Edad Then

                Return 1

            ElseIf x.Edad < y.Edad Then

                Return -1

            Else

                If x.Nombre > y.Nombre Then

                    Return 1

                ElseIf x.Nombre < y.Nombre Then

                    Return -1

                Else

                    Return 0

                End If

            End If

        End Function

    End Class


        Dim comparador As New ComparadorPersonasPorEdadYNombre

        gente.Sort(comparador)


¡Y finalmente estamos ordenando por Edad y Nombre! Imagina ahora que no sólo comparamos por Edad y Nombre, sino por muchos más criterios y además pudiendo seleccionar en cada caso por cuál de ellos queremos ordenar.

Después de haber visto IComparable e IComparer, cambiemos de tercio para ver una interfaz más sencilla, hablamos de ICloneable.

ICloneable crea una nueva instancia que es copia de la instancia clonada.

En este caso, un ejemplo vale más que mil palabras:

        Public Function Clone() As Object Implements System.ICloneable.Clone

            Dim nuevoObjeto As New Persona(Me.Nombre, Me.Edad)

            Return nuevoObjeto

        End Function

 

Otra interfaz que deberíamos conocer es IDisposable.

IDisposable permite liberar recursos no administrados (esto es recursos no gestionados por el framework de .NET).

Imagina que tenemos una clase que abre una conexión a una base de datos y la mantiene abierta mientras la instancia está viva. Una conexión a una base de datos es un recurso no administrado, es decir, no es manejado automáticamente por el recolector de basura – garbage collector – de .NET cuando la variable salga de ámbito, así que nadie cerrará por nosotros esa conexión. De este modo, tenemos que asegurarnos que nuestra clase suministra los mecanismos oportunos para liberar ese recurso administrado cuando ya no sea necesario. Pues bien, esto se consigue a través de la interfaz IDisposable.

IDisposable garantiza lo siguiente:

  • Si un programador ve que la clase que utiliza implementa IDisposable, se cuidará de llamar al método Dispose e incluso utilizará nuestra clase con una instrucción Using.
  • Si por algún motivo, el programador no es todo lo cuidadoso que procede, cuando el recolector de basura recolecte nuestro objeto, también llamará automáticamente al método Dispose (esta una buena solución pero como bien sabes la destrucción de objetos es .NET es indeterminista – no sabes cuándo va a suceder exactamente - y esa recolección podría ser inmediata pero también podría ser mucho después de lo que nos hubiera gustado que fuera).

Visual Studio nos ayuda enormemente con esta interfaz y nos suministra todo un patrón de implementación para que no tengamos que pensar en exceso:

#Region "IDisposable Support"

        Private disposedValue As Boolean ' Para detectar llamadas redundantes

 

        ' IDisposable

        Protected Overridable Sub Dispose(disposing As Boolean)

            If Not Me.disposedValue Then

                If disposing Then

                    ' TODO: eliminar estado administrado (objetos administrados).

                End If

 

                ' TODO: liberar recursos no administrados (objetos no administrados) e invalidar Finalize() below.

                ' TODO: Establecer campos grandes como Null.

            End If

            Me.disposedValue = True

        End Sub

 

        ' TODO: invalidar Finalize() sólo si la instrucción Dispose(ByVal disposing As Boolean) anterior tiene código para liberar recursos no administrados.

        Protected Overrides Sub Finalize()

            ' No cambie este código. Ponga el código de limpieza en la instrucción Dispose(ByVal disposing As Boolean) anterior.

            Dispose(False)

            MyBase.Finalize()

        End Sub

 

        ' Visual Basic agregó este código para implementar correctamente el modelo descartable.

        Public Sub Dispose() Implements IDisposable.Dispose

            ' No cambie este código. Coloque el código de limpieza en Dispose (ByVal que se dispone como Boolean).

            Dispose(True)

            GC.SuppressFinalize(Me)

        End Sub

#End Region


Por último, veremos una interfaz muy relacionada con las colecciones. Estamos hablando de IEnumerable.

Según MSDN, “IEnumerable expone un enumerador que admite una iteración simple en una colección no genérica”. No te preocupes por lo de “colección no genérica” porque también está disponible la interfaz IEnumerable(Of T) para colecciones genéricas, que además hereda de IEnumerable, así que los 2 códigos siguientes son válidos:

        Dim gente As New List(Of Persona)

 

        Dim antonio As New Persona("Antonio", 35)

        gente.Add(antonio)       

        Dim dani As New Persona("Dani", 41)

        gente.Add(dani)

 

        Dim enumerador1 As IEnumerator = gente.GetEnumerator

        While enumerador1.MoveNext

            ' enumerador1.Current es del tipo Object

            Console.WriteLine(enumerador1.Current.ToString)

        End While

 

        Dim enumerador2 As IEnumerator(Of Persona) = gente.GetEnumerator

        While enumerador2.MoveNext

            ' enumerador2.Current es del tipo Persona

            Console.WriteLine(enumerador2.Current.Nombre)

        End While


Como curiosidad, siempre que utilices For…Each en una colección, esa colección tendrá que haber implementado IEnumerable o IEnumerable(Of T)

IEnumerable sólo tiene un método que es GetEnumerator que devuelve un objeto de tipo IEnumerator, que a su vez sólo tiene 3 métodos: MoveNext, Current y Reset.

Si queremos poner un ejemplo práctico del uso de IEnumerable e IEnumerator, el más obvio es que fomenta el polimorfismo y podemos hacer funciones que iteren sobre colecciones sin conocer el tipo de esas colecciones. Aquí va el ejemplo:

        Dim gente As New List(Of Persona)

        gente.Add(New Persona("Antonio", 35))

        gente.Add(New Persona("Dani", 41))

        IterarSobreColeccion(gente)

 

        Dim miArrayList As New ArrayList

        miArrayList.Add(New Persona("Antonio", 35))

        miArrayList.Add(New Persona("Dani", 41))

        IterarSobreColeccion(miArrayList)

 

        Dim miArray(1) As Persona

        miArray(0) = New Persona("Antonio", 35)

        miArray(1) = New Persona("Dani", 41)

        IterarSobreColeccion(miArray)

 

    Public Sub IterarSobreColeccion(ByVal coleccion As IEnumerable)

        Dim enumerador As IEnumerator = coleccion.GetEnumerator

        While enumerador.MoveNext

            Console.WriteLine(enumerador.Current.ToString)

        End While

    End Sub

Seguro que echas en falta muchas interfaces que tú ya conoces, pero como yo estoy ahora mismo descubriéndolas… pues quizás en otro post ;-)

Un saludo!

No hay comentarios:

Publicar un comentario