有没有方法可以区分myFunc(1, 2, 3)和myFunc(new int[] { 1, 2, 3 })?

9
一个问题,面向所有C#巫师。我有一个方法,叫做myFunc,它接受可变长度/类型的参数列表。myFunc本身的参数签名是myFunc(params object[] args),我在列表上使用反射(例如类似于printf)。
我想要将myFunc(1, 2, 3)myFunc(new int[] { 1, 2, 3 })区分开来。也就是说,在myFunc的主体内,我想枚举我的参数类型,并且想要得到{int,int,int}而不是int[]。现在我得到了后者:实际上,我无法区分这两种情况,它们都被视为int[]。
我希望前者显示为obs[].Length=3,其中obs[0]=1等等。
我预期后者会显示为obs[].Length=1,其中obs[0]={ int[3] }。
这可以做到吗,还是我在要求不可能的事情?

9
你为什么要需要这样的东西? - Jan Jongboom
你怎么能期望 obs[0] 是一个数组呢?obsint[],它的类型在编译时已知,其元素只能是 int 类型。如果你有 params object[],那么你可能会认为 object[] 也是一个 object,但这是没有意义的。 - vgru
请查看isis2.codeplex.com。我需要去上课,将在90分钟后回来并重新访问此线程。问题实际上涉及对“Reply”进行“Query”调用。文档中有很多示例。Jongboom,很抱歉你觉得这很困惑。但是如果您看到目标可能会有所帮助。 - Ken Birman
@KenBirman:如果你的参数类型是params object[],那就没问题了。看看我简短但完整的程序示例... - Jon Skeet
6个回答

9

好的,这将会做到:

using System;

class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("First call");
        Foo(1, 2, 3);
        Console.WriteLine("Second call");
        Foo(new int[] { 1, 2, 3 });
    }

    static void Foo(params object[] values)
    {
        foreach (object x in values)
        {
            Console.WriteLine(x.GetType().Name);
        }
    }
}

或者,如果您使用DynamicObject,您可以使用动态类型来实现类似的结果:

using System;
using System.Dynamic;

class Program
{
    static void Main(string[] args)
    {
        dynamic d = new ArgumentDumper();
        Console.WriteLine("First call");
        d.Foo(1, 2, 3);
        Console.WriteLine("Second call");
        d.Bar(new int[] { 1, 2, 3 });
    }
}

class ArgumentDumper : DynamicObject
{
    public override bool TryInvokeMember
        (InvokeMemberBinder binder,
         Object[] args,
         out Object result)
    {
        result = null;
        foreach (object x in args)
        {
            Console.WriteLine(x.GetType().Name);
        }
        return true;
    }
}

两个程序的输出结果:

First call
Int32
Int32
Int32
Second call
Int32[]

现在根据上面的输出,不清楚你的问题真正来自哪里...尽管如果你提供了 Foo("1", "2", "3")Foo(new string[] { "1", "2", "3" }),那么这就是一个不同的问题 - 因为string[]object[]兼容,但int[]则不是。如果这是实际导致你问题的情况,请看动态版本 - 它将在两种情况下工作。

不知道那个。我会查一下。 - Ken Birman
这也是我最初发布的内容,但我不认为这是OP想要的。 - Ani
@Ani:嗯,输出似乎符合要求。不清楚原帖作者做了什么不同的尝试却失败了,但这个确实有效... - Jon Skeet
@KenBirman: 很难理解你的意思(特别是在谈论向量时 - 你是指数组吗) - 但如果你能提供一个简短但完整的例子来演示问题,我可能可以解释一下...请注意我回答底部的那部分 - 你可能会遇到数组协变性的问题。 - Jon Skeet
好的,我取消了勾选框。原因是这样的。你的示例对于int类型完美无缺。但是如果我做完全相同的事情,但现在我使用一个用户定义的类...相同的代码会错误地报告传入的对象! - Ken Birman
显示剩余5条评论

9

好的,假设我们放弃其他问题,您错误地认为其中任何一个是编译器错误,并实际回答您真正的问题。

首先,让我们尝试陈述真正的问题。这是我的看法:


导言:

“可变参数”方法是一种可以接受预先未指定数量参数的方法。

C#中实现可变参数方法的标准方式是:

void M(T1 t1, T2 t2, params P[] p)

也就是说,零个或多个必需的参数后跟一个标记为 "params" 的数组。

调用这样的方法时,该方法可以使用其普通形式(不带 params)或其扩展形式(带 params)。也就是说,对于以下调用:

void M(params object[] x){}

表格格式
M(1, 2, 3)

生成为

M(new object[] { 1, 2, 3 });

因为它只适用于其扩展形式。但是一个调用

M(new object[] { 4, 5, 6 });

生成为

M(new object[] { 4, 5, 6 });

而不是

M(new object[] { new object[] { 4, 5, 6 } });

因为它适用于其正常形式。

C#支持对引用类型元素的数组进行不安全的数组协变。也就是说,一个 string[] 可以隐式转换为 object[],即使试图将这样一个数组的第一个元素更改为非字符串也会产生运行时错误。

问题:

我想要进行以下形式的调用:

M(new string[] { "hello" });

并且让它的行为就像该方法只适用于扩展形式:
M(new object[] { new string[] { "hello" }});

而不是正常形式:
M((object[])(new string[] { "hello" }));

有没有一种在C#中实现可变参数方法的方式,不会受到不安全数组协变和方法优先适用于其正常形式的影响?
答案是肯定的,但你可能不喜欢它。如果你打算将单个数组传递给它,最好将方法设置为非可变参数方法。
微软的C#实现支持一种未记录的扩展,允许使用C风格的可变参数方法而不使用params数组。这种机制并不是为了普遍使用而设计的,只包括CLR团队和其他编写互操作库的人员,以便他们编写桥接C#和期望C风格可变参数方法的语言之间的互操作代码。我强烈建议不要尝试自己做。
这样做的机制涉及使用未记录的__arglist关键字。一个基本的草图是:
public static void M(__arglist) 
{
    var argumentIterator = new ArgIterator(__arglist);
    object argument = TypedReference.ToObject(argumentIterator.GetNextArg());

你可以使用参数迭代器的方法来遍历参数结构并获取所有参数。你还可以使用超级神奇的类型引用对象来获取参数的类型。使用这种技术甚至可以将变量的引用作为参数传递,但我不建议这样做。
这种技术特别糟糕的地方在于调用者需要这样说:
M(__arglist(new string[] { "hello" }));

在 C# 中,使用可变参数方法看起来相当糟糕。现在你知道为什么最好完全放弃可变参数方法;只需让调用方传递一个数组就可以了。

再次提醒,(1)切勿尝试使用这些未经记录的 C# 语言扩展,它们是 CLR 实现团队和交互操作库作者的便利工具,(2)应该放弃可变参数方法;因为它们似乎不适合您的问题空间。不要与工具作斗争;选择其他工具。


3

是的,您可以通过检查参数长度和检查参数类型来实现,以下是一份可行的代码示例:

class Program
{
    static void Main(string[] args)
    {
        myFunc(1, 2, 3);
        myFunc(new int[] { 1, 2, 3 });
    }

    static void myFunc(params object[] args)
    {
        if (args.Length == 1 && (args[0] is int[]))
        {
            // called using myFunc(new int[] { 1, 2, 3 });
        }
        else
        {
            //called using myFunc(1, 2, 3), or other
        }
    }
}

然而,上述代码将无法区分 myFunc(new object[] { 1, 2, 3 })myFunc(1, 2, 3) - Raymond Chen

2
您可以通过将列表中的第一个元素拆分出来并提供额外的重载来实现类似于这样的效果,例如:
class Foo
{
   public int Sum()
   {
      // the trivial case
      return 0;
   }
   public int Sum(int i)
   {
      // the singleton case
      return i;
   }
   public int Sum(int i, params int[] others)
   {
      // e.g. Sum(1, 2, 3, 4)
      return i + Sum(others);
   }
   public int Sum(int[] items)
   {
      // e.g. Sum(new int[] { 1, 2, 3, 4 });
      int i = 0;
      foreach(int q in items)
          i += q;
      return i;
   }
}

这实际上是我见过的这个问题的最佳解决方案 - 很好! - TVOHM

0

在 C# 中这是不可能的。C# 会在编译时用第二次调用替换你的第一次调用。

一种可能性是创建一个没有 params 的重载,并将其设置为 ref 参数。这可能没有意义。如果您想根据输入更改行为,也许给第二个 myFunc 另一个名称。

更新

我现在理解了您的问题。您想要的是不可能的。如果唯一的参数是可以解析为 object[] 的任何内容,则无法与此区分。

您需要一种替代方案,也许是由调用者创建的字典或数组来构建参数。


你没有理解这个问题。这里的想法是我的用户正在回复一个多播,他们提供的参数将由我进行编组(序列化)。因此,我需要知道在调用我的回复函数时使用的类型,逐个参数地重构签名,以便于另一端的人提取数据。参数的数量和类型都很重要,而且我需要只有一个函数可以接受几乎任意的参数... - Ken Birman
好的,如果那个代码可以运行(我现在打算检查一下),显然我可以用C#来实现... 如果它不能像广告中所说的那样工作,我会取消答案按钮的选择。否则,我就可以离开这里并感到非常高兴!谢谢大家... - Ken Birman

0
原来有一个真正的问题,它与C#进行类型推断的方式有关。请参见此其他线程上的讨论。

这不是类型推断的问题,而是重载决议的问题。 - phoog

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接