¿Cuál es el caso de esquina más extraño que has visto en C # o .NET? [cerrado]

322

Colecciono algunos casos de esquina y rompecabezas y siempre me gustaría escuchar más. La página solo cubre bits y bobs del lenguaje C #, pero también me parecen interesantes las cosas principales de .NET. 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));

Esperaría que eso imprima False: después de todo, "nuevo" (con un tipo de referencia) siempre crea un nuevo objeto, ¿no? Las especificaciones para C # y la CLI indican que debería. Bueno, no en este caso particular. Imprime True, y lo ha hecho en todas las versiones del marco con el que lo he probado. (No lo he probado en Mono, lo admito ...)

Para ser claros, este es solo un ejemplo del tipo de cosas que estoy buscando: no estaba buscando una discusión / explicación de esta rareza. (No es lo mismo que el internamiento de cadena normal; en particular, el internamiento de cadena normalmente no ocurre cuando se llama a un constructor). Realmente estaba pidiendo un comportamiento extraño similar.

¿Alguna otra gema al acecho?

Jon Skeet
fuente
64
Probado en Mono 2.0 rc; vuelve True
Marc Gravell
10
ambas cadenas terminan siendo cadenas. Vacío y parece que el marco solo guarda una referencia a eso
Adrian Zanescu el
34
Es una cosa de conservación de la memoria. Busque en la documentación de MSDN la cadena del método estático. El CLR mantiene un conjunto de cadenas. Es por eso que las cadenas con contenido idéntico aparecen como referencias a la misma memoria, es decir, al objeto.
John Leidegren
12
@John: el internamiento de cadenas solo ocurre automáticamente para literales . Ese no es el caso aquí. @DanielSwe: No se requiere internación para hacer que las cadenas sean inmutables. El hecho de que sea posible es un buen corolario de la inmutabilidad, pero la internación normal no está sucediendo aquí de todos modos.
Jon Skeet el
3
El detalle de implementación que causa este comportamiento se explica aquí: blog.liranchen.com/2010/08/brain-teasing-with-strings.html
Liran

Respuestas:

394

Creo que te mostré esto antes, pero me gusta la diversión aquí, ¡esto requirió 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: cualquiera Nullable<T>, como int?. Todos los métodos se anulan, excepto GetType () que no puede ser; por lo que se convierte (en caja) al objeto (y, por lo tanto, a nulo) para llamar a object.GetType () ... que llama a nulo ;-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 la misma indirección utilizada 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 se redirige al proxy ( MyFunnyProxyAttribute), que regresa null. ¡Ahora ve y lávate los ojos!

Marc Gravell
fuente
99
¿Por qué no se puede definir Nullable <T> .GetType ()? ¿No debería ser el resultado typeof (Nullable <T>)?
Drew Noakes el
69
Drew: el problema es que GetType () no es virtual, por lo que no se anula, lo que significa que el valor está encuadrado para la llamada al método. El cuadro se convierte en una referencia nula, de ahí la NRE.
Jon Skeet
10
@Dibujó; además, hay reglas de boxeo especiales para Nullable <T>, lo que significa que un Nullable <T> recuadro vacío a nulo, no un recuadro que contenga un Nullable <T> vacío (y un nulo desempaquetado a un Nullable <T vacío) >)
Marc Gravell
29
Muy muy genial. De una manera poco agradable. ;-)
Konrad Rudolph
66
Restricción de constructor, 10.1.5 en la especificación de lenguaje C # 3.0
Marc Gravell
216

Redondeo de los banqueros.

Este no es tanto un error o mal funcionamiento del compilador, sino ciertamente un extraño caso de esquina ...

.Net Framework emplea un esquema o redondeo conocido como redondeo bancario.

En el redondeo de los banqueros, los números 0.5 se redondean al número par más cercano, entonces

Math.Round(-0.5) == 0
Math.Round(0.5) == 0
Math.Round(1.5) == 2
Math.Round(2.5) == 2
etc...

Esto puede conducir a algunos errores inesperados en los cálculos financieros basados ​​en el redondeo Round-Half-Up más conocido.

Esto también es cierto de Visual Basic.

Samuel Kim
fuente
22
A mí también me pareció extraño. Eso es, al menos, hasta que haya redondeado una gran lista de números y calcule su suma. Entonces te das cuenta de que si simplemente redondeas, terminarás con una diferencia potencialmente enorme de la suma de los números no redondeados. ¡Muy mal si estás haciendo cálculos financieros!
Tsvetomir Tsonev
255
En caso de que la gente no lo supiera, puede hacer: Math.Round (x, MidpointRounding.AwayFromZero); Para cambiar el esquema de redondeo.
ICR
26
De los documentos: El comportamiento de este método sigue el Estándar 754 de IEEE, sección 4. Este tipo de redondeo a veces se llama redondeo al más cercano o redondeo bancario. Minimiza los errores de redondeo que resultan de redondear constantemente un valor de punto medio en una sola dirección.
ICR
8
Me pregunto si es por eso que veo con int(fVal + 0.5)tanta frecuencia incluso en idiomas que tienen una función de redondeo incorporada.
Ben Blank
32
Irónicamente, trabajé en un banco una vez y los otros programadores comenzaron a dar vueltas sobre esto, pensando que el redondeo estaba roto en el marco
dan
176

¿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 JIT de 32 bits, debería producir una StackOverflowException
  • En 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 llamada de cola , mientras que el JIT de 32 bits no.

Desafortunadamente, no tengo una máquina de 64 bits a mano para verificar esto, pero el método cumple con todas las condiciones para la optimización de llamadas de cola. Si alguien tiene uno, me interesaría ver si es verdad.

Greg Beech
fuente
10
Tiene que compilarse en modo de lanzamiento, pero definitivamente funciona en x64 =)
Neil Williams
3
podría valer la pena actualizar su respuesta cuando salga VS 2010, ya que todos los JIT actuales harán el TCO en modo de lanzamiento
ShuggyCoUk
3
Acabo de probar VS2010 Beta 1 en WinXP de 32 bits. Todavía recibo una StackOverflowException.
squillman
130
+1 para la StackOverflowException
calvinlough
77
Que ++allí me tiró por completo. ¿No puedes llamar Rec(i + 1)como una persona normal?
configurador
111

¡Asigna esto!


Esta es una que me gusta preguntar en las fiestas (que probablemente sea la razón por la que ya no me invitan):

¿Puedes hacer la siguiente compilación de código?

    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();
    }
}

Por lo tanto, es un hecho poco conocido que los tipos de valor (estructuras) pueden reasignar sus thisvariables.

Omer Mor
fuente
3
Las clases de C ++ también pueden hacer eso ... como descubrí hace poco, solo para que me griten por tratar de usarlo para una optimización: p
mpen
1
Estaba usando en el lugar nuevo en realidad. Solo quería una forma eficiente de actualizar todos los campos :)
mpen
70
Esto también es un truco: //this = new Teaser();:-)
AndrewJacksonZA
17
:-) Prefiero esos trucos en mi código de producción, que esta abominación de reasignación ...
Omer Mor
2
Desde CLR a través de C #: La razón por la que hicieron esto es porque puede llamar al constructor sin parámetros de una estructura en otro constructor. Si solo desea inicializar un valor de una estructura y desea que los otros valores sean cero / nulo (predeterminado), puede escribir public Foo(int bar){this = new Foo(); specialVar = bar;}. Esto no es eficiente y no está realmente justificado ( specialVarse asigna dos veces), sino solo para su información. (Esa es la razón dada en el libro, no sé por qué no deberíamos hacerlo public Foo(int bar) : this())
kizzx2
100

Hace unos años, cuando trabajábamos en un 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 de double a int.

En el código a continuación:

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 el operador Convertir y emitir, 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 con nuestros requisitos)

int i1 = Convert.ToInt32( Math.Ceiling(d) );
int i2 = (int) Math.Ceiling(d);
Jarek Kardas
fuente
44
Lanzar a un número entero no se redondea, solo lo corta (efectivamente siempre se redondea hacia abajo). Entonces esto tiene mucho sentido.
Max Schmeling
57
@Max: sí, pero ¿por qué se convierte Convertir?
Stefan Steinegger
18
@Stefan Steinegger Si todo lo que hizo fue emitir, no habría ninguna razón para ello, ¿verdad? También tenga en cuenta que el nombre de la clase es Convertir no emitir.
bug-a-lot el
3
En VB: CInt () redondea. Fix () se trunca. Me quemó una vez ( blog.wassupy.com/2006/01/i-can-believe-it-not-truncating.html )
Michael Haren
74

Deberían haber hecho 0 un número entero incluso cuando hay una sobrecarga de la función enum.

Conocía la razón fundamental del equipo de C # para asignar 0 a enum, 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");
        }
    }
}
Michael Buen
fuente
18
Wow, eso es nuevo para mí. También es extraño cómo funciona ConverTo.ToIn32 () pero la conversión a (int) 0 no. Y cualquier otro número> 0 funciona. (Por "obras" me refiero a llamar al objeto sobrecargado)
Lucas
Hay una regla de análisis de código recomendada para aplicar buenas prácticas en torno a este comportamiento: msdn.microsoft.com/en-us/library/ms182149%28VS.80%29.aspx Ese enlace también contiene una buena descripción de cómo funciona el 0-mapping .
Chris Clark
1
@ Chris Clark: intenté poner None = 0 en el símbolo de enumeración. todavía el compilador elige enum para 0 e incluso (int) 0
Michael Buen
2
En mi opinión, deberían haber introducido una palabra clave noneque se puede usar convertida a cualquier enumeración, y hacer 0 siempre un int y no convertible implícitamente en una enumeración.
CodesInChaos
55
ConverTo.ToIn32 () funciona porque su resultado no es una constante de tiempo de compilación. Y solo la constante de tiempo de compilación 0 es convertible en una enumeración. En versiones anteriores de .net, incluso solo el literal 0debería haber sido convertible a enum. Vea el blog de Eric Lippert: blogs.msdn.com/b/ericlippert/archive/2006/03/28/563282.aspx
CodesInChaos
67

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 cualquier clase en el centro con otra tortuga.

[broma] Supongo que son tortugas hasta el fondo ... [/ broma]

RCIX
fuente
34
Sin embargo, puede crear instancias:class RealTurtle : Turtle<RealTurtle> { } RealTurtle t = new RealTurtle();
Marc Gravell
24
En efecto. Este es el patrón que las enumeraciones Java utilizan con gran efecto. Lo uso en Protocol Buffers también.
Jon Skeet el
66
RCIX, oh sí, lo es.
Joshua
8
He usado mucho este patrón en cosas genéricas geniales. Permite cosas como un clon escrito correctamente o la creación de instancias de sí mismo.
Lucero
20
Este es el 'patrón de plantilla curiosamente recurrente' en.wikipedia.org/wiki/Curiously_recurring_template_pattern
porges
65

Aquí hay uno que 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, de verdad (aunque me he perdido una parte clave, pero no es nada obsceno como "agregar una clase llamada IFoo" o "agregar un usingalias para señalarIFoo un clase").

Vea si puede descubrir por qué, entonces: ¿Quién dice que no puede crear una instancia de una interfaz?

Marc Gravell
fuente
1
+1 para "usar alias" - ¡Nunca supe que podías hacer eso !
David
piratear el compilador para COM Interop :-)
Ion Todirel
¡Bastardo! Al menos podrías haber dicho "bajo ciertas circunstancias" ... ¡Mi compilador lo refuta!
MA Hanin
56

¿Cuándo un booleano no es verdadero ni falso?

Bill descubrió que puedes hackear un booleano para que si A es Verdadero y B sea Verdadero, (A y B) sea Falso.

Booleanos pirateados

Jonathan Allen
fuente
134
Cuando es FILE_NOT_FOUND, ¡por supuesto!
Greg
12
Esto es interesante porque significa, matemáticamente hablando, que ninguna declaración en C # es demostrable. Ooops
Simon Johnson
20
Algún día escribiré un programa que dependa de este comportamiento, y los demonios del infierno más oscuro prepararán una bienvenida para mí. Bwahahahahaha!
Jeffrey L Whitledge
18
Este ejemplo utiliza operadores bit a bit, no lógicos. ¿Cómo es eso sorprendente?
Josh Lee
66
Bueno, él piratea el diseño de la estructura, por supuesto obtendrá resultados extraños, ¡esto no es tan sorprendente o inesperado!
Ion Todirel
47

Llego un poco tarde a la fiesta, pero tengo tres cuatro cinco:

  1. Si sondea InvokeRequired en un control que no se ha cargado / mostrado, dirá falso y explotará en tu cara si intentas cambiarlo desde otro hilo ( la solución es hacer referencia a esto. Manejar en el creador del controlar).

  2. Otro que me hizo tropezar es que, dada una asamblea con:

    enum MyEnum
    {
        Red,
        Blue,
    }

    si calcula MyEnum.Red.ToString () en otro ensamblado, y en algún momento alguien ha vuelto a compilar su enumeración para:

    enum MyEnum
    {
        Black,
        Red,
        Blue,
    }

    en tiempo de ejecución, obtendrás "Black".

  3. Tenía un ensamblaje compartido con algunas constantes útiles. Mi predecesor había dejado una carga de propiedades de solo obtener feo, pensé en deshacerme del desorden y simplemente usar const público. Estaba más que un poco sorprendido cuando VS los compiló con sus valores, y no con referencias.

  4. Si implementa un nuevo método de una interfaz desde otro ensamblaje, pero reconstruye haciendo referencia a la versión anterior de ese ensamblaje, obtiene una excepción TypeLoadException (sin implementación de 'NewMethod'), aunque la haya implementado (consulte aquí ).

  5. Diccionario <,>: "El orden en que se devuelven los elementos no está definido". Esto es horrible , porque a veces puede morderte, pero trabajar en otros, y si has asumido ciegamente que Dictionary va a jugar bien ("¿por qué no debería? Pensé, List lo hace"), realmente tienes que hacerlo tenga la nariz puesta antes de que finalmente comience a cuestionar su suposición.

Benjol
fuente
66
# 2 es un ejemplo interesante. Las enumeraciones son asignaciones del compilador a valores integrales. Entonces, aunque no les asignó explícitamente valores, el compilador lo hizo, lo que resultó en MyEnum.Red = 0 y MyEnum.Blue = 1. Cuando agregó Black, redefinió el valor 0 para asignar de Rojo a Negro. Sospecho que el problema también se habría manifestado en otros usos, como la serialización.
LBushkin
3
Se requiere +1 para invocar. En el nuestro 'preferimos asignar explícitamente valores a enumeraciones como Rojo = 1, Azul = 2 para que se pueda insertar uno nuevo antes o después de que siempre resulte en el mismo valor. Es especialmente necesario si está guardando valores en bases de datos.
TheVillageIdiot
53
No estoy de acuerdo con que el # 5 sea un "caso límite". El diccionario no debe tener un orden definido en función de cuándo inserta valores. Si desea un orden definido, use una Lista, o use una clave que se pueda ordenar de una manera que sea útil para usted, o use una estructura de datos completamente diferente.
Wedge
21
@Wedge, ¿como SortedDictionary quizás?
Allon Guralnek
44
# 3 sucede porque las constantes se insertan como literales en todas partes donde se usan (en C #, al menos). Es posible que su predecesor ya lo haya notado, por eso usaron la propiedad get-only. Sin embargo, una variable de solo lectura (a diferencia de una constante) funcionaría igual de bien.
Remoun
33

VB.NET, nulables y el operador ternario:

Dim i As Integer? = If(True, Nothing, 5)

Esto me llevó algún tiempo depurar, ya que esperaba icontenerNothing .

¿Qué contengo realmente? 0.

Esto es sorprendente, pero en realidad es un comportamiento "correcto": Nothingen VB.NET no es exactamente lo mismo que nullen CLR: Nothingpuede significar nullo default(T)para un tipo de valor T, dependiendo del contexto. En el caso anterior, se Ifinfiere Integercomo el tipo común de Nothingy 5, por lo tanto, en este caso, Nothingsignifica 0.

Heinzi
fuente
Lo suficientemente interesante, no pude encontrar esta respuesta, así que tuve que crear una pregunta . Bueno, ¿quién sabía que la respuesta está en este hilo?
GSerg
28

Encontré un segundo caso de esquina realmente extraño que supera por mucho al primero.

El método String.Equals (String, String, StringComparison) no está libre de efectos secundarios.

Estaba trabajando en un bloque de código que tenía esto en una línea sola en la parte superior de alguna función:

stringvariable1.Equals(stringvariable2, StringComparison.InvariantCultureIgnoreCase);

Eliminar esa línea conduce a un desbordamiento de la pila en otro lugar del programa.

El código resultó estar instalando un controlador para lo que en esencia era un evento BeforeAssemblyLoad y tratando 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 usado antes en una comparación de cadenas provoca una carga de ensamblaje. InvariantCulture no es una excepción a esto.

Joshua
fuente
Supongo que "cargar un ensamblaje" es un efecto secundario, ya que puede observarlo con BeforeAssemblyLoad!
Jacob Krall
2
Guau. Este es un tiro perfecto en la pierna del mantenedor. Supongo que escribir un controlador BeforeAssemblyLoad puede generar muchas sorpresas.
wigy 01 de
20

Aquí hay un ejemplo de cómo puede crear una estructura que provoque el mensaje de error "Intentó leer o escribir memoria protegida. Esto es a menudo una indicación de 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);
        }
    }
cbp
fuente
Me duele la cabeza ... ¿Por qué esto no funciona?
jasonh
2
Hm, escribí esto hace unos meses, pero no recuerdo por qué sucedió exactamente.
cbp
10
Parece un error del compilador; las += 500llamadas: ldc.i4 500(empuja 500 como Int32), entonces call valuetype Program/MyStruct Program/MyStruct::op_Addition(valuetype Program/MyStruct, valuetype [mscorlib]System.Decimal), entonces trata como un decimal(96 bits) sin ninguna conversión. Si lo usa, += 500Mlo hace bien. Simplemente parece que el compilador cree que puede hacerlo de una manera (presumiblemente debido al operador int implícito) y luego decide hacerlo de otra manera.
Marc Gravell
1
Perdón por la doble publicación, aquí hay una explicación más calificada. Agregaré esto, he sido mordido por esto y esto apesta, aunque entiendo por qué sucede. Para mí, esta es una limitación desafortunada de la estructura / tipo de valor. bytes.com/topic/net/answers/…
Bennett Dill
2
@Ben obtiene errores del compilador o la modificación que no afecta a la estructura original está bien. Una violación de acceso es una bestia bastante diferente. El tiempo de ejecución nunca debería lanzarlo si solo está escribiendo código administrado puro y seguro.
CodesInChaos
18

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 son 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[]'
Peter van der Heijden
fuente
11
El ejemplo IList <T> es solo una conversión, porque la cadena [] ya implementa ICloneable, IList, ICollection, IEnumerable, IList <string>, ICollection <string> e IEnumerable <string>.
Lucas
15

Esto es lo más extraño que he encontrado por accidente:

public class DummyObject
{
    public override string ToString()
    {
        return null;
    }
}

Usado de la siguiente manera:

DummyObject obj = new DummyObject();
Console.WriteLine("The text: " + obj.GetType() + " is " + obj);

Lanzará un NullReferenceException. Resulta que las múltiples adiciones son compiladas por el compilador de C # a una llamada a String.Concat(object[]). Antes de .NET 4, hay un error solo en esa sobrecarga de Concat donde el objeto se verifica para 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 de 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 es null, se sustituye una cadena vacía. De lo contrario, cualquier operando que no sea de cadena se convierte en su representación de cadena invocando el ToStringmétodo virtual heredado de type object. Si se ToStringdevuelve null, se sustituye una cadena vacía.

Sam Harwell
fuente
3
Hmm, pero me imagino esta falla como .ToStringrealmente nunca debería volver nulo, sino cadena. Vacío. Sin embargo y error en el marco.
Dykam
12

Interesante: cuando lo vi por primera vez, asumí que era algo que el compilador de C # estaba buscando, pero incluso si emites el IL directamente para eliminar cualquier posibilidad de interferencia, todavía sucede, lo que significa que realmente es el newobjcódigo operativo que está haciendo el 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 truesi comprueba lo string.Emptyque significa que este código de operación debe tener un comportamiento especial para internar cadenas vacías.

Greg Beech
fuente
no para ser un inteligente ni nada, pero ¿has oído hablar del reflector ? es bastante útil en este tipo de casos;
RCIX
3
No estás siendo inteligente; te estás perdiendo el punto: quería generar IL específica para este caso. Y de todos modos, dado que Reflection.Emit es trivial para este tipo de escenario, probablemente sea tan rápido como escribir un programa en C # y luego abrir el reflector, encontrar el binario, encontrar el método, etc. Y ni siquiera tengo que hacerlo. deja el IDE para hacerlo.
Greg Beech el
10
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

El resultado es "Se intentó leer la memoria protegida. Esto es una indicación de que otra memoria está dañada".

Joshua
fuente
1
¡Interesante! Sin embargo, parece un error del compilador; He portado a C # y funciona bien. Dicho esto, hay muchos problemas con excepciones lanzadas en Load, y se comporta de manera diferente con / sin un depurador: puede atrapar con un depurador, pero no sin él (en algunos casos).
Marc Gravell
Lo siento, lo olvidé, debes agregar el cuadro combinado al formulario antes de que lo haga.
Joshua
¿Tiene esto que ver con la inicialización del diálogo usando un SEH como algún tipo de horrible mecanismo de comunicación interna? Recuerdo vagamente algo así en Win32.
Daniel Earwicker
1
Este es el mismo problema cbp anterior. El tipo de valor que se devuelve es una copia, por lo tanto, cualquier referencia a cualquier propiedad derivada de dicha copia se dirige a bit-bucket land ... bytes.com/topic/net/answers/…
Bennett Dill el
1
No No hay estructuras aquí. En realidad lo depuré. Agrega un NULL a la colección de elementos de la lista del cuadro combinado nativo que causa un bloqueo retrasado.
Joshua
10

PropertyInfo.SetValue () puede asignar entradas a enumeraciones, entradas a entradas anulables, enumeraciones a enumeraciones anulables, pero no entradas a enumeraciones anulables.

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 aquí

Anders Ivner
fuente
10

¿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 bidireccional. Quería escribir Get()métodos simétricos que devolvieran lo contrario de cualquier argumento que se pasara. Algo como esto:

class TwoWayRelationship<T1, T2>
{
    public T2 Get(T1 key) { /* ... */ }
    public T1 Get(T2 key) { /* ... */ }
}

Todo está bien si haces una instancia donde T1y T2son diferentes tipos:

var r1 = new TwoWayRelationship<int, string>();
r1.Get(1);
r1.Get("a");

Pero si T1y T2son lo mismo (y probablemente si uno era 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 demás métodos en el segundo caso todavía son utilizables; solo las llamadas al método ahora ambiguo causan un error del compilador. Caso interesante, aunque un poco improbable y oscuro.

tclem
fuente
Los opositores a la sobrecarga de métodos adorarán este ^^.
Christian Klauser
1
No sé, esto tiene mucho sentido para mí.
Scott Whitlock
10

Rompecabezas de accesibilidad C #


La siguiente clase derivada está accediendo a un campo privado desde su clase base, y el compilador mira silenciosamente hacia el 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 compilar ese código?

.

.

.

.

.

.

.

Responder


El truco es declarar Derivedcomo 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.

Omer Mor
fuente
Eso en realidad está bien documentado; msdn.microsoft.com/en-us/library/ms173120%28VS.80%29.aspx . A veces puede ser una característica útil, especialmente si la clase externa es estática.
Sí, por supuesto, está documentado. Sin embargo, muy pocas personas resolvieron este acertijo, así que pensé que era una pieza genial de trivia.
Omer Mor
2
Parece que tendrías una posibilidad muy fuerte de un desbordamiento de pila si una clase interna heredara a su dueño ...
Jamie Treworgy
Otro caso similar (y perfectamente correcto) es que un objeto puede acceder a un miembro privado de otro objeto del mismo tipo:class A { private int _i; public void foo(A other) { int res = other._i; } }
Olivier Jacot-Descombes
10

Acabo de encontrar una cosita linda 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 despacharse 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 aún se recibe como dinámico ...

TDaver
fuente
8

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 truesi se compara connull .

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 reescribir cualquiera de estos a lo siguiente:

return test ?? GetDefault();

Si el objeto de prueba es un objeto nulo devuelto por la API en lugar de un apropiado null, el comportamiento del código ahora ha cambiado, ya que el operador de fusión nula realmente busca null, no se ejecuta operator=o no Equals().

Tor Livar
fuente
1
no es realmente un caso de esquina, pero querido señor, ¿quién pensó eso?
Ray Booysen
¿No es este código solo el uso de tipos anulables? Por lo tanto, ReSharper recomienda el "??" utilizar. Como dijo Ray, no habría pensado que esto fuera un caso de esquina; o estoy equivocado?
Tony
1
Sí, los tipos son anulables, y hay un NullObject además. Si es un caso de esquina, no lo sé, pero al menos es un caso donde 'if (a! = Null) devuelve a; volver b; ' no es lo mismo que 'return a ?? si'. Estoy totalmente de acuerdo en que es un problema con el diseño del marco / API: sobrecargar == nulo para devolver verdadero en un objeto ciertamente no es una buena idea.
Tor Livar
8

Considere este caso extraño:

public interface MyInterface {
  void Method();
}
public class Base {
  public void Method() { }
}
public class Derived : Base, MyInterface { }

Si Basey Derivedse declaran en el mismo ensamblado, el compilador lo hará Base::Methodvirtual y sellado (en el CIL), aunque Baseno implemente la interfaz.

Si Basey Derivedestán en ensamblajes diferentes, al compilar el Derivedensamblaje, el compilador no cambiará el otro ensamblaje, por lo que introducirá un miembro Derivedque será una implementación explícita para MyInterface::Methodeso solo delegará la llamada aBase::Method .

El compilador debe hacer esto para admitir el envío polimórfico con respecto a la interfaz, es decir, debe hacer que ese método sea virtual.

Jordão
fuente
Eso sí que suena extraño. Tendré que investigar más tarde :)
Jon Skeet
@ Jon Skeet: Encontré esto mientras investigaba estrategias de implementación para roles en C # . ¡Sería genial recibir sus comentarios sobre eso!
Jordão
7

Lo 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 un poco el contexto, 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 está en el Derivedcontexto del objeto, puede obtener el mismo comportamiento al agregar base.Propertycomo reloj o al escribir base.Propertyen el reloj rápido.

Me tomó un tiempo darme cuenta de lo que estaba pasando. Al final fui iluminado por el Quickwatch. Al ingresar al Quickwatch y explorar el Derivedobjeto d (o desde el contexto del objeto this) y seleccionar el campo base, el campo de edición en la parte superior del Quickwatch muestra el siguiente reparto:

((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 información sobre herramientas de depuración del mouse, y entonces tendría sentido que se muestre en "AWESOME"lugar de "BASE_AWESOME"cuando se considera el polimorfismo. Todavía no estoy seguro de por qué lo transformaría en un elenco, una hipótesis es que callpodrí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.BasePropertytodavía volverá realmente "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 ese hecho durante sus sesiones de depuración, especialmente si Baseno se expone en su proyecto, sino que se hace referencia a él como un archivo DLL de terceros, lo que hace que los desarrolladores simplemente digan:

"Oi, espera ... ¿qué? Dios mío es como DLL, haciendo algo gracioso"

Dynami Le Savard
fuente
Eso no es nada especial, esa es la forma en que anula el trabajo.
Configurador
7

Este es bastante difícil de superar. Me encontré con él mientras intentaba construir una implementación RealProxy que realmente sea compatible con Begin / EndInvoke (gracias MS por hacer que esto sea imposible sin trucos horribles). Este ejemplo es básicamente un error en el CLR, la ruta de código no administrada para BeginInvoke no valida que el mensaje de retorno de RealProxy.PrivateInvoke (y mi anulación Invoke) está devolviendo una instancia de IAsyncResult. Una vez que se devuelve, el CLR se confunde increíblemente y pierde la idea de lo que está sucediendo, 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) 
Steve
fuente
6

No estoy seguro de si diría que esto es una rareza de Windows Vista / 7 o una rareza de .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 se escribirá realmente en C:\Users\<username>\Virtual Store\Program Files\my folder\test.txt

rev. Spencer Ruport
fuente
2
Esta es de hecho una mejora de seguridad de vista (no 7, afaik). Pero lo bueno es que puedes leer y abrir el archivo con la ruta de los archivos del programa, mientras que si miras allí con el explorador no hay nada. Este me llevó casi un día de trabajo @ un cliente antes de que finalmente lo descubriera.
Henri
Definitivamente es una cosa de Windows 7 también. Eso es lo que estaba usando cuando me encontré con él. Entiendo el razonamiento detrás de esto, pero todavía era frustrante descubrirlo.
Spencer Ruport
En Vista / Win 7 (winXP técnicamente también), las aplicaciones deben escribir en una carpeta AppData en la carpeta Carpeta de usuarios, como sus datos técnicos de usuario. Las aplicaciones no deberían escribir nunca en archivos de programa / windows / system32 / etc, a menos que tengan privilegios de administrador, y esos privilegios solo deberían estar ahí para decir que actualice el programa / desinstale / instale una nueva función. ¡PERO! Todavía no escriba en system32 / windows / etc :) Si ejecutó el código anterior como administrador (clic derecho> ejecutar como administrador), en teoría debería escribir en la carpeta de la aplicación de archivos de programa.
Steve Syfuhs
Suena como virtualización - crispybit.spaces.live.com/blog/cns!1B71C2122AD43308!134.entry
Colin Newell
6

¿Alguna vez pensó que el compilador de C # podría generar CIL no válido? Ejecute esto y obtendrá 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 de 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]
Jordão
fuente
Funciona para mí con el compilador C # 3.5 y el compilador C # 4 ...
Jon Skeet
En mi sistema, no funciona. Pegaré el resultado en la pregunta.
Jordão
Me falló en .NET 3.5 (no tengo tiempo para probar 4.0). Y puedo replicar el problema con el código VB.NET.
Mark Hurd
3

Hay algo realmente emocionante en C #, la forma en que maneja los cierres.

En lugar de copiar los valores de la variable de pila en la variable libre de cierre, hace que la magia del preprocesador envuelva todas las ocurrencias 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 aún más funcionalmente completo (o lambda-complete huh)) que el propio ML (que usa el valor de pila copiando AFAIK). F # también tiene esa característica, como lo hace C #.

Eso me alegra mucho, ¡gracias, muchachos de MS!

Sin embargo, no es un caso extraño o de esquina ... sino algo realmente inesperado de un lenguaje VM basado en pila :)

Bubba88
fuente
3

De una pregunta que hice hace poco:

¿El operador condicional no puede emitir implícitamente?

Dado:

Bool aBoolValue;

Dónde aBoolValue se le 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 también es bastante buena.

MPelletier
fuente
aunque estoy ve not test it Iseguro de que esto funcionará: Byte aByteValue = aBoolValue? (Byte) 1: (Byte) 0; O: Byte aByteValue = (Byte) (aBoolValue? 1: 0);
Alex Pacurar
2
Sí, Alex, eso funcionaría. La clave está en el casting implícito. 1 : 0solo se lanzará implícitamente a int, no a Byte.
MPelletier
2

El alcance en C # es realmente extraño a veces. Déjame darte un ejemplo:

if (true)
{
   OleDbCommand command = SQLServer.CreateCommand();
}

OleDbCommand command = SQLServer.CreateCommand();

Esto no se compila, porque el comando se vuelve a declarar? Hay algunas conjeturas interesantes sobre por qué funciona de esa manera en este hilo en stackoverflow y en mi blog .

Anders Rune Jensen
fuente
34
No veo eso como particularmente extraño. Lo que llamas "código perfectamente correcto" en tu blog es perfectamente incorrecto según la especificación del idioma. Puede ser correcto en algún lenguaje imaginario que le gustaría que C # sea, pero la especificación del lenguaje es bastante clara de que en C # no es válida.
Jon Skeet
77
Bueno, es válido en C / C ++. Y dado que es C #, me hubiera gustado que siguiera funcionando. Lo que más me molesta es que no hay razón para que el compilador haga esto. No es difícil hacer un alcance anidado. Supongo que todo se reduce al elemento de menor sorpresa. Lo que significa que puede ser que la especificación diga esto y aquello, pero eso realmente no me ayuda mucho si es completamente ilógico que se comporte de esa manera.
Anders Rune Jensen
66
C #! = C / C ++. ¿Te gustaría usar cout << "Hello World!" << endl; en lugar de Console.WriteLine ("Hello World!") ;? Además, no es ilógico, solo lea las especificaciones.
Kredns
99
Estoy hablando de reglas de alcance, que es parte del núcleo del lenguaje. Estás hablando de la biblioteca estándar. Pero ahora está claro para mí que simplemente debería leer la pequeña especificación del lenguaje C # antes de comenzar a programar en él.
Anders Rune Jensen
66
Eric Lippert realmente publicó las razones por las cuales C # está diseñado así recientemente: blogs.msdn.com/ericlippert/archive/2009/11/02/… . El resumen se debe a que es menos probable que los cambios tengan consecuencias no deseadas.
Helephant