Type reflection does not work correctly with generics

J

john

Hi to All

To demonstrate:

public class MyBaseGenericClass<T>
{
}

public class MyGenericClass1<T> : MyBaseGenericClass<T>
{
}

public class MyGenericClass2<T> : MyBaseGenericClass<T>
{
}

public class Test
{
public void Method()
{
Type baseGenericType = typeof (MyBaseGenericClass<>);
Type genericType1 = typeof(MyGenericClass1<>);
Type genericType2 = typeof(MyGenericClass2<>);

// .Equals gives the same results (correctly), so that's not the trick
bool b1 = genericType1.BaseType == baseGenericType; // False, why?
bool b2 = genericType2.BaseType == baseGenericType; // False, why?
bool b3 = genericType1.BaseType == genericType2.BaseType; // False, why?

// Some diag info:
DumpType(baseGenericType);
DumpType(genericType1.BaseType);
DumpType(genericType2.BaseType);
}

private void DumpType(Type type)
{
Debug.WriteLine(string.Format("Type.Name:{0}, Type.FullName: {1},
Type.GUID: {2}", type.Name, type.FullName, type.GUID ));
}
}

Diag output:

Type.Name:MyBaseGenericClass`1, Type.FullName:
WindowsApplication25.MyBaseGenericClass`1, Type.GUID:
807bb6c8-35ad-3408-8dfd-13eb15f39a0a
Type.Name:MyBaseGenericClass`1, Type.FullName: , Type.GUID:
807bb6c8-35ad-3408-8dfd-13eb15f39a0a
Type.Name:MyBaseGenericClass`1, Type.FullName: , Type.GUID:
807bb6c8-35ad-3408-8dfd-13eb15f39a0a

As it seems all three types are should be (and logically are) identical, but
we detect all three as different.
NOTE: All the three types Name and GUID is identical.

The first type what we got with typeof operator has FullName, but second and
third has not.
But not this is the fact what makes the equality operator fail. Type
instances as a convention shoud be singleton by Type what it describes, so
there is no more than one type instance by Type. As a consequence the
polymorphic overload of Equals method of Type only uses the simple reference
comparision.
(Well not exactly, it compares the UnderlyingTpye but this is the same in
our case)

It is clear as a diagnostic result there are three instances and not one
singleton.

As a consequence of erratic result of comparison some of the internal
algorithmi also works with error for example: IsSubTypeOf()

To wrap up: Both implementaion, both the behavior of this is erratic.
Again, the three GIUDs are identical.

Any ideas?
 
B

Barry Kelly

john said:
public class MyBaseGenericClass<T>
public class MyGenericClass1<T> : MyBaseGenericClass<T>
public class MyGenericClass2<T> : MyBaseGenericClass<T>

This is going to sound strange, and it's a little counterintuitive, but
it actually makes sense when you think about it for a while.

The base class of MyGenericClass1<> is MyBaseGenericClass<T> where T is
the type argument passed to MyGenericClass1<>. (It's a similar story for
MyGenericClass2<>.)

That is, the base class of MyGenericClass1<> isn't actually
MyBaseGenericClass<> (an open generic type). The base class is actually
a partially closed generic type - but it's closed using a type parameter
that comes from a different generic type definition (so,
Type.ContainsGenericParameters will still return true).

To demonstrate:

---8<---
using System;
using System.Reflection;

class Program
{
class Base<T> {}
class Desc<T> : Base<T> {}

static void Main()
{
Type t = typeof(Desc<>);
Type descArg = t.GetGenericArguments()[0];
Type baseArg = t.BaseType.GetGenericArguments()[0];
Console.WriteLine("Base<T>'s T == T? {0}", baseArg == descArg);
}
}
--->8---

To explain it for the third time, using naming to try and make clear
exactly what's going on:

class Base<TBase> {}
class Desc<TDesc> : Base<TDesc> {}

Hopefully, it's clearer here. You can see that Base<TBase> is distinct
the generic type parameter said:
The first type what we got with typeof operator has FullName, but second and
third has not.

This is another clue. Type.FullName should be round-trippable through
Type.GetType(string). There's no way to get a generic type which is
closed using a type parameter from a descendant generic type's
definition, so that's why Type.FullName doesn't return anything.

-- Barry
 
J

john

Hi Barry,

Thx for your answer,

I understand "partially" your answer.. :)

As I understand your answer still not explains why the second type and third
type equals...
(Actually they are identical, but not the same Type instances.)

Other question: If based on your explanation first and second type does not
the same, then why are they GUIDs the same?

I think there are some bugs here...

Barry Kelly said:
john said:
public class MyBaseGenericClass<T>
public class MyGenericClass1<T> : MyBaseGenericClass<T>
public class MyGenericClass2<T> : MyBaseGenericClass<T>

This is going to sound strange, and it's a little counterintuitive, but
it actually makes sense when you think about it for a while.

The base class of MyGenericClass1<> is MyBaseGenericClass<T> where T is
the type argument passed to MyGenericClass1<>. (It's a similar story for
MyGenericClass2<>.)

That is, the base class of MyGenericClass1<> isn't actually
MyBaseGenericClass<> (an open generic type). The base class is actually
a partially closed generic type - but it's closed using a type parameter
that comes from a different generic type definition (so,
Type.ContainsGenericParameters will still return true).

To demonstrate:

---8<---
using System;
using System.Reflection;

class Program
{
class Base<T> {}
class Desc<T> : Base<T> {}

static void Main()
{
Type t = typeof(Desc<>);
Type descArg = t.GetGenericArguments()[0];
Type baseArg = t.BaseType.GetGenericArguments()[0];
Console.WriteLine("Base<T>'s T == T? {0}", baseArg == descArg);
}
}
--->8---

To explain it for the third time, using naming to try and make clear
exactly what's going on:

class Base<TBase> {}
class Desc<TDesc> : Base<TDesc> {}

Hopefully, it's clearer here. You can see that Base<TBase> is distinct
the generic type parameter said:
The first type what we got with typeof operator has FullName, but second
and
third has not.

This is another clue. Type.FullName should be round-trippable through
Type.GetType(string). There's no way to get a generic type which is
closed using a type parameter from a descendant generic type's
definition, so that's why Type.FullName doesn't return anything.

-- Barry
 
B

Barry Kelly

john said:
As I understand your answer still not explains why the second type and third
type equals...
(Actually they are identical, but not the same Type instances.)

They are *not* identical. They have different type arguments.

MyGenericClass1<T>'s base class MyBaseGenericClass<> has
MyGenericClass1<T>'s type parameter as its type argument. Note that
carefully: the base class MyBaseGenericClass<> in this case *has* a type
argument value, and that type argument's value is a type parameter. The
type parameter belongs to the descendant type.

MyGenericClass2<T>'s base class MyBaseGenericClass<> has
MyGenericClass2<T>'s type parameter as its type argument.

The type parameter T in MyGenericClass1<T> is different from the type
parameter T in MyGenericClass2<T>. This is the difference between the
two descendant classes. It is why they are not identical.

I'm trying to be precise with terminology here:

Here's another program. Please run it! I hope you understand why it
prints what it does:

---8<---
using System;
using System.Reflection;
using System.Text;

class Program
{
class Base<TBase> {}
class DescA<TDescA> : Base<TDescA> {}
class DescB<TDescB> : Base<TDescB> {}

static void Main()
{
Console.WriteLine("1. The type argument doesn't change"
+ " the GUID:");
Console.WriteLine(" (This is why you have found the "
+ "GUIDs are the same.)");
Console.WriteLine(typeof(Base<int>).GUID);
Console.WriteLine(typeof(Base<string>).GUID);
Console.WriteLine();

Type genericBase = typeof(Base<>);
Type tbase = genericBase.GetGenericArguments()[0];

Type descA = typeof(DescA<>);
Type tdescA = descA.GetGenericArguments()[0];
Type descA_base_tdescA =
descA.BaseType.GetGenericArguments()[0];

Console.WriteLine("2. The base type of DescA<TDescA> "
+ "is specialized with TDescA:");
Console.WriteLine(tdescA == descA_base_tdescA);
Console.WriteLine();

Type descB = typeof(DescB<>);
Type tdescB = descB.GetGenericArguments()[0];
Type descB_base_tdescB =
descB.BaseType.GetGenericArguments()[0];

Console.WriteLine("2. The base type of DescB<TDescB> is"
+ " specialized with TDescB:");
Console.WriteLine(tdescB == descB_base_tdescB);
Console.WriteLine();

Console.WriteLine("3. The difference between DescA and"
+ " DescB's base types is the type argument:");
Console.WriteLine(DescribeType(descA.BaseType));
Console.WriteLine(DescribeType(descB.BaseType));

Console.WriteLine();
Console.WriteLine("The base types are different, but the"
+ " generic types they're based on are the same:");
Console.WriteLine(
descA.BaseType.GetGenericTypeDefinition()
==
descB.BaseType.GetGenericTypeDefinition());
}

static string DescribeType(Type type)
{
StringBuilder result = new StringBuilder();
DescribeType(type, result);
return result.ToString();
}

static void DescribeType(Type type, StringBuilder result)
{
result.Append(type.Name);
if (type.IsGenericType)
{
result.Length -= 2; // chop off "`n".
result.Append('<');
foreach (Type typeArgument in
type.GetGenericArguments())
{
DescribeType(typeArgument, result);
result.Append(',');
}
result.Length -= 1; // chop off ','.
result.Append('>');
}
}
}
--->8---

It prints the following:

---8<---
1. The type argument doesn't change the GUID:
(This is why you have found the GUIDs are the same.)
a3f34de9-6d33-31ce-b775-1eb9579d8e30
a3f34de9-6d33-31ce-b775-1eb9579d8e30

2. The base type of DescA<TDescA> is specialized with TDescA:
True

2. The base type of DescB<TDescB> is specialized with TDescB:
True

3. The difference between DescA and DescB's base types is the type
argument:
Base<TDescA>
Base<TDescB>

The base types are different, but the generic types they're based on are
the same:
True
--->8---
Other question: If based on your explanation first and second type does not
the same, then why are they GUIDs the same?

There is only a GUID for the generic type. The GUID doesn't change based
on the type parameters supplied to the base type. (And on Mono, the GUID
is all zeros...)
I think there are some bugs here...

I'm about 99.8% certain there aren't, or if there are, then they are in
the documentation.

-- Barry
 
Top