在C#中检查对象是否属于非特定泛型类型

15

假设我有以下类:

public class General<T> { }

我想确定一个对象是否为特定类型。

我知道可以使用反射来确定对象是否为该泛型类型,使用 Type.GetGenericTypeDefinition,但我想避免这样做。

类似 obj is General<T> 或者 obj.GetType().IsAssignableFrom(typeof(General<T>)) 这样的方法可行吗?

我很惊讶没有找到类似的问题,虽然可能是我在搜索时使用了错误的关键字。


1
出于好奇,如果你有答案,你打算拿它做什么?没有反射,你几乎不能做什么,而你的问题又要求避免使用反射。 - user743382
@EZSlaver 为了满足我的好奇心,你有示例可以演示这是一个问题吗?反射速度慢的论点已经提出过了,但我觉得它已经被贴上了迷信避免任何解决方案的标签。 - Grant Thomas
你的类不是“sealed”。如果obj的运行时类型为X,并且X有基类General<int>,那会怎样? - Jeppe Stig Nielsen
(I already rephrased, sorry.) - Jeppe Stig Nielsen
@GrantThomas,我正在一个函数内使用它,由于专有序列化,该函数被多次调用。将该函数更改为使用反射可能会严重影响整个系统的性能。 - EZLearner
显示剩余6条评论
5个回答

8
你可以这样做:
var obj = new General<int>();
var type = obj.GetType();
var isGeneral = 
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(General<>)) ||
type.GetBaseTypes().Any(x => x.IsGenericType && 
                             x.GetGenericTypeDefinition() == typeof(General<>));

下面是扩展方法GetBaseTypes:


public static IEnumerable<Type> GetBaseTypes(this Type type)
{
    if (type.BaseType == null) return type.GetInterfaces();

    return new []{type}.Concat(
           Enumerable.Repeat(type.BaseType, 1)
                     .Concat(type.GetInterfaces())
                     .Concat(type.GetInterfaces().SelectMany<Type, Type>(GetBaseTypes))
                     .Concat(type.BaseType.GetBaseTypes()));
}

致谢Slacks的回答


2
GetGenericTypeDefinition() 对于非泛型类型会抛出异常。从我阅读的问题来看,您应该能够为 int 调用它(并获得 false),并且您应该能够为 class I : General<int> 调用它(并获得 true)。 - user743382
很好了解。虽然我认为IsAssignableFrom对于开放式泛型类型没有意义,因为它们永远不可能是可变类型,因此永远不会相互分配。 - usr
2
那个检查避免了我的class I的异常,但是给出了错误的结果。 new I() is General<int>会返回true,但是你的检查不会。 - user743382
你能避免使用 Type.GetGenericTypeDefinition 吗? - EZLearner
1
几乎正确:type.GetBaseTypes() 不包括 type 本身,因此对于您答案中的测试 (new General<int>) 返回 false。 - user743382
type.GetInterfaces().SelectMany<Type, Type>(GetBaseTypes) 的需求是什么?type.GetInterfaces 不会返回所有接口,甚至包括基类吗? - nawfal

1

有很多关于相似 问题的答案,但它们都需要反思来遍历类型层次结构。我怀疑没有更好的方法。如果性能很关键,缓存结果可能是一个选项。这里有一个例子,使用ConcurrentDictionary作为简单的缓存。然后成本降低到一个简单的类型查找(通过GetType)和在缓存被初始化后进行ConcurrentDictionary查找。

using System.Collections.Concurrent;

private static ConcurrentDictionary<Tuple<Type,Type>, bool> cache = new ConcurrentDictionary<Tuple<Type,Type>, bool>();

public static bool IsSubclassOfRawGeneric(this Type toCheck, Type generic) {
    var input = Tuple.Create(toCheck, generic);
    bool isSubclass = cache.GetOrAdd(input, key => IsSubclassOfRawGenericInternal(toCheck, generic));
    return isSubclass;
}

private static bool IsSubclassOfRawGenericInternal(Type toCheck, Type generic) {
    while (toCheck != null && toCheck != typeof(object)) {
        var cur = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck;
        if (generic == cur) {
            return true;
        }
        toCheck = toCheck.BaseType;
    }
    return false;
}

您会像这样使用它:

class I : General<int> { }

object o = new I();
Console.WriteLine(o is General<int>); // true
Console.WriteLine(o.GetType().IsSubclassOfRawGeneric(typeof(General<>))); //true

请记住,这仅适用于类层次结构测试,而不适用于接口测试。 - nawfal

0

如果一个通用的类或接口有成员可以通过代码使用,这些代码在更普遍的形式下持有引用,比如Object,但没有实际的泛型类型可用,则应该在非泛型基类或接口中公开这些成员。在许多情况下,框架未能遵守这一原则,但没有理由必须遵循他们的例子。例如,像IList<T>这样的类型可能已从IListBase派生,后者包括或继承了这样的成员:

int Count {get;}
void Delete(int index);
void Clear();
void Swap(int index1, int index2);
int Compare(int index1, int index2);
// Return an object with a `StoreToIndex(int)` method
// which would store it to the list it came from.
ListItemHolder GetItemHolder(int index);
ListFeatures Features {get;}

这些成员都不会以任何方式依赖于列表中所持有的项目类型,一个人可以编写方法来执行诸如对列表进行排序(如果其Features指示它是可写的并且知道如何比较项目)之类的操作,而无需了解元素类型的任何信息。如果泛型接口继承自非泛型接口,需要使用非泛型函数的代码可以简单地将其强制转换为非泛型接口类型并直接使用。


0

使用类型参数实例化的通用类型定义与其他通用类型实例化根本没有关系。它们也与通用类型定义没有关系。当涉及到赋值和运行时转换时,它们是完全不兼容的。如果它们是兼容的,那么就有可能破坏类型系统。

因此,运行时转换是无法帮助的。您确实需要使用 Type.GetGenericTypeDefinition。您可以将其抽象为一个辅助函数,并以这种方式保持代码相对清晰。


1
在大多数情况下,这是正确的。我找到了一个例子X<T>,它似乎有点不同,即如果(1)类型X<>在涉及类型参数T时是协变或逆变的,并且(2)相同的类型参数T被限制为特定的基类B。在这种情况下,对于协变的X<>typeof(X<B>).IsAssignableFrom(typeof(X<>))或者对于逆变性,typeof(X<>).IsAssignableFrom(typeof(X<B>))。我不确定这是否是“意外”的。当然,X必须是接口或委托类型,以便成为协变或逆变。 - Jeppe Stig Nielsen

0

为了得到一个更加通用的解决方案,可以适用于任何父类型(包括基类和接口):

    public static bool IsCompatibleWith(this Type type, Type parentType)
    {
        if (type == null)
        {
            throw new ArgumentNullException(nameof(type));
        }

        if (parentType.IsAssignableFrom(type))
        {
            return true;
        }

        return type.GetAssignableTypes()
           .Where(t => t.IsGenericType)
           .Any(t=> t.GetGenericTypeDefinition() == parentType);
    }

    /// <summary>
    /// Gets all parent types including the currrent type.
    /// </summary>
    public static IEnumerable<Type> GetAssignableTypes(this Type type)
    {
        if (type == null)
        {
            throw new ArgumentNullException(nameof(type));
        }

        // First check for interfaces because interface types don't have base classes.
        foreach (Type iType in type.GetInterfaces())
        {
            yield return iType;
        }

        // Then check for base classes.
        do
        {
            yield return type;
            type = type.BaseType;
        }
        while (type != null);
    }

提出更好的方法名称。也许称其为IsCompatibleWith是具有误导性的。也许可以使用IsKindOf?此外,GetAssignableTypes也可以称为GetParentTypes,但这也是具有误导性的。命名很难。更好的做法是对其进行文档化。

一些测试:

IsCompatibleWith(typeof(List<int>), typeof(IList<int>))
true

IsCompatibleWith(typeof(List<>), typeof(IList<>))
true

IsCompatibleWith(typeof(List<int>), typeof(IList<>))
true

IsCompatibleWith(typeof(List<int>), typeof(IList<string>))
false


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接