I recently ran into a situation where I needed to compare two types to see if they were the same. What complicated matters is the types I was comparing were generics…and each “instance” of a generic type appears to be different even if they’re constructed from the same component types. For example, Type1 and Type2 are considered unequal even though you might assume they should be:
public class SomeGenericType<T1, T2> { ... } public class TestClass { public bool TypesEqual() { var value1 = new SomeGenericType<int, string>(); var value2 = new SomeGenericType<int, string>(); // will always return false return value1.GetType() == value2.GetType(); } }
To determine “equality” of generic types you need to disassemble them and compare both the generic type and the generic type parameter types:
private class TypeBoundOptionComparer : IEqualityComparer<ITypeBoundOption> { public bool Equals( ITypeBoundOption? x, ITypeBoundOption? y ) { if( ReferenceEquals( x, y ) ) return true; if( x is null) return false; if( y is null) return false; // this is messy because constructed generic types are not deemed to // be equal even if they are built from the same types. We are also // assuming the ITypeBoundOption instances are instances of // TypeBoundOption when they could, in reality, be instances of // something else entirely. if( !GetTypeBoundTypeParameters( x, out var xContainerType ) ) return false; if (!GetTypeBoundTypeParameters(y, out var yContainerType)) return false; return xContainerType!.IsAssignableFrom( yContainerType! ); } private bool GetTypeBoundTypeParameters( ITypeBoundOption tbOption, out Type? containerType ) { containerType = null; var tbType = tbOption.GetType(); // we assume we were given an instance of a generic type with two type parameters if (!tbType.IsGenericType || tbType.GetGenericArguments().Length != 2) return false; // we further assume we are derived from TypeBoundOption<,> if( !typeof(TypeBoundOption<,>).IsAssignableFrom( tbType.GetGenericTypeDefinition() ) ) return false; containerType = tbType.GetGenericArguments()[ 0 ]; return true; } public int GetHashCode( ITypeBoundOption obj ) { return obj.ContainerType.GetHashCode(); } }
Note that in my case what was actually comparing was the generic types of two objects I only “knew” thru their non-generic interface, ITypeBoundOption. So I had to assume they were actually instances of a particular generic type — TypeBoundOption<,> — when they could’ve been based on some totally different class.
That was okay in my situation because the comparer only got called in a situation where I knew the only objects that would ever be compared were instances of TypeBoundOption<,>. You’d have to tweak things if you can’t be sure of something similar.