Why Does My C# Array Lose Type Sign Information When Cast to Object

Why does my C# array lose type sign information when cast to object?

UPDATE: I've used this question as the basis for a blog entry, here:

https://web.archive.org/web/20190203221115/https://blogs.msdn.microsoft.com/ericlippert/2009/09/24/why-is-covariance-of-value-typed-arrays-inconsistent/

See the blog comments for an extended discussion of this issue. Thanks for the great question!


You have stumbled across an interesting and unfortunate inconsistency between the CLI type system and the C# type system.

The CLI has the concept of "assignment compatibility". If a value x of known data type S is "assignment compatible" with a particular storage location y of known data type T, then you can store x in y. If not, then doing so is not verifiable code and the verifier will disallow it.

The CLI type system says, for instance, that subtypes of reference type are assignment compatible with supertypes of reference type. If you have a string, you can store it in a variable of type object, because both are reference types and string is a subtype of object. But the opposite is not true; supertypes are not assignment compatible with subtypes. You can't stick something only known to be object into a variable of type string without first casting it.

Basically "assignment compatible" means "it makes sense to stick these exact bits into this variable". The assignment from source value to target variable has to be "representation preserving". See my article on that for details:

http://ericlippert.com/2009/03/03/representation-and-identity/

One of the rules of the CLI is "if X is assignment compatible with Y, then X[] is assignment compatible with Y[]".

That is, arrays are covariant with respect to assignment compatibility. This is actually a broken kind of covariance; see my article on that for details.

https://web.archive.org/web/20190118054040/https://blogs.msdn.microsoft.com/ericlippert/2007/10/17/covariance-and-contravariance-in-c-part-two-array-covariance/

That is NOT a rule of C#. C#'s array covariance rule is "if X is a reference type implicitly convertible to reference type Y, then X[] is implicitly convertible to Y[]". That is a subtly different rule, and hence your confusing situation.

In the CLI, uint and int are assignment compatible. But in C#, the conversion between int and uint is EXPLICIT, not IMPLICIT, and these are value types, not reference types. So in C#, it's not legal to convert an int[] to a uint[].

But it IS legal in the CLI. So now we are faced with a choice.

  1. Implement "is" so that when the compiler cannot determine the answer statically, it actually calls a method which checks all the C# rules for identity-preserving convertibility. This is slow, and 99.9% of the time matches what the CLR rules are. But we take the performance hit so as to be 100% compliant with the rules of C#.

  2. Implement "is" so that when the compiler cannot determine the answer statically, it does the incredibly fast CLR assignment compatibility check, and live with the fact that this says that a uint[] is an int[], even though that would not actually be legal in C#.

We chose the latter. It is unfortunate that C# and the CLI specifications disagree on this minor point but we are willing to live with the inconsistency.

Why do casts between arrays of some value types seemingly break type safety?

Arrays in .NET are covariant in a rather broken way.

Arrays in C# are covariant in a subset of the ways that they are covariant in .NET. It's still broken, but it doesn't allow:

uint[] test = (uint[])new int[10]; // Compiler Error CS0030 C#
// doesn't allow this covariance

But that's a C# rule; .NET allows it, it allows assignment between arrays where the types are integers (signed or unsigned) of the same size, or enums with the same size of underlying type.

And of course both .NET and C# both allow you to cast any array to object, and in both casting from object to any array type is legal but may fail at runtime. So uint[] test = (uint[])(object)new int[10]; is allowed, because it's like the following steps:

object temp = new int[10];  // normal enough asignmnt.
uint[] test = (uint[])temp; // C# doesn't allow assigning
// int[] to uint[] but temp is
// object so the compiler
// doesn't know that's what
// you are doing, and .NET
// does allow it.

From a comment:

Do you know why the CLR allows this conversion at all? Does it help with CLI compliance maybe (ie. allow languages without unsigned types to treat arrays as signed types)

Well consider that in CIL there is less difference when it comes to values on the stack between signed and unsigned values. If you use clt it will pop two values from the stack and compare them as signed values, whether they are signed or not, while clt.un will compare them as unsigned values, whether they are unsigned or not. Likewise

There's a freedom of moving between signed and unsigned types of the same size inherent to CIL.

Now, covariance means we can assign a value that is equivalent or narrower than what it is assigned to; that is it includes bivariance (assigning something narrower). C# doesn't consider int and uint bivariant; you have to explicitly cast between them, so it doesn't make sense to include them in the covariant assignment.

Will casting a variable as an object cause that variable to lose data?

Will my PointPair object lose data/information during the cast/uncast to/from object?

No, it won't.Because casting, doesn't change your object at all.It changes how is your object considered, for example when you cast it to object,even though the underlying type is still PointPair you can't access the members of PointPair because your newPoint will be treated as object.

Does it matter if I use object or Object?

No, because object is just an alias for Object class.

Why does int[] is uint[] == true in C#

C# and the CLR have somewhat different conversion rules.

You can't directly cast between int[] and uint[] in C# because the language doesn't believe any conversion is available. However, if you go via object the result is up to the CLI. From the CLI spec section 8.7 (I hope - I'm quoting an email exchange I had on this topic with Eric Lippert a while ago):

Signed and unsigned integral primitive
types can be assigned to each other;
e.g., int8 := uint8 is valid. For this
purpose, bool shall be considered
compatible with uint8 and vice versa,
which makes bool := uint8 valid, and
vice versa. This is also true for
arrays of signed and unsigned integral
primitive types of the same size;
e.g., int32[] := uint32[] is valid.

(I haven't checked, but I assume that this sort of reference type conversion being valid is what makes is return true as well.)

It's somewhat unfortunate that there are disconnects between the language and the underlying execution engine, but it's pretty much unavoidable in the long run, I suspect. There are a few other cases like this, but the good news is that they rarely seem to cause significant harm.

EDIT: As Marc deleted his answer, I've linked to the full mail from Eric, as posted to the C# newsgroup.

Accessing elements of an Array (of any element type) from an object

second code:

int[] array = new int[] { 0, 1, 2 };
object obj=array;
var obj_array=(Array)obj;
IEnumerable<object> collection = obj_array.Cast<object>();
string output=string.join(", ",collection.Select(x=>x.ToString()));

so main code will be:

Dictionary<string,string>[] getInfo(string k) {
// using `k` as management-key
var mos = new ManagementObjectSearcher($"select * from {k}");
var devices = new List<Dictionary<string, string>>();
var mosc = mos.Get(); // mosc is a collection of all devices with same key
foreach (var device in mosc) {
var properties = new Dictionary<string, string>();
foreach (var p in device.Properties) {
if (p.Value != null) {
if (p.IsArray) {
var array = (Array)p.Value;
var collection = array.Cast<object>();
properties[p.Name] = string.Join(", ", collection.Select(x=>x.ToString()));
} else
properties[p.Name] = p.Value.ToString();
} else properties[p.Name] = "";
}
devices.Add(properties);
}
return devices.ToArray();
}

How do I cast/convert an array of objects to generic type T when T is defined as an array?

You are not really using the features of generics here, except getting the type via typeof(T). So a solution without generics would be:

public class Foo
{
public object Convert(string value, Type t)
{
if (t.IsArray)
{
string[] values = value.Split(',');
Type elementType = t.GetElementType();
var array = Array.CreateInstance(elementType, values.Length);
for (var i = 0; i < values.Length; i++)
{
array.SetValue(elementType.IsEnum ? Enum.Parse(elementType, values[i], true) : ChangeType(values[i], elementType), i);
}
return array;
}
else
{
return ChangeType(value, t);
}
}

private object ChangeType(string value, Type type)
{
if (type.IsGenericType && type.GetGenericTypeDefinition().Equals(typeof(Nullable<>)))
{
if (value == null)
{
return default;
}
type = Nullable.GetUnderlyingType(type);
}
return System.Convert.ChangeType(value, type);
}
}

Note that I use Array.CreateInstance to create e.g. a proper int[] instead of the object[] created by your values.Select(v => elementType.IsEnum ? Enum.Parse(elementType, v, true) : ChangeType(v, elementType)).ToArray().

If you want, you can still have a generic version on top of it:

public class Foo<T> : Foo
{
public T Convert(string value)
{
return (T)Convert(value, typeof(T));
}
}

Can `typeof(T).IsAssignableFrom(x.GetType())` be safely rewritten as `x is T`?

There are more cases where is and IsAssignableFrom returns different results, not just the one you mentioned for COM objects. For pair of array types with elements of type ElementType1 and ElementType2, where underlaying types of both element types are integer types of the same size but with opposite signedness, then

typeof(ElementType1[]).IsAssignableFrom(typeof(ElementType2[])) returns true but

new ElementType2[0] is ElementType1[] returns false

Specifically this includes arrays with these pairs of their element types:

  • byte / sbyte, short / ushort, int / uint, long / ulong

  • IntPtr / UIntPtr

  • any combination of enum type and either integer type or another enum type as long as underlaying types are of the same size

  • any combination of IntPtr / UIntPtr / int / uint in 32-bit process

  • any combination of IntPtr / UIntPtr / long / ulong in 64-bit process

This is due to differences in type system of C# and CLR as explained in

  • Why does my C# array lose type sign information when cast to object?
  • Why does “int[] is uint[] == true” in C#
  • Why is covariance of value-typed arrays inconsistent?

Different results of is and IsAssignableFrom in all cases mentioned above results from the fact that for new ElementType2[0] is ElementType1[] C# compiler simply emits False at compile-time (because it sees no way that for example int[] can be cast to uint[] as these are completly different types from C# perspective), completly omitting any runtime type checks. Fortunately casting array to object ((object)new ElementType2[0]) is ElementType1[] forces compiler to emit isinst IL instruction, which performs runtime type-check, that returns results consistent with IsAssignableFrom. This is also true for cases where target type is generic parameter, because it's type is not known at compile-time and C# compiler must emit isinst. So if you intend to replace IsAssignableFrom by is only in places where target type is generic parameter (as suggested in question title), I believe that these differences does not metter for you.

Why can't I cast DateTime[] to object[]?

Array covariance only applies to arrays of reference types. DateTime is a value type so you can't assign a DateTime[] to an object[] variable. You'll have to explicitly create an object array and copy the values over. In other words, create a new array instance of type object[].

There are plenty of ways you can do this. A simple use of CopyTo() should be enough.

DateTime[] x = new DateTime[] { ... };
object[] y = new object[x.Length];
x.CopyTo(y, 0);

I ran some tests. Probably not the best way to do it but it should give a good idea of what it would be with a proper profiler.

class Program
{
static void Main(string[] args)
{
var now = DateTime.Now;
var dates = new DateTime[5000000];
for (int i = 0; i < dates.Length; i++)
dates[i] = now.AddSeconds(i);
for (int i = 0; i < 5; i++)
{
Test("Test1", () =>
{
var result = new object[dates.LongLength];
for (long l = 0; l < result.LongLength; l++)
result[l] = dates[l];
return result;
});
Test("Test2", () =>
{
var result = new object[dates.LongLength];
dates.CopyTo(result, 0);
return result;
});
Test("Test3", () =>
{
var result = new object[dates.LongLength];
Array.Copy(dates, result, dates.LongLength);
return result;
});
Test("Test4", () =>
{
var result = Array.ConvertAll(dates, d => (object)d);
return result;
});
Test("Test5", () =>
{
var result = dates.Cast<object>().ToArray();
return result;
});
Test("Test6", () =>
{
var result = dates.Select(d => (object)d).ToArray();
return result;
});
Console.WriteLine();
}
}

static void Test<T>(string name, Func<T> fn)
{
var startMem = GC.GetTotalMemory(true);
var sw = Stopwatch.StartNew();
var result = fn();
sw.Stop();
var endMem = GC.GetTotalMemory(false);
var diff = endMem - startMem;
Console.WriteLine("{0}\tMem: {1,7}/{2,7} ({3,7})", name, startMem, endMem, diff);
Console.WriteLine("\tTime: {0,7} ({1,7})", sw.ElapsedMilliseconds, sw.ElapsedTicks);
}
}

Specs:

Win7Pro x64, Core2Quad Q9550@2.83GHz, 4GiB DDR2 1066 (PC2-8500)

64-bit build (32-bit is roughly the same, just less memory overall)


Test1 Mem: 40086256/200087360 (160001104)
Time: 444 (1230723)
Test2 Mem: 40091352/200099272 (160007920)
Time: 751 (2078001)
Test3 Mem: 40091416/200099256 (160007840)
Time: 800 (2213764)
Test4 Mem: 40091480/200099256 (160007776)
Time: 490 (1358326)
Test5 Mem: 40091608/300762328 (260670720)
Time: 1407 (3893922)
Test6 Mem: 40091672/300762328 (260670656)
Time: 756 (2092566)

Test1 Mem: 40091736/200099184 (160007448)
Time: 515 (1425098)
Test2 Mem: 40091736/200099184 (160007448)
Time: 868 (2404151)
Test3 Mem: 40091736/200099160 (160007424)
Time: 885 (2448850)
Test4 Mem: 40091736/200099184 (160007448)
Time: 540 (1494429)
Test5 Mem: 40091736/300762240 (260670504)
Time: 1479 (4093676)
Test6 Mem: 40091736/300762216 (260670480)
Time: 746 (2065095)

Test1 Mem: 40091736/200099168 (160007432)
Time: 500 (1383656)
Test2 Mem: 40091736/200099160 (160007424)
Time: 781 (2162711)
Test3 Mem: 40091736/200099176 (160007440)
Time: 793 (2194605)
Test4 Mem: 40091736/200099184 (160007448)
Time: 486 (1346549)
Test5 Mem: 40091736/300762232 (260670496)
Time: 1448 (4008145)
Test6 Mem: 40091736/300762232 (260670496)
Time: 749 (2075019)

Test1 Mem: 40091736/200099184 (160007448)
Time: 487 (1349320)
Test2 Mem: 40091736/200099176 (160007440)
Time: 781 (2162729)
Test3 Mem: 40091736/200099184 (160007448)
Time: 800 (2214766)
Test4 Mem: 40091736/200099184 (160007448)
Time: 506 (1400698)
Test5 Mem: 40091736/300762224 (260670488)
Time: 1436 (3975880)
Test6 Mem: 40091736/300762232 (260670496)
Time: 743 (2058002)

Test1 Mem: 40091736/200099184 (160007448)
Time: 482 (1335709)
Test2 Mem: 40091736/200099184 (160007448)
Time: 777 (2150719)
Test3 Mem: 40091736/200099184 (160007448)
Time: 793 (2196184)
Test4 Mem: 40091736/200099184 (160007448)
Time: 493 (1365222)
Test5 Mem: 40091736/300762240 (260670504)
Time: 1434 (3969530)
Test6 Mem: 40091736/300762232 (260670496)
Time: 746 (2064278)

Interestingly, ConvertAll() performs much the same as a plain loop.

Losing enum type information in for in loop

TypeScript's definition for Object.entries is overloaded, but both overloads use string, explicitly, as the key type. From lib/lib.es2017.object.d.ts:

/**
* Returns an array of key/values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T>(o: { [s: string]: T } | ArrayLike<T>): [string, T][];

/**
* Returns an array of key/values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries(o: {}): [string, any][];

I'm sure there's a good reason for it. :-) But you can define your own entries that doesn't use string:

// Our `entries` function
function entries<T>(o: T): [keyof T, T[keyof T]][] {
return Object.entries(o) as unknown as [keyof T, T[keyof T]][];
}

Then it works:

for (const [name, hex] of entries(myColors)) {
// type of `name` is `Color` now
}

Taking it further, I've found that if I add the following declaration:

interface ObjectConstructor {
/**
* Returns an array of key/values of the enumerable properties of an object
* @param o Object that contains the properties and methods. This can be an object that you created or an existing Document Object Model (DOM) object.
*/
entries<T>(o: T): [keyof T, T[keyof T]][];
}

With that, your original code works as you expected:

for (const [name, hex] of Object.entries(myColors)) {
// type of `name` is `Color` now
}

(I double-checked this with the real compiler, not just the playground, by having const n: Color = name; in the loop body. TypeScript complains about it without the declaration, but with the declaration it's happy.)

But, some issues in the TypeScript issue list make me think that definition may cause trouble with other kinds of things you'd pass Object.entries, specifically this one and this comment. So you might want to have the separate function (which will get JIT'd away, most likely) and use it where relevant.



Related Topics



Leave a reply



Submit