10

Tengo un programa escrito con sintaxis fluida. La cadena de métodos tiene un "final" definitivo, antes del cual nada útil realmente se hace en el código (piense que la generación de consultas de NBuilder o Linq-to-SQL no llega a la base de datos hasta iterar sobre nuestros objetos con, digamos, ToList())¿Cómo hacer valer el uso del valor de retorno de un método en C#?

El problema que tengo es que hay confusión entre otros desarrolladores sobre el uso correcto del código. Están descuidando el método de "finalización" (¡y nunca "haciendo nada")!

Estoy interesado en aplicando el uso del valor de retorno de algunos de mis métodos para que nunca podamos "terminar la cadena" sin llamar a ese método "Finalizar()" o "Guardar()" que realmente la obra.

Considere el siguiente código:

//The "factory" class the user will be dealing with 
public class FluentClass 
{ 
    //The entry point for this software 
    public IntermediateClass<T> Init<T>() 
    { 
     return new IntermediateClass<T>(); 
    } 
} 

//The class that actually does the work 
public class IntermediateClass<T> 
{ 
    private List<T> _values; 

    //The user cannot call this constructor 
    internal IntermediateClass<T>() 
    { 
     _values = new List<T>(); 
    } 

    //Once generated, they can call "setup" methods such as this 
    public IntermediateClass<T> With(T value) 
    { 
     var instance = new IntermediateClass<T>() { _values = _values }; 
     instance._values.Add(value); 
     return instance; 
    } 

    //Picture "lazy loading" - you have to call this method to 
    //actually do anything worthwhile 
    public void Save() 
    { 
     var itemCount = _values.Count(); 
     . . . //save to database, write a log, do some real work 
    } 
} 

Como se puede ver, el uso adecuado de este código sería algo así como:

new FluentClass().Init<int>().With(-1).With(300).With(42).Save(); 

El problema es que las personas están utilizando de esta manera (pensamiento logra lo mismo que el anterior):

new FluentClass().Init<int>().With(-1).With(300).With(42); 

Este problema es tan generalizado, con totalmente buenas intenciones, otro desarrollador una vez realmente cambió el nombre del método "Init" para indicar que ese método estaba haciendo el "trabajo real" del software.

Errores de lógica como estos son muy difíciles de detectar, y, por supuesto, se compila, porque es perfectamente aceptable llamar a un método con un valor de retorno y simplemente "pretender" que devuelve vacío. A Visual Studio no le importa si haces esto; su software aún se compilará y ejecutará (aunque en algunos casos creo que arroja una advertencia). Esta es una gran característica para tener, por supuesto. Imagine un método simple "InsertToDatabase" que devuelve el ID de la nueva fila como un entero: es fácil ver que hay algunos casos en los que necesitamos ese ID, y algunos casos en los que podríamos prescindir de él.

En el caso de esta pieza de software, definitivamente nunca hay ninguna razón para evitar esa función de "Guardar" al final de la cadena de métodos. Es una utilidad muy especializada, y la única ganancia proviene del último paso.

Quiero que el software de alguien falle en el nivel de compilador si llaman "Con()" y no "Guardar()".

Parece una tarea imposible por los medios tradicionales, pero es por eso que vengo a ustedes. ¿Existe un atributo que pueda usar para evitar que un método sea "lanzado al vacío" o algo así?

Nota: La forma alternativa de lograr este objetivo que ya ha sido sugerido para mí está escribiendo una serie de pruebas de unidad para hacer cumplir esta regla, y usar algo como http://www.testdriven.net ligarlos con el compilador. Esta es una solución aceptable, pero estoy esperando algo más elegante.

+0

+1 para [valor de retorno] ;-) –

+0

Disparo total en la oscuridad, no tengo conocimiento de que esto sea posible, pero tal vez .NET Code Contracts? Creo que están integrados en .NET 4.0 – Roly

+1

Code Contracts no tiene un atributo o afirmación que requiera verificar el valor de retorno. – user7116

Respuesta

1

Después de una gran deliberación y prueba y error, resulta que arrojar una excepción del método Finalize() no iba a funcionar para mí. Aparentemente, simplemente no puedes hacer eso; la excepción se consume, porque la recolección de basura funciona de manera no determinista. Tampoco pude obtener el software para llamar a Dispose() automáticamente desde el destructor. El comentario de Jack V. explica esto bien; aquí fue el enlace que publicó, para la redundancia/énfasis:

The difference between a destructor and a finalizer?

Cambio de la sintaxis para utilizar una devolución de llamada fue una forma inteligente de hacer la prueba de tontos comportamiento, pero la acordada sintaxis se fija, y tuve para trabajar con eso Nuestra empresa se trata de cadenas de métodos fluidos. También fui un fan de la solución de "parámetros de salida" para ser honesto, pero una vez más, la conclusión es que las firmas de métodos simplemente no podían cambiar.

información útil acerca de mi problema particular incluye el hecho de que el software es solamente siempre a ser ejecutado como parte de un conjunto de pruebas unitarias - lo que la eficiencia no es un problema.

Lo que terminé haciendo fue utilizar Mono.Cecil para reflejar sobre el conjunto de llamadas (el código que llama a mi software). Tenga en cuenta que System.Reflection fue insuficiente para mis propósitos, porque no puede identificar referencias de método, pero aún necesitaba (?) Usarlo para obtener el "ensamblado de llamada" en sí (Mono.Cecil permanece infradocumentado, por lo que es posible que solo necesitará obtener más familiarizados con ella con el fin de acabar con System.Reflection por completo; lo que queda por ver ....)

coloqué el códigoMono.Cecil en el Init() método, y la estructura ahora se ve algo como:

public IntermediateClass<T> Init<T>() 
{ 
    ValidateUsage(Assembly.GetCallingAssembly()); 
    return new IntermediateClass<T>(); 
} 

void ValidateUsage(Assembly assembly) 
{ 
    // 1) Use Mono.Cecil to inspect the codebase inside the assembly 
    var assemblyLocation = assembly.CodeBase.Replace("file:///", ""); 
    var monoCecilAssembly = AssemblyFactory.GetAssembly(assemblyLocation); 

    // 2) Retrieve the list of Instructions in the calling method 
    var methods = monoCecilAssembly.Modules...Types...Methods...Instructions 
    // (It's a little more complicated than that... 
    // if anybody would like more specific information on how I got this, 
    // let me know... I just didn't want to clutter up this post) 

    // 3) Those instructions refer to OpCodes and Operands.... 
    // Defining "invalid method" as a method that calls "Init" but not "Save" 
    var methodCallingInit = method.Body.Instructions.Any 
     (instruction => instruction.OpCode.Name.Equals("callvirt") 
        && instruction.Operand is IMethodReference 
        && instruction.Operand.ToString.Equals(INITMETHODSIGNATURE); 

    var methodNotCallingSave = !method.Body.Instructions.Any 
     (instruction => instruction.OpCode.Name.Equals("callvirt") 
        && instruction.Operand is IMethodReference 
        && instruction.Operand.ToString.Equals(SAVEMETHODSIGNATURE); 

    var methodInvalid = methodCallingInit && methodNotCallingSave; 

    // Note: this is partially pseudocode; 
    // It doesn't 100% faithfully represent either Mono.Cecil's syntax or my own 
    // There are actually a lot of annoying casts involved, omitted for sanity 

    // 4) Obviously, if the method is invalid, throw 
    if (methodInvalid) 
    { 
     throw new Exception(String.Format("Bad developer! BAD! {0}", method.Name)); 
    } 
} 

Confía en mí, el código real es aún ugli es más que mi pseudocódigo ...:-)

Pero Mono.Cecil podría ser mi nuevo juguete favorito.

Ahora tengo un método que se niega a ejecutar su cuerpo principal a menos que el código de llamada "prometa" también llamar a un segundo método después. Es como un extraño tipo de contrato de código. De hecho, estoy pensando en hacer esto genérico y reutilizable. ¿Alguno de ustedes tiene un uso para tal cosa? Diga, si fuera un atributo?

9

No conozco una forma de aplicar esto a nivel de compilador. A menudo se solicita para objetos que implementan IDisposable también, pero no es realmente aplicable.

Una opción posible que puede ayudar, sin embargo, es configurar su clase, en DEPURAR solamente, para tener un finalizador que registre/arroje/etc. si Save() nunca fue llamado. Esto puede ayudarlo a descubrir estos problemas de tiempo de ejecución mientras depura en lugar de confiar en buscar el código, etc.

Sin embargo, asegúrese de que, en modo de lanzamiento, esto no se utiliza, ya que generará un gasto de rendimiento desde la adición de un finalizador innecesario es muy malo en el rendimiento del GC.

+0

+1, para el ingenioso truco del finalizador en Debug, lo usé antes para recordarme las fallas críticas administradas nativas en el código. – user7116

+0

Cuanto más lo pienso, más me gusta. Especialmente porque, y aquí está el truco que no mencioné en la publicación original, mi software ** en sí ** está hecho solo para pruebas; Ni siquiera tengo que especificar "solo depuración" porque el código ya no tiene lugar en la biblioteca de la clase principal. Probaré esto pronto para ver si puedo hacer que funcione, pero después de deliberar durante un tiempo, sospecho que lo has clavado (para mis propósitos al menos). ¡Gracias! – Bokonon

4

Se podría requerir métodos específicos para utilizar una devolución de llamada de esta manera:

new FluentClass().Init<int>(x => 
{ 
    x.Save(y => 
    { 
     y.With(-1), 
     y.With(300) 
    }); 
}); 

El método devuelve con un objeto específico, y la única manera de conseguir ese objeto es llamando x.Save(), que a su vez tiene una devolución de llamada que le permite configurar su número indeterminado de declaraciones. Por lo que el init necesita algo como esto:

public T Init<T>(Func<MyInitInputType, MySaveResultType> initSetup) 
0

En el modo de depuración lado de implementar IDisposable se puede configurar un temporizador que lanzar una excepción después de 1 segundo si el resultmethod no ha sido llamado.

+0

Un segundo podría ser peligrosamente corto si el sistema está sobrecargado. En su lugar, podría lanzar una excepción en el finalizador. Ambas técnicas agregan sobrecarga, pero el temporizador es probablemente más costoso. – Qwertie

1

¿Y si lo hiciste para que Init y With no devuelvan objetos del tipo FluentClass? Haga que regresen, por ejemplo, UninitializedFluentClass que envuelve un objeto FluentClass. Luego, al llamar al .Save(0 en el objeto UnitializedFluentClass, lo llama al objeto FluentClass envuelto y lo devuelve. Si no llaman al Save, no obtienen un objeto FluentClass.

0

¡Utilice un parámetro out! Se deben usar todos los out s.

Editar: No estoy seguro de que ayude, aunque ... Rompería la sintaxis fluida.

1

Puedo pensar en tres algunas soluciones, no es ideal.

  1. AIUI lo que quiere es una función que se llama cuando la variable temporal se sale del ámbito (como en, cuando se encuentre disponible para la recolección de basura, pero probablemente no será recolectado por algún tiempo). (Ver: The difference between a destructor and a finalizer?) Esta función hipotética diría "si ha creado una consulta en este objeto pero no se llama guardar, produce un error". C++/CLI llama a esta RAII, y en C++/CLI hay un concepto de "destructor" cuando el objeto ya no se usa y un "finalizador" que se invoca cuando finalmente se recolecta la basura. Muy confuso, C# solo tiene un llamado destructor, pero esto es solo llamado por el recolector de basura (sería válido para el framework llamarlo antes, como si estuviera limpiando parcialmente el objeto inmediatamente, pero no lo hace AFAIK hacer algo como eso). Entonces, lo que le gustaría es un destructor C++/CLI. Desafortunadamente, AIUI se basa en el concepto de IDisposable, que expone un método dispose() que se puede llamar cuando se llamará a un destructor C++/CLI, o cuando se llama al destructor C#, pero AIUI aún tiene que llamar "disponer "manualmente, ¿cuál derrota el punto?

  2. Refactorice la interfaz ligeramente para transmitir el concepto con mayor precisión. Llame a la función init algo así como "prepareQuery" o "AAA" o "initRememberToCallSaveOrThisWontDoAnything". (El último es una exageración, pero podría ser necesario aclarar el punto).

  3. Esto es más un problema social que técnico. La interfaz debería facilitar el hacer lo correcto, ¡pero los programadores deben saber cómo usar el código! Reúna a todos los programadores. Explicar simplemente una vez y para siempre este simple hecho. Si es necesario, pídales a todos que firmen un papel diciendo que entienden, y si voluntariamente siguen escribiendo un código que no hace nada, son peores que inútiles para la empresa y serán despedidos.

  4. Violín con la forma en que los operadores están encadenados, p. Ej. haga que cada una de las funciones de Clase intermedia ensamble un objeto intermediario intermedio que contenga todos los parámetros (en su mayoría esto ya lo hizo (?)) pero requiere una función de tipo inicial de la clase original para tomar eso como argumento, en lugar de tenerlos encadenado después, y luego puede tener save y las otras funciones devuelven dos tipos de clases diferentes (con esencialmente los mismos contenidos), y init solo acepta una clase del tipo correcto.

El hecho de que es todavía un problema sugiere que cualquiera de sus compañeros de trabajo necesitan un recordatorio útil, o son más bien por debajo del par, o la interfaz no era muy claro (tal vez su perfectamente bien, pero el autor no se dio cuenta de que no estaría claro si solo lo usó de pasada en vez de llegar a conocerlo), o usted mismo ha malentendido la situación. Una solución técnica sería buena, pero probablemente debería pensar por qué ocurrió el problema y cómo comunicarse más claramente, probablemente pidiendo la opinión de alguien mayor.

Cuestiones relacionadas