Resolución del tipo de valor del comstackdor y valores enteros “0” codificados

Primero, un poco de fondo. Lea la pregunta y la respuesta aceptada publicada aquí para un escenario específico para mi pregunta. No estoy seguro si existen otros casos similares, pero este es el único caso que conozco.

El “capricho” anterior es algo de lo que he tenido conocimiento durante mucho tiempo. No entendí todo el scope de la causa hasta hace poco.

La documentación de Microsoft sobre la clase SqlParameter arroja un poco más de luz sobre la situación.

Cuando especifica un Object en el parámetro de valor, el SqlDbType se deduce del tipo de Microsoft .NET Framework del objeto.

Tenga cuidado cuando use esta sobrecarga del constructor SqlParameter para especificar valores de parámetros enteros. Debido a que esta sobrecarga toma un valor de tipo Object , debe convertir el valor integral a un Tipo de Object cuando el valor es cero , como lo demuestra el siguiente ejemplo de C #.

Parameter = new SqlParameter("@pname", Convert.ToInt32(0));

Si no realiza esta conversión, el comstackdor supone que está tratando de llamar a la sobrecarga del constructor SqlParameter (cadena, SqlDbType).

(emph agregado)

Mi pregunta es: ¿por qué el comstackdor supone que cuando especifica un “0” codificado (y solo el valor “0”) está tratando de especificar un tipo de enumeración, en lugar de un tipo entero? En este caso, asume que está declarando valor SqlDbType , en lugar del valor 0.

Esto no es intuitivo y, para empeorar las cosas, el error es inconsistente. Tengo aplicaciones antiguas que he escrito que han llamado a procedimientos almacenados durante años. Haré un cambio en la aplicación (muchas veces ni siquiera está asociado con mis clases de SQL Server), publicaré una actualización, y este problema romperá la aplicación de repente.

¿Por qué el comstackdor se confunde con el valor 0 cuando un objeto que contiene múltiples firmas de métodos contiene dos firmas similares donde un parámetro es un objeto / entero y el otro acepta una enumeración?

Como mencioné, nunca he visto esto como un problema con ningún otro constructor o método en ninguna otra clase. ¿Esto es exclusivo de la clase SqlParameter o es un error heredado dentro de C # /. Net?

Es porque un entero cero es implícitamente convertible a una enumeración:

 enum SqlDbType { Zero = 0, One = 1 } class TestClass { public TestClass(string s, object o) { System.Console.WriteLine("{0} => TestClass(object)", s); } public TestClass(string s, SqlDbType e) { System.Console.WriteLine("{0} => TestClass(Enum SqlDbType)", s); } } // This is perfectly valid: SqlDbType valid = 0; // Whilst this is not: SqlDbType ohNoYouDont = 1; var a1 = new TestClass("0", 0); // 0 => TestClass(Enum SqlDbType) var a2 = new TestClass("1", 1); // => 1 => TestClass(object) 

(Adaptado de Visual C # 2008 Breaking Changes – change 12 )

Cuando el comstackdor realiza la resolución de sobrecarga 0 es un miembro de función aplicable tanto para SqlDbType como para los constructores de object porque:

una conversión implícita (Sección 6.1) existe desde el tipo de argumento hasta el tipo del parámetro correspondiente

(Tanto SqlDbType x = 0 como object x = 0 son válidos)

El parámetro SqlDbType es mejor que el parámetro del object debido a las mejores reglas de conversión :

  • Si T1 y T2 son del mismo tipo, ninguna conversión es mejor.
    • object y SqlDbType no son del mismo tipo
  • Si S es T1 , C1 es la mejor conversión.
    • 0 no es un object
  • Si S es T2 , C2 es la mejor conversión.
    • 0 no es un SqlDbType
  • Si existe una conversión implícita de T1 a T2 , y no existe una conversión implícita de T2 a T1 , C1 es la mejor conversión.
    • No existe una conversión implícita de object a SqlDbType
  • Si existe una conversión implícita de T2 a T1 , y no existe una conversión implícita de T1 a T2 , C2 es la mejor conversión.
    • Existe una conversión implícita de SqlDbType a object , por lo que SqlDbType es la mejor conversión

Tenga en cuenta que lo que constituye exactamente una constante 0 ha cambiado (bastante sutilmente) en Visual C # 2008 (la implementación de Microsoft de la especificación C #) como lo explica @Eric en su respuesta.

La respuesta de RichardTowers es excelente, pero pensé que podría agregarle un poco.

Como las otras respuestas han señalado, la razón para el comportamiento es (1) cero es convertible a cualquier enumeración, y obviamente a objeto, y (2) cualquier tipo de enumeración es más específico que ese objeto, por lo que el método que toma una enumeración es por lo tanto, elegido por resolución de sobrecarga como el mejor método. El segundo punto es que espero que se explique por sí mismo, pero ¿qué explica el primer punto?

En primer lugar, hay una desafortunada desviación de la especificación aquí. La especificación dice que cualquier cero literal , es decir, el número 0 que aparece literalmente en el código fuente, se puede convertir implícitamente a cualquier tipo de enumeración. El comstackdor realmente implementa que cualquier cero constante se puede convertir así. La razón de esto es debido a un error por el cual el comstackdor a veces permite ceros constantes y otras no, de una manera extraña e incoherente. La forma más fácil de resolver el problema fue permitir consistentemente ceros constantes. Puede leer sobre esto en detalle aquí:

http://blogs.msdn.com/b/ericlippert/archive/2006/03/28/the-root-of-all-evil-part-one.aspx

En segundo lugar, la razón para permitir que los ceros se conviertan a cualquier enumeración es para garantizar que siempre es posible poner a cero una enumeración de “indicadores”. Una buena práctica de progtwigción es que cada enumeración “flags” tiene un valor “None” que es igual a cero, pero eso es una guía, no un requisito. Los diseñadores de C # 1.0 pensaron que parecía extraño que tuvieras que decir

 for (MyFlags f = (MyFlags)0; ... 

para inicializar un local. Mi opinión personal es que esta decisión ha causado más problemas de los que valió la pena, tanto en términos de la pena por el error mencionado como en términos de las rarezas que introduce en la resolución de sobrecarga que ha descubierto.

Finalmente, los diseñadores de los constructores podrían haberse dado cuenta de que esto sería un problema en primer lugar, e hicieron las firmas de las sobrecargas de manera que el desarrollador pudiera decidir claramente a qué ctor llamar sin tener que insertar moldes. Desafortunadamente, este es un problema bastante oscuro y muchos diseñadores no lo saben. Espero que cualquiera que lea esto no cometa el mismo error; no cree una ambigüedad entre el objeto y cualquier enumeración si pretende que las dos anulaciones tengan una semántica diferente .

Aparentemente, se trata de un comportamiento conocido y afecta a cualquier sobrecarga de función cuando hay una enumeración y un tipo de objeto. No lo entiendo todo, pero Eric Lippert lo resumió bastante bien en su blog

Esto es causado por el hecho de que el entero literal 0 tiene una conversión implícita a cualquier tipo de enumeración. La especificación C # establece:

6.1.3 Conversiones de enumeración implícitas

Una conversión de enumeración implícita permite que el decimal-entero-literal 0 se convierta en cualquier tipo de enumeración y para cualquier tipo anulable cuyo tipo subyacente sea un tipo enum. En el último caso, la conversión se evalúa convirtiendo al tipo de enume subyacente y envolviendo el resultado.

Como resultado, la sobrecarga más específica en este caso es SqlParameter(string, DbType) .

Esto no se aplica a otros valores int, por lo que el SqlParameter(string, object) es el más específico.

Al resolver el tipo de un método sobrecargado, C # selecciona la opción más específica. La clase SqlParameter tiene dos constructores que toman exactamente dos argumentos, SqlParameter(String, SqlDbType) y SqlParameter(String, Object) . Cuando proporciona el 0 literal, puede interpretarse como un Objeto o como un SqlDbType. Como SqlDbType es más específico que Object, se supone que es la intención.

Puede leer más sobre la resolución de sobrecarga en esta respuesta .