【问题标题】:How to pass a Func<T> with a variable number of parameters如何传递具有可变数量参数的 Func<T>
【发布时间】:2023-03-24 07:29:02
【问题描述】:

所以我试图能够传递具有可变数量参数的 Func。

类似:

public object GetValue<T>(string name, Func<object> func) { 
    var result = func.DynamicInvoke();
}

当 func 的参数数量已知时,上述函数/签名非常有效。但是,如果您希望在运行时之前不知道参数的数量,它就会很快崩溃。

我想更改方法签名以允许以下情况,而不使用方法重载:

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var result = GetValue("Joe", (i) => { return "Joe " + i.ToString(); });

// 2 arguments
var result = GetValue("Henry", (i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

现在不需要超过 2 个参数。但将来可能需要。调用语法对我来说是最重要的一点。我宁愿不让调用者投射任何东西。

我查看了this question and the answers,但它们似乎都需要一些我不想使用的调用语法。

有什么想法可以实现吗?

【问题讨论】:

  • 我不知道有什么方法可以做到这一点。鉴于目前您只需要三个重载(它们都可以将它们的实现委托给私有方法),我怀疑这是最好的方法。请注意,您还没有展示在“直到运行时参数的数量[将]未知”的上下文中您将如何结束。你还在源代码中使用 lambda 表达式,对吧?目前这是一个 XY 问题。如果您能给我们提供更多背景信息,我们更有可能为您提供帮助。
  • @DaisyShipton:感谢您的 cmets。不幸的是,我很难描述这个用例,因为我仍在尝试构建它。我有一种情况,我需要调用者提供回调。我将为回调提供一些参数。把它想象成 jQuery .Each 方法。我希望调用者能够指定他们是否对任何 args、1 个 args 或两者都不感兴趣。我想我可能只需要一个 2-arg 签名.. 但这不会完全达到我希望的效果:)
  • 您似乎关心如何编写 Func,因此它将接受可变数量的参数。但对我而言,真正的问题是——您打算如何编写调用该函数的代码?该代码不需要填充参数吗?它从哪里获得论点,它如何知道要获得多少?
  • 不想使用重载的原因是什么?这就是大多数 API 在这种情况下所做的事情。例如,LINQ 的 Select 方法有类似的场景。一个重载仅提供该项目。另一个重载提供索引和项目。
  • OP,我知道参数是由被调用的方法生成的。我的问题是如何。它如何知道传递 1、2 或 3 个参数?它如何知道应该进入它们的内容?如果调用者不能是通用的,那么将接收器设为通用是没有意义的。

标签: c# func


【解决方案1】:

答案是不要。首先,您试图调用一个不带参数并返回某个对象的方法。您不能只“制作”一个需要这种类型参数的函数,否则您将如何使用所需的参数调用它。

您已经在创建 lambda,您需要关闭所需的“参数”,这样您才能有效地添加其他参数。

// No arguments
var result = GetValue("Bob", () => { return "Bob Smith"; });

// 1 argument
var i = ...;
var result = GetValue("Joe", () => { return "Joe " + i.ToString(); });

// 2 arguments
var i = ...;
var e = ...;
var result = GetValue("Henry", () => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
});

否则,如果您真的想传入具有任意数量参数的任何委托,请将参数设为 Delegate,但您必须提供委托的确切类型,并且必须提供参数来电。

public object GetValue<T>(string name, Delegate func, params object[] args) { 
    var result = func.DynamicInvoke(args);
}
var result = GetValue("Bob", new Func<object>(() => { return "Bob Smith"; }));

// 1 argument
var result = GetValue("Joe", new Func<T, object>((i) => { return "Joe " + i.ToString(); }), argI);

// 2 arguments
var result = GetValue("Henry", new Func<T1, T2, object>((i,e) => { 
    return $"i: {i.ToString()}, e: {e.ToString()}"; 
}), argI, argE);

【讨论】:

    【解决方案2】:

    首先,你违背了静态类型的目的:在静态类型中,重点是在编译时你可以准确地知道某个对象的类型以及编译器将允许您做某些事情。您在这里所期望的是动态类型通常提供的东西,您可以在其中传递“一些东西”,然后您就可以在运行时弄清楚如何使用它。

    你真的应该重新考虑你的这个要求,看看你是否不能以更好的静态类型方式解决这个问题。


    话虽如此,使用自定义类型和一些隐式类型转换(在编译时发生!)有一种丑陋的方法可以使这个静态理智:

    public object GetValue(string name, DynamicFunc func)
    {
        return func.DynamicInvoke("a", "b");
    }
    
    public class DynamicFunc
    {
        public Func<object> None { get; private set; }
        public Func<object, object> One {get; private set;}
        public Func<object, object, object> Two { get; private set; }
    
        public object DynamicInvoke(object param1 = null, object param2 = null)
        {
            if (Two != null)
                return Two(param1, param2);
            else if (One != null)
                return One(param1 ?? param2);
            else if (None != null)
                return None();
            return null;
        }
    
        public static implicit operator DynamicFunc(Func<object> func)
            => new DynamicFunc { None = func };
        public static implicit operator DynamicFunc(Func<object, object> func)
            => new DynamicFunc { One = func };
        public static implicit operator DynamicFunc(Func<object, object, object> func)
            => new DynamicFunc { Two = func };
    }
    

    然后你可以像这样使用它:

    var result0 = GetValue("Bob", (Func<object>)(() => { return "Bob Smith"; }));
    var result1 = GetValue("Joe", (Func<object, object>)((i) => { return "Joe " + i.ToString(); }));
    var result2 = GetValue("Henry", (Func<object, object, object>)((i, e) =>
    {
        return $"i: {i.ToString()}, e: {e.ToString()}";
    }));
    

    请注意,您需要为 lambda 表达式提供显式类型,否则编译器将无法确定类型。

    这看起来不错还是易于理解?我不这么认为。如果您想要正确的静态类型,只需在此处使用方法重载。这样,您也不需要动态调用该函数,这也使得调用它实际上更容易。

    【讨论】:

      【解决方案3】:

      首先,我的答案不是你是否应该这样做;这篇文章的其他答案对此给出了很好解释的建议/意见。

      我的回答只是关于技术 C# 部分:如何将可变数量的参数传递给 Func

      它可以通过委托来完成,可以通过params array 将可变数量的参数传递给该委托,并且与Func“兼容”。

      由于params array,Func 必须是第一个参数。请注意,我将您问题中的“名称”参数移动到通过params array 传递,以便可以从Func 中访问。

      下面的所有字符串都被赋值为“Bob Smith”,由 0 或 2 个参数构成。

      public delegate T ParamsDelegate<T>(params Object[] args);
      
      public T GetValue<T>(ParamsDelegate<T> f, params Object[] args)
      {
          return f(args);            
      }
      
      // 0 passed argument values.  
      String s0 = GetValue(args => "Bob Smith");
      
      // 1 argument.
      String s1 = GetValue(
          args => String.Format("{0} Smith", args),
          "Bob"
          );
      
      // 2 arguments.            
      String s2 = GetValue(
          args => String.Format("{0} {1}", args),
          "Bob", "Smith"
          );
      

      编辑:

      从 C# 7 开始,您可以通过 discards 更清楚地表明不传递任何参数的意图。

      Discards 是局部变量,可以赋值但不能读取 从。即它们是“只写”局部变量。他们没有 名称,相反,它们表示为 _(下划线)。_ 是 上下文关键字,和var很像,_不能读 (即不能出现在作业的右侧。)

      所以如下:

      String s0 = GetValue(args => "Bob Smith");
      

      可以改写为:

      String s0 = GetValue(_ => "Bob Smith");
      

      【讨论】:

      • 我非常感谢 cmets。如果我错了,请纠正我,但您的建议似乎让调用者将参数提供给GetValue(...)。我的问题是让GetValue(...) 将参数提供给传入的 Func 或委托。 (例如:string value = GetValue&lt;string&gt;((a,b) =&gt; { return $"arg-a: {a}; arg-b: {b}"; });
      • 我理解并有意这样做,否则将无法在string value = GetValue&lt;string&gt;((a,b) =&gt; { ...}); 中为ab 分配值。参数值总是从最外层传递给函数/方法。
      【解决方案4】:

      据我了解,您希望能够忽略部分参数,以防调用者不需要它们。但是,我认为您应该从决定如何调用委托开始。请注意,DynamicInvoke 的参数数量必须与实际方法的参数数量相匹配,因此如果您指定的参数多于它们接受的数量,则它不适用于您传递的所有委托。

      除非您使用反射和发射来调用委托,否则您必须使用重载。你制作它们的方式取决于参数是否应该是“懒惰的”。如果是这样,每个过载的主体将与其他的主体有很大不同,以证明其存在是合理的。如果没有,它是这样的:

      object GetValue(Func<int, object> f)
      {
          return GetValue((i,s) => f(i));
      }
      
      object GetValue(Func<int, string, object> f)
      {
          return f(1, "0");
      }
      

      我认为这不是一个好的解决方案,因为第一个重载表明只产生了第一个参数,而在幕后,它们都被传递了。在这种情况下,最好将您打算从方法中获取的所有信息包装在一个类中并传递它的实例。如果您使用Lazy&lt;T&gt;,这也适用于第一种情况,但代码会变得更加复杂。

      与您的问题相关的另一件事:delegate{ ... } 语法可以转换为带有任何参数的委托类型,然后将其忽略。不过,这发生在呼叫站点。

      【讨论】:

        【解决方案5】:

        只提供一个参数,但将其设为复杂类型

        如果您有一个正在传入的委托可能需要或不需要的变量清单,您可以将它们作为单个参数提供,可能命名为 CallingContext

        class CallingContext
        {
            private CallingContext(string name, int index)
            {
                Name = name;
                Index = index;
            }
            public string Name { get; private set; }
            public int Index { get; private set; }
        }
        
        public TOut GetValue<TOut>(string name, Func<CallingContext,TOut> func) { 
            var i = LookupIndex(name);
            var context = new CallingContext(name, i);        
            return func(context);
        }
        

        这样,调用者的 Func 可以拉取它需要的任何东西,而您根本不必担心推送正确的东西,或者 Func 的泛型参数。

        var result = x.GetValue("Bob", ctxt => ctxt.Name + ctxt.Id.ToString());
        

        以后,如果你必须添加额外的参数,只需将它们添加到 CallingContext 类中即可。如果您希望该类变得非常丰富,请考虑将其公开为接口,并使用Func&lt;ICallingContext,TOut&gt; 的委托签名,以便您可以填充并存根它以进行单元测试。此外,如果任何参数的计算成本很高,您可以将它们公开为惰性属性,并且仅在调用时才获取它们。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-10-02
          • 2014-03-08
          • 1970-01-01
          • 1970-01-01
          • 2013-01-29
          • 2010-09-17
          相关资源
          最近更新 更多