Muitas das linguagens de programação mais populares (como C ++, Java, Python etc.) têm o conceito de ocultar / sombrear variáveis ou funções. Quando encontrei esconderijo ou sombreamento, eles causaram erros difíceis de encontrar e nunca vi um caso em que fosse necessário usar esses recursos dos idiomas.
Para mim, seria melhor não permitir ocultar e sombrear.
Alguém sabe de um bom uso desses conceitos?
Atualização:
não estou me referindo ao encapsulamento de membros da classe (membros privados / protegidos).
Respostas:
Se você não permitir ocultar e sombrear, o que você tem é um idioma no qual todas as variáveis são globais.
Isso é claramente pior do que permitir variáveis ou funções locais que podem ocultar variáveis ou funções globais.
Se você não permitir ocultar e sombrear E tentar "proteger" determinadas variáveis globais, criará uma situação em que o compilador informará o programador "Sinto muito, Dave, mas você não pode usar esse nome, ele já está em uso. . " A experiência com o COBOL mostra que os programadores quase imediatamente recorrem a palavrões nessa situação.
A questão fundamental não é ocultar / sombrear, mas variáveis globais.
fonte
Usar identificadores precisos e descritivos é sempre um bom uso.
Eu poderia argumentar que a ocultação de variáveis não causa muitos erros, uma vez que ter duas variáveis com nomes muito semelhantes dos mesmos / tipos semelhantes (o que você faria se a ocultação de variáveis não fosse permitida) provavelmente causará tantos erros e / ou erros graves. Não sei se esse argumento está correto , mas é pelo menos plausivelmente discutível.
O uso de algum tipo de notação húngara para diferenciar campos versus variáveis locais contorna isso, mas tem seu próprio impacto na manutenção (e na sanidade do programador).
E (provavelmente o motivo pelo qual o conceito é conhecido em primeiro lugar) é muito mais fácil para as linguagens implementar ocultação / sombreamento do que desaprovar. Uma implementação mais fácil significa que é menos provável que os compiladores tenham bugs. A implementação mais fácil significa que os compiladores levam menos tempo para escrever, causando uma adoção mais ampla e anterior da plataforma.
fonte
Apenas para garantir que estamos na mesma página, o método "oculto" é quando uma classe derivada define um membro com o mesmo nome que um membro da classe base (que, se for um método / propriedade, não é marcado como virtual / substituível ) e, quando invocado de uma instância da classe derivada no "contexto derivado", o membro derivado é usado, enquanto que, se invocado pela mesma instância no contexto de sua classe base, o membro da classe base é usado. Isso é diferente da abstração / substituição do membro, na qual o membro da classe base espera que a classe derivada defina uma substituição e dos modificadores de escopo / visibilidade que "ocultam" um membro dos consumidores fora do escopo desejado.
A resposta curta para por que é permitido é que isso não forçaria os desenvolvedores a violar vários princípios fundamentais do design orientado a objetos.
Aqui está a resposta mais longa; primeiro, considere a seguinte estrutura de classes em um universo alternativo em que o C # não permite ocultar membros:
Queremos descomentar o membro em Bar e, ao fazê-lo, permitir que Bar forneça um MyFooString diferente. No entanto, não podemos fazê-lo porque isso violaria a proibição de realidade alternativa ao esconderijo de membros. Este exemplo em particular estaria repleto de bugs e é um excelente exemplo de por que você pode querer bani-lo; por exemplo, qual saída do console você obteria se fizesse o seguinte?
No topo da minha cabeça, na verdade não tenho certeza se você conseguiria "Foo" ou "Bar" nessa última linha. Você definitivamente obteria "Foo" para a primeira linha e "Bar" para a segunda, embora todas as três variáveis façam referência exatamente à mesma instância com exatamente o mesmo estado.
Assim, os designers da linguagem, em nosso universo alternativo, desencorajam esse código obviamente ruim, impedindo a ocultação de propriedades. Agora, você como codificador tem uma necessidade genuína de fazer exatamente isso. Como você contorna a limitação? Bem, uma maneira é nomear a propriedade de Bar de maneira diferente:
Perfeitamente legal, mas não é o comportamento que queremos. Uma instância de Bar sempre produzirá "Foo" para a propriedade MyFooString, quando desejamos que ela produza "Bar". Não apenas precisamos saber que nosso IFoo é especificamente uma barra, também precisamos saber para usar o acessador diferente.
Também podemos, de maneira bastante plausível, esquecer o relacionamento pai-filho e implementar a interface diretamente:
Para este exemplo simples, é uma resposta perfeita, desde que você se preocupe apenas com Foo e Bar serem ambos IFoos. O código de uso de alguns exemplos falharia em compilar porque uma barra não é um Foo e não pode ser atribuída como tal. No entanto, se o Foo tivesse algum método útil "FooMethod" que o Bar precisava, agora você não poderá herdar esse método; você precisaria clonar seu código em Bar ou ser criativo:
Este é um truque óbvio e, embora algumas implementações das especificações da linguagem OO cheguem a pouco mais do que isso, conceitualmente está errado; se os consumidores de Bar necessidade de expor a funcionalidade do Foo, Bar deve ser um Foo, não têm uma Foo.
Obviamente, se controlamos o Foo, podemos torná-lo virtual e substituí-lo. Esta é a melhor prática conceitual em nosso universo atual, quando se espera que um membro seja substituído, e se manteria em qualquer universo alternativo que não permitisse ocultar:
O problema disso é que o acesso de membro virtual é, relativamente, mais caro de executar e, portanto, você normalmente deseja fazê-lo apenas quando necessário. A falta de ocultação, no entanto, força você a ser pessimista em relação aos membros que outro codificador que não controla seu código-fonte pode querer reimplementar; a "melhor prática" para qualquer classe não selada seria tornar tudo virtual, a menos que você não desejasse especificamente. Também ainda não fornece o comportamento exato de se esconder; a cadeia sempre será "Bar" se a instância for uma barra. Às vezes, é realmente útil aproveitar as camadas de dados de estado oculto, com base no nível de herança em que você está trabalhando.
Em resumo, permitir a ocultação de membros é o menor desses males. Não tê-lo geralmente levaria a piores atrocidades cometidas contra princípios orientados a objetos do que permitir.
fonte
IEnumerable
eIEnumerable<T>
, descrita na postagem de Eric Libbert no blog sobre o tópico.Honestamente, Eric Lippert, o principal desenvolvedor da equipe de compiladores C #, explica muito bem (obrigado Lescai Ionel pelo link). .NET
IEnumerable
eIEnumerable<T>
interfaces são bons exemplos de quando a ocultação de membros é útil.Nos primeiros dias do .NET, não tínhamos genéricos. Portanto, a
IEnumerable
interface ficou assim:Essa interface é o que nos permitiu
foreach
sobre uma coleção de objetos, no entanto, tivemos que converter todos esses objetos para usá-los adequadamente.Então vieram os genéricos. Quando adquirimos genéricos, também obtivemos uma nova interface:
Agora não precisamos lançar objetos enquanto estamos iterando através deles! Woot! Agora, se a ocultação de membros não fosse permitida, a interface teria que se parecer com algo assim:
Isso seria meio bobo, porque
GetEnumerator()
eGetEnumeratorGeneric()
em ambos os casos fazem exatamente a mesma coisa , mas eles têm valores de retorno ligeiramente diferentes. Eles são tão parecidos, de fato, que você quase sempre deseja usar como padrão a forma genéricaGetEnumerator
, a menos que esteja trabalhando com código legado que foi escrito antes da introdução de genéricos no .NET.Às vezes membro do esconderijo não permitir mais espaço para o código desagradável e erros difíceis de encontrar. No entanto, às vezes é útil, como quando você deseja alterar um tipo de retorno sem quebrar o código herdado. Essa é apenas uma dessas decisões que os designers de linguagem precisam tomar: Incomodamos os desenvolvedores que legitimamente precisam desse recurso e o deixam de fora ou incluímos esse recurso no idioma e capturamos críticas daqueles que são vítimas de uso indevido?
fonte
IEnumerable<T>.GetEnumerator()
oculteIEnumerable.GetEnumerator()
, isso ocorre apenas porque o C # não possui tipos de retorno covariantes ao substituir. Logicamente, é uma substituição, totalmente alinhada com o LSP. Ocultar é quando você tem uma variável localmap
em função no arquivo que fazusing namespace std
(em C ++).Sua pergunta pode ser lida de duas maneiras: você está perguntando sobre o escopo de variável / função em geral ou está fazendo uma pergunta mais específica sobre o escopo em uma hierarquia de herança. Você não mencionou especificamente a herança, mas mencionou erros difíceis de encontrar, o que parece mais o escopo no contexto da herança do que o escopo simples; portanto, responderei às duas perguntas.
O escopo, em geral, é uma boa idéia, porque nos permite focar nossa atenção em uma parte específica (que se espera que seja pequena) do programa. Como ele permite que os nomes locais sempre ganhem, se você ler apenas a parte do programa que está em um determinado escopo, saberá exatamente quais partes foram definidas localmente e o que foi definido em outro lugar. O nome se refere a algo local; nesse caso, o código que o define está bem à sua frente ou é uma referência a algo fora do escopo local. Se não houver referências não locais que possam mudar sob nós (especialmente variáveis globais, que podem ser alteradas de qualquer lugar), podemos avaliar se a parte do programa no escopo local está correta ou não sem consultar a qualquer parte do restante do programa .
Ocasionalmente, pode levar a alguns erros, mas é mais do que compensado, impedindo uma quantidade enorme de erros possíveis. Além de fazer uma definição local com o mesmo nome de uma função de biblioteca (não faça isso), não vejo uma maneira fácil de introduzir bugs com escopo local, mas escopo local é o que permite que muitas partes do mesmo programa usem i como o contador de índice de um loop sem bater um no outro, e permite que Fred no corredor escreva uma função que usa uma string chamada str que não irá bater na sua string com o mesmo nome.
eu encontrei um artigo interessante de Bertrand Meyer que discute a sobrecarga no contexto da herança. Ele traz uma distinção interessante entre o que ele chama de sobrecarga sintática (significando que existem duas coisas diferentes com o mesmo nome) e sobrecarga semântica (significando que existem duas implementações diferentes da mesma idéia abstrata). Sobrecarga semântica seria boa, pois você pretendia implementá-la de maneira diferente na subclasse; sobrecarga sintática seria a colisão acidental de nomes que causou um bug.
A diferença entre sobrecarga em uma situação de herança que se destina e que é um bug é semântica (o significado); portanto, o compilador não tem como saber se o que você fez é certo ou errado. Em uma situação de escopo simples, a resposta certa é sempre a coisa local, para que o compilador possa descobrir qual é a coisa certa.
A sugestão de Bertrand Meyer seria usar uma linguagem como Eiffel, que não permite conflitos de nomes como esse e força o programador a renomear um ou ambos, evitando assim o problema completamente. Minha sugestão seria evitar o uso completo da herança, evitando também o problema por completo. Se você não pode ou não quer fazer uma dessas coisas, ainda há coisas a serem reduzidas para reduzir a probabilidade de ter um problema de herança: siga o LSP (Princípio de Substituição de Liskov), prefira composição ao invés de herança, mantenha suas hierarquias de herança são rasas e mantenha as classes em uma hierarquia de herança pequena. Além disso, alguns idiomas podem emitir um aviso, mesmo que não apresentem erro, como um idioma como o Eiffel faria.
fonte
Aqui estão meus dois centavos.
Os programas podem ser estruturados em blocos (funções, procedimentos) que são unidades independentes da lógica do programa. Cada bloco pode se referir a "coisas" (variáveis, funções, procedimentos) usando nomes / identificadores. Esse mapeamento de nomes para coisas é chamado de ligação .
Os nomes usados por um bloco se enquadram em três categorias:
Considere, por exemplo, o seguinte programa C
A função
print_double_int
tem um nome local (variável local)d
e argumenton
, e usa o nome global externoprintf
, que está no escopo, mas não está definido localmente.Observe que
printf
também pode ser passado como argumento:Normalmente, um argumento é usado para especificar parâmetros de entrada / saída de uma função (procedimento, bloco), enquanto nomes globais são usados para se referir a coisas como funções de biblioteca que "existem no ambiente" e, portanto, é mais conveniente mencioná-las somente quando eles são necessários. Usar argumentos em vez de nomes globais é a ideia principal da injeção de dependência , usada quando as dependências devem ser explicitadas em vez de serem resolvidas observando o contexto.
Outro uso semelhante de nomes definidos externamente pode ser encontrado nos fechamentos. Nesse caso, um nome definido no contexto lexical de um bloco pode ser usado dentro do bloco, e o valor associado a esse nome (normalmente) continuará a existir enquanto o bloco se referir a ele.
Tomemos, por exemplo, este código Scala:
O valor de retorno da função
createMultiplier
é o fechamento(m: Int) => m * n
, que contém o argumentom
e o nome externon
. O nomen
é resolvido observando o contexto em que o fechamento é definido: o nome é vinculado ao argumenton
da funçãocreateMultiplier
. Observe que essa ligação é criada quando o fechamento é criado, ou seja, quandocreateMultiplier
é chamado. Portanto, o nomen
está vinculado ao valor real de um argumento para uma chamada específica da função. Compare isso com o caso de uma função de biblioteca comoprintf
, que é resolvida pelo vinculador quando o executável do programa é criado.Resumindo, pode ser útil consultar nomes externos dentro de um bloco de código local para que você
O sombreamento aparece quando você considera que, em um bloco, você está interessado apenas em nomes relevantes definidos no ambiente, por exemplo, na
printf
função que deseja usar. Se por acaso você quiser usar um nome local (getc
,putc
,scanf
, ...) que já foi usado no ambiente, você simples deseja ignorar (sombra) o nome global. Portanto, ao pensar localmente, você não deseja considerar todo o contexto (possivelmente muito grande).Na outra direção, ao pensar globalmente, você deseja ignorar os detalhes internos dos contextos locais (encapsulamento). Portanto, você precisa sombrear, caso contrário, a adição de um nome global poderia interromper todos os blocos locais que já estavam usando esse nome.
Bottom line, se você deseja que um bloco de código se refira a ligações definidas externamente, é necessário sombreamento para proteger nomes locais de nomes globais.
fonte