¿Por qué necesitamos la nueva palabra clave y por qué el comportamiento predeterminado es ocultar y no anular?

81

Estaba mirando esta publicación de blog y tenía las siguientes preguntas:

  • ¿Por qué necesitamos la newpalabra clave? ¿Es solo para especificar que se está ocultando un método de clase base? Quiero decir, ¿por qué lo necesitamos? Si no usamos la overridepalabra clave, ¿no estamos ocultando el método de la clase base?
  • ¿Por qué el valor predeterminado en C # es ocultar y no anular? ¿Por qué los diseñadores lo implementaron de esta manera?
Salvadera
fuente
6
Una pregunta un poco más interesante (para mí) es por qué esconderse es legal. Por lo general, es un error (de ahí la advertencia del compilador), rara vez se desea y siempre es problemático en el mejor de los casos.
Konrad Rudolph

Respuestas:

127

Buena pregunta. Permítanme reformularlos.

¿Por qué es legal ocultar un método con otro método?

Déjame responder esa pregunta con un ejemplo. Tiene una interfaz de CLR v1:

interface IEnumerable
{
    IEnumerator GetEnumerator();
}

Súper. Ahora, en CLR v2 tienes genéricos y piensas "hombre, si solo hubiéramos tenido genéricos en v1, habría hecho de esta una interfaz genérica. Pero no lo hice. Debería hacer algo compatible con él ahora que sea genérico para que Obtengo los beneficios de los genéricos sin perder la compatibilidad con versiones anteriores del código que espera IEnumerable ".

interface IEnumerable<T> : IEnumerable
{
    IEnumerator<T> .... uh oh

¿De qué vas a llamar al método GetEnumerator IEnumerable<T>? Recuerde, desea que oculte GetEnumerator en la interfaz base no genérica. Usted no quiere que lo que se llamará a menos que esté explícitamente en una situación revés-comp.

Eso solo justifica el método de ocultación. Para obtener más ideas sobre las justificaciones del método de ocultación, consulte mi artículo sobre el tema .

¿Por qué esconderse sin "nuevo" causa una advertencia?

Porque queremos advertirle que está ocultando algo y podría estar haciéndolo accidentalmente. Recuerde, es posible que esté ocultando algo accidentalmente debido a una edición de la clase base realizada por otra persona, en lugar de por usted editando su clase derivada.

¿Por qué esconderse sin "nuevo" es una advertencia en lugar de un error?

Misma razón. Es posible que esté ocultando algo accidentalmente porque acaba de adquirir una nueva versión de una clase base. Esto sucede todo el tiempo. FooCorp crea una clase base B. BarCorp crea una clase D derivada con un método Bar, porque a sus clientes les gusta ese método. FooCorp ve eso y dice bueno, es una buena idea, podemos poner esa funcionalidad en la clase base. Lo hacen y envían una nueva versión de Foo.DLL, y cuando BarCorp adquiere la nueva versión, sería bueno que se les dijera que su método ahora oculta el método de la clase base.

Queremos que esa situación sea una advertencia y no un error porque convertirlo en un error significa que esta es otra forma del problema de la clase base frágil . C # se ha diseñado cuidadosamente para que cuando alguien realiza un cambio en una clase base, se minimizan los efectos en el código que usa una clase derivada.

¿Por qué se oculta y no se anula el valor predeterminado?

Porque la anulación virtual es peligrosa . La anulación virtual permite que las clases derivadas cambien el comportamiento del código compilado para usar clases base. Hacer algo peligroso, como anular, debe ser algo que haga consciente y deliberadamente , no por accidente.

Eric Lippert
fuente
1
FWIW, no estoy de acuerdo con que "[eso] solo justifica el método de ocultación". A veces es mejor romper la compatibilidad con versiones anteriores. Python 3 ha demostrado que esto realmente funciona sin destruir demasiado trabajo. Es importante tener en cuenta que la "compatibilidad con versiones anteriores a toda costa" no es la única opción razonable. Y esta razón se traslada al otro punto: cambiar una clase base siempre será frágil. De hecho, podría ser preferible romper una construcción dependiente. (Pero +1 por responder la pregunta de mi comentario debajo de la pregunta real.)
Konrad Rudolph
@Konrad: Estoy de acuerdo en que la compatibilidad hacia atrás puede doler. Mono ha decidido dejar de admitir 1.1 en futuras versiones.
simendsjo
1
@Konrad: Romper la compatibilidad con versiones anteriores de su idioma es muy diferente a facilitar que las personas que usan su idioma rompan la compatibilidad con versiones anteriores con su propio código. El ejemplo de Eric es el segundo caso. Y la persona que escribe ese código ha tomado la decisión de que quiere compatibilidad con versiones anteriores. El lenguaje debe apoyar esa decisión si es posible.
Brian
Dios te responda, Eric. Sin embargo, hay una razón más específica por la que anular por defecto es malo. Si una clase base Base introduce un nuevo método Duck () (que devuelve un objeto pato) que simplemente tiene la misma firma que su método existente Duck () (que convierte a Mario en pato) en una clase derivada Derived (es decir, sin comunicación entre tú y el escritor de Base), no quieres que los clientes de ambas clases hagan que Mario se agache cuando solo quieren un pato.
jyoungdev
En resumen, tiene que ver principalmente con cambiar clases e interfaces base.
jyoungdev
15

Si el método en la clase derivada está precedido por la nueva palabra clave, el método se define como independiente del método en la clase base.

Sin embargo, si no especifica new o overrides, la salida resultante es la misma que si hubiera especificado new, pero recibirá una advertencia del compilador (ya que es posible que no sepa que está ocultando un método en el método de la clase base, o, de hecho, es posible que haya querido anularlo y simplemente se olvidó de incluir la palabra clave).

Por lo tanto, le ayuda a evitar errores y a mostrar explícitamente lo que quiere hacer y hace que el código sea más legible, por lo que uno puede entenderlo fácilmente.

Incógnito
fuente
12

Vale la pena señalar que el único efecto de newen este contexto es suprimir una Advertencia. No hay cambio de semántica.

Entonces, una respuesta es: debemos newindicarle al compilador que la ocultación es intencional y eliminar la advertencia.

La pregunta de seguimiento es: si no puede o no puede anular un método, ¿por qué introduciría otro método con el mismo nombre? Porque esconderse es en esencia un conflicto de nombres. Y, por supuesto, lo evitaría en la mayoría de los casos.

La única buena razón que se me ocurre para la ocultación intencional es cuando una interfaz te impone un nombre.

Henk Holterman
fuente
6

En C #, los miembros están sellados de forma predeterminada, lo que significa que no puede anularlos (a menos que estén marcados con las palabras clave virtualo abstract) y esto por razones de rendimiento . El nuevo modificador se usa para ocultar explícitamente un miembro heredado.

Darin Dimitrov
fuente
4
Pero, ¿cuándo realmente quieres usar el nuevo modificador?
simendsjo
1
Cuando desee ocultar explícitamente un miembro heredado de una clase base. Ocultar un miembro heredado significa que la versión derivada del miembro reemplaza la versión de la clase base.
Darin Dimitrov
3
Pero cualquier método que utilice la clase base seguirá llamando a la implementación base, no a la derivada. Esto me parece una situación peligrosa.
simendsjo
@simendsjo, @Darin Dimitrov: También me cuesta pensar en un caso de uso en el que la nueva palabra clave sea realmente necesaria. Me parece que siempre hay mejores formas. También me pregunto si ocultar una implementación de clase base no es un defecto de diseño que conduce fácilmente a errores.
Dirk Vollmar
2

Si la anulación era predeterminada sin especificar la overridepalabra clave, podría anular accidentalmente algún método de su base solo debido a la igualdad de nombres.

La estrategia del compilador .Net es emitir advertencias si algo podría salir mal, solo para estar seguro, por lo que, en este caso, si la anulación fuera la predeterminada, debería haber una advertencia para cada método anulado, algo como 'advertencia: verifique si realmente quiere para anular'.

František Žiačik
fuente
0

Mi conjetura se debe principalmente a la herencia de múltiples interfaces. Usando interfaces discretas, sería muy posible que dos interfaces distintas usen la misma firma de método. Permitir el uso de la newpalabra clave le permitiría crear estas diferentes implementaciones con una clase, en lugar de tener que crear dos clases distintas.

Actualizado ... Eric me dio una idea sobre cómo mejorar este ejemplo.

public interface IAction1
{
    int DoWork();
}
public interface IAction2
{
    string DoWork();
}

public class MyBase : IAction1
{
    public int DoWork() { return 0; }
}
public class MyClass : MyBase, IAction2
{
    public new string DoWork() { return "Hi"; }
}

class Program
{
    static void Main(string[] args)
    {
        var myClass = new MyClass();
        var ret0 = myClass.DoWork(); //Hi
        var ret1 = ((IAction1)myClass).DoWork(); //0
        var ret2 = ((IAction2)myClass).DoWork(); //Hi
        var ret3 = ((MyBase)myClass).DoWork(); //0
        var ret4 = ((MyClass)myClass).DoWork(); //Hi
    }
}
Matthew Whited
fuente
1
Cuando utilice varias interfaces, puede nombrar los métodos de la interfaz explícitamente para evitar colisiones. No entiendo lo que quiere decir con "implementación diferente para una clase", ambas tienen la misma firma ...
simendsjo
La newpalabra clave le permite también cambiar la base de herencia para todos los hijos de ese punto.
Matthew Whited
-1

Como se señaló, la ocultación de método / propiedad hace posible cambiar cosas sobre un método o propiedad que no podrían cambiarse fácilmente de otra manera. Una situación en la que esto puede ser útil es permitir que una clase heredada tenga propiedades de lectura y escritura que son de solo lectura en la clase base. Por ejemplo, suponga que una clase base tiene un grupo de propiedades de solo lectura llamadas Value1-Value40 (por supuesto, una clase real usaría mejores nombres). Un descendiente sellado de esta clase tiene un constructor que toma un objeto de la clase base y copia los valores de allí; la clase no permite que se cambien después de eso. Un descendiente diferente, heredable, declara una propiedad de lectura-escritura llamada Value1-Value40 que, cuando se lee, se comporta igual que las versiones de la clase base pero, cuando se escribe, permite que se escriban los valores.

Una molestia con este enfoque, tal vez alguien pueda ayudarme, es que no conozco una forma de sombrear y anular una propiedad en particular dentro de la misma clase. ¿Alguno de los lenguajes CLR lo permite (yo uso vb 2005)? Sería útil si el objeto de la clase base y sus propiedades pudieran ser abstractos, pero eso requeriría que una clase intermedia anulara las propiedades Value1 a Value40 antes de que una clase descendiente pudiera sombrearlas.

Super gato
fuente