如何获取 "do once" 模式的 lambda 函数的唯一 ID?

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

我想实现 "do once" 模式,让我避免写 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 once and only once"));
               Console.Write("It should write 6 times");
       }
   }

或者这个:

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");
   }

如果我使用 ObjectIDGenerator,它并不能解决我的问题,因为它为这个实现实现中的每个调用提供了不同的 Action 行为 ID:

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();
    }
}

如何获取传递的 lambda 函数的唯一 ID? 我认为这可以通过将其作为表达式树遍历并计算哈希或以其他方式将其序列化为可以放入 HashSet 中的东西来完成。

但我希望能够在定义该 lambda 函数的源代码中找到文件名和行号并将其用作 ID。这将很容易解决不同位置的定义具有不同唯一标识的问题,即使定义是复制粘贴的。

我想一种方法是对表示此 lambda 函数的表达式树对象使用 ObjectIDGenerator。 ObjectIDGenerator return 表达式树的 id 是否相同?

另一个例子:如何实现 Once class 并将嵌套循环变量 it2 包含到 once.Do 调用中,这样它只会被调用两次 - 一次 it2 = 4 一次 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");
       }
   }

另一个例子:如何实现 Once class 并将外部循环变量包含到 once.Do 调用中,这样它只会被调用 3 次 - 一次为它 = 1,一次为它 = 2 一次 =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");
       }
   }

另外说明: 如果在我的代码中的其他地方定义了第二个 lambda 函数,我希望它具有不同的 id,即使它是第一个 lambda 的复制和粘贴并且具有相同的实现。

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");
   }

现在考虑一下,在一个理想的解决方案中,我将从 id 中排除任何作为显式参数之一传递的内容,例如 (x,y,z,...)=>... 并包括该 lambda 函数引用的任何捕获的上下文变量的值。所以

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");
   }

或者倒置更好:

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");
   }

无论哪种方式,最后 2 个示例的目标是展示如何能够清楚地控制确定唯一性的内容和不包含的内容。

Jon 的解决方案是另一个说明:

我想按照相同的顺序定义一次和非一次操作,就好像它们都是非一次操作一样,这样 a、b、c 在我的源代码中的出现顺序就不必是如果我决定只写 b 一次或不写一次,则更改:

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

不必更改:

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

现实示例 - 想象一些 table 有许多可能的金额字段,想象我无法生成脚本并批量执行它(Azure 和其他云数据库可能就是这种情况),也想象一下我们还在 class Once:

中定义了 AfterFirst 方法
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);
            );
          }
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))
  }

这里我将其定义为根据可枚举本身的操作。考虑:

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());
}

现在我可以做到:

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))
  );

这里传递给 OnceAndAll 的第一个 Action 只执行一次,但第二个在第一个序列中每次都执行。它依次设置一个 Action,每个序列仅在第二个 2 项序列中使用一次。

其用途的变体也可以处理您的许多其他情况,但不是您的第一个:

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"));
  }

为此,我需要创建一个新动作的东西:

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;
  };
}

然后按原样使用:

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();
}

这意味着在循环之外定义 "onceness",但这就是 "onceness" 的范围,即包含循环的范围。 (在应用程序生命周期中,我们已经有了简单的基于 static 的方法)。

我刚刚发现优雅而简单的解决方案:

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();
    }
}

如你所见,lambda函数需要的id是Tuple(file,line).

当然,我们可以通过不存储文件路径或只存储一次来优化内存使用,但是获取 lambda 函数的唯一 id 的主要问题已经解决,假设它只在它被调用的地方被定义并且只在它被定义的地方被调用.鉴于 "do once" 模式的使用示例,该假设是有效的。

Expression<Action> 对于此解决方案不是必需的(我们可以只传递 Action),但它可用于扫描该表达式内的参数和捕获的上下文变量,并将它们的值包含在确定 lambda 函数 ID 中.

可以从这个问题的答案中得出通过扫描表达式树来识别 lambda 函数的潜在增强功能 Most efficient way to test equality of lambda expressions which was discovered by DLeh