Attribute with params object[] constructor gives inconsistent compiler errors

谁说胖子不能爱 提交于 2019-12-01 03:41:02

tldr:

The correct workaround is to tell the compiler to not use the expanded form:

[DataRow(new[] { 1 }, new object[] { new[] { "1" } })]

Excessive analysis:

The answer of Michael Randall is basically correct. Let's dig in by simplifying your example:

using System;
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
public class MyAttribute : Attribute {
    public MyAttribute(params object[] x){}
}
public class Program
{
    [MyAttribute()]
    [MyAttribute(new int[0])]
    [MyAttribute(new string[0])] // ERROR
    [MyAttribute(new object[0])]
    [MyAttribute(new string[0], new string[0])]  
    public static void Main() { }
}

Let's first consider the non error cases.

    [MyAttribute()]

There are not enough arguments for the normal form. The constructor is applicable in its expanded form. The compiler compiles this as though you had written:

    [MyAttribute(new object[0])]

Next, what about

    [MyAttribute(new int[0])]

? Now we must decide if the constructor is applicable in its normal or expanded form. It is not applicable in normal form because int[] is not convertible to object[]. It is applicable in expanded form, so this is compiled as though you'd written

    [MyAttribute(new object[1] { new int[0] } )]

Now what about

    [MyAttribute(new object[0])]

The constructor is applicable in both its normal and expanded form. In that circumstance the normal form wins. The compiler generates the call as written. It does NOT wrap the object array in a second object array.

What about

    [MyAttribute(new string[0], new string[0])]  

? There are too many arguments for the normal form. The expanded form is used:

    [MyAttribute(new object[2] { new string[0], new string[0] })] 

That should all be straightforward. What then is wrong with:

    [MyAttribute(new string[0])] // ERROR

? Well, first, is it applicable in normal or expanded form? Plainly it is applicable in expanded form. What is not so obvious is that it is also applicable in normal form. int[] does not implicitly convert to object[] but string[] does! This is an unsafe covariant array reference conversion, and it tops my list for "worst C# feature".

Since overload resolution says that this is applicable in both normal and expanded form, normal form wins, and this is compiled as though you'd written

[MyAttribute((object[]) new string[0] )] // ERROR

Let's explore that. If we modify some of our working cases above:

    [MyAttribute((object[])new object[0])] // SOMETIMES ERROR!
    [MyAttribute((object[])new object[1] { new int[0] } )]
    [MyAttribute((object[])new object[2] { new string[0], new string[0] })]

All of these now fail in earlier versions of C# and succeed in the current version.

Apparently the compiler previously allowed no conversion, not even an identity conversion, on the object array. Now it allows identity conversions, but not covariant array conversions.

Casts that can be handled by the compile time constant value analysis are allowed; you can do

[MyAttribute(new int[1] { (int) 100} )]

if you like, because that conversion is removed by the constant analyzer. But the attribute analyzer has no clue what to do with an unexpected cast to object[], so it gives an error.

What about the other case you mention? This is the interesting one!

[MyAttribute((object)new string[0])]

Again, let's reason it through. That's applicable only in its expanded form, so this should be compiled as though you'd written

[MyAttribute(new object[1] { (object)new string[0] } )]

But that is legal. To be consistent, either both these forms should be legal, or both should be illegal -- frankly, I don't really care either way -- but it is bizarre that one is legal and the other isn't. Consider reporting a bug. (If this is in fact a bug it is probably my fault. Sorry about that.)

The long and the short of it is: mixing params object[] with array arguments is a recipe for confusion. Try to avoid it. If you are in a situation where you are passing arrays to a params object[] method, call it in its normal form. Make a new object[] { ... } and put the arguments into the array yourself.

TheGeneral

Assuming your constructor is

public Foo(params object[] vals) { }

Then i think you are running up against some overlooked and non obvious compiler Dark Magic.

For example, obviously the below will work

[Foo(new object[] { "abc", "def" },new object[] { "abc", "def" })]
[Foo(new string[] { "abc", "def" },new string[] { "abc", "def" })]

This also works for me

[Foo(new [] { 2 }, new [] { "abc"})]
[Foo(new [] { 1 }, new [] { "a"})]

However this does not

[Foo(new [] { "a" })]
[Foo(new [] { "aaa"})]
[Foo(new string[] { "aaa" })]

An attribute argument must be a constant expression, typeof expression or array creation expression of an attribute parameter type

I think the key take-home peice of information here is

A method with a params array may be called in either "normal" or "expanded" form. Normal form is as if there was no "params". Expanded form takes the params and bundles them up into an array that is automatically generated. If both forms are applicable then normal form wins over expanded form.

As an example

PrintLength(new string[] {"hello"}); // normal form
PrintLength("hello"); // expanded form, translated into normal form by compiler.

When given a call that is applicable in both forms, the compiler always chooses the normal form over the expanded form.

However i think this gets even messier again with object[] and even attributes.

I'm not going to pretend i know exactly what the CLR is doing (and there are many more qualified people that may answer). However for reference, take a look at the CLR SO wizard Eric Lippert's similar answers for a more detailed illumination of what might be going on

C# params object[] strange behavior

Why does params behave like this?

Is there a way to distingish myFunc(1, 2, 3) from myFunc(new int[] { 1, 2, 3 })?

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!