Tabla de Contenido
0. Introducción1. El Tipo object
1.2. Namespace y assembly de object
1.3 Sintaxis declarativa en C#, C++, F#, y VB
2. Miembros de la Clase Object
3. Compatibilidad de Tipos
4. Sobreescritura de Métodos Virtuales de la Clase Object
4.1 Sobreescritura ToString
4.2 Sobreescritura Equals
4.3 Sobreescritura GetHashCode
5. Consideraciones de Rendimiento
6. Historial de Versiones de Object
7. Seguridad de Hilos (Thread)
Conclusiones
Glosario
Enlaces & Literatura
0. Introducción
Este artículo va a resultar enriquecedor y muy interesante debido a que vamos a tratar el elemento raíz en la jerarquía de herencia no sólo del lenguaje de programación C# sino de todo el Framework .NET: object (System.Object). Incluiré detalles contundentes que sustentan la existencia de este tipo de dato fundamental e ineludible en la construcción del lenguaje y el soporte base de la programación orientada a objetos, más específicamente, la capacidad de polimorfismo (explicaré esto en el desarrollo). Avanzaremos conociendo las características del sistema unificado de tipos, en donde object juega un rol de conversión fundamental. También nos desenvolveremos en los miembros claves que dan la «belleza polimórfica» sobre el Reino de Microsoft .NET Framework. Evidentemente, también presentaré ejemplos para demostrar las capacidades de esta SUPER-CLASE. Soyez le vienvenu!
1. El Tipo object
object es el alias para la clase Object [2] (nombre completamente calificado: System.Object). Esta clase se clasifica como la clase base de todo la jerarquía de herencia de Microsoft .NET Framework. La Figura 1 ilustra este concepto.
Si citamos a [2]:
Tanto el lenguaje de programación C# como todos los demás lenguajes (VB.NET, F#, C++, J#, &c.) siguen la misma línea jerárquica gracias a la conveniencia y la compatibilidad con la especificación que requiere la CLR.
Archivo SistemaTiposUnificado.cs:
[Nota: Para mayor acerca de este tema recomiendo la lectura de Orientación a Objetos en C#.]
Figura 1. Jerarquía de Herencia de object (System.Object). |
Si citamos a [2]:
Supports all classes in the .NET Framework class hierarchy and provides low-level services to derived classes. This is the ultimate base class of all classes in the .NET Framework; it is the root of the type hierarchy.Podemos reafirmar el concepto de clase raíz de la jerarquía de herencia del Framework .NET: "...This is the ultimate base class of all classes...".
Figura 2. Object Inheritance Hierarchy [2]. |
Tanto el lenguaje de programación C# como todos los demás lenguajes (VB.NET, F#, C++, J#, &c.) siguen la misma línea jerárquica gracias a la conveniencia y la compatibilidad con la especificación que requiere la CLR.
1.2 Sistema Unificado de Tipos
Por otro lado, en un artículo previo (casi de los primeros) describí otro concepto importante relacionado con la programación orientada objetos: "C# has a unified type system, where all types ultimately share a common base type." Con esto me refiero al sistema unificado de tipos (unified type system) que provee el Framework .NET: todos los objetos comparten una funcionalidad básica por ejemplo, si tuviéramos un número entero, un valor lógico booleano, una referencia a un objeto de una clase, podríamos invocar el método ToString. Ejemplo en código:Archivo SistemaTiposUnificado.cs:
[Nota: Para mayor acerca de este tema recomiendo la lectura de Orientación a Objetos en C#.]
1.3 Namespace y assembly de object
La clase Object reside en el namespace System, y se haya almacenado en el assembly mscorlib.dll.
1.4 Sintaxis de declarativa de object en C#, C++, F#, y VB
Aunque el foco, y como el título lo indica, está orientado a C#, pero quiero hacer la excepción por el grado de importancia de esta clase e incluir las representaciones sintácticas en otros lenguajes compatible con CLI: C++ [9], F# [10], y VB [11].
1.4.1 Sintaxis C#
Archivo Object.cs:
1.4.2 Sintaxis C++
Archivo Object.fs:
1.4.4 Sintaxis VB
Archivo Object.vb:
2. Miembros de la Clase Object
En la Introducción mencioné que la Object tiene definidos los miembros comunes a todos los demás objetos en la jerarquía de herencia, incluyendo los tipos definidos en el Framework .NET, y para las mismas definiciones del propio programador. La riqueza de un framework empieza por las definiciones más abstractas que permitan especificar un grado detalle mínimo para resolución de problemas específicos de modelamiento del dominio de un problema.
2.1 Constructores
La clase Object sólo cuenta con un constructor, el cual no contiene argumentos (constructor por defecto). Veamos su uso:
Object obj = new Object();
En cualquier tipo predefinido o definido por el programador, este constructor se invoca implícitamente, para dar el acabo final al objeto. Una vez declarado una instancia de esta clase, podemos manipular su estado y realizar operaciones a través de los métodos miembros definidos (tema de la siguiente sub-sección).
2.2 Métodos
En la Figura 3 se enlistan todos los métodos (públicos y privados) definidos en la clase Object.
Figura 3. Métodos de la clase Object. |
3. Compatibilidad de Tipos
A razón del espacio lógico que ocupa la clase Object en la jerarquía de herencia del Framework .NET, se facilita cualquier tipo de dato puede ser casteado hacia arriba (disculpen el abuso de los extranjerismos), es decir, convertido al tipo object. Para demostrar esto, veamos el ejemplo que nos presentan en [1].
El ejemplo consiste en la definición de la estructura de datos Stack (pila) [12] capaz de almacenar cualquier tipo de objeto. Estos datos se estructuran siguiendo el principio LIFO [13]. Las operaciones básicas de esta estructuran son:
- Push: Inserción de un elemento (dato) en la pila (stack).
- Pop: Extracción de un elemento (dato) desde la pila (stack).
Tomo la definición literal en código fuente hecha en [1]:
Archivo Stack.cs:
Las operaciones, push y pop, manipulan cualquier tipo d dato, podemos proceder de la siguiente manera:
Stack pila = new Stack();
pila.Push ("OrtizOL");
Con el método Push insertamos un objeto de tipo string y automáticamente se convierte (conversión implícita) al tipo object: upcast.
Ahora para extraer ese elemento, usamos el método Pop. Así:
string nombre = (string) stack.Pop(); // Convierte al tipo de forma explícita.
Y podemos mostrar este resultado:
Console.WriteLine (nombre); // Invocación implícita de ToString: OrtizOL
Los tipos primitivos (integrales) también son convertidos a través de una operación conocida como boxing [15] que es llevada a cabo por la propia CLR. Un ejemplo:
pila.Push (3); // boxing
int tres = (int) pila.Pop(); // unboxing
Stack pila = new Stack();
pila.Push ("OrtizOL");
Con el método Push insertamos un objeto de tipo string y automáticamente se convierte (conversión implícita) al tipo object: upcast.
Ahora para extraer ese elemento, usamos el método Pop. Así:
string nombre = (string) stack.Pop(); // Convierte al tipo de forma explícita.
Y podemos mostrar este resultado:
Console.WriteLine (nombre); // Invocación implícita de ToString: OrtizOL
Los tipos primitivos (integrales) también son convertidos a través de una operación conocida como boxing [15] que es llevada a cabo por la propia CLR. Un ejemplo:
pila.Push (3); // boxing
int tres = (int) pila.Pop(); // unboxing
4. Sobreescritura de Métodos Virtuales de la Clase Object
Son cuatro los métodos de la clase Object que han sido definidos con el modificar virtual:
- Equals
- GetHashCode
- ToString
Todos estos métodos son heredados de manera implícita al declara un tipo sin especificar herencia. Por ejemplo:
Archivo Empleado.cs:
Evidentemente se trata de una clase sin miembros (más adelante enriqueceremos su definición).
Si usamos IntelliSense [16] de Visual Studio [17], observamos que este Empleado sólo posee los métodos enlistados arriba. La Figura 4 lo ilustra.
En las sub-secciones que vienen enseguida se sobreescribirá cada uno de estos métodos para aparender de las capacidades polimórficas que aporta Object para el resto de tipos de datos de la jerarquía de herencia.
Si usamos IntelliSense [16] de Visual Studio [17], observamos que este Empleado sólo posee los métodos enlistados arriba. La Figura 4 lo ilustra.
Figura 4. Métodos virtual detectados por IntelliSense [16]. |
En las sub-secciones que vienen enseguida se sobreescribirá cada uno de estos métodos para aparender de las capacidades polimórficas que aporta Object para el resto de tipos de datos de la jerarquía de herencia.
4.1 Sobreescritura de ToString
La funcionalidad por defecto de ToString [19] de Object es mostrar el nombre completamente calificado del tipo. En el caso de la clase Empleado mostrará:
Es decir, en la salida estándar se mostrará el nombre de la clase: Empleado.
El paso a seguir es sobreescribir el método ToString para alterar su comportamiento. En la nueva versión mostraremos los datos completos de los campos miembros de la clase Empleado (versión con nuevas definiciones):
Archivo Empleado.cs:
Si quisiéramos realizar la misma prueba de ejecución utilizando esta versión de la clase Empleado el resultado sería el mismo de la ejecución previa.
Ahora sí, sobreescríbamos el método ToString:
// Sobreescritura del método ToString
public override ToString()
{
return String.Format("Nombre: {0}, Apellido: {1}, y Edad: {2}.", primerNombre, apellido, edad);
}
Vayamos a ver la prueba de ejecución:
► Prueba de ejecución.
Con los datos de prueba obtenemos: Nombre: Juan, Apellido: Ortiz, y Edad: 40.
Empleado emp1 = new Empleado();
emp1.PrimerNombre = "Juan";
emp1.Apellido = "Ortiz";
emp1.Edad = 40;
emp1.IdEmpleado = 123456789;
Empleado emp2 = emp1; // referencias al mismo objeto
// Returna true dado que ambos objetos apuntan a los mismos valores del heap
Console.WriteLine (emp1.Equals (emp2));
► Prueba de ejecución.
Resultado: True
Si alteramos sutilmente la prueba creando dos referencias distintas pero con valores de atributos iguales, tendremos:
Empleado emp1 = new Empleado();
emp1.PimerNombre = "Juan";
emp1.Apellido = "Ortiz";
emp1.Edad = 40;
emp1.IdEmpleado = 123456789;
Empleado emp2 = new Empleado();
emp2.PimerNombre = "Juan";
emp2.Apellido = "Ortiz";
emp2.Edad = 40;
emp2.IdEmpleado = 123456789;
// Comparación de dos instancias de Epleado diferentes.
Console.WriteLine (emp1.Equals (emp2));
► Prueba de ejecución.
Resultado: False
Nuestra nueva consideración dictamina que dos objetos son iguales por su contenido y no por la dirección en memoria que ocupan. Entonces sobreescribiremos el método Equals comparando los valores de los campos de instancia ( primerNombre, apellido, edad, y idEmpleado). Procedamos:
// Sobreescritura del método Equals
public override bool Equals(object obj)
{
if (obj != null && obj is Empleado)
{
Empleado emp = (Empleado) obj;
if (emp.primerNombre == this.primerNombre &&
emp.apellido == this.apellido &&
emp.edad == this.edad &&
emp.idEmpleado == this.idEmpleado )
{
return true;
}
}
return false;
}
En la prueba de ejecución anterior obtuvimos False, sin embargo, con la sobreescritura de Equals, ahora resultado es diferente:
► Prueba de ejecución.
Resultado: True
Entonces, ¿qué ocurre cuando obtenemos el hash de dos instancias diferentes?:
Asumimos que las instancias de Empleado: emp1 y emp2 tienen los mismos datos de los ejemplos de arriba. Ahora obtenemos el código hash:
Console.WriteLine ("Hash de emp1: {0}", emp1.GetHashCode());
Console.WriteLine ("Hash de emp2: {0}", emp2.GetHashCode());
► Prueba de ejecución.
Resultado:
En este caso, podemos aprovechar la sobreescritura para la generación del código de hash que retorne un código único para aquellas instancias que contienen los mismos valores de atributos. Para eso utilizaremos la llamada a ToString y invocaremos seguidamente GetHashCode para obtener un ID equivalente para aquellos valores de cadena iguales. Veamos:
// Sobreescritura del método GetHashCode
public override int GetHashCode()
{
return ToString().GetHashCode();
}
► Prueba de ejecución.
Resultado:
Clase Empleado con todas los cambios en los métodos sobreescritos:
Archivo Empleado.cs:
Ahora sí, sobreescríbamos el método ToString:
// Sobreescritura del método ToString
public override ToString()
{
return String.Format("Nombre: {0}, Apellido: {1}, y Edad: {2}.", primerNombre, apellido, edad);
}
Vayamos a ver la prueba de ejecución:
► Prueba de ejecución.
Con los datos de prueba obtenemos: Nombre: Juan, Apellido: Ortiz, y Edad: 40.
4.2 Sobreescritura de Equals
Con Equals [19] podemos conocer si un determinado objeto es igual al objeto actual. Vale agregar que este método devuelve true los dos objetos en comparación apuntan a los mismos valores en el heap (Variables y Parámetros en C# - Parte 1). Por ejemplo:Empleado emp1 = new Empleado();
emp1.PrimerNombre = "Juan";
emp1.Apellido = "Ortiz";
emp1.Edad = 40;
emp1.IdEmpleado = 123456789;
Empleado emp2 = emp1; // referencias al mismo objeto
// Returna true dado que ambos objetos apuntan a los mismos valores del heap
Console.WriteLine (emp1.Equals (emp2));
► Prueba de ejecución.
Resultado: True
Si alteramos sutilmente la prueba creando dos referencias distintas pero con valores de atributos iguales, tendremos:
Empleado emp1 = new Empleado();
emp1.PimerNombre = "Juan";
emp1.Apellido = "Ortiz";
emp1.Edad = 40;
emp1.IdEmpleado = 123456789;
Empleado emp2 = new Empleado();
emp2.PimerNombre = "Juan";
emp2.Apellido = "Ortiz";
emp2.Edad = 40;
emp2.IdEmpleado = 123456789;
// Comparación de dos instancias de Epleado diferentes.
Console.WriteLine (emp1.Equals (emp2));
► Prueba de ejecución.
Resultado: False
Nuestra nueva consideración dictamina que dos objetos son iguales por su contenido y no por la dirección en memoria que ocupan. Entonces sobreescribiremos el método Equals comparando los valores de los campos de instancia ( primerNombre, apellido, edad, y idEmpleado). Procedamos:
// Sobreescritura del método Equals
public override bool Equals(object obj)
{
if (obj != null && obj is Empleado)
{
Empleado emp = (Empleado) obj;
if (emp.primerNombre == this.primerNombre &&
emp.apellido == this.apellido &&
emp.edad == this.edad &&
emp.idEmpleado == this.idEmpleado )
{
return true;
}
}
return false;
}
En la prueba de ejecución anterior obtuvimos False, sin embargo, con la sobreescritura de Equals, ahora resultado es diferente:
► Prueba de ejecución.
Resultado: True
4.3 Sobreescritura de GetHashCode
El método GetHashCode [20] se usa para obtener el valor número (hash [21]) que identifica un objeto con base en sus datos internos.Entonces, ¿qué ocurre cuando obtenemos el hash de dos instancias diferentes?:
Asumimos que las instancias de Empleado: emp1 y emp2 tienen los mismos datos de los ejemplos de arriba. Ahora obtenemos el código hash:
Console.WriteLine ("Hash de emp1: {0}", emp1.GetHashCode());
Console.WriteLine ("Hash de emp2: {0}", emp2.GetHashCode());
► Prueba de ejecución.
Resultado:
Hash de emp1: -418916032
Hash de emp2: 1030526080
En este caso, podemos aprovechar la sobreescritura para la generación del código de hash que retorne un código único para aquellas instancias que contienen los mismos valores de atributos. Para eso utilizaremos la llamada a ToString y invocaremos seguidamente GetHashCode para obtener un ID equivalente para aquellos valores de cadena iguales. Veamos:
// Sobreescritura del método GetHashCode
public override int GetHashCode()
{
return ToString().GetHashCode();
}
► Prueba de ejecución.
Resultado:
Hash de emp1: -1363340276
Hash de emp2: -1363340276
Clase Empleado con todas los cambios en los métodos sobreescritos:
Archivo Empleado.cs:
5. Consideraciones de Rendimiento
Desde [2] (parafraseo) nos enumeran las siguientes consideraciones de rendimiento sobre la clase Object:
- Para estructuras de datos como colecciones que internamente manipulen instancias de de cualquier tipo, se requerirá el uso de Object. Sin embargo, el proceso de conversión tiene un costo de rendimiento sobre la CLR. Para minimizar el coste, en [2] nos aconsejan usar una de las dos siguientes tácticas:
- Create a general method that accepts an Object type, and a set of type-specific method overloads that accept each value type you expect your class to frequently handle. If a type-specific method exists that accepts the calling parameter type, no boxing occurs and the type-specific method is invoked. If there is no method argument that matches the calling parameter type, the parameter is boxed and the general method is invoked.
- Design your type and its members to use generics. The common language runtime creates a closed generic type when you create an instance of your class and specify a generic type argument. The generic method is type-specific and can be invoked without boxing the calling parameter.
La elección de cualquiera de las tácticas dependerá del escenario y los requerimientos funcionales y no funcionales de la aplicación que estemos diseñando.
6. Historial de Versiones de Object
7. Seguridad de Hilos
[2]:Public static (Shared in Visual Basic) members of this type are thread safe. Instance members are not guaranteed to be thread-safe.
8. Conclusiones
En este artículo comprendimos la importancia de la clase Object en la jerarquía de herencia de clases del Framework .NET. También hicimos el recorrido a través de las diferentes formas sintácticas de Object en los lenguajes: C#, C++, F#, y VB. Object brinda el soporte más abstracto para mantener una instancia de cualquier nivel del árbol de herencia. Aprovechamos la riqueza polimórfica de los métodos ToString, Equals, y GetHashCode sobreescribiéndolos apoyándonos en ejemplos de código reales y funcionales. Al final, rápidamente, recorrimos asuntos de rendimiento de la clase Object, el historial de versiones de la misma, y por último, la seguridad de hilos sobre miembros públicos estáticos.
Glosario
- .NET Framework
- CLI
- CLR
- F#
- object
- Polimorfismo
- Programación orientada objetos
- Tipo
- VB
Enlaces & Literatura
[1]: C# 5.0 in a Nutshell by Joseph Albahari and Ben Albahari. Copyright 2012 Joseph Albahari and Ben Albahari, 978-1-449-32010-2.[2]: Object Class (System) - http://msdn.microsoft.com/en-us/library/system.object%28v=vs.110%29.aspx
[3]: VB Tutorial | System.Object - http://www.vb-training-guide.com/system-object.html
[4]: C# 4.0 Tutorial: System.Object - http://www.bogotobogo.com/CSharp/csharp_system_object.php
[5]: Type: System.Object - http://www1.cs.columbia.edu/~lok/csharp/refdocs/System/types/Object.html
[6]: Classes, Structs, and Objects-System.Object - Visual C# Developer Center - http://tutorials.csharp-online.net/index.php?title=Classes,_Structs,_and_Objects%E2%80%94System.Object
[7]: C# Station: Overview of the Object Class - http://www.csharp-station.com/Articles/ObjectClass.aspx
[8]: Overriding the System.Object class methods - http://www.c-sharpcorner.com/uploadfile/Ashush/overriding-the-system-object-class-methods/
[9]: C++/CLI, the free encyclopedia - https://en.wikipedia.org/wiki/C%2B%2B/CLI
[10]: F Sharp (programming language), the free encyclopedia - https://en.wikipedia.org/wiki/F_Sharp_%28programming_language%29
[11]: Visual Basic .NET, the free encyclopedia - https://en.wikipedia.org/wiki/VB.NET
[12]: Stack (abstract data type), the free encyclopedia - https://en.wikipedia.org/wiki/Stack_%28abstract_data_type%29
[13]: LIFO (computing), the free encyclopedia - https://en.wikipedia.org/wiki/LIFO_%28computing%29
[14]: Boxing and Unboxing (C# Programming Guide) - http://msdn.microsoft.com/en-us/library/yz2be5wk.aspx
[15]: virtual (C# Reference) - http://msdn.microsoft.com/en-us/library/9fkccyh4.aspx
[16]: Intelligent code completion, the free encyclopedia - https://en.wikipedia.org/wiki/Intellisense#IntelliSense
[17]: Microsoft Visual Studio, the free encyclopedia - https://en.wikipedia.org/wiki/Visual_Studio
[18]: Object.ToString Method (System) - http://msdn.microsoft.com/en-us/library/system.object.tostring%28v=vs.110%29.aspx
[19]: Object.Equals Method (Object) (System) - http://msdn.microsoft.com/en-us/library/bsc2ak47%28v=vs.110%29.aspx
[20]: Object.GetHashCode Method (System) - http://msdn.microsoft.com/en-us/library/system.object.gethashcode%28v=vs.110%29.aspx
[21]: Hash function, the free encyclopedia - https://en.wikipedia.org/wiki/Hash_function
J
No hay comentarios:
Publicar un comentario
Envíe sus comentarios, dudas, sugerencias, críticas. Gracias.