Por que a palavra-chave 'out' é usada em dois contextos aparentemente díspares?


11

Em C #, a outpalavra - chave pode ser usada de duas maneiras diferentes.

  1. Como um modificador de parâmetro no qual um argumento é passado por referência

    class OutExample
    {
        static void Method(out int i)
        {
            i = 44;
        }
        static void Main()
        {
            int value;
            Method(out value);
            // value is now 44
        }
    }
    
  2. Como um modificador de parâmetro de tipo para especificar covariância .

    // Covariant interface. 
    interface ICovariant<out R> { }
    
    // Extending covariant interface. 
    interface IExtCovariant<out R> : ICovariant<R> { }
    
    // Implementing covariant interface. 
    class Sample<R> : ICovariant<R> { }
    
    class Program
    {
        static void Test()
        {
            ICovariant<Object> iobj = new Sample<Object>();
            ICovariant<String> istr = new Sample<String>();
    
            // You can assign istr to iobj because 
            // the ICovariant interface is covariant.
            iobj = istr;
        }
    }
    

Minha pergunta é: por que?

Para um iniciante, a conexão entre os dois não parece intuitiva . O uso com genéricos não parece ter nada a ver com a passagem por referência.

Aprendi pela primeira vez o que outhavia em relação à passagem de argumentos por referência, e isso dificultou minha compreensão do uso da definição de covariância com genéricos.

Existe uma conexão entre esses usos que estou perdendo?


5
A conexão é um pouco mais compreensível se você observar o uso de covariância e contravariância no System.Func<in T, out TResult>delegado .
rwong 10/02

4
Além disso, a maioria dos designers de linguagem tentar minimizar o número de palavras-chave, e adicionar uma nova palavra-chave em alguma linguagem existente com uma grande base de código é doloroso (possível conflito com algum código existente usando essa palavra como um nome)
Basile Starynkevitch

Respostas:


20

Existe uma conexão, no entanto, é um pouco frouxa. Em C #, as palavras-chave 'in' e 'out', como o nome sugere, representam entrada e saída. Isso é muito claro no caso dos parâmetros de saída, mas menos limpo o que isso tem a ver com os parâmetros do modelo.

Vamos dar uma olhada no princípio de substituição de Liskov :

...

O princípio de Liskov impõe alguns requisitos padrão às assinaturas adotadas em novas linguagens de programação orientada a objetos (geralmente no nível de classes e não de tipos; veja subtipo nominal versus estrutural para a distinção):

  • Contravariância dos argumentos do método no subtipo.
  • Covariância de tipos de retorno no subtipo.

...

Veja como a contravariância está associada à entrada e a covariância está associada à saída? Em C #, se você sinalizar uma variável de modelo outpara torná-la covariável, mas observe que você só poderá fazer isso se o parâmetro de tipo mencionado aparecer apenas como saída (tipo de retorno da função). Portanto, o seguinte é inválido:

interface I<out T>
{
  void func(T t); //Invalid variance: The type parameter 'T' must be
                  //contravariantly valid on 'I<T>.func(T)'.
                  //'T' is covariant.

}

Semelhante, se você sinalizar um parâmetro de tipo in, significa que você pode usá-lo apenas como entrada (parâmetro de função). Portanto, o seguinte é inválido:

interface I<in T>
{
  T func(); //Invalid variance: The type parameter 'T' must
            //be covariantly valid on 'I<T>.func()'. 
            //'T' is contravariant.

}

Então, para resumir, a conexão com a outpalavra-chave é que, com parâmetros de função, significa que é um parâmetro de saída e, para parâmetros de tipo, significa que o tipo é usado apenas no contexto de saída .

System.Functambém é um bom exemplo do que rwong mencionou em seu comentário. Em System.Functodos os parâmetros de entrada, são alterados com ine o parâmetro de saída é alterado com out. A razão é exatamente o que eu descrevi.


2
Boa resposta! Me salvou um pouco ... espere ... digitando! A propósito: a parte do LSP que você citou era realmente conhecida muito antes de Liskov. São apenas as regras de subtipagem padrão para os tipos de função. (Os tipos de parâmetro são contravariantes, os tipos de retorno são covariantes). A novidade da abordagem de Liskov foi: a) redigir as regras não em termos de covariância / contravariância, mas em termos de substituibilidade comportamental (conforme definido por pré / pós-condições) eb) a regra da história , que permite aplicar todo esse raciocínio para tipos de dados mutáveis, o que anteriormente não era possível.
Jörg W Mittag

10

@ Gábor já explicou a conexão (contravariância para tudo que entra ", covariância para tudo que sai"), mas por que reutilizar palavras-chave?

Bem, as palavras-chave são muito caras. Você não pode usá-los como identificadores em seus programas. Mas há apenas tantas palavras no idioma inglês. Portanto, às vezes você se depara com conflitos e precisa renomear suas variáveis, métodos, campos, propriedades, classes, interfaces ou estruturas de maneira desajeitada para evitar conflitos com uma palavra-chave. Por exemplo, se você está modelando uma escola, como chama uma turma? Você não pode chamar de classe, porque classé uma palavra-chave!

Adicionar uma palavra-chave a um idioma é ainda mais caro. Basicamente, torna ilegal todo o código que usa essa palavra-chave como identificador, quebrando a compatibilidade com versões anteriores em todo o lugar.

As palavras in- outchave e já existiam, para que pudessem ser reutilizadas.

Eles poderiam ter adicionado palavras-chave contextuais, que são apenas palavras-chave no contexto de uma lista de parâmetros de tipo, mas que palavras-chave eles escolheriam? covariante contravariant? +e -(como Scala fez, por exemplo)? supere extendscomo Java fez? Você consegue se lembrar de que parâmetros são covariantes e contravariantes?

Com a solução atual, há um bom mnemônico: parâmetros do tipo de saída obtêm a outpalavra - chave, parâmetros do tipo de entrada obtêm a inpalavra - chave. Observe a agradável simetria com os parâmetros do método: os parâmetros de saída obtêm a outpalavra - chave, os parâmetros de entrada obtêm a inpalavra - chave (bem, na verdade, nenhuma palavra-chave, pois a entrada é o padrão, mas você entendeu).

[Nota: se você olhar o histórico de edições, verá que, na verdade, eu mudei os dois na minha frase introdutória. E até recebi uma votação durante esse tempo! Isso apenas mostra como é realmente importante esse mnemônico.]


A maneira de lembrar a co-versus-variância é considerar o que acontece se uma função em uma interface usa um parâmetro de um tipo de interface genérico. Se alguém tiver um interface Accepter<in T> { void Accept(T it);};, um Accepter<Foo<T>>aceitará Tcomo parâmetro de entrada se o Foo<T>aceitar como parâmetro de saída e vice-versa. Assim, contra- variância. Por outro lado, interface ISupplier<out T> { T get();};a Supplier<Foo<T>>terá qualquer tipo de variação Foo- portanto, co- variação.
Supercat
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.