¿Cuál es el caso de esquina más extraño que has visto en C#o.NET?
(30)
Colecciono algunos casos de esquinas y enigmas y siempre me gustaría escuchar más. La página solo cubre los bits y bobs del lenguaje C #, pero también creo que las cosas básicas de .NET también son interesantes. Por ejemplo, aquí hay uno que no está en la página, pero que me parece increíble:
string x = new string(new char[0]);
string y = new string(new char[0]);
Console.WriteLine(object.ReferenceEquals(x, y));
Espero que se imprima Falso: después de todo, "nuevo" (con un tipo de referencia) siempre crea un nuevo objeto, ¿no es así? Las especificaciones tanto para C # como para la CLI indican que debería. Bueno, no en este caso particular. Se imprime en Verdadero, y lo ha hecho en todas las versiones del marco con el que lo he probado. (No lo he probado en Mono, es cierto ...)
Para ser claros, este es solo un ejemplo del tipo de cosas que estoy buscando, no estaba buscando particularmente una discusión / explicación de esta rareza. (No es lo mismo que el internado de cadena normal; en particular, el internado de cadena normalmente no ocurre cuando se llama a un constructor). Realmente estaba pidiendo un comportamiento extraño similar.
¿Alguna otra gema acechando por ahí?
Asigna esto!
Este es uno de los que me gusta preguntar en las fiestas (que es probablemente la razón por la que ya no me invitan):
¿Puedes hacer la siguiente pieza de código compilar?
public void Foo()
{
this = new Teaser();
}
Un truco fácil podría ser:
string cheat = @"
public void Foo()
{
this = new Teaser();
}
";
Pero la verdadera solución es esta:
public struct Teaser
{
public void Foo()
{
this = new Teaser();
}
}
Entonces, es un hecho poco conocido que los tipos de valor (estructuras) pueden reasignar su variable.
C # Accesibilidad Puzzler
La siguiente clase derivada está accediendo a un campo privado desde su clase base, y el compilador mira silenciosamente al otro lado:
public class Derived : Base
{
public int BrokenAccess()
{
return base.m_basePrivateField;
}
}
El campo es de hecho privado:
private int m_basePrivateField = 0;
¿Te importaría adivinar cómo podemos hacer compilar dicho código?
.
.
.
.
.
.
.
Responder
El truco es declarar Derived
como una clase interna de Base
:
public class Base
{
private int m_basePrivateField = 0;
public class Derived : Base
{
public int BrokenAccess()
{
return base.m_basePrivateField;
}
}
}
Las clases internas tienen acceso completo a los miembros de la clase externa. En este caso, la clase interna también deriva de la clase externa. Esto nos permite "romper" la encapsulación de miembros privados.
¿Qué hará esta función si se llama como Rec(0)
(no bajo el depurador)?
static void Rec(int i)
{
Console.WriteLine(i);
if (i < int.MaxValue)
{
Rec(i + 1);
}
}
Responder:
- En el JIT de 32 bits debería dar como resultado una excepción Exception
- En el JIT de 64 bits debe imprimir todos los números en int.MaxValue
Esto se debe a que el compilador JIT de 64 bits aplica la optimización de la llamada de cola , mientras que el JIT de 32 bits no lo hace.
Desafortunadamente, no tengo una máquina de 64 bits a mano para verificar esto, pero el método cumple todas las condiciones para la optimización de la llamada de cola. Si alguien tiene uno, me interesaría ver si es verdad.
¿Qué sucede si tiene una clase genérica que tiene métodos que podrían hacerse ambiguos dependiendo de los argumentos de tipo? Me encontré con esta situación recientemente escribiendo un diccionario de dos vías. Quería escribir métodos Get()
simétricos que devolvieran lo contrario a cualquier argumento que se haya pasado. Algo como esto:
class TwoWayRelationship<T1, T2>
{
public T2 Get(T1 key) { /* ... */ }
public T1 Get(T2 key) { /* ... */ }
}
Todo está bien si creas una instancia en la que T1
y T2
son tipos diferentes:
var r1 = new TwoWayRelationship<int, string>();
r1.Get(1);
r1.Get("a");
Pero si T1
y T2
son iguales (y probablemente si uno fuera una subclase de otro), es un error del compilador:
var r2 = new TwoWayRelationship<int, int>();
r2.Get(1); // "The call is ambiguous..."
Curiosamente, todos los otros métodos en el segundo caso todavía son utilizables; solo llama al método ahora ambiguo que causa un error de compilación. Caso interesante, aunque un poco improbable y oscuro.
Aquí hay uno que solo descubrí recientemente ...
interface IFoo
{
string Message {get;}
}
...
IFoo obj = new IFoo("abc");
Console.WriteLine(obj.Message);
Lo anterior parece una locura a primera vista, pero en realidad es legal. No, en realidad (aunque me he perdido una parte clave, pero no es nada intrépido como "agregar una clase llamada IFoo
" o "agregar un alias using
para señalar" IFoo
en una clase ").
Vea si puede averiguar por qué, entonces: ¿Quién dice que no puede crear una instancia de una interfaz?
C # admite conversiones entre matrices y listas siempre que las matrices no sean multidimensionales y haya una relación de herencia entre los tipos y los tipos sean tipos de referencia
object[] oArray = new string[] { "one", "two", "three" };
string[] sArray = (string[])oArray;
// Also works for IList (and IEnumerable, ICollection)
IList<string> sList = (IList<string>)oArray;
IList<object> oList = new string[] { "one", "two", "three" };
Tenga en cuenta que esto no funciona:
object[] oArray2 = new int[] { 1, 2, 3 }; // Error: Cannot implicitly convert type ''int[]'' to ''object[]''
int[] iArray = (int[])oArray2; // Error: Cannot convert type ''object[]'' to ''int[]''
Creo que te mostré esto antes, pero me gusta la diversión aquí, ¡esto necesitó un poco de depuración para localizarlo! (El código original era obviamente más complejo y sutil ...)
static void Foo<T>() where T : new()
{
T t = new T();
Console.WriteLine(t.ToString()); // works fine
Console.WriteLine(t.GetHashCode()); // works fine
Console.WriteLine(t.Equals(t)); // works fine
// so it looks like an object and smells like an object...
// but this throws a NullReferenceException...
Console.WriteLine(t.GetType());
}
Entonces, ¿qué era T ...
Respuesta: cualquier Nullable<T>
, como int?
. Todos los métodos están anulados, excepto GetType () que no puede ser; por lo tanto, se convierte (encuadrado) en objeto (y, por tanto, en nulo) para llamar a object.GetType () ... que llama a null ;-p
Actualización: la trama se complica ... Ayende Rahien lanzó un desafío similar en su blog , pero con un where T : class, new()
:
private static void Main() {
CanThisHappen<MyFunnyType>();
}
public static void CanThisHappen<T>() where T : class, new() {
var instance = new T(); // new() on a ref-type; should be non-null, then
Debug.Assert(instance != null, "How did we break the CLR?");
}
¡Pero puede ser derrotado! Usando el mismo direccionamiento utilizado por cosas como la comunicación remota; advertencia - lo siguiente es puro mal :
class MyFunnyProxyAttribute : ProxyAttribute {
public override MarshalByRefObject CreateInstance(Type serverType) {
return null;
}
}
[MyFunnyProxy]
class MyFunnyType : ContextBoundObject { }
Con esto en su lugar, la new()
llamada new()
se redirige al proxy ( MyFunnyProxyAttribute
), que devuelve null
. Ahora ve y lava tus ojos!
Deberían haber convertido 0 en un entero incluso cuando hay una sobrecarga de la función de enumeración.
Conocía la lógica del equipo central de C # para asignar 0 a enumeración, pero aún así, no es tan ortogonal como debería ser. Ejemplo de Npgsql .
Ejemplo de prueba:
namespace Craft
{
enum Symbol { Alpha = 1, Beta = 2, Gamma = 3, Delta = 4 };
class Mate
{
static void Main(string[] args)
{
JustTest(Symbol.Alpha); // enum
JustTest(0); // why enum
JustTest((int)0); // why still enum
int i = 0;
JustTest(Convert.ToInt32(0)); // have to use Convert.ToInt32 to convince the compiler to make the call site use the object version
JustTest(i); // it''s ok from down here and below
JustTest(1);
JustTest("string");
JustTest(Guid.NewGuid());
JustTest(new DataTable());
Console.ReadLine();
}
static void JustTest(Symbol a)
{
Console.WriteLine("Enum");
}
static void JustTest(object o)
{
Console.WriteLine("Object");
}
}
}
Encontré un segundo caso de esquina realmente extraño que supera a mi primero por un tiro largo.
String.Equals Method (String, String, StringComparison) no está realmente libre de efectos secundarios.
Estaba trabajando en un bloque de código que tenía esto solo en una línea en la parte superior de alguna función:
stringvariable1.Equals(stringvariable2, StringComparison.InvariantCultureIgnoreCase);
Eliminar esa línea lleva a un desbordamiento de pila en otro lugar del programa.
El código resultó ser instalar un controlador para lo que en esencia era un evento BeforeAssemblyLoad y tratar de hacer
if (assemblyfilename.EndsWith("someparticular.dll", StringComparison.InvariantCultureIgnoreCase))
{
assemblyfilename = "someparticular_modified.dll";
}
Por ahora no debería tener que decírtelo. El uso de una cultura que no se haya utilizado antes en una comparación de cadenas provoca una carga de ensamblaje. InvariantCulture no es una excepción a esto.
Este es el más extraño que he encontrado por accidente:
public class DummyObject
{
public override string ToString()
{
return null;
}
}
Utilizado de la siguiente manera:
DummyObject obj = new DummyObject();
Console.WriteLine("The text: " + obj.GetType() + " is " + obj);
NullReferenceException
una NullReferenceException
. Resulta que las múltiples adiciones son compiladas por el compilador de C # para una llamada a String.Concat(object[])
. Antes de .NET 4, hay un error en esa sobrecarga de Concat donde el objeto se comprueba como nulo, pero no el resultado de ToString ():
object obj2 = args[i];
string text = (obj2 != null) ? obj2.ToString() : string.Empty;
// if obj2 is non-null, but obj2.ToString() returns null, then text==null
int length = text.Length;
Este es un error por ECMA-334 §14.7.4:
El operador binario + realiza la concatenación de cadenas cuando uno o ambos operandos son de tipo
string
. Si un operando de concatenación de cadenas esnull
, se sustituye una cadena vacía. De lo contrario, cualquier operando que no sea una cadena se convierte a su representación de cadena invocando el método virtualToString
heredado delobject
tipo. SiToString
devuelve unnull
, se sustituye una cadena vacía.
Este es un ejemplo de cómo puede crear una estructura que provoca el mensaje de error "Se intentó leer o escribir en la memoria protegida. Esto suele indicar que otra memoria está dañada". La diferencia entre el éxito y el fracaso es muy sutil.
La siguiente prueba de unidad demuestra el problema.
Vea si puede resolver lo que salió mal.
[Test]
public void Test()
{
var bar = new MyClass
{
Foo = 500
};
bar.Foo += 500;
Assert.That(bar.Foo.Value.Amount, Is.EqualTo(1000));
}
private class MyClass
{
public MyStruct? Foo { get; set; }
}
private struct MyStruct
{
public decimal Amount { get; private set; }
public MyStruct(decimal amount) : this()
{
Amount = amount;
}
public static MyStruct operator +(MyStruct x, MyStruct y)
{
return new MyStruct(x.Amount + y.Amount);
}
public static MyStruct operator +(MyStruct x, decimal y)
{
return new MyStruct(x.Amount + y);
}
public static implicit operator MyStruct(int value)
{
return new MyStruct(value);
}
public static implicit operator MyStruct(decimal value)
{
return new MyStruct(value);
}
}
Este es uno de los más inusuales que he visto hasta ahora (¡aparte de los que están aquí, por supuesto!):
public class Turtle<T> where T : Turtle<T>
{
}
Te permite declararlo, pero no tiene ningún uso real, ya que siempre te pedirá que envuelvas la clase que escribas en el centro con otra Tortuga.
[broma] Supongo que son tortugas hasta abajo ... [/ broma]
Hace algunos años, al trabajar en el programa de lealtad, tuvimos un problema con la cantidad de puntos otorgados a los clientes. El problema estaba relacionado con la conversión / conversión doble a int.
En el código de abajo:
double d = 13.6;
int i1 = Convert.ToInt32(d);
int i2 = (int)d;
¿i1 == i2 ?
Resulta que i1! = I2. Debido a las diferentes políticas de redondeo en Convert y cast operator, los valores reales son:
i1 == 14
i2 == 13
Siempre es mejor llamar a Math.Ceiling () o Math.Floor () (o Math.Round con MidpointRounding que cumple nuestros requisitos)
int i1 = Convert.ToInt32( Math.Ceiling(d) );
int i2 = (int) Math.Ceiling(d);
Interesante: cuando lo vi por primera vez, asumí que era algo que el compilador de C # estaba buscando, pero incluso si se emite el IL directamente para eliminar cualquier posibilidad de interferencia, todavía ocurre, lo que significa que realmente es el código de newobj
que está haciendo. La comprobación.
var method = new DynamicMethod("Test", null, null);
var il = method.GetILGenerator();
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Newarr, typeof(char));
il.Emit(OpCodes.Newobj, typeof(string).GetConstructor(new[] { typeof(char[]) }));
il.Emit(OpCodes.Ldc_I4_0);
il.Emit(OpCodes.Newarr, typeof(char));
il.Emit(OpCodes.Newobj, typeof(string).GetConstructor(new[] { typeof(char[]) }));
il.Emit(OpCodes.Call, typeof(object).GetMethod("ReferenceEquals"));
il.Emit(OpCodes.Box, typeof(bool));
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(object) }));
il.Emit(OpCodes.Ret);
method.Invoke(null, null);
También equivale a true
si se comprueba con la string.Empty
que significa que este código de string.Empty
debe tener un comportamiento especial para internar cadenas vacías.
Llego un poco tarde a la fiesta, pero tengo tres cuatro cinco:
Si sondea InvokeRequired en un control que no se ha cargado / mostrado, dirá que es falso, y explotarás en tu cara si intentas cambiarlo desde otro hilo ( la solución es hacer referencia a esto. Manéjate en el creador del controlar).
Otro que me hizo tropezar es el hecho de tener una asamblea con:
enum MyEnum { Red, Blue, }
si calcula MyEnum.Red.ToString () en otro ensamblaje, y en algún momento alguien ha recompilado su enumeración para:
enum MyEnum { Black, Red, Blue, }
en el tiempo de ejecución, obtendrá "Negro".
Tenía un ensamblaje compartido con algunas constantes útiles. Mi predecesor había dejado un montón de propiedades de apariencia desagradable, pensé que me libraría del desorden y solo usaría const de público. Me sorprendió un poco más cuando VS los compiló con sus valores y no con referencias.
Si implementa un nuevo método de una interfaz desde otro conjunto, pero reconstruye haciendo referencia a la versión anterior de ese conjunto, obtiene una excepción TypeLoadException (sin implementación de ''NewMethod''), aunque lo haya implementado (consulte here ).
Diccionario <,>: "El orden en el que se devuelven los elementos no está definido". Esto es horrible , porque a veces te puede morder, pero funciona con otros, y si simplemente has asumido ciegamente que el Diccionario va a jugar bien ("¿por qué no debería hacerlo? Pensé, List hace"), realmente tienes que ponga su nariz en ella antes de que finalmente comience a cuestionar su suposición.
PropertyInfo.SetValue () puede asignar ints a enums, ints a nullable ints, enums a nullable enums, pero no ints a nullable enums.
enumProperty.SetValue(obj, 1, null); //works
nullableIntProperty.SetValue(obj, 1, null); //works
nullableEnumProperty.SetValue(obj, MyEnum.Foo, null); //works
nullableEnumProperty.SetValue(obj, 1, null); // throws an exception !!!
Descripción completa here
Redondeo de los banqueros.
Este no es tanto un error de compilación o un mal funcionamiento, sino ciertamente un extraño caso de esquina ...
El .Net Framework emplea un esquema o redondeo conocido como Banker Rounding.
En el redondeo de los banqueros, los números 0.5 se redondean al número par más cercano, por lo que
Math.Round(-0.5) == 0
Math.Round(0.5) == 0
Math.Round(1.5) == 2
Math.Round(2.5) == 2
etc...
Esto puede llevar a algunos errores inesperados en los cálculos financieros basados en el mejor redondeo Round-Half-Up.
Esto también es cierto de Visual Basic.
VB.NET, nullables y el operador ternario:
Dim i As Integer? = If(True, Nothing, 5)
Esto me tomó algo de tiempo para depurar, ya que esperaba que no contuviera Nothing
.
¿Qué contiene realmente? 0
.
Esto es sorprendente, pero en realidad es un comportamiento "correcto": Nothing
en VB.NET no es exactamente lo mismo que null
en CLR: Nothing
puede significar null
o default(T)
para un tipo de valor T
, según el contexto. En el caso anterior, If
infiere Integer
como el tipo común de Nothing
y 5
, entonces, en este caso, Nothing
significa 0
.
¿Cuándo un booleano no es verdadero ni falso?
Bill descubrió que puedes hackear un booleano de modo que si A es verdadero y B es verdadero, (A y B) es falso.
Acabo de encontrar una pequeña cosa agradable hoy:
public class Base
{
public virtual void Initialize(dynamic stuff) {
//...
}
}
public class Derived:Base
{
public override void Initialize(dynamic stuff) {
base.Initialize(stuff);
//...
}
}
Esto arroja un error de compilación.
La llamada al método ''Inicializar'' debe enviarse dinámicamente, pero no puede ser porque es parte de una expresión de acceso base. Considere lanzar los argumentos dinámicos o eliminar el acceso base.
Si escribo base.Initialize (cosas como objeto); Funciona perfectamente, sin embargo, esta parece ser una "palabra mágica" aquí, ya que hace exactamente lo mismo, todo se sigue recibiendo como dinámico ...
Considera este extraño caso:
public interface MyInterface {
void Method();
}
public class Base {
public void Method() { }
}
public class Derived : Base, MyInterface { }
Si Base
y Derived
son declarados en el mismo conjunto, el compilador hará Base::Method
virtual y sellada (en el CIL), a pesar de que Base
no implementa la interfaz.
Si Base
y Derived
se encuentran en diferentes montajes, al elaborar el Derived
montaje, el compilador no cambia el otro conjunto, por lo que se introduce en un miembro Derived
que será una implementación explícita de MyInterface::Method
que se acaba de delegar la llamada a Base::Method
.
El compilador tiene que hacer esto para soportar el envío polimórfico con respecto a la interfaz, es decir, tiene que hacer que el método sea virtual.
De una pregunta que hice hace poco:
¿El operador condicional no puede lanzar implícitamente?
Dado:
Bool aBoolValue;
Donde aBoolValue
se asigna Verdadero o Falso;
Lo siguiente no se compilará:
Byte aByteValue = aBoolValue ? 1 : 0;
Pero esto haría:
Int anIntValue = aBoolValue ? 1 : 0;
La respuesta proporcionada es bastante buena también.
Hay algo realmente emocionante en C #, la forma en que maneja los cierres.
En lugar de copiar los valores de la variable de la pila en la variable libre de cierre, hace que la magia del preprocesador envuelva todas las apariciones de la variable en un objeto y, por lo tanto, la saque de la pila, ¡directamente al montón! :)
Supongo que eso hace que C # sea un lenguaje aún más funcionalmente completo (o lambda-completo eh) que ML en sí (que utiliza el AFAIK para copiar el valor de la pila). F # tiene esa característica también, como lo hace C #.
Eso me alegra mucho, gracias chicos, MS!
Sin embargo, no es un caso extraño o de esquina ... pero algo realmente inesperado de un lenguaje de máquina virtual basado en la pila :)
No estoy seguro si dirías que esto es una rareza de Windows Vista / 7 o una rareza .Net, pero me hizo rascarme la cabeza por un tiempo.
string filename = @"c:/program files/my folder/test.txt";
System.IO.File.WriteAllText(filename, "Hello world.");
bool exists = System.IO.File.Exists(filename); // returns true;
string text = System.IO.File.ReadAllText(filename); // Returns "Hello world."
En Windows Vista / 7 el archivo será escrito en C:/Users/<username>/Virtual Store/Program Files/my folder/test.txt
¿Alguna vez has pensado que el compilador de C # podría generar un CIL no válido? Ejecuta esto y obtendrás un TypeLoadException
:
interface I<T> {
T M(T p);
}
abstract class A<T> : I<T> {
public abstract T M(T p);
}
abstract class B<T> : A<T>, I<int> {
public override T M(T p) { return p; }
public int M(int p) { return p * 2; }
}
class C : B<int> { }
class Program {
static void Main(string[] args) {
Console.WriteLine(new C().M(42));
}
}
Sin embargo, no sé cómo le va en el compilador C # 4.0.
EDITAR : esta es la salida de mi sistema:
C:/Temp>type Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ConsoleApplication1 {
interface I<T> {
T M(T p);
}
abstract class A<T> : I<T> {
public abstract T M(T p);
}
abstract class B<T> : A<T>, I<int> {
public override T M(T p) { return p; }
public int M(int p) { return p * 2; }
}
class C : B<int> { }
class Program {
static void Main(string[] args) {
Console.WriteLine(new C().M(11));
}
}
}
C:/Temp>csc Program.cs
Microsoft (R) Visual C# 2008 Compiler version 3.5.30729.1
for Microsoft (R) .NET Framework version 3.5
Copyright (C) Microsoft Corporation. All rights reserved.
C:/Temp>Program
Unhandled Exception: System.TypeLoadException: Could not load type ''ConsoleAppli
cation1.C'' from assembly ''Program, Version=0.0.0.0, Culture=neutral, PublicKeyTo
ken=null''.
at ConsoleApplication1.Program.Main(String[] args)
C:/Temp>peverify Program.exe
Microsoft (R) .NET Framework PE Verifier. Version 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.
[token 0x02000005] Type load failed.
[IL]: Error: [C:/Temp/Program.exe : ConsoleApplication1.Program::Main][offset 0x
00000001] Unable to resolve token.
2 Error(s) Verifying Program.exe
C:/Temp>ver
Microsoft Windows XP [Version 5.1.2600]
El alcance en c # es realmente extraño a veces. Te voy a dar un ejemplo:
if (true)
{
OleDbCommand command = SQLServer.CreateCommand();
}
OleDbCommand command = SQLServer.CreateCommand();
¿Esto no puede compilar, porque el comando es redeclarado? Hay algunas conjeturas sobre por qué funciona de esa manera en este hilo en y en mi blog .
El siguiente podría ser conocimiento general que simplemente me faltaba, pero eh. Hace algún tiempo, tuvimos un caso de error que incluía propiedades virtuales. Resumiendo el contexto un poco, considere el siguiente código y aplique el punto de interrupción al área especificada:
class Program
{
static void Main(string[] args)
{
Derived d = new Derived();
d.Property = "AWESOME";
}
}
class Base
{
string _baseProp;
public virtual string Property
{
get
{
return "BASE_" + _baseProp;
}
set
{
_baseProp = value;
//do work with the base property which might
//not be exposed to derived types
//here
Console.Out.WriteLine("_baseProp is BASE_" + value.ToString());
}
}
}
class Derived : Base
{
string _prop;
public override string Property
{
get { return _prop; }
set
{
_prop = value;
base.Property = value;
} //<- put a breakpoint here then mouse over BaseProperty,
// and then mouse over the base.Property call inside it.
}
public string BaseProperty { get { return base.Property; } private set { } }
}
Mientras se encuentra en el Derived
contexto del objeto, puede obtener el mismo comportamiento al agregar base.Property
como reloj o al escribir base.Property
en el reloj rápido.
Me tomó un tiempo darme cuenta de lo que estaba pasando. Al final me iluminé con el Quickwatch. Al entrar en Quickwatch y explorar el Derived
objeto d (o desde el contexto del objeto this
) y seleccionar el campo base
, el campo de edición en la parte superior de Quickwatch muestra el siguiente lanzamiento:
((TestProject1.Base)(d))
Lo que significa que si la base se reemplaza como tal, la llamada sería
public string BaseProperty { get { return ((TestProject1.Base)(d)).Property; } private set { } }
para los Watches, Quickwatch y la depuración de la información sobre el mouse sobre herramientas, y entonces tendría sentido que se muestre en "AWESOME"
lugar de "BASE_AWESOME"
considerar el polimorfismo. Todavía no estoy seguro de por qué lo transformaría en un reparto, una hipótesis es que call
podría no estar disponible desde el contexto de esos módulos, y solo callvirt
.
De todos modos, eso obviamente no altera nada en términos de funcionalidad, Derived.BaseProperty
aún así volverá "BASE_AWESOME"
y, por lo tanto, esta no fue la raíz de nuestro error en el trabajo, simplemente un componente confuso. Sin embargo, me pareció interesante cómo podría engañar a los desarrolladores que desconocen este hecho durante sus sesiones de depuración, especialmente si Base
no está expuesto en su proyecto, sino que se hace referencia a él como un DLL de terceros, lo que hace que Devs simplemente diga:
"Oi, espera ... ¿qué? Dios mío, ese archivo DLL es como ... hacer algo gracioso"
En una API que estamos usando, los métodos que devuelven un objeto de dominio pueden devolver un "objeto nulo" especial. En la implementación de esto, el operador de comparación y el Equals()
método se anulan para devolver true
si se compara con null
.
Entonces, un usuario de esta API podría tener un código como este:
return test != null ? test : GetDefault();
O quizás un poco más detallado, como este:
if (test == null)
return GetDefault();
return test;
donde GetDefault()
es un método que devuelve algún valor predeterminado que queremos usar en lugar de null
. La sorpresa me golpeó cuando estaba usando ReSharper y siguiendo su recomendación de volver a escribir cualquiera de esto en lo siguiente:
return test ?? GetDefault();
Si el objeto de prueba es un objeto nulo devuelto por la API en lugar de un correcto null
, el comportamiento del código ahora ha cambiado, ya que el operador de unión nula realmente verifica null
, no se está ejecutando operator=
o Equals()
.
Este es bastante difícil de superar. Me topé con él mientras intentaba construir una implementación RealProxy que realmente soporta Begin / EndInvoke (gracias a MS por hacer esto imposible sin trucos horribles). Este ejemplo es básicamente un error en el CLR, la ruta del código no administrado para BeginInvoke no valida que el mensaje de retorno de RealProxy.PrivateInvoke (y mi anulación de invocación) esté devolviendo una instancia de un IAsyncResult. Una vez que se devuelve, el CLR se confunde increíblemente y pierde cualquier idea de lo que está pasando, como lo demuestran las pruebas en la parte inferior.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Remoting.Proxies;
using System.Reflection;
using System.Runtime.Remoting.Messaging;
namespace BrokenProxy
{
class NotAnIAsyncResult
{
public string SomeProperty { get; set; }
}
class BrokenProxy : RealProxy
{
private void HackFlags()
{
var flagsField = typeof(RealProxy).GetField("_flags", BindingFlags.NonPublic | BindingFlags.Instance);
int val = (int)flagsField.GetValue(this);
val |= 1; // 1 = RemotingProxy, check out System.Runtime.Remoting.Proxies.RealProxyFlags
flagsField.SetValue(this, val);
}
public BrokenProxy(Type t)
: base(t)
{
HackFlags();
}
public override IMessage Invoke(IMessage msg)
{
var naiar = new NotAnIAsyncResult();
naiar.SomeProperty = "o noes";
return new ReturnMessage(naiar, null, 0, null, (IMethodCallMessage)msg);
}
}
interface IRandomInterface
{
int DoSomething();
}
class Program
{
static void Main(string[] args)
{
BrokenProxy bp = new BrokenProxy(typeof(IRandomInterface));
var instance = (IRandomInterface)bp.GetTransparentProxy();
Func<int> doSomethingDelegate = instance.DoSomething;
IAsyncResult notAnIAsyncResult = doSomethingDelegate.BeginInvoke(null, null);
var interfaces = notAnIAsyncResult.GetType().GetInterfaces();
Console.WriteLine(!interfaces.Any() ? "No interfaces on notAnIAsyncResult" : "Interfaces");
Console.WriteLine(notAnIAsyncResult is IAsyncResult); // Should be false, is it?!
Console.WriteLine(((NotAnIAsyncResult)notAnIAsyncResult).SomeProperty);
Console.WriteLine(((IAsyncResult)notAnIAsyncResult).IsCompleted); // No way this works.
}
}
}
Salida:
No interfaces on notAnIAsyncResult
True
o noes
Unhandled Exception: System.EntryPointNotFoundException: Entry point was not found.
at System.IAsyncResult.get_IsCompleted()
at BrokenProxy.Program.Main(String[] args)
Public Class Item
Public ID As Guid
Public Text As String
Public Sub New(ByVal id As Guid, ByVal name As String)
Me.ID = id
Me.Text = name
End Sub
End Class
Public Sub Load(sender As Object, e As EventArgs) Handles Me.Load
Dim box As New ComboBox
Me.Controls.Add(box) ''Sorry I forgot this line the first time.''
Dim h As IntPtr = box.Handle ''Im not sure you need this but you might.''
Try
box.Items.Add(New Item(Guid.Empty, Nothing))
Catch ex As Exception
MsgBox(ex.ToString())
End Try
End Sub
La salida es "Se intentó leer la memoria protegida. Esto indica que otra memoria está dañada".