Estoy diseñando una biblioteca de clases que tiene un montón de métodos de tipo "EnsureXXX". La idea de este método debe invocarse siempre que un código de llamada requiera algo más de lo que puede requerir una inicialización específica de los argumentos. Es similar al método EnsureChildControls
de ASP.Net, pero con argumentos como discriminadores.¿Cómo asegurar que la lógica del método se ejecute solo una vez por combinación de argumentos?
Ex:
public static class SomeUtilityClass {
public static void EnsureSomething(string arg1, int arg2, object arg3)
{
// Logic should be called once for each args combination
}
}
public class CallerClass
{
public void Foo()
{
SomeUtilityClass.EnsureSomething("mycustomerid", 4, myData.SomeProperty);
}
public void Foo2()
{
SomeUtilityClass.EnsureSomething("mycustomerid", 4, myData.SomeProperty);
}
}
Como tal patrón se reutilizan en varios lugares y llama muy a menudo, tengo que mantener el rendimiento como un objetivo. También tengo que tener un método seguro para subprocesos.
Para este fin, escribí una pequeña clase de utilidad:
public sealed class CallHelper
{
private static readonly HashSet<int> g_YetCalled = new HashSet<int>();
private static readonly object g_SyncRoot = new object();
public static void EnsureOnce(Type type, Action a, params object[] arguments)
{
// algorithm for hashing adapted from http://stackoverflow.com/a/263416/588868
int hash = 17;
hash = hash * 41 + type.GetHashCode();
hash = hash * 41 + a.GetHashCode();
for (int i = 0; i < arguments.Length; i++)
{
hash = hash * 41 + (arguments[i] ?? 0).GetHashCode();
}
if (!g_YetCalled.Contains(hash))
{
lock (g_SyncRoot)
{
if (!g_YetCalled.Contains(hash))
{
a();
g_YetCalled.Add(hash);
}
}
}
}
}
El código de consumir es así:
public static class Program
{
static void Main()
{
SomeMethod("1", 1, 1);
SomeMethod("2", 1, 1);
SomeMethod("1", 1, 1);
SomeMethod("1", 1, null);
Console.ReadLine();
}
static void SomeMethod(string arg1, int arg2, object arg3)
{
CallHelper.EnsureOnce(typeof(Program),()=>
{
Console.WriteLine("SomeMethod called only once for {0}, {1} and {2}", arg1, arg2, arg3);
}, arg1, arg2, arg3);
}
}
La salida es, como se esperaba:
SomeMethod called only once for 1, 1 and 1
SomeMethod called only once for 2, 1 and 1
SomeMethod called only once for 1, 1 and
Tengo algunas preguntas relacionadas con este enfoque:
- Creo que he bloqueado correctamente la clase para garantizar la seguridad del hilo, pero ¿estoy bien?
- ¿Está
HashSet<int>
y mi método de cálculo del hash es correcto? Me pregunto especialmente si el manejo denull
es correcto, y si puedo "cortar" un delegadoAction
de esta manera. - Actualmente, mis métodos solo admiten métodos estáticos. ¿Cómo puedo pasar a un método compatible con la instancia (agregando la instancia como un discriminador) sin pérdida de memoria?
- ¿Hay alguna manera de evitar pasar todos los argumentos manualmente al método de utilidad (simplemente especificando la acción), sin explorar la pila (debido al impacto en el rendimiento)? Me temo que se introdujeron muchos errores debido a la falta de argumentos del método externo.
Gracias de antemano
Esta técnica de "EnsureOnce" se llama Memoization: encontrará muchas cosas en Google si las busca. –