函数参数 (params object[]) 有何不同?

3

我正在尝试理解一个复杂的库(LinqToCodeDom),其中有很多lambda表达式和委托等。在我的观点中,两段相同的代码却有不同的工作方式。 在这两种情况下,我都尝试传递一个TypeA对象数组。

可以正常工作:

Func(new TypeA[] { new TypeA("value") });

不起作用:

TypeA [] v = new TypeA[] { new TypeA("value") }; 
Func(v);

函数接受对象数组作为参数。

当它不工作时,会在库的深处某处崩溃,出现空引用错误。

更新:完整的行。可能不仅仅是一个函数调用那么简单:

CodeMemberMethod cm = cls.AddMethod(
    MemberAttributes.Public, 
    m.ReturnType, 
    paramsAndName,
    Emit.@stmt(() =>
        CodeDom.Call(CodeDom.VarRef("obj"), m.Name)( *** PLACE FOR PARAM HERE*** )
);

1
请您创建一个简短但完整的程序来演示问题,我之所以这样问是因为根据您提供的信息,编译器处理这两个语句时不应该有任何区别,因此必须有其他原因导致了这种差异。想了一想,我已经在LINQPad中进行了测试,它很可能使用.NET 4.5,因此这可能与版本有关,我将在旧版本上进行测试。 - Lasse V. Karlsen
似乎是的,我错了。我猜需要完整的代码才能知道为什么在你的情况下不同。 - Mahmoud Darwish
你能加入更多的代码吗?比如变量m是什么,我想要重现这个问题。 - Mahmoud Darwish
Func(...)的重载存在吗?接受params参数的重载可能会导致这种混淆。 - Drew Noakes
3个回答

1

编辑

运行和调试后,我得出了这个结论 - 我仍然不知道问题的确切原因,但是它与表达式有关,此调用在执行时生成两种不同类型的表达式

当您使用

    Func(new TypeA[] { new TypeA("value") });

它生成


    {() => Invoke(Call(VarRef("obj"), value(Demo.Program+<>c__DisplayClass1).m.Name),new [] {new TypeA("value")})}

当你使用时

    TypeA [] v = new TypeA[] { new TypeA("value") }; 
    Func(v);

它生成


   {() => Invoke(Call(VarRef("obj"), value(Demo.Program+<>c__DisplayClass1).m.Name),value(Demo.Program+<>c__DisplayClass1).v)}

注意区别。
   new [] {new TypeA("value")}

vs

   value(Demo.Program+<>c__DisplayClass1).v

其中,Demo是命名空间的名称,Program是类的名称,v是变量。

编辑2.2

为了更好地说明,我制作了这个示例应用程序。

using System;

using System.Linq.Expressions;

namespace TestXml
{
    public class MyClass
    {
        public string Value { get; set; }
        public MyClass(string value)
        {
            Value = value;
        }

        public override string ToString()
        {
            return Value;
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            MyClass[] parameter = new MyClass[] { new MyClass("1") };
            execute(() => TestInput( new MyClass[] { new MyClass("1") }));
            execute(() => TestInput(parameter));
        }

        public static void TestInput(params object[] parameters)
        {
            if (parameters != null && parameters.Length > 0) Console.WriteLine(parameters.GetType().FullName);
        }

        public static void execute(Expression<Action> exp)
        {
            Console.WriteLine(exp);
        }

        public delegate void ParamsDelegate(params object[] param);
    }
}

输出

() => TestInput(new [] {new MyClass("1")})
() => TestInput(value(TestXml.Program+<>c__DisplayClass0).parameter)

编辑3

为了知道行为差异的原因,我在MSDN上提出了问题,得到了以下答案:

"execute(() => TestInput( new MyClass[] { new MyClass("1") }));"

不会捕获任何东西,lambda表达式不使用任何变量。

"捕获指向参数变量的指针吗?"

我想你可以这么说。它实际上不是指针,而是将变量参数存储在对象字段中,而不是存储在堆栈中,你看到的c__DisplayClass0是编译器生成的类,用于保存参数变量

这表明这主要是LinqToCodeDom评估表达式时存在的一个bug,似乎他们没有处理这种情况。


好的。我也不是很明白。你能再解释一下吗? - MJM
我在LINQPad中进行了测试(请参见https://www.dropbox.com/s/mz8kbxx67z0ztwp/SO17504773.linq),并且在两种情况下都将数组作为参数传递,没有任何不同对待。换句话说,在`Func`内部的数组内容始终只是一个类型为`TypeA`的对象,而不是该对象的数组。 - Lasse V. Karlsen
Meywd Func(new TypeA[] { new TypeA("value"),new TypeA("value2") }); 也可以工作。我认为在这两种情况下都是一个数组。 - user1309871

1

这两个方法调用在功能上是相同的。

在第一种情况下,C#编译器将生成一个变量来保存与第二种情况等效的数组。

考虑以下C#代码(在LinqPad中):

void Main()
{
    CallFunc(new [] { new Foo() });

    var foos = new [] { new Foo() };
    CallFunc(foos);
}

public class Foo { }

void CallFunc(Foo[] foos) { }

生成的 IL:

IL_0001:  ldarg.0     // These first two lines load 1
IL_0002:  ldc.i4.1    // for the size of the array
IL_0003:  newarr      Foo // Create the array of type Foo with the size
IL_0008:  stloc.1     // Pops the array into a variable
IL_0009:  ldloc.1     // These next two lines load
IL_000A:  ldc.i4.0    // the first index (0) of the array
IL_000B:  newobj      Foo..ctor // Creates a new Foo
IL_0010:  stelem.ref  // Loads Foo into the array
IL_0011:  ldloc.1     // Loads the array onto the stack
IL_0012:  call        CallFunc // Calls the function
IL_0017:  nop         // Same thing repeats below with some extra variable loading
IL_0018:  ldc.i4.1    
IL_0019:  newarr      Foo
IL_001E:  stloc.1     
IL_001F:  ldloc.1     
IL_0020:  ldc.i4.0    
IL_0021:  newobj      Foo..ctor
IL_0026:  stelem.ref  
IL_0027:  ldloc.1     
IL_0028:  stloc.0     // Pops the array into foos
IL_0029:  ldarg.0     
IL_002A:  ldloc.0     // Loads the array from foos
IL_002B:  call        CallFunc

CallFunc:
IL_0000:  nop         
IL_0001:  ret         

Foo..ctor:
IL_0000:  ldarg.0     
IL_0001:  call        System.Object..ctor
IL_0006:  ret         

这段文本需要翻译为:

这段代码的区别在于加载和读取foos所需的指令数量。

对应C#代码如下:

var arrayLength = 0;
var foos = new Foo[arrayLength];
var firstIndex = 0;
var foo = new Foo();
foos[firstIndex] = foo;
CallFunc(foos);

0

在你的两个代码示例中并没有任何区别,当你仅调用时也是如此。

Func(new TypeA("value")); //the compiler will create an array for you because of params

3
他的代码与你的代码之间的区别在于,在你的情况下底层数组将是object[],而在他的情况下是TypeA[],这只有当他将新对象存储到其中时才会产生影响。 - Lasse V. Karlsen
哦,现在我明白你的意思了,我认为你说得对。 - Oleksii Aza
@LasseV.Karlsen 写成 new TypeA[]{new TypeA()} 而不是 new []{new TypeA()}。这两者之间有区别吗? - MJM
1
仅在语法上,在编译后的代码中,new[] { ... } 不会有任何区别。它是“为数组找到最佳类型”的简写。在这种情况下,类型将是 TypeA[],因此没有区别。 - Lasse V. Karlsen

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