¿Por qué todas las clases en .NET heredan globalmente de la clase Object?

16

Es muy interesante para mí qué ventajas da el enfoque de "clase raíz global" para framework. En palabras simples, las razones que dieron como resultado .NET Framework fue diseñado para tener una clase de objeto raíz con funcionalidad general adecuada para todas las clases.

Hoy en día estamos diseñando un nuevo marco para uso interno (el marco bajo la plataforma SAP) y todos nos dividimos en dos campos: primero, quién piensa que ese marco debe tener una raíz global, y el segundo, quién piensa en sentido contrario.

Estoy en el campamento de "raíz global". Y mis razones por las cuales dicho enfoque produciría una buena flexibilidad y una reducción de los costos de desarrollo hacen que ya no desarrollemos una funcionalidad general.

Por lo tanto, estoy muy interesado en saber qué razones realmente empujan a los arquitectos .NET a diseñar el marco de esta manera.

Anton Semenov
fuente
66
Recuerde que .NET Framework es parte de un entorno de desarrollo de software generalizado. Los marcos que son más específicos, como el suyo, pueden no necesitar un objeto "raíz". Hay una raíz objecten el marco .NET en parte porque proporciona algunas capacidades básicas en todos los objetos, como ToString()yGetHashCode()
Robert Harvey
10
"Me interesa mucho saber qué razones realmente empujan a los arquitectos .NET a diseñar el framework de esa manera". Hay tres muy buenas razones para eso: 1. Java lo hizo de esa manera, 2. Java lo hizo de esa manera, y 3. Java lo hizo de esa manera.
dasblinkenlight
2
@vcsjones: y Java, por ejemplo, ya están contaminados con wait()/ notify()/ notifyAll()y clone().
Joachim Sauer
3
@daskblinkenlight Eso es caca. Java no lo hizo de esa manera.
Dante
2
@dasblinkenlight: FYI, Java es el que actualmente copiando C #, con lambdas, funciones LINQ, etc ...
user541686

Respuestas:

18

La causa más apremiante Objectson los contenedores (antes de los genéricos) que podrían contener cualquier cosa en lugar de tener que ir al estilo C "Escríbelo nuevamente para todo lo que necesites". Por supuesto, podría decirse que la idea de que todo debería heredar de una clase específica y luego abusar de este hecho para perder por completo cada mota de seguridad de tipo es tan terrible que debería haber sido una gran luz de advertencia sobre el envío del idioma sin genéricos, y también significa eso Objectes completamente redundante para el nuevo código.

DeadMG
fuente
66
Esta es la respuesta correcta. La falta de soporte genérico fue la única razón. Otros argumentos de "métodos comunes" no son argumentos reales porque los métodos comunes no necesitan ser comunes. Ejemplo: 1) ToString: se abusa en la mayoría de los casos; 2) GetHashCode: dependiendo de lo que haga el programa, la mayoría de las clases no necesitan este método; 3) GetType: no tiene que ser un método de instancia
Codism
2
¿De qué manera .NET "pierde por completo cada mota de seguridad de tipo" al "abusar" de la idea de que todo debería heredar de una clase específica? ¿Hay algún código de mierda que se pueda escribir Objectque no se pueda escribir void *en C ++?
Carson63000
3
¿Quién dijo que pensaba que void*era mejor? No lo es Si algo. Es aún peor .
DeadMG
void*es peor en C con todos los lanzamientos de puntero implícitos, pero C ++ es más estricto a este respecto. Al menos tienes que lanzar explícitamente.
Tamás Szelei
2
@fish: Una objeción terminológica: en C, no existe un "elenco implícito". Un reparto es un operador explícito que especifica una conversión. Se refiere a " conversiones de puntero implícitas ".
Keith Thompson
11

Recuerdo que Eric Lippert dijo una vez que heredar de la System.Objectclase proporcionaba "el mejor valor para el cliente". [editar: sí, lo dijo aquí ]:

... No necesitan un tipo base común. Esta elección no fue hecha por necesidad. Fue creado por el deseo de proporcionar el mejor valor para el cliente.

Cuando se diseña un sistema de tipos, o cualquier otra cosa, a veces se llega a puntos de decisión: debe decidir X o no X ... Los beneficios de tener un tipo base común superan los costos, y el beneficio neto por lo tanto acumulado es mayor que el beneficio neto de no tener un tipo base común. Por lo tanto, elegimos tener un tipo base común.

Esa es una respuesta bastante vaga. Si desea una respuesta más específica, intente hacer una pregunta más específica.

Tener todo derivado de la System.Objectclase proporciona una fiabilidad y utilidad que he llegado a respetar mucho. Sé que todos los objetos tendrán un tipo ( GetType), que estarán en línea con los métodos de finalización de CLR ( Finalize), y que puedo usarlos GetHashCodecuando se trate de colecciones.

gws2
fuente
2
Esto es defectuoso. No necesita GetType, simplemente puede extender typeofpara reemplazar su funcionalidad. En cuanto a Finalize, entonces, en realidad, muchos tipos no son completamente finalizables y no poseen un método de Finalización significativo. Además, muchos tipos no son intercambiables ni convertibles en cadenas, especialmente los tipos internos que nunca están destinados a tal cosa y nunca se dan para uso público. Todos estos tipos tienen sus interfaces innecesariamente contaminadas.
DeadMG
55
No, no está viciado - Nunca me dijo que se utilizaría GetType, Finalizeo GetHashCodepara cada System.Object. Simplemente dije que estaban allí si los querías. En cuanto a "sus interfaces están contaminadas innecesariamente", me referiré a mi cita original de elippert: "el mejor valor para el cliente". Obviamente, el equipo .NET estaba dispuesto a lidiar con las compensaciones.
gws2
Si no es necesario, entonces es la contaminación de la interfaz. "Si quieres" nunca es una buena razón para incluir un método. Solo debe estar allí porque lo necesita y sus consumidores lo llamarán . De lo contrario, no es bueno. Además, su cita de Lippert es una apelación a la falacia de autoridad, ya que él también dice nada más que "es bueno".
DeadMG
No estoy tratando de discutir su (s) punto (s) @DeadMG: la pregunta era específicamente "Por qué todas las clases en .NET heredan globalmente de la clase Object" y, por lo tanto, me vinculé a una publicación anterior de alguien muy cercano al equipo de .NET en Microsoft que había dado una respuesta legítima, aunque vaga, de por qué el equipo de desarrollo de .NET eligió este enfoque.
gws2
Demasiado vago para ser una respuesta a esta pregunta.
DeadMG
4

Creo que los diseñadores de Java y C # agregaron un objeto raíz porque era esencialmente gratuito para ellos, como en el almuerzo gratis .

Los costos de agregar un objeto raíz son muy similares a los costos de agregar su primera función virtual. Dado que tanto CLR como JVM son entornos de recolección de basura con finalizadores de objetos, debe tener al menos uno virtual java.lang.Object.finalizepara su System.Object.Finalizemétodo o para él . Por lo tanto, los costos de agregar su objeto raíz ya están pagados por adelantado, puede obtener todos los beneficios sin pagar por ello. Esto es lo mejor de ambos mundos: los usuarios que necesitan una clase raíz común obtendrían lo que desean, y los usuarios a los que no les importaría menos podrían programar como si no estuvieran allí.

dasblinkenlight
fuente
4

Me parece que tiene tres preguntas aquí: una es por qué se introdujo una raíz común en .NET, la segunda es cuáles son las ventajas y desventajas de eso, y la tercera es si es una buena idea que un elemento del marco tenga un valor global. raíz.

¿Por qué .NET tiene una raíz común?

En el sentido más técnico, tener una raíz común es esencial para la reflexión y para los contenedores pre-genéricos.

Además, que yo sepa, tener una raíz común con métodos básicos como equals()y hashCode()fue recibido de manera muy positiva en Java, y C # fue influenciado por (entre otros) Java, por lo que también querían tener esa característica.

¿Cuáles son las ventajas y desventajas de tener una raíz común en un idioma?

La ventaja de tener una raíz común:

  • Puede permitir que todos los objetos tengan una determinada funcionalidad que considere importante, comoequals() y hashCode(). Eso significa, por ejemplo, que cada objeto en Java o C # se puede usar en un mapa hash: compare esa situación con C ++.
  • Puede hacer referencia a objetos de tipos desconocidos, por ejemplo, si simplemente transfiere información, como lo hicieron los contenedores pregenéricos.
  • Puedes referirte a objetos de incognoscible tipo , por ejemplo, cuando usa la reflexión.
  • Se puede usar en los casos excepcionales en los que desea poder aceptar un objeto que puede ser de tipos diferentes y no relacionados, por ejemplo, como un parámetro de un printf método similar.
  • Le brinda la flexibilidad de cambiar el comportamiento de todos los objetos cambiando solo una clase. Por ejemplo, si desea agregar un método a todos los objetos en su programa C #, puede agregar un método de extensión object. Quizás no sea muy común, pero sin una raíz común no sería posible.

La desventaja de tener una raíz común:

  • Se puede abusar de él para referirse a un objeto de algún valor, incluso si hubiera podido usar un tipo más preciso para él.

¿Deberías ir con una raíz común en tu proyecto de marco?

Muy subjetivo, por supuesto, pero cuando miro la lista pro-con anterior, definitivamente respondería que . Específicamente, la última viñeta en la lista profesional, la flexibilidad de cambiar más tarde el comportamiento de todos los objetos al cambiar la raíz, se vuelve muy útil en una plataforma, en la que los clientes tienen más probabilidades de aceptar los cambios en la raíz que los cambios en un todo idioma.

Además, aunque esto es aún más subjetivo, el concepto de raíz común me parece muy elegante y atractivo. También facilita el uso de algunas herramientas, por ejemplo, ahora es fácil pedirle a una herramienta que muestre a todos los descendientes de esa raíz común y recibir una descripción rápida y buena de los tipos del marco.

Por supuesto, los tipos tienen que estar al menos ligeramente relacionados para eso, y en particular, nunca sacrificaría la regla "es-una" solo para tener una raíz común.

Roble
fuente
Creo que c # no fue simplemente influenciado por Java, sino que en algún momento fue Java. Microsoft ya no podía usar Java, por lo que perdería miles de millones en inversiones. Comenzaron dando el idioma de que ya tenían un nuevo nombre.
Mike Braun
3

Matemáticamente, es un poco más elegante tener un sistema de tipos que contenga top , y le permite definir un lenguaje un poco más completamente.

Si está creando un marco, las cosas necesariamente serán consumidas por una base de código existente. No puede tener un supertipo universal ya que existen todos los otros tipos en lo que sea que consuma el marco.

En ese momento, la decisión de tener una clase base común depende de lo que esté haciendo. Es muy raro que muchas cosas tengan un comportamiento común y que sea útil hacer referencia a estas cosas solo a través del comportamiento común.

Pero sucede Si es así, entonces sigue adelante abstrayendo ese comportamiento común.

Telastyn
fuente
2

Se presionó para un objeto raíz porque querían que todas las clases en el marco admitieran ciertas cosas (obtener un código hash, convertir a una cadena, verificaciones de igualdad, etc.). Tanto C # como Java encontraron útil poner todas estas características comunes de un Objeto en alguna clase raíz.

Tenga en cuenta que no están violando ningún principio de OOP ni nada. Cualquier cosa en la clase Object tiene sentido en cualquier subclase (léase: cualquier clase) en el sistema. Si elige adoptar este diseño, asegúrese de seguir este patrón. Es decir, no incluya cosas en la raíz que no pertenezcan a todas las clases de su sistema. Si sigue esta regla cuidadosamente, no veo ninguna razón por la que no deba tener una clase raíz que contenga código útil y común para todo el sistema.

Oleksi
fuente
2
Eso no es cierto en absoluto. Hay muchas clases solo internas que nunca se dividen, nunca se reflejan, nunca se convierten en cadenas. Herencia innecesario y superfluo métodos más definitivamente hace violan muchos principios.
DeadMG
2

Un par de razones, funcionalidad común que todas las clases secundarias pueden compartir. Y también les dio a los escritores del marco la capacidad de escribir muchas otras funcionalidades en el marco que todo podría usar. Un ejemplo sería el almacenamiento en caché y las sesiones de ASP.NET. Casi todo se puede almacenar en ellos, porque escribieron los métodos de agregar para aceptar objetos.

Una clase raíz puede ser muy atractiva, pero es muy, muy fácil de usar mal. ¿Sería posible tener una interfaz raíz en su lugar? ¿Y solo tienes uno o dos métodos pequeños? ¿O eso agregaría mucho más código del que se requiere para su marco?

Le pregunto si tengo curiosidad acerca de qué funcionalidad necesita exponer a todas las clases posibles en el marco que está escribiendo. ¿Será realmente utilizado por todos los objetos? ¿O por la mayoría de ellos? Y una vez que cree la clase raíz, ¿cómo evitará que las personas agreguen funcionalidad aleatoria? ¿O las variables aleatorias quieren ser "globales"?

Hace varios años había una aplicación muy grande donde creé una clase raíz. Después de unos meses de desarrollo, estaba poblado por un código que no tenía nada que ver allí. Estábamos convirtiendo una vieja aplicación ASP y la clase raíz era el reemplazo del viejo archivo global.inc que habíamos usado en el pasado. Tuve que aprender esa lección de la manera difícil.

bwalk2895
fuente
2

Todos los objetos de montón independientes heredan de Object ; eso tiene sentido porque todos los objetos de montón independientes deben tener ciertos aspectos comunes, como un medio para identificar su tipo. De lo contrario, si el recolector de basura tuviera una referencia a un objeto de montón de tipo desconocido, no tendría forma de saber qué bits dentro del blob de memoria asociado con ese objeto deberían considerarse referencias a otros objetos de montón.

Además, dentro del sistema de tipos, es conveniente utilizar el mismo mecanismo para definir los miembros de las estructuras y los miembros de las clases. El comportamiento de las ubicaciones de almacenamiento de tipo de valor (variables, parámetros, campos, ranuras de matriz, etc.) es muy diferente del de las ubicaciones de almacenamiento de tipo de clase, pero tales diferencias de comportamiento se logran en los compiladores de código fuente y el motor de ejecución (incluyendo el compilador JIT) en lugar de expresarse en el sistema de tipos.

Una consecuencia de esto es que definir un tipo de valor define efectivamente dos tipos: un tipo de ubicación de almacenamiento y un tipo de objeto de almacenamiento dinámico. El primero se puede convertir implícitamente en el segundo, y el último se puede convertir en el primero mediante la conversión de texto. Ambos tipos de conversión funcionan copiando todos los campos públicos y privados de una instancia del tipo en cuestión a otra. Además, es posible usar restricciones genéricas para invocar a los miembros de la interfaz en una ubicación de almacenamiento de tipo de valor directamente, sin hacer una copia de ella primero.

Todo esto es importante porque las referencias a objetos de montón de tipo de valor se comportan como referencias de clase y no como tipos de valor. Considere, por ejemplo, el siguiente código:

string testEnumerator <T> (T it) donde T: IEnumerator <string>
{
  var it2 = it;
  it.MoveNext ();
  it2.MoveNext ();
  devuélvelo.
}
prueba de vacío público ()
{
  var theList = new List <string> ();
  theList.Add ("Fred");
  theList.Add ("George");
  theList.Add ("Percy");
  theList.Add ("Molly");
  theList.Add ("Ron");

  var enum1 = theList.GetEnumerator ();
  IEnumerator <cadena> enum2 = enum1;

  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum1));
  Debug.Print (testEnumerator (enum2));
  Debug.Print (testEnumerator (enum2));
}

Si testEnumerator()se pasa el método a una ubicación de almacenamiento de tipo de valor, itrecibirá una instancia cuyos campos públicos y privados se copian del valor pasado. La variable local it2contendrá otra instancia cuyos campos se copian todos it. Llamando MoveNexta it2no afectará it.

Si el código anterior se pasa a una ubicación de almacenamiento de tipo de clase, entonces el valor pasado it, y it2, todos se referirán al mismo objeto y, por lo tanto, llamar MoveNext()a cualquiera de ellos lo llamará efectivamente a todos.

Tenga en cuenta que la conversión List<String>.Enumeratora IEnumerator<String>efectivamente lo convierte de un tipo de valor a un tipo de clase. El tipo del objeto de montón es List<String>.Enumeratorpero su comportamiento será muy diferente del tipo de valor del mismo nombre.

Super gato
fuente
+1 por no mencionar ToString ()
JeffO
1
Esto se trata de detalles de implementación. No es necesario exponerlos al usuario.
DeadMG
@DeadMG: ¿Qué quieres decir? El comportamiento observable de un List<T>.Enumeratorque se almacena como a List<T>.Enumeratores significativamente diferente del comportamiento de uno que se almacena en un IEnumerator<T>, ya que almacenar un valor del tipo anterior en una ubicación de almacenamiento de cualquier tipo hará una copia del estado del enumerador, mientras se almacena un valor del último tipo en una ubicación de almacenamiento que también es del último tipo, no se realizará una copia.
supercat
Sí, pero noObject tiene nada que ver con ese hecho.
DeadMG
@DeadMG: Mi punto es que una instancia de tipo de montón System.Int32también es una instancia de System.Object, pero una ubicación de almacenamiento de tipo System.Int32no contiene ni una System.Objectinstancia ni una referencia a una.
supercat
2

Este diseño realmente se remonta a Smalltalk, que vería en gran medida como un intento de perseguir la orientación a objetos a expensas de casi cualquier otra inquietud. Como tal, tiende (en mi opinión) a usar la orientación a objetos, incluso cuando otras técnicas son probablemente (o incluso ciertamente) superiores.

Tener una sola jerarquía con Object(o algo similar) en la raíz hace que sea bastante fácil (por ejemplo) crear sus clases de colección como colecciones Object, por lo que es trivial que una colección contenga cualquier tipo de objeto.

Sin embargo, a cambio de esta pequeña ventaja, obtienes una gran cantidad de desventajas. Primero, desde el punto de vista del diseño, terminas con algunas ideas verdaderamente locas. Al menos según la visión Java del universo, ¿qué tienen en común el ateísmo y un bosque? ¡Que ambos tienen códigos hash! ¿Es un mapa una colección? Según Java, no, ¡no lo es!

En los años 70, cuando se estaba diseñando Smalltalk, este tipo de tonterías fueron aceptadas, principalmente porque nadie había diseñado una alternativa razonable. Sin embargo, Smalltalk se finalizó en 1980 y en 1983 se diseñó Ada (que incluye genéricos). Aunque Ada nunca alcanzó el tipo de popularidad que algunos predijeron, sus genéricos fueron suficientes para soportar colecciones de objetos de tipos arbitrarios, sin la locura inherente a las jerarquías monolíticas.

Cuando se diseñó Java (y, en menor medida, .NET), la jerarquía de clases monolítica probablemente se vio como una opción "segura", una con problemas, pero en su mayoría problemas conocidos . La programación genérica, por el contrario, era una que casi todo el mundo (incluso entonces) se dio cuenta de que era al menos teóricamente un enfoque mucho mejor para el problema, pero que muchos desarrolladores con orientación comercial consideraron poco explorado y / o riesgoso (es decir, en el mundo comercial). Ada fue despedida en gran medida como un fracaso).

Sin embargo, déjenme ser claro: la jerarquía monolítica fue un error. Las razones de ese error eran al menos comprensibles, pero de todos modos fue un error. Es un mal diseño, y sus problemas de diseño impregnan casi todo el código que lo usa.

Para un nuevo diseño hoy, sin embargo, no hay una pregunta razonable: usar una jerarquía monolítica es un claro error y una mala idea.

Jerry Coffin
fuente
Vale la pena decir que se sabía que las plantillas eran increíbles y útiles antes de que se enviara .NET.
DeadMG
En el mundo comercial, Ada fue un fracaso, no tanto por su verbosidad, sino por la falta de interfaces estandarizadas para los servicios del sistema operativo. Ninguna API estándar para el sistema de archivos, gráficos, red, etc. hizo que el desarrollo de Ada de aplicaciones 'alojadas' fuera extremadamente costoso.
Kevin Cline
@kevincline: Probablemente debería haberlo expresado de manera un poco diferente, en el sentido de que Ada en su conjunto fue desestimada como una falla debido a su falla en el mercado. Negarse a usar Ada era (IMO) bastante razonable, pero se niega a aprender mucho menos (en el mejor de los casos).
Jerry Coffin
@Jerry: Creo que las plantillas de C ++ adoptaron las ideas de los genéricos Ada, luego las mejoraron enormemente con la creación de instancias implícitas.
Kevin Cline
1

Lo que quieres hacer es realmente interesante, es difícil probar si está bien o mal hasta que hayas terminado.

Aquí hay algunas cosas para pensar:

  • ¿Cuándo pasarías deliberadamente este objeto de nivel superior sobre un objeto más específico?
  • ¿Qué código piensa poner en cada una de sus clases?

Esas son las dos únicas cosas que pueden beneficiarse de una raíz compartida. En un lenguaje se usa para definir algunos métodos muy comunes y le permite pasar objetos que aún no se han definido, pero que no deberían aplicarse a usted.

Además, por experiencia personal:

Utilicé un kit de herramientas una vez escrito por un desarrollador cuyo fondo era Smalltalk. Lo hizo para que todas sus clases de "Datos" extendieran una sola clase y todos los métodos tomaran esa clase, hasta ahora muy bien. El problema es que las diferentes clases de datos no siempre eran intercambiables, por lo que ahora mi editor y compilador no podían darme NINGUNA ayuda sobre qué pasar en una situación dada y tuve que referirme a sus documentos que no siempre ayudaron. .

Esa fue la biblioteca más difícil de usar con la que he tenido que lidiar.

Bill K
fuente
0

Porque ese es el camino de la OOP. Es importante tener un tipo (objeto) que pueda referirse a cualquier cosa. Un argumento de tipo objeto puede aceptar cualquier cosa. También hay herencia, puede estar seguro de que ToString (), GetHashCode (), etc. están disponibles en cualquier cosa y en todo.

Incluso Oracle se ha dado cuenta de la importancia de tener ese tipo de base y planea eliminar primitivas en Java cerca de 2017

Dante
fuente
66
No es el camino de la OOP ni importante. No das argumentos reales aparte de "Es bueno".
DeadMG
1
@DeadMG Puede leer los argumentos más allá de la primera oración. Las primitivas se comportan de manera diferente a los objetos, lo que obliga al programador a escribir muchas variantes del mismo código de propósito para objetos y primitivas.
Dante
2
@DeadMG, bueno, dijo que significa que puedes asumir ToString()y estarás GetType()en cada objeto. Probablemente valga la pena señalar que puede usar una variable de tipo objectpara almacenar cualquier objeto .NET.
JohnL
Además, es perfectamente posible escribir un tipo definido por el usuario que pueda contener una referencia de cualquier tipo. No necesita funciones de lenguaje especiales para lograrlo.
DeadMG
Solo debe tener un Objeto en niveles que contengan código específico, nunca debe tener uno solo para unir su gráfico. El "Objeto" en realidad implementa algunos métodos importantes y actúa como una forma de pasar un objeto a herramientas donde el tipo aún no se ha creado.
Bill K