Muchos de los lenguajes de programación más populares (como C ++, Java, Python, etc.) tienen el concepto de ocultar / sombrear variables o funciones. Cuando me he encontrado oculto o sombreado, han sido la causa de errores difíciles de encontrar y nunca he visto un caso en el que me pareció necesario utilizar estas características de los idiomas.
A mí me parecería mejor no permitir que se esconda y se sombree.
¿Alguien sabe de un buen uso de estos conceptos?
Actualización: no
me estoy refiriendo a la encapsulación de miembros de la clase (miembros privados / protegidos).
Respuestas:
Si no permite ocultar y sombrear, lo que tiene es un lenguaje en el que todas las variables son globales.
Eso es claramente peor que permitir variables o funciones locales que pueden ocultar variables o funciones globales.
Si no permitir ocultar y sombreado, y se intenta "proteger" a ciertas variables globales, se crea una situación en la que el compilador dice que el programador "Lo siento, Dave, pero no se puede utilizar ese nombre, que ya está en uso ". La experiencia con COBOL muestra que los programadores recurren casi de inmediato a las blasfemias en esta situación.
El problema fundamental no es ocultar / sombrear, sino variables globales.
fuente
Usar identificadores descriptivos precisos es siempre un buen uso.
Podría argumentar que el ocultamiento de variables no causa muchos errores, ya que tener dos variables con nombres muy similares del mismo tipo / similares (lo que haría si no se permitiera el ocultamiento de variables) es probable que provoque tantos errores y / o solo errores graves No sé si ese argumento es correcto , pero al menos es plausiblemente discutible.
El uso de algún tipo de notación húngara para diferenciar los campos de las variables locales evita esto, pero tiene su propio impacto en el mantenimiento (y la cordura del programador).
Y (quizás la razón más probable por la que se conoce el concepto en primer lugar) es mucho más fácil para los idiomas implementar el ocultamiento / sombreado que no permitirlo. Una implementación más fácil significa que los compiladores tienen menos probabilidades de tener errores. Una implementación más fácil significa que los compiladores tardan menos en escribir, lo que provoca una adopción de plataforma más temprana y más amplia.
fuente
Solo para asegurarnos de que estamos en la misma página, el método "ocultar" es cuando una clase derivada define un miembro del mismo nombre como uno en la clase base (que, si es un método / propiedad, no se marca virtual / reemplazable ), y cuando se invoca desde una instancia de la clase derivada en "contexto derivado", se usa el miembro derivado, mientras que si es invocado por la misma instancia en el contexto de su clase base, se usa el miembro de la clase base. Esto es diferente de la abstracción / anulación de miembros donde el miembro de la clase base espera que la clase derivada defina un reemplazo, y de los modificadores de alcance / visibilidad que "ocultan" un miembro de los consumidores fuera del alcance deseado.
La respuesta breve a por qué está permitido es que no hacerlo obligaría a los desarrolladores a violar varios principios clave del diseño orientado a objetos.
Aquí está la respuesta más larga; primero, considere la siguiente estructura de clase en un universo alternativo donde C # no permite ocultar miembros:
Queremos descomentar al miembro en Bar y, al hacerlo, permitir que Bar proporcione un MyFooString diferente. Sin embargo, no podemos hacerlo porque violaría la prohibición de la realidad alternativa de ocultar miembros. Este ejemplo particular sería abundante para los errores y es un excelente ejemplo de por qué es posible que desee prohibirlo; por ejemplo, ¿qué salida de consola obtendrías si hicieras lo siguiente?
Fuera de mi cabeza, en realidad no estoy seguro de si obtendrías "Foo" o "Bar" en esa última línea. Definitivamente obtendría "Foo" para la primera línea y "Bar" para la segunda, a pesar de que las tres variables hacen referencia exactamente a la misma instancia con exactamente el mismo estado.
Entonces, los diseñadores del lenguaje, en nuestro universo alternativo, desalientan este código obviamente malo al evitar la ocultación de propiedades. Ahora, usted como codificador tiene una verdadera necesidad de hacer exactamente esto. ¿Cómo esquivas la limitación? Bueno, una forma es nombrar la propiedad de Bar de manera diferente:
Perfectamente legal, pero no es el comportamiento que queremos. Una instancia de Bar siempre producirá "Foo" para la propiedad MyFooString, cuando queramos que produzca "Bar". No solo tenemos que saber que nuestro IFoo es específicamente un Bar, también tenemos que saber usar los diferentes accesorios.
También podríamos, muy plausiblemente, olvidar la relación padre-hijo e implementar la interfaz directamente:
Para este simple ejemplo, es una respuesta perfecta, siempre y cuando solo le importe que Foo y Bar sean ambos IFoos. El código de uso de un par de ejemplos no podría compilarse porque un Bar no es un Foo y no se puede asignar como tal. Sin embargo, si Foo tenía algún método útil "FooMethod" que Bar necesitaba, ahora no puede heredar ese método; tendrías que clonar su código en Bar o ser creativo:
Este es un truco obvio, y aunque algunas implementaciones de las especificaciones del lenguaje OO ascienden a poco más que esto, conceptualmente está mal; si los consumidores de bar necesidad de exponer la funcionalidad de Foo, Bar debería ser un Foo, no tiene un Foo.
Obviamente, si controlamos Foo, podemos hacerlo virtual y luego anularlo. Esta es la mejor práctica conceptual en nuestro universo actual cuando se espera que un miembro sea anulado, y se mantendría en cualquier universo alternativo que no permitiera ocultarse:
El problema con esto es que el acceso virtual de los miembros es, bajo el capó, relativamente más costoso de realizar, por lo que normalmente solo desea hacerlo cuando lo necesita. La falta de ocultación, sin embargo, te obliga a ser pesimista sobre los miembros que otro codificador que no controla tu código fuente podría querer volver a implementar; la "mejor práctica" para cualquier clase no sellada sería hacer que todo sea virtual a menos que específicamente no lo desees. También todavía no le otorga el comportamiento exacto de su escondite; la cadena siempre será "Bar" si la instancia es una barra. A veces es realmente útil aprovechar las capas de datos de estado ocultos, según el nivel de herencia en el que está trabajando.
En resumen, permitir que los miembros se escondan es el menor de estos males. No tenerlo generalmente conduciría a peores atrocidades cometidas contra principios orientados a objetos que permitirlo.
fuente
IEnumerable
yIEnumerable<T>
, que se describe en la publicación del blog de Eric Libbert sobre el tema.Honestamente, Eric Lippert, el desarrollador principal del equipo del compilador de C #, lo explica bastante bien (gracias Lescai Ionel por el enlace). Las interfaces
IEnumerable
y .NETIEnumerable<T>
son buenos ejemplos de cuándo es útil la ocultación de miembros.En los primeros días de .NET, no teníamos genéricos. Entonces la
IEnumerable
interfaz se veía así:Esta interfaz es lo que nos permitió
foreach
sobre una colección de objetos, sin embargo, tuvimos que lanzar todos esos objetos para poder usarlos correctamente.Luego vinieron los genéricos. Cuando obtuvimos genéricos, también obtuvimos una nueva interfaz:
¡Ahora no tenemos que lanzar objetos mientras los iteramos! Woot! Ahora, si no se permitiera ocultar miembros, la interfaz tendría que verse así:
Esto sería un poco tonto, porque
GetEnumerator()
yGetEnumeratorGeneric()
en ambos casos hacen exactamente lo mismo , pero tienen valores de retorno ligeramente diferentes. De hecho, son tan similares que casi siempre desea utilizar la forma genérica de forma predeterminadaGetEnumerator
, a menos que esté trabajando con código heredado que se escribió antes de que se introdujeran los genéricos en .NET.A veces escondite miembro de no permitir más espacio para el código desagradable y errores difíciles de encontrar. Sin embargo, a veces es útil, como cuando desea cambiar un tipo de retorno sin romper el código heredado. Esa es solo una de esas decisiones que deben tomar los diseñadores de idiomas: ¿incomodamos a los desarrolladores que legítimamente necesitan esta característica y la dejamos de lado, o incluimos esta característica en el lenguaje y atrapamos críticas de aquellos que son víctimas de su mal uso?
fuente
IEnumerable<T>.GetEnumerator()
ocultaIEnumerable.GetEnumerator()
, esto se debe solo a que C # no tiene tipos de retorno covariantes cuando se anula. Lógicamente es una anulación, totalmente en línea con LSP. Ocultar es cuando tienes una variable localmap
en función en un archivo que síusing namespace std
(en C ++).Su pregunta podría leerse de dos maneras: o está preguntando sobre el alcance de la variable / función en general, o está haciendo una pregunta más específica sobre el alcance en una jerarquía de herencia. No mencionó la herencia específicamente, pero mencionó errores difíciles de encontrar, lo que parece más un alcance en el contexto de la herencia que un alcance simple, por lo que responderé ambas preguntas.
El alcance en general es una buena idea, ya que nos permite centrar nuestra atención en una parte específica (con suerte pequeña) del programa. Debido a que permite que los nombres locales siempre ganen, si lee solo la parte del programa que se encuentra en un alcance determinado, entonces sabrá exactamente qué partes se definieron localmente y qué se definió en otro lugar. O el nombre se refiere a algo local, en cuyo caso el código que lo define está justo en frente de usted, o es una referencia a algo fuera del alcance local. Si no hay referencias no locales que puedan cambiar por debajo de nosotros (especialmente las variables globales, que podrían cambiarse desde cualquier lugar), entonces podemos evaluar si la parte del programa en el ámbito local es correcta o no sin consultar a cualquier parte del resto del programa .
Es posible que ocasionalmente ocasione algunos errores, pero compensa más que nada evitando una enorme cantidad de errores posibles. Aparte de hacer una definición local con el mismo nombre que una función de biblioteca (no hagas eso), no puedo ver una manera fácil de introducir errores con alcance local, pero el alcance local es lo que permite que muchas partes del mismo programa usen i como el contador de índice para un bucle sin golpearse entre sí, y permite que Fred al final del pasillo escriba una función que utiliza una cadena llamada str que no golpeará su cadena con el mismo nombre.
Encontré un artículo interesante de Bertrand Meyer que analiza la sobrecarga en el contexto de la herencia. Trae una distinción interesante, entre lo que él llama sobrecarga sintáctica (lo que significa que hay dos cosas diferentes con el mismo nombre) y sobrecarga semántica (lo que significa que hay dos implementaciones diferentes de la misma idea abstracta). La sobrecarga semántica estaría bien, ya que pretendía implementarla de manera diferente en la subclase; la sobrecarga sintáctica sería la colisión accidental de nombres que causó un error.
La diferencia entre sobrecargar en una situación de herencia que se pretende y que es un error es la semántica (el significado), por lo que el compilador no tiene forma de saber si lo que hizo es correcto o incorrecto. En una situación de alcance simple, la respuesta correcta es siempre lo local, por lo que el compilador puede descubrir qué es lo correcto.
La sugerencia de Bertrand Meyer sería utilizar un lenguaje como Eiffel, que no permite conflictos de nombres como este y obliga al programador a cambiar el nombre de uno o ambos, evitando así el problema por completo. Mi sugerencia sería evitar el uso de la herencia por completo, también evitando el problema por completo. Si no puede o no quiere hacer ninguna de esas cosas, todavía hay cosas que puede hacer para reducir la probabilidad de tener un problema de herencia: siga el LSP (Principio de sustitución de Liskov), prefiera la composición sobre la herencia, mantenga sus jerarquías de herencia son superficiales y mantenga las clases en una jerarquía de herencia pequeña. Además, algunos idiomas pueden emitir una advertencia, incluso si no emitieran un error, como lo haría un idioma como Eiffel.
fuente
Aquí están mis dos centavos.
Los programas pueden estructurarse en bloques (funciones, procedimientos) que son unidades independientes de la lógica del programa. Cada bloque puede referirse a "cosas" (variables, funciones, procedimientos) usando nombres / identificadores. Este mapeo de nombres a cosas se llama enlace .
Los nombres utilizados por un bloque se dividen en tres categorías:
Considere, por ejemplo, el siguiente programa C
La función
print_double_int
tiene un nombre local (variable local)d
y un argumenton
, y usa el nombre global externoprintf
, que está dentro del alcance pero no está definido localmente.Tenga en cuenta que
printf
también podría pasarse como argumento:Normalmente, se usa un argumento para especificar los parámetros de entrada / salida de una función (procedimiento, bloque), mientras que los nombres globales se usan para referirse a cosas como las funciones de la biblioteca que "existen en el entorno" y, por lo tanto, es más conveniente mencionarlas solo cuando son necesarios. El uso de argumentos en lugar de nombres globales es la idea principal de la inyección de dependencias , que se utiliza cuando las dependencias deben hacerse explícitas en lugar de resolverse mirando el contexto.
Otro uso similar de nombres definidos externamente se puede encontrar en los cierres. En este caso, se puede usar un nombre definido en el contexto léxico de un bloque dentro del bloque, y el valor vinculado a ese nombre (típicamente) continuará existiendo mientras el bloque se refiera a él.
Tomemos por ejemplo este código Scala:
El valor de retorno de la función
createMultiplier
es el cierre(m: Int) => m * n
, que contiene el argumentom
y el nombre externon
. El nombren
se resuelve mirando el contexto en el que se define el cierre: el nombre está vinculado al argumenton
de la funcióncreateMultiplier
. Tenga en cuenta que este enlace se crea cuando se crea el cierre, es decir, cuandocreateMultiplier
se invoca. Por lo tanto, el nombren
está vinculado al valor real de un argumento para una invocación particular de la función. Contraste esto con el caso de una función de biblioteca comoprintf
, que se resuelve mediante el vinculador cuando se construye el ejecutable del programa.En resumen, puede ser útil hacer referencia a nombres externos dentro de un bloque de código local para que pueda
El sombreado aparece cuando considera que en un bloque solo le interesan los nombres relevantes que se definen en el entorno, por ejemplo, en la
printf
función que desea utilizar. Si por casualidad usted quiere utilizar un nombre local (getc
,putc
,scanf
, ...) que ya ha sido utilizado en el entorno, sencilla desea ignorar (sombra) el nombre global. Por lo tanto, cuando piensa localmente, no desea considerar todo el contexto (posiblemente muy grande).En la otra dirección, al pensar globalmente, desea ignorar los detalles internos de los contextos locales (encapsulación). Por lo tanto, debe sombrearse, de lo contrario, agregar un nombre global podría romper todos los bloques locales que ya usaban ese nombre.
En pocas palabras, si desea que un bloque de código haga referencia a enlaces definidos externamente, necesita sombreado para proteger los nombres locales de los globales.
fuente