c# - uso - ¿Cómo puedo determinar de manera confiable el tipo de variable que se declara utilizando var en tiempo de diseño?
tipos de variables en java netbeans (8)
Estoy trabajando en una instalación de terminación (intellisense) para C # en emacs.
La idea es que, si un usuario escribe un fragmento y luego solicita completarlo mediante una combinación de teclas determinada, el recurso de finalización utilizará la reflexión .NET para determinar las posibles terminaciones.
Hacer esto requiere que se conozca el tipo de cosa que se está completando. Si es una cadena, hay un conjunto conocido de métodos y propiedades posibles; si es un Int32, tiene un conjunto separado, y así sucesivamente.
Utilizando la semántica, un paquete de código lexer / analizador disponible en emacs, puedo ubicar las declaraciones de variables y sus tipos. Dado eso, es sencillo usar la reflexión para obtener los métodos y las propiedades del tipo, y luego presentar la lista de opciones al usuario. (Bueno, no es muy fácil hacerlo dentro de emacs, pero usando la capacidad de ejecutar un proceso de PowerShell dentro de emacs , se vuelve mucho más fácil. Escribo un ensamblado .NET personalizado para hacer un reflejo, cargarlo en el powershell y luego ejecutarlo dentro de emacs puede enviar comandos a powershell y leer respuestas, a través de comint. Como resultado, emacs puede obtener los resultados de la reflexión rápidamente).
El problema llega cuando el código usa var
en la declaración de lo que se está completando. Eso significa que el tipo no está explícitamente especificado, y la finalización no funcionará.
¿Cómo puedo determinar de manera confiable el tipo real utilizado, cuando la variable se declara con la palabra clave var
? Para que quede claro, no necesito determinarlo en tiempo de ejecución. Quiero determinarlo en "Tiempo de diseño".
Hasta ahora tengo estas ideas:
- compilar e invocar:
- extraer la declaración declaración, por ejemplo `var foo =" un valor de cadena ";`
- concatenar una declaración `foo.GetType ();`
- compilar dinámicamente el fragmento de C # resultante en un nuevo ensamblaje
- cargue el ensamblado en un nuevo AppDomain, ejecute el encuadre y obtenga el tipo de devolución.
- descargar y descartar el ensamblaje
Sé cómo hacer todo esto. Pero suena muy pesado, por cada solicitud de finalización en el editor.
Supongo que no necesito un nuevo AppDomain nuevo cada vez. Podría volver a utilizar un único dominio de aplicación para varios ensambles temporales y amortizar el costo de configurarlo y desmantelarlo en varias solicitudes de finalización. Eso es más un ajuste de la idea básica.
- compilar e inspeccionar IL
Simplemente compile la declaración en un módulo, y luego inspeccione el IL, para determinar el tipo real que fue inferido por el compilador. ¿Cómo podría ser esto posible? ¿Qué usaría para examinar el IL?
Alguna mejor idea por ahí? ¿Comentarios? sugerencias?
EDITAR - pensar en esto más, compilar e invocar no es aceptable, porque la invocación puede tener efectos secundarios. Entonces la primera opción debe ser descartada.
Además, creo que no puedo asumir la presencia de .NET 4.0.
ACTUALIZACIÓN - La respuesta correcta, no mencionada anteriormente, pero cuidadosamente señalada por Eric Lippert, es implementar un sistema de inferencia de tipo de fidelidad completa. Es la única manera de determinar de manera confiable el tipo de var en el momento del diseño. Pero, tampoco es fácil de hacer. Como no me hago ilusiones de que intente construir tal cosa, tomé el atajo de la opción 2: extraje el código de declaración relevante, lo compilo y luego inspecciono el IL resultante.
Esto realmente funciona, para un subconjunto justo de los escenarios de finalización.
Por ejemplo, supongamos en los siguientes fragmentos de código, el? es la posición en la que el usuario solicita la finalización. Esto funciona:
var x = "hello there";
x.?
La finalización se da cuenta de que x es una cadena y proporciona las opciones adecuadas. Lo hace al generar y luego compilar el siguiente código fuente:
namespace N1 {
static class dmriiann5he { // randomly-generated class name
static void M1 () {
var x = "hello there";
}
}
}
... y luego inspeccionando el IL con simple reflejo.
Esto también funciona:
var x = new XmlDocument();
x.?
El motor agrega las cláusulas de uso apropiadas al código fuente generado, para que se compile correctamente, y luego la inspección IL es la misma.
Esto funciona, también:
var x = "hello";
var y = x.ToCharArray();
var z = y.?
Simplemente significa que la inspección IL debe encontrar el tipo de la tercera variable local, en lugar de la primera.
Y esto:
var foo = "Tra la la";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var x = z.?
... que es solo un nivel más profundo que el ejemplo anterior.
Pero lo que no funciona es completar cualquier variable local cuya inicialización dependa en cualquier punto de un miembro de instancia o argumento de método local. Me gusta:
var foo = this.InstanceMethod();
foo.?
Ni la sintaxis LINQ.
Tendré que pensar en lo valiosas que son esas cosas antes de considerar abordarlas a través de lo que definitivamente es un "diseño limitado" (palabra educada para hack) para completar.
Un enfoque para abordar el problema con las dependencias de los argumentos de métodos o métodos de instancia sería reemplazar, en el fragmento de código que se genera, compila y luego analiza IL, las referencias a esas cosas con variables locales "sintéticas" del mismo tipo.
Otra actualización : completar en vars que dependen de los miembros de la instancia, ahora funciona.
Lo que hice fue interrogar el tipo (a través de la semántica), y luego generar miembros suplentes sintéticos para todos los miembros existentes. Para un buffer C # como este:
public class CsharpCompletion
{
private static int PrivateStaticField1 = 17;
string InstanceMethod1(int index)
{
...lots of code here...
return result;
}
public void Run(int count)
{
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String>
{
foo,
foo.Length.ToString()
};
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
var fff = nnn.?
...more code here...
... el código generado que se compila, para que pueda aprender de la salida IL el tipo de varnnn local, se ve así:
namespace Nsbwhi0rdami {
class CsharpCompletion {
private static int PrivateStaticField1 = default(int);
string InstanceMethod1(int index) { return default(string); }
void M0zpstti30f4 (int count) {
var foo = "this is a string";
var fred = new System.Collections.Generic.List<String> { foo, foo.Length.ToString() };
var z = fred.Count;
var mmm = count + z + CsharpCompletion.PrivateStaticField1;
var nnn = this.InstanceMethod1(mmm);
}
}
}
Todos los miembros de instancia y de tipo estático están disponibles en el código de esqueleto. Compila con éxito. En ese punto, determinar el tipo de var local es sencillo a través de Reflection.
Lo que hace esto posible es:
- la capacidad de ejecutar powershell en emacs
- el compilador de C # es realmente rápido. En mi máquina, toma alrededor de 0.5 segundos para compilar un ensamblaje en memoria. No lo suficientemente rápido para el análisis entre pulsaciones de teclas, pero lo suficientemente rápido como para admitir la generación a demanda de listas de finalización.
No he investigado LINQ todavía.
Ese será un problema mucho mayor porque el semántico lexer / analizador emacs tiene para C #, no "hace" LINQ.
Dado que tiene como objetivo Emacs, puede ser mejor comenzar con la suite CEDET. Todos los detalles que Eric Lippert están cubiertos en el analizador de código en la herramienta CEDET / Semántica para C ++ ya. También hay un analizador C # (que probablemente necesita un poco de TLC) por lo que las únicas partes que faltan están relacionadas con ajustar las partes necesarias para C #.
Los comportamientos básicos se definen en los algoritmos principales que dependen de las funciones sobrecargables que se definen por idioma. El éxito del motor de finalización depende de cuánto ajuste se haya realizado. Con c ++ como guía, obtener soporte similar a C ++ no debería ser tan malo.
La respuesta de Daniel sugiere usar MonoDevelop para hacer análisis sintáctico y análisis. Esto podría ser un mecanismo alternativo en lugar del analizador C # existente, o podría usarse para aumentar el analizador existente.
Es un problema difícil hacerlo bien. Básicamente, debe modelar la especificación / compilador de idioma a través de la mayoría de las operaciones de lexing / parsing / typechecking y crear un modelo interno del código fuente que luego puede consultar. Eric lo describe en detalle para C #. Siempre puedes descargar el código fuente del compilador F # (parte del F # CTP) y echar un vistazo a service.fsi
para ver la interfaz expuesta fuera del compilador F # que consume el servicio de lenguaje F # para proporcionar intellisense, información sobre herramientas para tipos inferidos, etc. Da la sensación de una posible ''interfaz'' si ya tenías el compilador disponible como una API para llamar.
La otra ruta es volver a utilizar los compiladores tal como está describiendo, y luego usar la reflexión o mirar el código generado. Esto es problemático desde el punto de vista de que necesita ''programas completos'' para obtener un resultado de compilación de un compilador, mientras que al editar el código fuente en el editor, a menudo solo tiene ''programas parciales'' que aún no se analizan, no lo hacen tener todos los métodos implementados, etc.
En resumen, creo que la versión de ''bajo presupuesto'' es muy difícil de hacer bien, y la versión ''real'' es muy, muy difícil de hacer bien. (Donde "difícil" aquí mide tanto "esfuerzo" como "dificultad técnica").
Los sistemas Intellisense típicamente representan el código usando un Árbol de sintaxis abstracto, que les permite resolver el tipo de retorno de la función que se le asigna a la variable ''var'' más o menos de la misma manera que lo hará el compilador. Si usa VS Intellisense, puede notar que no le dará el tipo de var hasta que haya terminado de ingresar una expresión de asignación válida (resoluble). Si la expresión aún es ambigua (por ejemplo, no puede inferir por completo los argumentos genéricos para la expresión), el tipo var no se resolverá. Este puede ser un proceso bastante complejo, ya que podría necesitar caminar bastante profundo dentro de un árbol para resolver el tipo. Por ejemplo:
var items = myList.OfType<Foo>().Select(foo => foo.Bar);
El tipo de devolución es IEnumerable<Bar>
, pero al resolver esto se requiere saber:
- myList es del tipo que implementa
IEnumerable
. - Existe un método de extensión
OfType<T>
que se aplica a IEnumerable. - El valor resultante es
IEnumerable<Foo>
y hay un método de extensiónSelect
que se aplica a esto. - La expresión lambda
foo => foo.Bar
tiene el parámetro foo de tipo Foo. Esto se infiere del uso de Select, que toma unFunc<TIn,TOut>
y dado que se conoce a TIn (Foo), se puede inferir el tipo de foo. - El tipo Foo tiene una barra de propiedades, que es de tipo Bar. Sabemos que Select devuelve
IEnumerable<TOut>
y TOut se pueden deducir del resultado de la expresión lambda, por lo que el tipo resultante de elementos debe serIEnumerable<Bar>
.
Para la solución "1" tiene una nueva instalación en .NET 4 para hacer esto de manera rápida y fácil. Entonces, si puede convertir su programa a .NET 4, sería su mejor opción.
Puedo decir más o menos cómo el Delphi IDE funciona con el compilador Delphi para hacer intellisense (la visión del código es lo que Delphi llama). No es 100% aplicable a C #, pero es un enfoque interesante que merece consideración.
La mayoría del análisis semántico en Delphi se hace en el analizador mismo. Las expresiones se escriben a medida que se analizan, excepto en situaciones en las que esto no es fácil, en cuyo caso el análisis de anticipación se utiliza para determinar qué se pretende y, a continuación, se utiliza esa decisión en el análisis.
El análisis es en gran parte LL (2) descenso recursivo, a excepción de las expresiones, que se analizan utilizando la precedencia del operador. Una de las cosas distintivas de Delphi es que es un lenguaje de un solo pase, por lo que los constructos deben declararse antes de ser utilizados, por lo que no se necesita un pase de nivel superior para sacar esa información.
Esta combinación de características significa que el analizador tiene aproximadamente toda la información necesaria para la comprensión del código para cualquier punto donde sea necesario. La forma en que funciona es la siguiente: el IDE informa al lexer del compilador de la posición del cursor (el punto donde se desea obtener información del código) y el lexer lo convierte en un token especial (se llama token de kibitz). Cada vez que el analizador se encuentra con este token (que podría estar en cualquier lugar), sabe que esta es la señal para devolver toda la información que tiene al editor. Hace esto usando longjmp porque está escrito en C; lo que hace es notificar al último que llama del tipo de constructo sintáctico (es decir, el contexto gramatical) en el que se encontró el punto kibitz, así como todas las tablas simbólicas necesarias para ese punto. Entonces, por ejemplo, si el contexto está en una expresión que es un argumento para un método, podemos verificar las sobrecargas del método, mirar los tipos de argumentos y filtrar los símbolos válidos solo a aquellos que pueden resolver ese tipo de argumento (esto reduce mucho crumble irrelevante en el menú desplegable). Si está en un contexto de ámbito anidado (por ejemplo, después de un "."), El analizador habrá devuelto una referencia al alcance, y el IDE podrá enumerar todos los símbolos encontrados en ese ámbito.
Otras cosas también se hacen; por ejemplo, los cuerpos de método se omiten si el token kibitz no está dentro de su rango; esto se hace de forma optimista y se retrotrae si se salta el token. El equivalente de los métodos de extensión - ayudantes de clase en Delphi - tiene un tipo de caché versionado, por lo que su búsqueda es razonablemente rápida. Pero la inferencia del tipo genérico de Delphi es mucho más débil que la de C #.
Ahora, a la pregunta específica: inferir los tipos de variables declaradas con var
es equivalente a la forma en que Pascal infiere el tipo de constantes. Viene del tipo de la expresión de inicialización. Estos tipos se construyen de abajo hacia arriba. Si x
es de tipo Integer
, e y
es de tipo Double
, entonces x + y
será de tipo Double
, porque esas son las reglas del lenguaje; etc. Siga estas reglas hasta que tenga un tipo para la expresión completa en el lado derecho, y ese es el tipo que usa para el símbolo de la izquierda.
Puedo describirte cómo lo hacemos de manera eficiente en el C # IDE "real".
Lo primero que hacemos es ejecutar un pase que analiza solo las cosas de "nivel superior" en el código fuente. Saltamos todos los cuerpos del método. Eso nos permite crear rápidamente una base de datos de información sobre qué espacio de nombres, tipos y métodos (y constructores, etc.) se encuentran en el código fuente del programa. Analizar cada línea de código en cada cuerpo del método tomaría demasiado tiempo si está tratando de hacerlo entre pulsaciones de teclas.
Cuando el IDE necesita resolver el tipo de una expresión particular dentro de un cuerpo de método, digamos que ha escrito "foo". y tenemos que averiguar cuáles son los miembros de foo; hacemos lo mismo; omitimos todo el trabajo que razonablemente podamos.
Comenzamos con un pase que analiza solo las declaraciones de variables locales dentro de ese método. Cuando ejecutamos ese pase hacemos una asignación de un par de "alcance" y "nombre" a un "determinador de tipo". El "determinador de tipo" es un objeto que representa la noción de "Puedo resolver el tipo de este local si es necesario". Calcular el tipo de un local puede ser costoso, por lo que queremos diferir ese trabajo si es necesario.
Ahora tenemos una base de datos construida de forma lenta que nos puede decir el tipo de cada local. Entonces, volviendo a ese "foo". - averiguamos en qué enunciado está la expresión relevante y luego ejecutamos el analizador semántico en contra de esa afirmación. Por ejemplo, supongamos que tienes el cuerpo del método:
String x = "hello";
var y = x.ToCharArray();
var z = from foo in y where foo.
y ahora tenemos que pensar que foo es de tipo char. Construimos una base de datos que tiene todos los metadatos, métodos de extensión, tipos de código fuente, etc. Construimos una base de datos que tiene determinantes de tipo para x, y y z. Analizamos la declaración que contiene la expresión interesante. Comenzamos por transformarlo sintácticamente a
var z = y.Where(foo=>foo.
Para resolver el tipo de foo, primero debemos saber el tipo de y. Entonces, en este punto, preguntamos al determinador de tipo "¿cuál es el tipo de y"? A continuación, inicia un evaluador de expresiones que analiza x.ToCharArray () y pregunta "¿cuál es el tipo de x"? Tenemos un determinador de tipo para lo que dice "Necesito buscar" Cadena "en el contexto actual". No hay ningún tipo de cadena en el tipo actual, por lo que buscamos en el espacio de nombres. Tampoco está allí, así que buscamos en las directivas de uso y descubrimos que hay un "sistema que usa" y que el sistema tiene un tipo de cadena. OK, entonces ese es el tipo de x.
Luego consultamos los metadatos de System.String para el tipo de ToCharArray y dice que es un System.Char []. Súper. Entonces tenemos un tipo para y.
Ahora preguntamos "¿System.Char [] tiene un método ¿Dónde?" No. Entonces buscamos en las directivas de uso; ya hemos precalculado una base de datos que contiene todos los metadatos para los métodos de extensión que posiblemente podrían usarse.
Ahora decimos "OK, hay dieciocho docenas de métodos de extensión llamados Where in scope, ¿alguno de ellos tiene un primer parámetro formal cuyo tipo es compatible con System.Char []?" Entonces comenzamos una ronda de pruebas de convertibilidad. Sin embargo, los métodos de extensión Where son genéricos , lo que significa que tenemos que hacer una inferencia de tipo.
He escrito un motor especial que no funciona y que puede manejar inferencias incompletas desde el primer argumento hasta un método de extensión. Ejecutamos el tipo inferir y descubrimos que hay un método Where que toma un IEnumerable<T>
, y que podemos hacer una inferencia de System.Char [] a IEnumerable<System.Char>
, entonces T es System.Char.
La firma de este método es Where<T>(this IEnumerable<T> items, Func<T, bool> predicate)
, y sabemos que T es System.Char. También sabemos que el primer argumento dentro del paréntesis del método de extensión es un lambda. Entonces comenzamos un inferidor de tipo de expresión lambda que dice "el parámetro formal foo se supone que es System.Char", use este hecho cuando analice el resto de la lambda.
Ahora tenemos toda la información que necesitamos para analizar el cuerpo de la lambda, que es "foo". Buscamos el tipo de foo, descubrimos que según el enlazador lambda es System.Char, y hemos terminado; mostramos información de tipo para System.Char.
Y hacemos todo menos el análisis de "nivel superior" entre las pulsaciones de teclas . Ese es el verdadero truco. En realidad, escribir todo el análisis no es difícil; lo está haciendo lo suficientemente rápido como para hacerlo a la velocidad de tipeo que es el verdadero truco.
¡Buena suerte!
Si no desea tener que escribir su propio analizador para construir el árbol de sintaxis abstracta, podría considerar usar los analizadores de SharpDevelop o MonoDevelop , ambos de código abierto.
NRefactory hará esto por usted.