Aunque las otras respuestas son correcta, en el sentido de que hacen declaraciones verdaderas y relevantes, aquí hay algunos puntos sutiles de diseño del lenguaje que aún no se han expresado. Muchos factores diferentes contribuyen al diseño actual del operador condicional.
Primero, es deseable que tantas expresiones como sea posible tengan un tipo no ambiguo que pueda determinarse únicamente a partir del contenido de la expresión. Esto es deseable por varias razones. Por ejemplo: hace que construir un motor IntelliSense sea mucho más fácil. Escribe x.M(some-expression.
e IntelliSense necesita poder analizar alguna expresión, determinar su tipo y generar un menú desplegable ANTES de que IntelliSense sepa a qué método se refiere x.M. IntelliSense no puede saber a qué se refiere X.M con seguridad si M está sobrecargado hasta que ve todos los argumentos, pero aún no ha escrito el primer argumento.
En segundo lugar, preferimos que la información de tipo fluya "de adentro hacia afuera", precisamente por el escenario que acabo de mencionar: resolución de sobrecarga. Considere lo siguiente:
void M(object x) {}
void M(int x) {}
void M(string x) {}
...
M(b ? 1 : "hello");
¿Qué debe hacer esto? ¿Debería llamar a la sobrecarga del objeto? ¿Debería a veces llamar a la sobrecarga de cadena y a veces llamar a la sobrecarga int? ¿Qué sucede si tiene otra sobrecarga, por ejemplo, M(IComparable x)
, cuando la elige?
Las cosas se vuelven muy complicadas cuando la información del tipo "fluye en ambos sentidos". Diciendo "Estoy asignando esto a una variable de tipo objeto, por lo tanto, el compilador debe saber que está bien elegir objeto, ya que el tipo" no se lava; a menudo es el caso que no conocemos el tipo de la variable que estamos asignando porque eso es lo que estamos intentando descifrar. La resolución de sobrecarga es exactamente el proceso de calcular los tipos de parámetros, que son las variables a las que se asignan los argumentos, a partir de los tipos de argumentos. Si los tipos de los argumentos dependen de los tipos a los que se les asigna, entonces tenemos una circularidad en nuestro razonamiento.
La información de tipo "fluye en ambos sentidos" para las expresiones lambda; implementar eso de manera eficiente me llevó la mejor parte de un año. He escrito una larga serie de artículos que describen algunas de las dificultades al diseñar e implementar un compilador que puede hacer análisis donde el tipo de información fluye en expresiones complejas basadas en el contexto en el que posiblemente se usa la expresión; la primera parte está aquí:
http://blogs.msdn.com/ericlippert/archive/2007/01/10/lambda-expressions-vs-anonymous-methods-part-one.aspx
se podría decir "bueno, vale, no veo por qué el hecho de que estoy asignando a objeto no puede ser utilizado con seguridad por el compilador, y no veo por qué es necesario para la expresión para tener un tipo inequívoco, pero ¿por qué no es el tipo de objeto de expresión, ya que tanto int como string son convertibles a objeto?"Esto me lleva a mi tercer punto:
En tercer lugar, uno de los principios de diseño sutiles pero consistentes de C# es" no producir tipos por magia ". Cuando se le presente una lista de expresiones de la cual debemos determinar tipo, el tipo que determinamos está siempre en la lista en algún lugar. Nunca inventamos un nuevo tipo y lo elegimos para usted, el tipo que obtiene es siempre uno que usted nos dio para elegir. Si usted dice encontrar el mejor escriba un conjunto de tipos, encontramos el mejor tipo EN ese conjunto de tipos. En el conjunto {int, cadena}, no hay mejor tipo común, la forma en que está, por ejemplo, "Animal, Tortuga, Mamífero, Wallaby" "Esta decisión de diseño se aplica al operador condicional, para escribir escenarios de unificación inferencial, para la inferencia de tipos de matriz tipados implícitamente, y así sucesivamente.
El motivo de esta decisión de diseño es que hace que sea más fácil para los seres humanos normales determinar qué hará el compilador en cualquier situación en la que se deba determinar el mejor tipo; si sabes que se va a elegir un tipo que está justo allí, mirándote a la cara, entonces es mucho más fácil averiguar lo que va a suceder.
También nos evita tener que elaborar muchas reglas complejas sobre cuál es el mejor tipo común de un conjunto de tipos cuando hay conflictos. Supongamos que tiene tipos {Foo, Bar}, donde ambas clases implementan IBlah, y ambas clases heredan de Baz. ¿Cuál es el mejor tipo común, IBlah, que ambos implementan, o Baz, que ambos extienden? No queremos tener que responder esta pregunta; queremos evitarlo por completo
Finalmente, observo que el compilador de C# en realidad obtiene la determinación de los tipos sutilmente incorrectos en algunos casos poco claros. Mi primer artículo sobre el que está aquí:
http://blogs.msdn.com/ericlippert/archive/2006/05/24/type-inference-woes-part-one.aspx
Es discutible que, de hecho, el compilador lo hace bien y la especificación es incorrecta; el diseño de implementación es en mi opinión mejor que el diseño específico.
De todos modos, eso es solo algunas de las razones para el diseño de este aspecto particular del operador ternario. Hay otras sutilezas aquí, por ejemplo, cómo el verificador CLR determina si se garantiza que un conjunto dado de rutas de bifurcación deja el tipo correcto en la pila en todas las rutas posibles. Discutir eso en detalle me llevaría bastante lejos.
Puedes corregir el error tipográfico en el título de tu pregunta, gracias. No tengo suficiente reputación para editarlo yo mismo. –
Gracias por la corrección –
En su segundo ejemplo, podría escribir: Value = subscriptionID ?? DBNull.Value; –