Este é Esparta, ou é?

121

O seguinte é uma pergunta da entrevista. Eu vim com uma solução, mas não sei por que isso funciona.


Questão:

Sem modificar a Spartaclasse, escreva algum código que MakeItReturnFalseretorne false.

public class Sparta : Place
{
    public bool MakeItReturnFalse()
    {
        return this is Sparta;
    }
}

Minha solução: (SPOILER)

public class Place
{
public interface Sparta { }
}

Mas por que Spartaem MakeItReturnFalse()referem-se {namespace}.Place.Sparta, em vez de {namespace}.Sparta?

budi
fonte
5
Você poderia adicionar o Alerta de Spoiler ou algo à Solution, por favor? Estou tão decepcionado que não tenho a chance de resolvê-lo sozinho. A pergunta é realmente surpreendente.
Karolis Kajenas
1
Inicialmente, incluí tags de spoiler, mas ele foi editado pela comunidade várias vezes ao longo da vida deste post. Me desculpe por isso.
budi 27/05
1
Não gosto do título deste post; é mais adequado para codegolf.SE. Podemos mudar para algo que realmente descreve a pergunta?
Supuhstar
3
Quebra-cabeça bonito. Pergunta de entrevista terrível, mas quebra-cabeça fofa. Agora que você sabe como e por que isso funciona, você deve tomar um tiro a esta versão mais difícil: stackoverflow.com/q/41319962/88656
Eric Lippert

Respostas:

117

Mas por que Spartaem MakeItReturnFalse()referem-se {namespace}.Place.Sparta, em vez de {namespace}.Sparta?

Basicamente, porque é o que dizem as regras de pesquisa de nome. Na especificação C # 5, as regras de nomenclatura relevantes estão na seção 3.8 ("Namespace e nomes de tipo").

O primeiro par de marcadores - truncados e anotados - dizia:

  • Se o namespace-ou-type-name for da forma Iou da forma I<A1, ..., AK> [então K = 0 no nosso caso] :
    • Se K for zero e o namespace-ou-type-name aparecer em uma declaração genérica de método [não, nenhum método genérico]
    • Caso contrário, se o namespace ou nome do tipo aparecer em uma declaração de tipo, para cada instância digite T (§10.3.1), começando com o tipo de instância dessa declaração de tipo e continuando com o tipo de instância de cada classe envolvente ou declaração struct (se houver):
      • Se Kfor zero e a declaração de Tincluir um parâmetro de tipo com nome I, o namespace-ou-type-name se refere a esse parâmetro de tipo. [Não]
      • Caso contrário, se o namespace-ou-type-name aparecer no corpo da declaração de tipo, T ou se algum de seus tipos de base contiver um tipo acessível aninhado com parâmetros de nome Ie Ktipo, o namespace-ou-type-name se refere a esse tipo construído com os argumentos de tipo fornecidos. [Bingo!]
  • Se as etapas anteriores não tiveram êxito, para cada espaço de nome N , começando com o espaço para nome no qual o espaço para nome ou nome do tipo ocorre, continuando com cada espaço para nome (se houver) e terminando com o espaço para nome global, as etapas a seguir serão avaliadas até que uma entidade esteja localizada:
    • Se Kfor zero e Ifor o nome de um espaço para nome N, então ... [Sim, isso seria bem-sucedido]

Portanto, esse ponto final é o que seleciona a Sparta classe se o primeiro marcador não encontrar nada ... mas quando a classe base Placedefine uma interface Sparta, ela é encontrada antes de considerarmos a Spartaclasse.

Observe que, se você tornar o tipo aninhado Place.Spartauma classe em vez de uma interface, ele ainda compila e retorna false- mas o compilador emite um aviso porque sabe que uma instância de Spartanunca será uma instância da classe Place.Sparta. Da mesma forma, se você mantiver Place.Spartauma interface, mas fizer a Spartaclasse sealed, receberá um aviso porque nenhuma Spartainstância jamais poderia implementar a interface.

Jon Skeet
fonte
2
Outra observação aleatória: Usando a Spartaclasse original , this is Placeretorna true. No entanto, adicionar public interface Place { }à Spartaclasse faz this is Placecom que retorne false. Faz minha cabeça girar.
budi
@ budi: Certo, porque, novamente, o marcador anterior encontra Placecomo interface.
Jon Skeet
22

Ao resolver um nome com seu valor, a "proximidade" da definição é usada para resolver ambiguidades. Qualquer definição que seja "mais próxima" é a que é escolhida.

A interface Spartaé definida dentro de uma classe base. A classe Spartaé definida no espaço para nome que contém. As coisas definidas em uma classe base são "mais próximas" do que as definidas no mesmo espaço para nome.

Servy
fonte
1
E imagine se a pesquisa de nome não funcionasse dessa maneira. Em seguida, o código de trabalho que contém uma classe interna seria quebrado se alguém adicionasse uma classe de nível superior com o mesmo nome.
Dan04
1
@ dan04: Mas o código de trabalho que não contém uma classe aninhada é quebrado se alguém adicionar uma classe aninhada com o mesmo nome que uma classe de nível superior. Portanto, não é exatamente um cenário de "vitória" total.
Jon Skeet
1
@JonSkeet, eu diria que adicionar uma classe aninhada é uma alteração em uma área na qual o código de trabalho tem motivos razoáveis ​​para ser afetado e observe as alterações. A adição de uma classe de nível superior totalmente não relacionada é muito mais distante.
Angew não está mais orgulhoso de SO
2
@ JonSkeet Mas isso não é apenas o problema da classe base quebradiça, mas com uma aparência um pouco diferente?
João Mendes
1
@ JoãoMendes: Sim, praticamente.
Jon Skeet
1

Bela pergunta! Gostaria de adicionar uma explicação um pouco mais longa para quem não faz C # diariamente ... porque a pergunta é um bom lembrete dos problemas de resolução de nomes em geral.

Pegue o código original, ligeiramente modificado das seguintes maneiras:

  • Vamos imprimir os nomes dos tipos em vez de compará-los como na expressão original (ie return this is Sparta).
  • Vamos definir a interface Athenana Placesuperclasse para ilustrar a resolução do nome da interface.
  • Vamos também imprimir o nome do tipo thiscomo está vinculado na Spartaclasse, apenas para deixar tudo muito claro.

O código fica assim:

public class Place {
    public interface Athena { }
}

public class Sparta : Place
{
    public void printTypeOfThis()
    {
        Console.WriteLine (this.GetType().Name);
    }

    public void printTypeOfSparta()
    {
        Console.WriteLine (typeof(Sparta));
    }

    public void printTypeOfAthena()
    {
        Console.WriteLine (typeof(Athena));
    }
}

Agora criamos um Spartaobjeto e chamamos os três métodos.

public static void Main(string[] args)
    {
        Sparta s = new Sparta();
        s.printTypeOfThis();
        s.printTypeOfSparta();
        s.printTypeOfAthena();
    }
}

A saída que obtemos é:

Sparta
Athena
Place+Athena

No entanto, se modificarmos a classe Place e definirmos a interface Sparta:

   public class Place {
        public interface Athena { }
        public interface Sparta { } 
    }

então é isso Sparta- a interface - que estará disponível primeiro para o mecanismo de pesquisa de nome e a saída do nosso código será alterada para:

Sparta
Place+Sparta
Place+Athena

Então, nós estragamos efetivamente a comparação de tipos no MakeItReturnFalse definição de função, apenas definindo a interface Sparta na superclasse, que é encontrada primeiro pela resolução de nomes.

Mas por que o C # optou por priorizar as interfaces definidas na superclasse na resolução de nomes? @JonSkeet sabe! E se você ler a resposta dele, obterá os detalhes do protocolo de resolução de nomes em C #.

mircealungu
fonte