(this == null) em c #!

129

Devido a um erro corrigido no C # 4, o programa a seguir é impresso true. (Experimente no LINQPad)

void Main() { new Derived(); }

class Base {
    public Base(Func<string> valueMaker) { Console.WriteLine(valueMaker()); }
}
class Derived : Base {
    string CheckNull() { return "Am I null? " + (this == null); }
    public Derived() : base(() => CheckNull()) { }
}

No VS2008 no modo Release, lança uma InvalidProgramException. (No modo de depuração, funciona bem)

No VS2010 Beta 2, ele não é compilado (não tentei o Beta 1); Eu aprendi isso da pior maneira

Existe alguma outra maneira de criar this == nullem c # puro?

SLaks
fonte
3
Provavelmente é um bug no compilador C # 3.0. Funciona da maneira que deveria no C # 4.0.
Mehrdad Afshari
82
@ Slaks: O problema com os bugs é que você pode esperar que eles sejam corrigidos em algum momento, portanto, encontrá-los "úteis" provavelmente não é aconselhável.
AnthonyWJones
6
obrigado! não sabia sobre o LINQPad. é legal!
Thorn #
8
De que maneira exatamente isso é útil?
Allen Rice
6
como esse bug foi útil?
BlackTigerX

Respostas:

73

Esta observação foi publicada no StackOverflow em outra pergunta hoje cedo.

A grande resposta de Marc para essa pergunta indica que, de acordo com a especificação (seção 7.5.7), você não deve poder acessar thisnesse contexto e a capacidade de fazê-lo no compilador C # 3.0 é um bug. O compilador C # 4.0 está se comportando corretamente de acordo com as especificações (mesmo na Beta 1, esse é um erro de tempo de compilação):

§ 7.5.7 Este acesso

Um acesso a este consiste na palavra reservada this.

este acesso:

this

Um acesso a este é permitido apenas no bloco de um construtor de instância, um método de instância ou um acessador de instância.

Mehrdad Afshari
fonte
2
Não vejo, por que, no código apresentado nesta pergunta, o uso da palavra-chave "this" é inválido. O método CheckNull é um método de instância normal, não estático . O uso de "this" é 100% válido nesse método e até a comparação com nulo é válida. O erro está na linha de inicialização da base: é a tentativa de passar o delegado limitado à instância como um parâmetro para o processador de base. Este é o bug (um furo nas verificações somáticas) no compilador: NÃO deve ser possível. Você não tem permissão para escrever : base(CheckNull())se o CheckNull não for estático e, da mesma forma, não poderá incorporar uma lambda vinculada à instância.
quetzalcoatl
4
@quetzalcoatl: thisno CheckNullmétodo é legal. O que não é legal é o implícita este acesso em () => CheckNull(), essencialmente () => this.CheckNull(), que está sendo executado fora do bloco de um construtor de instância. Concordo que a parte das especificações que cito se concentra principalmente na legalidade sintática da thispalavra-chave, e provavelmente outra parte aborda esse problema com mais precisão, mas também é fácil extrapolar conceitualmente essa parte das especificações.
Mehrdad Afshari
2
Desculpe, eu discordo. Enquanto eu sei disso (e escrevi isso no comentário acima) e você também sabe disso - você não mencionou a causa real do problema na sua resposta (aceita). A resposta é aceita - tão aparentemente o autor também a agarrou. Mas duvido que todos os leitores sejam tão brilhantes e fluentes nas lambdas ao reconhecerem uma lambda ligada à instância versus lambda estática à primeira vista e mapeiem isso para 'isso' e problemas com a IL emitida :) É por isso que adicionei meus três centavos. Além disso, concordo com tudo o que foi encontrado, analisado e descrito por você e os outros :)
quetzalcoatl
24

A descompilação bruta (Refletor sem otimizações) do binário do modo Debug é:

private class Derived : Program.Base
{
    // Methods
    public Derived()
    {
        base..ctor(new Func<string>(Program.Derived.<.ctor>b__0));
        return;
    }

    [CompilerGenerated]
    private static string <.ctor>b__0()
    {
        string CS$1$0000;
        CS$1$0000 = CS$1$0000.CheckNull();
    Label_0009:
        return CS$1$0000;
    }

    private string CheckNull()
    {
        string CS$1$0000;
        CS$1$0000 = "Am I null? " + ((bool) (this == null));
    Label_0017:
        return CS$1$0000;
    }
}

O método CompilerGenerated não faz sentido; se você olhar para o IL (abaixo), ele está chamando o método em uma sequência nula (!).

   .locals init (
        [0] string CS$1$0000)
    L_0000: ldloc.0 
    L_0001: call instance string CompilerBug.Program/Derived::CheckNull()
    L_0006: stloc.0 
    L_0007: br.s L_0009
    L_0009: ldloc.0 
    L_000a: ret 

No modo Release, a variável local é otimizada, portanto, tenta empurrar uma variável inexistente para a pilha.

    L_0000: ldloc.0 
    L_0001: call instance string CompilerBug.Program/Derived::CheckNull()
    L_0006: ret 

(O refletor trava ao transformá-lo em c #)


Edição : Alguém (Eric Lippert?) Sabe por que o compilador emite o ldloc?

SLaks
fonte
11

Eu tive isso! (e tenho provas também)

texto alternativo

leppie
fonte
2
Estava atrasado, era um sinal de que eu deveria parar de codificar :) Estava hackeando nosso com coisas DLR IIRC.
21310 leppie
faça um visualizador de depurador (DebuggerDisplay) para o que quer que seja 'isso' e faça de você esse tolo que é nulo? : D apenas dizendo ''
10

Este não é um "bug". É você que está abusando do sistema de tipos. Você nunca deve passar uma referência à instância atual ( this) para alguém dentro de um construtor.

Eu poderia criar um "bug" semelhante chamando um método virtual também no construtor da classe base.

Só porque você pode fazer algo ruim não significa que é um bug quando você fica com ele.


fonte
14
É um bug do compilador. Gera IL inválido. (Leia minha resposta)
SLaks
O contexto é estático, portanto, você não deve receber uma referência de método de instância nesse estágio.
21310 Leppie
10
@ Will: É um bug do compilador. O compilador deve gerar código válido e verificável para esse trecho de código ou emitir uma mensagem de erro. Quando um compilador não se comporta de acordo com as especificações, é incorreto .
Mehrdad Afshari
2
@ Will # 4: Quando escrevi o código, não havia pensado nas implicações. Eu só percebi que não fazia sentido quando parou de compilar no VS2010. -
SLaks
3
A propósito, a chamada de método virtual no construtor é uma operação completamente válida. Apenas não é recomendado. Isso pode resultar em desastres lógicos, mas nunca um InvalidProgramException.
Mehrdad Afshari
3

Eu posso estar errado, mas tenho certeza de que, se seu objeto for null, nunca haverá um cenário onde thisse aplique.

Por exemplo, como você ligaria CheckNull?

Derived derived = null;
Console.WriteLine(derived.CheckNull()); // this should throw a NullReferenceException
Dan Tao
fonte
3
Em um lambda no argumento do construtor. Leia o trecho de código inteiro. (E tentar se você não acredita em mim)
SLaks
Eu concordo, embora me lembre um pouco sobre como, em C ++, um objeto não tinha referência em seu construtor e estou me perguntando se o cenário (this == null) é usado nesses casos para verificar se uma chamada para um método foi feita a partir do construtor do objeto antes de expor um ponteiro para "this". Embora, até onde eu saiba em C #, não deve haver casos em que "isto" seja nulo, nem mesmo nos métodos Dispose ou finalização.
jpierson
Acho que meu argumento é que a própria idéia de thisé mutuamente exclusiva da possibilidade de ser nula - uma espécie de "cogito, ergo sum" de programação de computadores. Portanto, seu desejo de usar a expressão this == nulle de sempre tê-la retornado verdadeiro me parece equivocado.
21119 Dan Tao
Em outras palavras: eu li o seu código; o que estou dizendo é que questiono o que você estava tentando realizar em primeiro lugar.
21119 Dan Tao
Este código simplesmente demonstra o bug e, como você apontou, é totalmente inútil. Para ver um código realmente útil, leia minha segunda resposta.
21310 Slaks
-1

Não tenho certeza se é isso que você está procurando

    public static T CheckForNull<T>(object primary, T Default)
    {
        try
        {
            if (primary != null && !(primary is DBNull))
                return (T)Convert.ChangeType(primary, typeof(T));
            else if (Default.GetType() == typeof(T))
                return Default;
        }
        catch (Exception e)
        {
            throw new Exception("C:CFN.1 - " + e.Message + "Unexpected object type of " + primary.GetType().ToString() + " instead of " + typeof(T).ToString());
        }
        return default(T);
    }

exemplo: UserID = CheckForNull (Request.QueryString ["UserID"], 147);

Scott e a equipe de desenvolvimento
fonte
13
Você entendeu completamente a pergunta.
SLaks
1
Eu imaginei isso. Pensei em tentar de qualquer maneira.
Scott e a equipe de desenvolvimento