Por que 'ref' e 'out' não suportam polimorfismo?

124

Faça o seguinte:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= compile-time error: 
                     // "The 'ref' argument doesn't match the parameter type"
    }

    void Foo(A a) {}

    void Foo2(ref A a) {}  
}

Por que ocorre o erro em tempo de compilação acima? Isso acontece com ambos refe outargumentos.

Andreas Grech
fonte

Respostas:

169

=============

ATUALIZAÇÃO: usei esta resposta como base para esta entrada do blog:

Por que os parâmetros ref e out não permitem variação de tipo?

Veja a página do blog para mais comentários sobre esse assunto. Obrigado pela ótima pergunta.

=============

Vamos supor que você tem aulas Animal, Mammal, Reptile, Giraffe, TurtleeTiger , com os relacionamentos subclassificação óbvias.

Agora, suponha que você tenha um método void M(ref Mammal m). Mpode ler e escrever m.


Você pode passar uma variável do tipo Animalpara M?

Não. Essa variável pode conter a Turtle, mas Massumirá que ela contém apenas mamíferos. A Turtlenão é a Mammal.

Conclusão 1 : refparâmetros não podem ser "maiores". (Existem mais animais que mamíferos, a variável está ficando "maior" porque pode conter mais coisas.)


Você pode passar uma variável do tipo Giraffepara M?

Não Mpode escrever para m, e Mpode querer escrever uma Tigerem m. Agora você colocou um Tigerem uma variável que é realmente do tipo Giraffe.

Conclusão 2 : os refparâmetros não podem ser "menores".


Agora considere N(out Mammal n).

Você pode passar uma variável do tipo Giraffepara N?

Não. NPode escrever para ne Npode querer escrever a Tiger.

Conclusão 3 : os outparâmetros não podem ser "menores".


Você pode passar uma variável do tipo Animalpara N?

Hmm.

Bem, porque não? Nnão pode ler n, só pode escrever, certo? Você escreve a Tigerpara uma variável do tipo Animale está tudo pronto, certo?

Errado. A regra não é " Nsó pode escrever para n".

As regras são, brevemente:

1) Ntem que escrever nantes de Nretornar normalmente. (Se Njogar, todas as apostas estão desativadas.)

2) Ntem que escrever algo nantes de ler algo n.

Isso permite esta sequência de eventos:

  • Declare um campo xdo tipo Animal.
  • Passe xcomo um outparâmetro para N.
  • Nescreve a Tigerinto n, que é um alias para x.
  • Em outro segmento, alguém escreve um Turtlepara x.
  • Ntenta ler o conteúdo de ne descobre um Turtleno que ele acha que é uma variável do tipo Mammal.

Claramente, queremos tornar isso ilegal.

Conclusão 4 : os outparâmetros não podem ser "maiores".


Conclusão final : Nem refnem outparâmetros podem variar seus tipos. Fazer o contrário é quebrar a segurança do tipo verificável.

Se essas questões na teoria básica de tipos lhe interessam, considere ler minha série sobre como a covariância e a contravariância funcionam no C # 4.0 .

Eric Lippert
fonte
6
+1. Ótima explicação usando exemplos de classe do mundo real que demonstram claramente os problemas (por exemplo, explicar com A, B e C torna mais difícil demonstrar por que não funciona).
Grant Wagner
4
Sinto-me humilhado ao ler esse processo de pensamento. Acho melhor voltar aos livros!
22411 Scott McKenzie
Nesse caso, não podemos realmente usar a variável de classe Abstract como argumentos e transmitir seu objeto de classe derivado !!
Prashant Cholachagudda 23/09/09
Ainda assim, por que os outparâmetros não podem ser "maiores"? A sequência que você descreveu pode ser aplicada a qualquer variável, não apenas à outvariável de parâmetro. E também as necessidades do leitor para lançar o valor do argumento para Mammalantes de ele tenta acessá-lo como Mammale, claro, ele pode falhar se ele não tem consideração
astef
29

Porque em ambos os casos, você deve poder atribuir valor ao parâmetro ref / out.

Se você tentar passar b para o método Foo2 como referência, e no Foo2 tentar atribuir a = new A (), isso seria inválido.
Mesmo motivo que você não pode escrever:

B b = new A();
maciejkow
fonte
+1 Direto ao ponto e explica perfeitamente o motivo.
Rui Craveiro
10

Você está lutando com o clássico problema de covariância (e contravariância) da OOP , consulte a Wikipedia : por mais que esse fato possa desafiar as expectativas intuitivas, é matematicamente impossível permitir a substituição de classes derivadas em vez das básicas por argumentos mutáveis ​​(atribuíveis) (e também contêineres cujos itens são atribuíveis, pelo mesmo motivo), ainda respeitando o princípio de Liskov . Por que isso acontece, é esboçado nas respostas existentes e explorado mais profundamente nesses artigos e links da wiki.

As linguagens OOP que parecem fazê-lo enquanto permanecem tradicionalmente tipicamente seguras estão "trapaceando" (inserindo verificações ocultas de tipo dinâmico ou exigindo um exame em tempo de compilação de TODAS as fontes para verificar); a escolha fundamental é: desistir dessa covariância e aceitar a perplexidade dos profissionais (como o C # faz aqui) ou passar para uma abordagem de digitação dinâmica (como a primeira linguagem OOP, Smalltalk, fez) ou passar para imutável (single- dados de atribuição), como as linguagens funcionais (sob imutabilidade, você pode oferecer suporte à covariância e também evitar outros quebra-cabeças relacionados, como o fato de que você não pode ter a subclasse Square Rectangle em um mundo de dados mutáveis).

Alex Martelli
fonte
4

Considerar:

class C : A {}
class B : A {}

void Foo2(ref A a) { a = new C(); } 

B b = null;
Foo2(ref b);

Isso violaria a segurança de tipo

Henk Holterman
fonte
É mais o tipo de "b" inferido pouco claro devido à var que é o problema lá.
Eu acho que na linha 6 você quis dizer => B b = null;
Alejandro Miralles
@amiralles - sim, isso varfoi totalmente errado. Fixo.
Henk Holterman 24/09
4

Enquanto as outras respostas explicaram sucintamente o raciocínio por trás desse comportamento, acho que vale a pena mencionar que, se você realmente precisar fazer algo dessa natureza, poderá obter funcionalidade semelhante transformando o Foo2 em um método genérico, como tal:

class A {}

class B : A {}

class C
{
    C()
    {
        var b = new B();
        Foo(b);
        Foo2(ref b); // <= no compile error!
    }

    void Foo(A a) {}

    void Foo2<AType> (ref AType a) where AType: A {}  
}
BrendanLoBuglio
fonte
2

Porque dar Foo2um ref Bresultaria em um objeto malformado, porque Foo2só sabe preencher Aparte dele B.

CannibalSmith
fonte
0

Não é o compilador dizendo que você deseja converter o objeto explicitamente para que tenha certeza de que sabe quais são suas intenções?

Foo2(ref (A)b)
dlamblin
fonte
Não é possível fazer isso, "Um argumento ref ou out deve ser uma variável atribuível"
0

Faz sentido do ponto de vista da segurança, mas eu preferiria que o compilador desse um aviso em vez de um erro, já que existem usos legítimos de objetos polimétricos passados ​​por referência. por exemplo

class Derp : interfaceX
{
   int somevalue=0; //specified that this class contains somevalue by interfaceX
   public Derp(int val)
    {
    somevalue = val;
    }

}


void Foo(ref object obj){
    int result = (interfaceX)obj.somevalue;
    //do stuff to result variable... in my case data access
    obj = Activator.CreateInstance(obj.GetType(), result);
}

main()
{
   Derp x = new Derp();
   Foo(ref Derp);
}

Isso não será compilado, mas funcionaria?

Oofpez
fonte
0

Se você usar exemplos práticos para seus tipos, verá:

SqlConnection connection = new SqlConnection();
Foo(ref connection);

E agora você tem sua função que leva o ancestral ( ie Object ):

void Foo2(ref Object connection) { }

O que pode estar errado com isso?

void Foo2(ref Object connection)
{
   connection = new Bitmap();
}

Você acabou de atribuir um Bitmapao seu SqlConnection.

Isso não é bom.


Tente novamente com outras pessoas:

SqlConnection conn = new SqlConnection();
Foo2(ref conn);

void Foo2(ref DbConnection connection)
{
    conn = new OracleConnection();
}

Você colocou um OracleConnectionover-top no seu SqlConnection.

Ian Boyd
fonte
0

No meu caso, minha função aceitou um objeto e não pude enviar nada, então simplesmente

object bla = myVar;
Foo(ref bla);

E isso funciona

My Foo está no VB.NET e verifica o tipo interno e faz muita lógica

Peço desculpas se minha resposta é duplicada, mas outras foram muito longas

Shereef Marzouk
fonte