Pergunta

No Microsoft IL, para chamar um método em um tipo de valor, você precisa de uma referência indireta.Digamos que temos um ILGenerator chamado "il" e que atualmente temos um Nullable no topo da pilha, se quisermos verificar se ele tem um valor então poderíamos emitir o seguinte:

var local = il.DeclareLocal(typeof(Nullable<int>));
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ldloca, local);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

No entanto, seria bom pular o salvamento como uma variável local e simplesmente chamar o método no endereço da variável já na pilha, algo como:

il.Emit(/* not sure */);
var method = typeof(Nullable<int>).GetMethod("get_HasValue");
il.EmitCall(OpCodes.Call, method, null);

A família de instruções ldind parece promissora (particularmente ldind_ref), mas não consigo encontrar documentação suficiente para saber se isso causaria boxing do valor, o que suspeito que possa acontecer.

Dei uma olhada na saída do compilador C#, mas ele usa variáveis ​​locais para conseguir isso, o que me faz acreditar que a primeira maneira pode ser a única.Alguém tem alguma ideia melhor?

**** Editar:Notas Adicionais ****

Tentar chamar o método diretamente, como no programa a seguir com as linhas comentadas, não funciona (o erro será "A operação pode desestabilizar o tempo de execução").Remova o comentário das linhas e você verá que funciona conforme o esperado, retornando "True".

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
//var local = il.DeclareLocal(typeof(Nullable<int>));
//il.Emit(OpCodes.Stloc, local);
//il.Emit(OpCodes.Ldloca, local);
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

Portanto, você não pode simplesmente chamar o método com o valor na pilha porque é um tipo de valor (embora você pudesse, se fosse um tipo de referência).

O que eu gostaria de conseguir (ou saber se é possível) é substituir as três linhas que aparecem comentadas, mas manter o programa funcionando, sem usar um local temporário.

Foi útil?

Solução

Se a variável já estiver na pilha, você pode prosseguir e apenas emitir a chamada do método.

Parece que o construtor não coloca a variável na pilha de forma digitada.Depois de nos aprofundarmos um pouco no IL, parece que há duas maneiras de usar a variável após construí-la.

Você pode carregar a variável que armazenará a referência na pilha de avaliação antes de chamar o construtor e, em seguida, carregar essa variável novamente após chamar o construtor da seguinte forma:

DynamicMethod method = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
Type nullable = typeof(Nullable<int>);
ConstructorInfo ctor = nullable.GetConstructor(new Type[] { typeof(int) });
MethodInfo getValue = nullable.GetProperty("HasValue").GetGetMethod();
LocalBuilder value = il.DeclareLocal(nullable);         

// load the variable to assign the value from the ctor to
il.Emit(OpCodes.Ldloca_S, value);
// load constructor args
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Call, ctor);
il.Emit(OpCodes.Ldloca_S, value);

il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(method.Invoke(null, null));

A outra opção é fazer do jeito que você mostrou.A única razão para isso que vejo é que os métodos ctor retornam void, portanto, não colocam seu valor na pilha como outros métodos.Parece estranho que você possa chamar Setloc se o novo objeto não estiver na pilha.

Outras dicas

Depois de analisar as opções um pouco mais e mais, acho que você está certo ao presumir que isso não pode ser feito.Se você examinar o comportamento da pilha das instruções MSIL, verá que nenhuma operação deixa seu(s) operando(s) na pilha.Como isso seria um requisito para uma operação de 'obter endereço de entrada de pilha', estou bastante confiante de que não existe.

Isso deixa você com dup+box ou stloc+ldloca.Como você apontou, o último é provavelmente mais eficiente.

@greg:Muitas instruções deixam seus resultado na pilha, mas nenhuma instrução deixa nenhum de seus operandos na pilha, o que seria necessário para uma instrução 'obter endereço do elemento da pilha'.

Eu descobri!Felizmente eu estava lendo sobre o unbox opcode e notei que ele empurra o endereço do valor. unbox.any empurra o valor real.Então, para chamar um método em um tipo de valor sem precisar armazená-lo em uma variável local e depois carregar seu endereço, você pode simplesmente box seguido pela unbox.Usando seu último exemplo:

var m = new DynamicMethod("M", typeof(bool), Type.EmptyTypes);
var il = m.GetILGenerator();
var ctor = typeof(Nullable<int>).GetConstructor(new[] { typeof(int) });
il.Emit(OpCodes.Ldc_I4_6);
il.Emit(OpCodes.Newobj, ctor);
il.Emit(OpCodes.Box, typeof(Nullable<int>)); // box followed by unbox
il.Emit(OpCodes.Unbox, typeof(Nullable<int>));
var getValue = typeof(Nullable<int>).GetMethod("get_HasValue");
il.Emit(OpCodes.Call, getValue);
il.Emit(OpCodes.Ret);
Console.WriteLine(m.Invoke(null, null));

A desvantagem disso é que o boxing causa alocação de memória para o objeto in a box, portanto é um pouco mais lento do que usar variáveis ​​locais (que já estariam alocadas).Porém, evita que você tenha que determinar, declarar e referenciar todas as variáveis ​​locais necessárias.

Acabei de escrever uma classe que faz o que o OP está pedindo...aqui está o código IL que o compilador C# produz:

  IL_0008:  ldarg.0
  IL_0009:  ldarg.1
  IL_000a:  newobj     instance void valuetype [mscorlib]System.Nullable`1<int32>::.ctor(!0)
  IL_000f:  stfld      valuetype [mscorlib]System.Nullable`1<int32> ConsoleApplication3.Temptress::_X
  IL_0014:  nop
  IL_0015:  ret
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top