expressions - Reflejando el nombre del parámetro: ¿abuso de las expresiones lambda de C#o brillo de Sintaxis?
linq lambda c# (21)
Estoy mirando el componente Grid de MvcContrib y estoy fascinado, pero al mismo tiempo rechazado por un truco sintáctico utilizado en la sintaxis de Grid :
.Attributes(style => "width:100%")
La sintaxis anterior establece el atributo de estilo del HTML generado en width:100%
. Ahora, si presta atención, el ''estilo'' no se especifica en ninguna parte, ¡se deduce del nombre del parámetro en la expresión! Tuve que profundizar en esto y encontré donde ocurre la ''magia'':
Hash(params Func<object, TValue>[] hash)
{
foreach (var func in hash)
{
Add(func.Method.GetParameters()[0].Name, func(null));
}
}
Entonces, de hecho, el código utiliza el nombre formal, el tiempo de compilación, el nombre de los parámetros para crear el diccionario de pares nombre-valor de atributo. La construcción de sintaxis resultante es muy expresiva, pero al mismo tiempo muy peligrosa. El uso general de las expresiones lambda permite la sustitución de los nombres utilizados sin efectos secundarios. Veo un ejemplo en un libro que dice collection.ForEach(book => Fire.Burn(book))
Sé que puedo escribir en mi collection.ForEach(log => Fire.Burn(log))
y significa lo mismo cosa ¡Pero con la sintaxis de la cuadrícula de MvcContrib aquí de repente, encuentro un código que se ve activamente y toma decisiones en base a los nombres que elijo para mis variables!
Entonces, ¿esta práctica común con la comunidad C # 3.5 / 4.0 y los amantes de las expresiones lambda? ¿O es un rebelde truco que no debería preocuparme?
¿Puedo usar esto para acuñar una frase?
lambda mágica (n): una función lambda utilizada únicamente para reemplazar una cadena mágica.
¿Qué está mal con lo siguiente:
html.Attributes["style"] = "width:100%";
Ambos. Es un abuso de las expresiones lambda y el brillo de la sintaxis.
Bienvenido a Rails Land :)
Realmente no hay nada de malo en esto mientras sepas lo que está pasando. (Es cuando este tipo de cosas no están bien documentadas que hay un problema).
La totalidad del marco de Rails se basa en la idea de la convención sobre la configuración. Nombrar las cosas de una manera determinada lo convierte en una convención que están usando y obtienes una gran cantidad de funciones de forma gratuita. Seguir la convención de nombres te lleva a donde vas más rápido. Todo funciona de manera brillante.
Otro lugar donde he visto un truco como este es en las llamadas de método en Moq. Se pasa una lambda, pero la lambda nunca se ejecuta. Simplemente usan la expresión para asegurarse de que se realizó la llamada al método y, si no, lanzan una excepción.
Casi nunca encontré este tipo de uso. Creo que es "inapropiado" :)
Esta no es una forma común de uso, es inconsistente con las convenciones generales. Este tipo de sintaxis tiene sus pros y sus contras, por supuesto:
Contras
- El código no es intuitivo (las convenciones habituales son diferentes)
- Tiende a ser frágil (el cambio de nombre del parámetro romperá la funcionalidad).
- Es un poco más difícil de probar (falsificar la API requerirá el uso de la reflexión en las pruebas).
- Si la expresión se usa de forma intensiva, será más lenta debido a la necesidad de analizar el parámetro y no solo el valor (costo de reflexión)
Pros
- Es más legible después de que el desarrollador se haya ajustado a esta sintaxis.
Línea inferior : en el diseño de API pública, habría elegido una forma más explícita.
Creo que esto no es mejor que "cuerdas mágicas". Tampoco soy un fan de los tipos anónimos para esto. Necesita un enfoque mejor y fuertemente tipado.
El código es muy inteligente, pero potencialmente causa más problemas que resuelve.
Como ha señalado, ahora hay una oscura dependencia entre el nombre del parámetro (estilo) y un atributo HTML. No se realiza ninguna comprobación de tiempo de compilación. Si el nombre del parámetro está mal escrito, es probable que la página no tenga un mensaje de error de tiempo de ejecución, pero sí un error lógico mucho más difícil de encontrar (no error, pero comportamiento incorrecto).
Una mejor solución sería tener un miembro de datos que pueda verificarse en el momento de la compilación. Así que en lugar de esto:
.Attributes(style => "width:100%");
El compilador podría verificar el código con una propiedad de estilo:
.Attributes.Style = "width:100%";
o incluso:
.Attributes.Style.Width.Percent = 100;
Eso es más trabajo para los autores del código, pero este enfoque aprovecha la fuerte capacidad de verificación de tipos de C #, que ayuda a evitar que los errores ingresen al código en primer lugar.
En mi humilde opinión, es una buena manera de hacerlo. A todos nos encanta el hecho de que nombrar un controlador de clase lo convertirá en un controlador en MVC, ¿verdad? Así que hay casos en que el nombramiento no importa.
También la intención es muy clara aquí. Es muy fácil de entender que .Attribute( book => "something")
dará como resultado book="something"
y .Attribute( log => "something")
dará como resultado log="something"
Supongo que no debería ser un problema si lo tratas como una convención. Soy de la opinión de que cualquier cosa que te haga escribir menos código y que la intención sea obvia es algo bueno.
En mi opinión es abuso de los lambdas.
En cuanto a la brillantez de la sintaxis, encuentro que el style=>"width:100%"
confuso. Particularmente debido a la =>
lugar de =
Encuentro eso extraño no tanto por el nombre , sino porque la lambda es innecesaria ; podría usar un tipo anónimo y ser más flexible:
.Attributes(new { style = "width:100%", @class="foo", blip=123 });
Este es un patrón utilizado en gran parte de ASP.NET MVC (por ejemplo), y tiene otros usos (una caveat , tenga en cuenta también los pensamientos de Ayende si el nombre es un valor mágico en lugar de un llamante específico)
Es un enfoque interesante. Si restringe el lado derecho de la expresión para que sea solo constantes, entonces podría implementar usando
Expression<Func<object, string>>
Creo que es lo que realmente quieres en lugar del delegado (estás usando la lambda para obtener nombres de ambos lados). Ver la implementación ingenua a continuación:
public static IDictionary<string, string> Hash(params Expression<Func<object, string>>[] hash) {
Dictionary<string, string> values = new Dictionary<string,string>();
foreach (var func in hash) {
values[func.Parameters[0].Name] = ((ConstantExpression)func.Body).Value.ToString();
}
return values;
}
Esto podría incluso abordar la preocupación de interoperabilidad entre idiomas que se mencionó anteriormente en el hilo.
Este es uno de los beneficios de los árboles de expresión: se puede examinar el código en sí para obtener información adicional. Así es como .Where(e => e.Name == "Jamie")
se puede convertir en la cláusula equivalente de SQL Where. Este es un uso inteligente de los árboles de expresión, aunque espero que no vaya más allá de esto. Es probable que cualquier cosa más compleja sea más difícil que el código que espera reemplazar, por lo que sospecho que será autolimitante.
Esto es horrible en más de un nivel. Y no, esto no se parece en nada a Ruby. Es un abuso de C # y .Net.
Ha habido muchas sugerencias de cómo hacer esto de una manera más directa: tuplas, tipos anónimos, una interfaz fluida, etc.
Lo que lo hace tan malo es que es simplemente una manera de imaginar por su propio bien:
¿Qué pasa cuando necesitas llamar a esto desde VB?
.Attributes(Function(style) "width:100%")
Es totalmente contrario a la intuición e inteligencia proporciona poca ayuda para descubrir cómo pasar las cosas.
Es innecesariamente ineficiente.
Nadie tendrá idea de cómo mantenerlo.
¿Cuál es el tipo de argumento que va a los atributos, es
Func<object,string>
? ¿Cómo está revelando esa intención? ¿Qué va a decir su documentación de inteligencia? "No tenga en cuenta todos los valores del objeto"
Creo que estás completamente justificado por tener esos sentimientos de repulsión.
Esto tiene pobre interoperabilidad. Por ejemplo, considere este ejemplo C # - F #
DO#:
public class Class1
{
public static void Foo(Func<object, string> f)
{
Console.WriteLine(f.Method.GetParameters()[0].Name);
}
}
F#:
Class1.Foo(fun yadda -> "hello")
Resultado:
Se imprime "arg" (no "yadda").
Como resultado, los diseñadores de bibliotecas deben evitar este tipo de ''abusos'', o al menos proporcionar una sobrecarga ''estándar'' (por ejemplo, que toma el nombre de la cadena como un parámetro adicional) si quieren tener una buena interoperabilidad en los lenguajes .Net.
Estoy en el campo de "sintaxis brillante", si lo documentan claramente, y se ve muy bien, ¡casi no hay problema con eso!
No, ciertamente no es una práctica común. Es contrario a la intuición, no hay manera de mirar el código para averiguar qué hace. Tienes que saber cómo se usa para entender cómo se usa.
En lugar de proporcionar atributos utilizando una matriz de delegados, los métodos de encadenamiento serían más claros y con mejor rendimiento:
.Attribute("style", "width:100%;").Attribute("class", "test")
Aunque esto es un poco más para escribir, es claro e intuitivo.
Si los nombres de los métodos (func) están bien elegidos, entonces esta es una manera brillante de evitar problemas de mantenimiento (es decir, agregar una nueva función, pero se olvidó de agregarlo a la lista de asignación de parámetros de función). Por supuesto, necesita documentarlo en gran medida y será mejor que genere automáticamente la documentación para los parámetros de la documentación para las funciones en esa clase ...
Solo quería expresar mi opinión (soy el autor del componente de cuadrícula MvcContrib).
Esto definitivamente es un abuso del lenguaje, no hay duda de ello. Sin embargo, realmente no lo consideraría muy intuitivo, cuando nos fijamos en una llamada a los Attributes(style => "width:100%", @class => "foo")
Creo que es bastante obvio lo que está pasando (ciertamente no es peor que el enfoque de tipo anónimo). Desde una perspectiva inteligente, estoy de acuerdo en que es bastante opaco.
Para aquellos interesados, alguna información de fondo sobre su uso en MvcContrib ...
Agregué esto a la cuadrícula como una preferencia personal: no me gusta el uso de tipos anónimos como diccionarios (tener un parámetro que toma "objeto" es tan opaco como uno que toma parámetros Func []) y el inicializador de la colección del diccionario es bastante detallado (tampoco soy un fanático de las interfaces fluidas, por ejemplo, tener que encadenar varias llamadas a un atributo ("estilo", "mostrar: ninguna"). Atributo ("clase", "foo"), etc.)
Si C # tuviera una sintaxis menos detallada para los literales del diccionario, entonces no me hubiera molestado en incluir esta sintaxis en el componente de la cuadrícula :)
También quiero señalar que el uso de esto en MvcContrib es completamente opcional: estos son métodos de extensión que envuelven sobrecargas que toman un IDictionary en su lugar. Creo que es importante que si proporciona un método como este también debería apoyar un enfoque más "normal", por ejemplo, para interoperar con otros idiomas.
Además, alguien mencionó la ''sobrecarga de reflexión'' y solo quería señalar que realmente no hay mucha sobrecarga con este enfoque: no hay una reflexión de tiempo de ejecución o compilación de expresiones involucrada (consulte http://blog.bittercoder.com/PermaLink,guid,206e64d1-29ae-4362-874b-83f5b103727f.aspx ).
Todo este repique sobre "horror" es un grupo de chicos de C # que han reaccionado de forma exagerada (y yo soy un programador de C # desde hace mucho tiempo y todavía soy un gran fanático del lenguaje). No hay nada horrible en esta sintaxis. Es simplemente un intento de hacer que la sintaxis se parezca más a lo que estás tratando de expresar. Cuanto menos "ruido" haya en la sintaxis de algo, más fácilmente lo podrá entender el programador. Disminuir el ruido en una línea de código solo ayuda un poco, pero deje que se acumule a través de cada vez más código, y que se convierta en un beneficio sustancial.
Este es un intento del autor por esforzarse por los mismos beneficios que le brinda DSL: cuando el código "parece" lo que intenta decir, ha alcanzado un lugar mágico. Puede debatir si esto es bueno para interoperabilidad, o si es lo suficientemente mejor que los métodos anónimos para justificar parte del costo de la "complejidad". Justo lo suficiente ... por lo que en su proyecto debe tomar la decisión correcta de usar este tipo de sintaxis. Pero aún así ... este es un intento inteligente de un programador para hacer lo que, al final del día, todos intentamos hacer (nos demos cuenta o no). Y lo que todos intentamos hacer es lo siguiente: "Dígale a la computadora lo que queremos que haga en un lenguaje lo más cercano posible a cómo pensamos acerca de lo que queremos que haga".
Acercarnos a expresar nuestras instrucciones a las computadoras de la misma manera que pensamos internamente es una clave para hacer que el software sea más fácil de mantener y más preciso.
EDITAR: Yo había dicho "la clave para hacer que el software sea más fácil de mantener y más preciso", que es un poco exageradamente loca de unicornio. Lo cambié a "una llave".
Yo preferiría
Attributes.Add(string name, string value);
Es mucho más explícito y estándar y no se gana nada con el uso de lambdas.
de hecho, parece que Ruby =), al menos para mí el uso de un recurso estático para una "búsqueda" dinámica posterior no se ajusta a las consideraciones de diseño de la API, espero que este truco inteligente sea opcional en esa API.
Podríamos heredar de IDictionary (o no) y proporcionar un indexador que se comporte como una matriz php cuando no necesite agregar una clave para establecer un valor. Será un uso válido de la semántica .net, no solo de c #, y aún necesitará documentación.
espero que esto ayude