How to get unique id of lambda function for “do once” pattern?

余生长醉 提交于 2019-12-25 03:33:04

问题


I want to implement "do once" pattern that allows me to avoid writing 3 things:

  • declaring var first = true
  • if(first) Do(...) statement inside repeated block of code
  • first = false assignment inside repeated block of code

I also want to avoid workarounds like these:

  • manually maintaining and passing unique identification into Do function
  • defining once context variable multiple times

So my code should look as simple as this:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write once and only once"));
       Console.Write("It should write 3 times");
       foreach(var it2 in new[]{4,5}){
               once.Do(()=>Console.Write("Inner loop should write once and only once"));
               Console.Write("It should write 6 times");
       }
   }

or this:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write once and only once"));
       Console.Write("It should write 3 times");
       once.Do(()=>Console.Write("It should write once, but only after first instance (out of 3) of previous write."));
       foreach(var it2 in new[]{4,5}){
           once.Do(()=>Console.Write("Inner loop should write once and only once"));
           Console.Write("It should write 6 times");
           once.Do(()=>Console.Write("It should write once, but only after first instance (out of 6) of previous write."));
       }
       Console.Write("Repeated ending should appear 3 times");
   }

If I use ObjectIDGenerator, it does not solve my problem because it gives different Id for Action act for every call in this implementation implementation:

public class Once : IDisposable{
    HashSet<long> passed;
    static ObjectIDGenerator idgen = new ObjectIDGenerator();

    public Once(){
        passed = passed.New();
    }

    public bool Do(Action act){
        if(act != null){
            bool firstTime;
            var id = idgen.GetId(act,out firstTime);
            if(!passed.Contains(id)){
                act();
                passed.Add(id);
                return true;
            }
            else
                return false;
        }
        else
            return false;
    }

    void IDisposable.Dispose() {
        passed.Clear();
    }
}

How to get unique id of passed lambda function ? I think it can be done by traversing it as Expression Tree and calculating hash or otherwise serializing it into something that can be placed into HashSet.

But I would prefer if it was possible to find file name and line number in source code that defines that lambda function and use it as id. That would easily solve problem of different places of definition having different unique ids even if definition was copy&pasted.

I guess one way would be to use ObjectIDGenerator for Expression Tree object that represents this lambda function. Would ObjectIDGenerator return same id for that Expression Tree ?

Another example: how to implement Once class and include nested loop variable it2 into once.Do invokation so that it would be called only twice - once for it2 = 4 and once for it2 = 5:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write once and only once"));
       Console.Write("It should write 3 times");
       foreach(var it2 in new[]{4,5}){
               once.Do(()=>Console.Write("Inner loop should write twice and only twice: {0}", it2));
               Console.Write("It should write 6 times");
       }
   }

Another example: how to implement Once class and include outer loop variable it into once.Do invokation so that it would be called only 3 times - once for it = 1, once for it = 2 and once for it=3:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write once and only once"));
       Console.Write("It should write 3 times");
       foreach(var it2 in new[]{4,5}){
               once.Do(()=>Console.Write("Inner loop should write 3 times and only 3 times: {0}", it));
               Console.Write("It should write 6 times");
       }
   }

Another clarification: If there is second lambda function that is defined somewhere else in my code, I want it to have different id even if it's a copy&paste of first lambda and has identical implementation.

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write twice because it's defined in different lines of code"));
       once.Do(()=>Console.Write("It should write twice because it's defined in different lines of code"));
       Console.Write("It should write 3 times");
   }

Now thinking about it, in an ideal solution I would exclude from id anything that is passed as one of explicit parameters like this (x,y,z,...)=>... and include values of any captured context variables referenced by that lambda function. So

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do((arg)=>Console.Write("It should write once {0}",arg));
       once.Do(()=>Console.Write("It should write 3 times {0}",it));
       Console.Write("It should write 3 times");
   }

Or may be inversion is better:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       once.Do(()=>Console.Write("It should write once {0}",it));
       once.Do((arg)=>Console.Write("It should write 3 times {0}",arg));
       Console.Write("It should write 3 times");
   }

Either way the goal of last 2 examples is to show how be able to control cleanly what is included into determination of uniqueness and what is not.

Addressing solution from Jon here is another clarification:

I want to keep definition of once and non-once actions in the same sequence as if they were all non-once, so that order of appearance of a,b,c in my source code does not have to be changed if I decide to write b only once or not:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       Console.Write("a");
       Console.Write("b");
       Console.Write("c");
   }

does not have to be changed:

using(var once = new Once())
   foreach(var it in new[]{1,2,3}){
       Console.Write("a");
       once.Do(()=>Console.Write("b"));
       Console.Write("c");
   }

Realistic example - imagine some table with many possible amount fields, imagine that I cannot generate script and execute it in one batch (which is the case with Azure and probably other cloud databases), also imagine that we also define AfterFirst method in our class Once:

using(var tblOnce = new Once())
    foreach(var tbl in db.Tables)
       using(var fldOnce = new Once())
          foreach(var fld in tbl.Fields){
            fldOnce.Do(        ()=>conn.Exec(" CREATE TABLE {0}({1} {2})",tbl.Name, fld.Name, fld.SqlType));
            if(fld.Name.EndsWith("Amount"))
                fldOnce.Do(    ()=>conn.Exec(" ALTER TABLE {0} ADD Total money", tbl.Name));
            fldOnce.AfterFirst(()=>conn.Exec(" ALTER TABLE {0} ADD {1} {2}", tbl.Name, fld.Name, fld.SqlType));
            if(fld.PrimaryKey)
                fldOnce.Do(    ()=>conn.Exec(" ALTER TABLE {0} ADD CONSTRAINT PK_{0}_{1} PRIMARY KEY CLUSTERED({1})", tbl.Name, fld.Name));
            fldOnce.Do(()=>
                tblOnce.Do(    ()=>conn.Exec(" CREATE TABLE Tables (name varchar(50))"));
                conn.Exec("                    INSERT tables (name) select " + tbl.Name);
            );
          }

回答1:


using(var once = new Once())
  foreach(var it in new[]{1,2,3})
  {
    once.Do(()=>Console.Write("It will write once and only once"))
    foreach(var it2 in new[]{4,5})
      once.Do(()=>Console.Write("Inner loop will write 3 times and only 3 times: {0}", it))
  }

Here I would define this as an operation in terms of the enumerable itself. Consider:

public static void OnceAndAll<T>(this IEnumerable<T> source, Action<T> once, Action<T> all)
{
  using(var en = source.GetEnumerator())
    if(en.MoveNext())
    {
      var current = en.Current;
      once(current);
      all(current);
      while(en.MoveNext())
        all(en.Current);
    }
}
public static void Once<T>(this IEnumerable<T> source, Action<T> once)
{
  using(var en = source.GetEnumerator())
    if(en.MoveNext())
      once(en.Current);
}
//Overrides for where the value is not actually used:
public static void OnceAndAll<T>(this IEnumerable<T> source, Action once, Action<T> all)
{
  source.OnceAndAll(_ => once(), all);
}
public static void OnceAndAll<T>(this IEnumerable<T> source, Action<T> once, Action all)
{
  source.OnceAndAll(once, _ => all());
}
public static void OnceAndAll<T>(this IEnumerable<T> source, Action once, Action all)
{
  source.OnceAndAll(once, _ => all());
}
public static void Once<T>(this IEnumerable<T> source, Action once)
{
  source.Once(_ => once());
}

Now I can do:

new[]{ 1, 2, 3 }.OnceAndAll(
  () => Console.Write("It will write once and only once"),
  it => new[]{4,5}.Once(() => Console.Write("Inner loop will write 3 times and only 3 times: {0}", it))
  );

Here the first Action passed to OnceAndAll is executed only once, but the second is executed every time for that first sequence. It in turn sets up an Action to be used in the second 2-item sequence only once per sequence.

Variants of its uses can also handle a lot of your other cases, but not your first:

using(var once = new Once())
  foreach(var it in new[]{1,2,3})
  {
     once.Do(()=>Console.Write("It will write once and only once"));
       foreach(var it2 in new[]{4,5})
         once.Do(()=>Console.Write("Inner loop will write once and only once"));
  }

For this I would have something that creates a new action:

public static Action Once(this Action toDoOnce)
{
  return () => {
    if(toDoOnce != null)
      toDoOnce();
    toDoOnce = null;
  };
}
public static Action<T> Once<T>(this Action<T> toDoOnce)
{
  return obj => {
    if(toDoOnce != null)
      toDoOnce(obj);
    toDoOnce = null;
  };
}
public static Action<T1, T2> Once<T1, T2>(this Action<T1, T2> toDoOnce)
{
  return (arg1, arg2) => {
    if(toDoOnce != null)
      toDoOnce(arg1, arg2);
    toDoOnce = null;
  };
}
/* and so on … */
public static Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> Once<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this Action<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16> toDoOnce)
{
  return (arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16) => {
    if(toDoOnce != null)
      toDoOnce(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16);
    toDoOnce = null;
  };
}

And then use it as so:

Action outer = () => Console.Write("It will write once and only once");
var outerOnce = outer.Once();
var innerOnce = ((Action)(()=>Console.Write("Inner loop will write once and only once"))).Once();
foreach(var it in new[]{1,2,3})
{
  outerOnce();
    foreach(var it2 in new[]{4,5})
      innerOnce();
}

It means defining the "onceness" outside of the loop, but then that's what the "onceness" is scoped to, to the scope that contains the loop. (For once ever in an application lifetime we already have simple static-based approaches).




回答2:


I just discovered elegant and simple solution:

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

public class Once : IDisposable
{
    HashSet<Tuple<string,int>> passed;

    public Once(){
        passed = passed.New();
    }

    public bool Do(Expression<Action> act,
        [CallerFilePath] string file = "",
        [CallerLineNumber] int line = 0
    ){
        if(act != null){
            var id = Tuple.Create(file,line);
            if(!passed.Contains(id)){
                act.Compile().Invoke();
                passed.Add(id);
                return true;
            }
        }
        return false;
    }

    void IDisposable.Dispose() {
        passed.Clear();
    }
}

As you can see, the needed id of lambda function is Tuple(file,line).

Of course, we could optimize memory usage by not storing file path or storing it once, but the main problem of getting unique id of lambda function is solved assuming that it's defined only where it's invoked and invoked only where it's defined. That assumption is valid given examples of usage of "do once" pattern.

Expression<Action> is not necessary for this solution (we could pass just Action), but it can be used to scan for parameters and captured context variables inside that expression and include their values into determination of lambda function id.

Potential enhancements of identification of lambda functions through scanning of expression tree can be derived from answers to this question Most efficient way to test equality of lambda expressions which was discovered by DLeh



来源:https://stackoverflow.com/questions/30623682/how-to-get-unique-id-of-lambda-function-for-do-once-pattern

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!