¿Por qué C # implementa métodos como no virtuales de forma predeterminada?

105

A diferencia de Java, ¿por qué C # trata los métodos como funciones no virtuales de forma predeterminada? ¿Es más probable que sea un problema de desempeño que otros posibles resultados?

Recuerdo haber leído un párrafo de Anders Hejlsberg sobre varias ventajas que está aportando la arquitectura existente. Pero, ¿qué pasa con los efectos secundarios? ¿Es realmente una buena compensación tener métodos no virtuales por defecto?

Burcu Dogan
fuente
1
Las respuestas que mencionan razones de rendimiento pasan por alto el hecho de que el compilador de C # compila principalmente llamadas de método a callvirt y no a call . Es por eso que en C # no es posible tener un método que se comporte de manera diferente si la thisreferencia es nula. Consulte aquí para obtener más información.
Andy
¡Cierto! La instrucción call IL es principalmente para llamadas realizadas a métodos estáticos.
RBT
1
Los pensamientos del arquitecto de C # Anders Hejlsberg aquí y aquí .
RBT

Respuestas:

98

Las clases deben estar diseñadas para la herencia para poder aprovecharla. Tener métodos virtualpor defecto significa que todas las funciones de la clase se pueden desconectar y reemplazar por otras, lo cual no es realmente bueno. Mucha gente incluso cree que las clases deberían haber sido sealedpor defecto.

virtualLos métodos también pueden tener una ligera implicación en el rendimiento. Sin embargo, no es probable que esta sea la razón principal.

Mehrdad Afshari
fuente
7
Personalmente, dudo de esa parte del rendimiento. Incluso para las funciones virtuales, el compilador es muy capaz de averiguar dónde reemplazar callvirtpor un simple callen el código IL (o incluso más adelante cuando se hace JIT). Java HotSpot hace lo mismo. El resto de la respuesta es acertada.
Konrad Rudolph
5
Estoy de acuerdo en que el desempeño no está a la vanguardia, pero también puede tener implicaciones en el desempeño. Probablemente sea muy pequeño. Sin embargo, ciertamente, no es el motivo de esta elección. Solo quería mencionar eso.
Mehrdad Afshari
71
Las clases selladas me dan ganas de golpear a los bebés.
mxmissile
6
@mxmissile: yo también, pero un buen diseño de API es difícil de todos modos. Creo que sellado por defecto tiene mucho sentido para el código que posee. La mayoría de las veces, el otro programador de su equipo no consideró la herencia cuando implementó esa clase que necesita, y el sellado por defecto transmitirá ese mensaje bastante bien.
Roman Starkov
49
Muchas personas también son psicópatas, no significa que debamos escucharlas. Virtual debería ser el predeterminado en C #, el código debería ser extensible sin tener que cambiar las implementaciones o reescribirlo por completo. Las personas que sobreprotegen sus API a menudo terminan con API muertas o medio usadas, lo que es mucho peor que el escenario de que alguien las use incorrectamente.
Chris Nicola
89

Me sorprende que parezca haber tal consenso aquí de que no virtual por defecto es la forma correcta de hacer las cosas. Voy a caer en el otro lado, creo que pragmático, de la valla.

La mayoría de las justificaciones me parecen el viejo argumento de "Si te damos el poder, podrías lastimarte". ¿De los programadores?

Me parece que el codificador que no sabía lo suficiente (o no tenía suficiente tiempo) para diseñar su biblioteca para herencia y / o extensibilidad es el codificador que produjo exactamente la biblioteca que probablemente tenga que arreglar o modificar, exactamente el biblioteca donde la capacidad de anular sería más útil.

La cantidad de veces que tuve que escribir un código de solución feo y desesperado (o abandonar el uso y lanzar mi propia solución alternativa) porque no puedo anular mucho, supera con creces la cantidad de veces que me han mordido ( por ejemplo, en Java) anulando donde el diseñador podría no haber considerado que yo podría.

No virtual por defecto hace mi vida más difícil.

ACTUALIZACIÓN: Se ha señalado [bastante correctamente] que en realidad no respondí la pregunta. Entonces, y con disculpas por llegar bastante tarde ...

Quería un poco poder escribir algo conciso como "C # implementa métodos como no virtuales por defecto porque se tomó una mala decisión que valoraba los programas más que los programadores". (Creo que eso podría estar algo justificado en función de algunas de las otras respuestas a esta pregunta, como el rendimiento (optimización prematura, ¿alguien?), O garantizar el comportamiento de las clases).

Sin embargo, me doy cuenta de que solo estaría expresando mi opinión y no esa respuesta definitiva que desea Stack Overflow. Seguramente, pensé, en el nivel más alto, la respuesta definitiva (pero inútil) es:

No son virtuales de forma predeterminada porque los diseñadores del lenguaje tenían que tomar una decisión y eso es lo que eligieron.

Ahora supongo que la razón exacta por la que tomaron esa decisión nunca la vamos a ... ¡oh, espera! ¡La transcripción de una conversación!

Por lo tanto, parecería que las respuestas y comentarios aquí sobre los peligros de anular las API y la necesidad de diseñar explícitamente para la herencia están en el camino correcto, pero a todas les falta un aspecto temporal importante: la principal preocupación de Anders era mantener una clase o API implícita contrato entre versiones . Y creo que en realidad está más preocupado por permitir que la plataforma .Net / C # cambie bajo el código que por el cambio de código de usuario en la parte superior de la plataforma. (Y su punto de vista "pragmático" es exactamente lo opuesto al mío porque mira desde el otro lado).

(¿Pero no podrían simplemente haber elegido virtual por defecto y luego salpicado de "final" a través de la base de código? Quizás eso no sea lo mismo ... y Anders es claramente más inteligente que yo, así que lo dejaré mentir).

mwardm
fuente
2
No podría estar más de acuerdo. Muy frustrante al consumir una API de terceros y querer anular algún comportamiento y no poder hacerlo.
Andy
3
Estoy de acuerdo con entusiasmo. Si va a publicar una API (ya sea internamente en una empresa o en el mundo externo), realmente puede y debe asegurarse de que su código esté diseñado para la herencia. Debería hacer que la API sea buena y pulida si la va a publicar para que la utilicen muchas personas. En comparación con el esfuerzo necesario para un buen diseño general (buen contenido, casos de uso claros, pruebas, documentación), diseñar para la herencia no es tan malo. Y si no está publicando, lo virtual de forma predeterminada requiere menos tiempo y siempre puede solucionar la pequeña parte de los casos en los que es problemático.
Gravity
2
Ahora, si solo hubiera una función de editor en Visual Studio para etiquetar automáticamente todos los métodos / propiedades como virtual... ¿Complemento de Visual Studio, alguien?
kevinarpe
1
Aunque estoy totalmente de acuerdo contigo, esta no es exactamente una respuesta.
Chris Morgan
2
Teniendo en cuenta las prácticas de desarrollo modernas como la inyección de dependencias, los frameworks de burla y los ORM, parece claro que nuestros diseñadores de C # se equivocaron un poco. Es muy frustrante tener que probar una dependencia cuando no puede anular una propiedad de forma predeterminada.
Jeremy Holovacs
17

Porque es demasiado fácil olvidar que un método puede ser anulado y no diseñado para eso. C # te hace pensar antes de hacerlo virtual. Creo que esta es una gran decisión de diseño. Algunas personas (como Jon Skeet) incluso han dicho que las clases deberían estar selladas por defecto.

Zifre
fuente
12

Para resumir lo que otros dijeron, hay algunas razones:

1- En C #, hay muchas cosas en sintaxis y semántica que vienen directamente de C ++. El hecho de que los métodos no fueran virtuales de forma predeterminada en C ++ influyó en C #.

2- Tener todos los métodos virtuales por defecto es una preocupación de rendimiento porque cada llamada a un método debe usar la Tabla Virtual del objeto. Además, esto limita en gran medida la capacidad del compilador Just-In-Time para incorporar métodos y realizar otros tipos de optimización.

3- Lo más importante, si los métodos no son virtuales por defecto, puedes garantizar el comportamiento de tus clases. Cuando son virtuales de forma predeterminada, como en Java, ni siquiera puede garantizar que un método getter simple funcione como se espera porque podría anularse para hacer cualquier cosa en una clase derivada (por supuesto, puede, y debe, hacer el método y / o la clase final).

Uno podría preguntarse, como mencionó Zifre, por qué el lenguaje C # no dio un paso más y selló las clases por defecto. Eso es parte de todo el debate sobre los problemas de la herencia de implementación, que es un tema muy interesante.

Trillian
fuente
1
También habría preferido clases selladas por defecto. Entonces sería consistente.
Andy
9

C # está influenciado por C ++ (y más). C ++ no habilita el envío dinámico (funciones virtuales) de forma predeterminada. Un argumento (¿bueno?) Para esto es la pregunta: "¿Con qué frecuencia implementa clases que son miembros de una jerarquía de clases?". Otra razón para evitar habilitar el despacho dinámico de forma predeterminada es la huella de memoria. Una clase sin un puntero virtual (vpointer) que apunte a una tabla virtual , es por supuesto más pequeña que la clase correspondiente con enlace tardío habilitado.

No es tan fácil decir "sí" o "no" a la cuestión del rendimiento. La razón de esto es la compilación Just In Time (JIT), que es una optimización del tiempo de ejecución en C #.

Otra pregunta similar sobre la " velocidad de las llamadas virtuales ... "

Schildmeijer
fuente
Dudo un poco que los métodos virtuales tengan implicaciones de rendimiento para C #, debido al compilador JIT. Esa es una de las áreas en las que JIT puede ser mejor que la compilación sin conexión, ya que pueden realizar llamadas de funciones en línea que son "desconocidas" antes del tiempo de ejecución
David Cournapeau
1
En realidad, creo que está más influenciado por Java que por C ++, que lo hace de forma predeterminada.
Mehrdad Afshari
5

La simple razón es el costo de diseño y mantenimiento además de los costos de desempeño. Un método virtual tiene un costo adicional en comparación con un método no virtual porque el diseñador de la clase debe planificar lo que sucede cuando el método es anulado por otra clase. Esto tiene un gran impacto si espera que un método en particular actualice el estado interno o tenga un comportamiento particular. Ahora tiene que planificar lo que sucede cuando una clase derivada cambia ese comportamiento. Es mucho más difícil escribir código confiable en esa situación.

Con un método no virtual tienes el control total. Todo lo que sale mal es culpa del autor original. El código es mucho más fácil de razonar.

JaredPar
fuente
Esta es una publicación realmente antigua, pero para un novato que está escribiendo su primer proyecto, me preocupo constantemente por las consecuencias no deseadas / desconocidas del código que escribo. Es muy reconfortante saber que los métodos no virtuales son totalmente culpa Mía.
trevorc
2

Si todos los métodos de C # fueran virtuales, el vtbl sería mucho mayor.

Los objetos de C # solo tienen métodos virtuales si la clase tiene métodos virtuales definidos. Es cierto que todos los objetos tienen información de tipo que incluye un equivalente de vtbl, pero si no se definen métodos virtuales, solo estarán presentes los métodos de Objeto base.

@Tom Hawtin: Probablemente sea más exacto decir que C ++, C # y Java son todos de la familia de lenguajes C :)

Thomas Bratt
fuente
1
¿Por qué sería un problema que la vtable fuera mucho más grande? Solo hay 1 vtable por clase (y no por instancia), por lo que su tamaño no hace una gran diferencia.
Gravity
1

Viniendo de un fondo de perl, creo que C # selló la perdición de todos los desarrolladores que podrían haber querido extender y modificar el comportamiento de una clase base 'a través de un método no virtual sin forzar a todos los usuarios de la nueva clase a estar al tanto de potencialmente detrás de escena detalles.

Considere el método Add de la clase List. ¿Qué pasa si un desarrollador quisiera actualizar una de varias bases de datos potenciales cada vez que se 'agrega' una lista en particular? Si 'Agregar' hubiera sido virtual por defecto, el desarrollador podría desarrollar una clase 'BackedList' que anulara el método 'Agregar' sin forzar a todo el código del cliente a saber que era una 'BackedList' en lugar de una 'Lista' normal. Para todos los propósitos prácticos, la 'BackedList' se puede ver simplemente como otra 'Lista' del código del cliente.

Esto tiene sentido desde la perspectiva de una clase principal grande que podría proporcionar acceso a uno o más componentes de la lista que están respaldados por uno o más esquemas en una base de datos. Dado que los métodos de C # no son virtuales de forma predeterminada, la lista proporcionada por la clase principal no puede ser una simple IEnumerable o ICollection o incluso una instancia de List, sino que debe anunciarse al cliente como una 'BackedList' para garantizar que la nueva versión de la operación 'Agregar' se llama para actualizar el esquema correcto.

Travis
fuente
Es cierto que los métodos virtuales por defecto facilitan el trabajo, pero ¿tiene sentido? Hay muchas cosas que podría emplear para hacer la vida más fácil. ¿Por qué no solo campos públicos en las clases? Modificar su comportamiento es demasiado fácil. En mi opinión, todo en el lenguaje debería estar estrictamente encapsulado, ser rígido y resistente al cambio de forma predeterminada. Cámbielo solo si es necesario. Simplemente siendo un poco filosófico sobre decisiones de diseño. Continuación ..
nawfal
... Para hablar sobre el tema actual, el modelo de herencia debe usarse solo si Bes A. Si Brequiere algo diferente, Aentonces no lo es A. Creo que anular como capacidad en el lenguaje en sí es un defecto de diseño. Si necesita un Addmétodo diferente , entonces su clase de colección no es una List. Tratar de decirlo es fingir. El enfoque correcto aquí es la composición (y no la falsificación). Es cierto que todo el marco se basa en capacidades primordiales, pero simplemente no me gusta.
nawfal
Creo que entiendo su punto, pero en el ejemplo concreto proporcionado: 'BackedList' podría simplemente implementar la interfaz 'IList' y el cliente solo conoce la interfaz. ¿Derecha? ¿Me estoy perdiendo de algo? sin embargo, entiendo el punto más amplio que está tratando de hacer.
Vetras
0

Ciertamente no es un problema de rendimiento. El intérprete de Java de Sun usa el mismo código para enviar (código de invokevirtualbytes) y HotSpot genera exactamente el mismo código, ya finalsea ​​que no. Creo que todos los objetos de C # (pero no las estructuras) tienen métodos virtuales, por lo que siempre necesitará la vtblidentificación de clase / runtime. C # es un dialecto de "lenguajes similares a Java". Sugerir que proviene de C ++ no es del todo honesto.

Existe la idea de que debería "diseñar para la herencia o prohibirla". Lo cual suena como una gran idea hasta el momento en que tiene un caso de negocios serio para solucionarlo rápidamente. Quizás heredando un código que no controlas.

Tom Hawtin - tackline
fuente
Hotspot se ve obligado a hacer todo lo posible para hacer esa optimización, precisamente porque todos los métodos son virtuales de forma predeterminada, lo que tiene un gran impacto en el rendimiento. CoreCLR puede lograr un rendimiento similar mientras es mucho más simple
Yair Halberstadt
@YairHalberstadt Hotspot necesita poder retroceder el código compilado por una variedad de otras razones. Han pasado años desde que miré la fuente, pero la diferencia entre los métodos finalefectivos finales trivial. También vale la pena señalar que puede hacer alineación bimórfica, es decir, métodos en línea con dos implementaciones diferentes.
Tom Hawtin - tackline