【问题标题】:How to get unique id of lambda function for "do once" pattern?如何为“做一次”模式获取 lambda 函数的唯一 ID?
【发布时间】:2015-08-17 20:59:46
【问题描述】:

我想实现“做一次”模式,让我避免写三件事:

  • 首先声明 var = true
  • if(first) Do(...) 重复代码块中的语句
  • first = 重复代码块内的错误赋值

我也想避免这样的变通方法:

  • 手动维护唯一标识并将其传递给 Do 函数
  • 多次定义一个上下文变量

所以我的代码应该像这样简单:

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,它并不能解决我的问题,因为它为这个实现实现中的每个调用提供了不同的 Id for Action 行为:

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 的源代码中找到文件名和行号,我更愿意。即使定义是复制和粘贴的,这也可以轻松解决不同定义位置具有不同唯一 ID 的问题。

我想一种方法是将 ObjectIDGenerator 用于表示此 lambda 函数的表达式树对象。 ObjectIDGenerator 会为该表达式树返回相同的 id 吗?

另一个例子:如何实现Once类并将嵌套循环变量it2包含在一次中。执行调用以便它只被调用两次-一次为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类并将外部循环变量包含在一次中。执行调用以便它只被调用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,...)=&gt;...,并包括该 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");
   }

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

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

现实的例子 - 想象一个有许多可能的金额字段的表,想象我无法生成脚本并批量执行它(Azure 和可能其他云数据库就是这种情况),还想象我们还在我们的班级曾经:

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

【问题讨论】:

  • 只需在序列上调用Take(1),就不会对第一个之后的任何项目采取行动。
  • 如果唯一标识每个操作有问题,您可以将上下文字符串(例如“context1”、“context2”等)传递给 Do 函数。一个简单的哈希集可以跟踪已经执行过一次的上下文。也许不是一个理想的解决方案,但尝试对您的操作的表达式树进行哈希处理以识别它对我来说感觉很重。
  • 鉴于我的问题的性质以及带有布尔标志的简单解决方案的可用性,我不是在寻找“不是理想的解决方案”。
  • @alpav 好吧,现在您只是在每次进行更新时更改要求。显然,您还没有仔细考虑所有案例以发现您真正想要的功能。

标签: c# lambda anonymous-function uniqueidentifier


【解决方案1】:

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

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&lt;Action&gt; 对于此解决方案不是必需的(我们可以只传递 Action),但它可用于扫描该表达式中的参数和捕获的上下文变量,并将它们的值包含到 lambda 函数 id 的确定中。

可以从Most efficient way to test equality of lambda expressionsDLeh 发现的问题的答案中得出通过扫描表达式树来识别 lambda 函数的潜在增强

【讨论】:

  • 如果有人想知道传递了什么。New(),这里是定义: [DebuggerStepThrough] public static T New(this T @this, params object[] args) { return (T) Activator.CreateInstance(typeof(T), args); }
【解决方案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 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();
}

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

【讨论】:

  • 我想保持一次和非一次动作的顺序相同,就好像它们都是非一次一样,所以 Write(a);Write(b);Write(c) 的顺序不必分解为 wb = Write(b);写一个); wb();如果我想将 Write(b) 转换为一次操作,请使用 Write(c)。
  • 我给出的第二种方法可以用于此。
猜你喜欢
  • 2019-11-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-19
  • 1970-01-01
  • 1970-01-01
  • 2023-03-25
相关资源
最近更新 更多