Can't Convert Value Type Array to Params Object[]

Can't convert value type array to params object[]

If C# can cast an int to an object, why not an int[] to an object[]?

Your question could be also stated as "what are the covariance rules for array conversions in C#?"

They are a bit tricky, and broken in several interesting and unfortunate ways.

First off, we should clearly state what we mean by "covariance". Covariance is the property that a mapping preserves a relationship. The mapping here is "T goes to array of T". The relationship is "can be implicitly converted". For example:

Giraffe can be implicitly converted to Mammal.

That's a relationship between two types. Now apply the mapping to both sides of the relationship:

Giraffe[] can be converted to Mammal[].

If the truth of the first statement always entails the truth of the second statement -- that is, if the mapping preserves the truth of the relationship -- then the mapping is said to be "covariant".

As a shorthand, instead of saying "the mapping from T to array of T is a covariant mapping over the implicit conversion relation", we just say "arrays are covariant" and hope that the rest of that is understood from context.

OK, now that we have the definition down: Arrays with reference type elements are covariant in C#. Tragically, this is broken covariance:

class Mammal {}
class Giraffe : Mammal {}
class Tiger : Mammal {}
...
Mammal[] mammals = new Giraffe[1];

This is perfectly legal because arrays of reference type elements are covariant in C#. But then this crashes at runtime:

mammals[0] = new Tiger();

because mammals is really an array of Giraffes.

This means that every time you write to an array whose elements are unsealed reference types, the runtime performs a type check and might crash if the type check fails.

This is my candidate for "worst feature of C#", but it does in fact work.

Your question is "why does array covariance not work when the source array is an array of value type and the target array is an array of reference type?"

Because those two things have a different form at runtime. Suppose you have a byte[] with ten elements. The actual storage reserved for the array elements is ten bytes long. Suppose you are on a 64 bit machine and you have an object[] with ten elements. The storage is eight times bigger!

Clearly you cannot convert via reference conversion a reference to storage for ten bytes to storage for ten eight-byte references to bytes. The extra seventy bytes don't come out of nowhere; someone has to allocate them.

Moreover: who does the boxing? If you have an array of ten objects and each object is a byte, each one of those bytes is boxed. But bytes in a byte array are not boxed. So when you do the conversion, who does the boxing?

In general in C#, covariant conversions always preserve representation. The representation of a "reference to Animal" is exactly the same as the representation of "reference to Giraffe". But the representations of "int" and "reference to object" are completely different.

One expects that casting one array type to another does not allocate and copy a huge array. But we cannot have referential identity between an array of ten bytes and an array of eighty bytes containing ten references, and therefore the entire thing is simply made illegal.

Now, you might then say, well, what happens when the representations are the same for value types? In fact, this is illegal in C#:

int[] x = new uint[10];

because in C# the rule is that only covariant array conversions involving only reference types are legal. But if you force it to be done by the runtime:

int[] x = (int[])(object) new uint[10];

Then the runtime allows it because a four byte int and a four byte uint have the same representation.

If you want to understand this better then you should probably read my entire series of articles on how covariance and contravariance works in C#:

  • The whole series

  • The specifics of unsafe reference-element array covariance

  • More about value-element array covariance

C# params object[] strange behavior

Nice find!

Is this undefined behavior?

No. This is by-design behaviour. Weird design, but by design.

Does that depend on the .Net framework version / Mono version?

No. All versions of C# have this behaviour.

This is a consequence of the collision of some interesting rules of C#.

The first relevant rule 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.

Now, that perhaps seems sensible; if you have an array of objects in hand, odds are good that you want to pass the array of objects, not an array that contains an array of objects.

The second relevant rule is that C# allows unsafe array covariance when the element type is a reference type. That is, an array of strings may be converted to an array of objects implicitly. You'll note that this has two implications. First, it means that when you have an array of objects, it might actually be an array of strings, so putting, say, a turtle into that array of objects might cause a type error. This is very surprising! You expect that every array of objects can take any object, but that is not true in C#. Some arrays of objects are lying.

The second implication is: since putting that turtle into what is really an array of strings must throw, it means that every time you put something into an array of base type, the runtime must verify that the types check. So array writes are extra expensive in C# on every write, so that the vanishingly small minority of bad writes can be caught.

This is a mess, and this is why unsafe array covariance tops my list of unfortunate C# features.

The combination of these two rules explains your observations. The array of strings is convertible to an array of objects, and therefore the method is applicable in normal form.

For the array of ints, well, covariance does not apply to value types. So the array of ints is not convertible to an array of objects, so the method is not applicable in its normal form. But an array of ints is an object, so it is applicable in expanded form.

See also:

Why does params behave like this?

Your question is arguably a duplicate of:

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

C#: Convert array to use in params with additional parameters

If your problem is just to add another element to your array, you could use a List

List<object> list = new List<object> { "hello" };
list.AddRange(objs);
Console.WriteLine("{0}, {1}, {2}, ...", list.ToArray());

params is not a datatype. The parameters datatype is just still a plain array.

Cannot convert from object to System.ValueType

That code only works when you pass it a value type. The object you're trying to pass is a reference type, so the compiler forbids you to use that as an argument:

MSDN: System.ValueType

[...] can be used as a parameter in method calls to restrict possible arguments to value types instead of all objects

So you should just change the parameter type from ValueType to object, which would make this code work for any argument type and still execute the desired behavior.

But from the variables and code it looks like you're trying to do something else entirely. Are you looking for decimal.TryParse()?

Why does param object[] array become jagged if you pass an int[] array into it?

Whenever you have a params parameter the compiler will attempt to accept an array representing all of the values for the params argument, if the parameter at the position in question is valid in that context. If it's not, then it tries to treat it as one item in the array of params values, rather than as the whole array. If it can't do that either then it will fail to compile.

In your second example a string[] can be implicitly converted to an object[], so it is passed as the entire list of parameters. This implicit conversion is valid because of array covariance.

In your first example the int[] cannot be implicitly converted to an object[] (array covariance is limited to reference types), so it is treated as one value in the array. An int[] can be implicitly converted to object, so what is passed is an object array containing an int[] as its only item. Note that an array with another array as an item is dramatically different from a multi-dimensional array.

Typescript - Convert from Array of types to Object of types

Because you can't manipulate tuple labels in the type system, the best you can do is manually supply a tuple of property keys, one for every parameter in the original function.

By adapting some utilities from this answer by jcalz, here's a generic utility that will give you a mapped object for the parameters in a function: You supply the function type and a tuple of labels for each parameter:

TS Playground

type ZipTuples<Keys extends readonly any[], Values extends readonly any[]> = {
[K in keyof Keys]: [Keys[K], K extends keyof Values ? Values[K] : never];
};

type ZipTuplesAsObject<
Keys extends readonly PropertyKey[],
Values extends readonly any[],
> = { [T in ZipTuples<Keys, Values>[number] as T[0]]: T[1] };

type ParamsAsObject<
Fn extends (...params: readonly any[]) => any,
Keys extends readonly PropertyKey[],
> = ZipTuplesAsObject<Keys, Parameters<Fn>>;

// Use

declare function otherFunction (
irrelevantLabel: string,
alsoNotUsed: number,
onlyTheTypesMatter: boolean,
): void;

function example (objectParam: ParamsAsObject<typeof otherFunction, ['a', 'b', 'c']>) {
// objectParam; // { a: string; b: number; c: boolean; }
const {a, b, c} = objectParam;
a; // string
b; // number
c; // boolean
}

How to pass a single object[] to a params object[]

A simple typecast will ensure the compiler knows what you mean in this case.

Foo((object)new object[]{ (object)"1", (object)"2" }));

As an array is a subtype of object, this all works out. Bit of an odd solution though, I'll agree.



Related Topics



Leave a reply



Submit