如何检查一个对象是否可为空?

233

如何检查给定对象是否可为空,换句话说,如何实现以下方法...

bool IsNullableValueType(object o)
{
    ...
}

我正在寻找可空的 值类型。 我没有考虑引用类型。

//Note: This is just a sample. The code has been simplified 
//to fit in a post.

public class BoolContainer
{
    bool? myBool = true;
}

var bc = new BoolContainer();

const BindingFlags bindingFlags = BindingFlags.Public
                        | BindingFlags.NonPublic
                        | BindingFlags.Instance
                        ;


object obj;
object o = (object)bc;

foreach (var fieldInfo in o.GetType().GetFields(bindingFlags))
{
    obj = (object)fieldInfo.GetValue(o);
}

obj现在指向一个类型为bool (System.Boolean)且值等于true的对象。我真正想要的是一个类型为Nullable<bool>的对象。

所以现在,我决定通过检查o是否可为空来实现一个解决方法,并创建一个可为空的包装器。


代码中应该将字符串包括在可为空的类型中吗?它们是一种非泛型值类型,似乎是可为空的。或者它们不是值类型? - TamusJRoyce
1
字符串不是值类型。它是引用类型。 - Suncat2000
这是一个非常好的问题!'Type.IsNullableType()' 有点具有欺骗性,因为它实际上只检查类型是否为 'Nullable<T>',如果您真正想要检查任何可以接受 null 值的类型,则不会返回预期结果(例如,我尝试在运行时使用 a.IsNullableType(),其中 'a' 是 typeof(string) 确定的)。 - ErrCode
在 fieldInfo.FieldType 中,检查 FieldType 是否为泛型类型,并且泛型类型是 Nullable<> 类型。例如:if (FieldType.IsGenericType && FieldType.GetGenericTypeDefinition() == typeof(Nullable<>))。不要尝试获取 obj.GetType(),它的 UndelyingSystemType 变量 T(在您的情况下为 Boolean 类型,而不是 Nullable<Boolean>)会存在装箱问题。 - SoLaR
14个回答

311

有两种可空类型 - Nullable<T> 和引用类型。

Jon纠正了我,如果装箱了就很难获取类型,但是你可以使用泛型: - 那么下面怎么样?实际上这是测试类型T,但使用obj参数纯粹是为了泛型类型推断(使调用变得容易) - 即使没有obj参数,它的工作方式也几乎相同。

static bool IsNullable<T>(T obj)
{
    if (obj == null) return true; // obvious
    Type type = typeof(T);
    if (!type.IsValueType) return true; // ref-type
    if (Nullable.GetUnderlyingType(type) != null) return true; // Nullable<T>
    return false; // value-type
}

但是,如果你已经将值装箱到一个对象变量中,这种方法可能不太适用。
微软文档:https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/nullable-types/how-to-identify-a-nullable-type

7
如果你在装箱时得到一个Nullable<T>的装箱对象,那么最后一行才有效。据我所知,这是可能的,但比较棘手。 - Jon Skeet
1
@Abel 如果你的意思是关于他编辑以澄清他没有考虑引用类型,我认为我的回答早于那个编辑;读者可以根据自己的需求做出自己的决定,我猜想(确认:他有关引用类型的评论是在14:42添加的;我的回答都在14:34之前)。 - Marc Gravell
1
当 obj = 1 时,(obj == null) 会抛出异常吗? - Qi Fan
3
如果T是一个由T:struct约束的泛型参数,那么T不能是Nullable<>,所以在这种情况下你不需要检查!我知道类型Nullable<>是一个结构体,但在C#中,约束where T:struct特别排除可空值类型。规范指出:“请注意,尽管被分类为值类型,但可空类型(§4.1.10)并不满足值类型约束条件。” - Jeppe Stig Nielsen
@smartcaveman 不需要;泛型不支持运算符,因此这将始终是一个unbox_any加引用检查。 - Marc Gravell
显示剩余20条评论

50

使用方法重载有一个非常简单的解决方案。

http://deanchalk.com/is-it-nullable/

摘自:

public static class ValueTypeHelper
{
    public static bool IsNullable<T>(T t) { return false; }
    public static bool IsNullable<T>(T? t) where T : struct { return true; }
}

那么

static void Main(string[] args)
{
    int a = 123;
    int? b = null;
    object c = new object();
    object d = null;
    int? e = 456;
    var f = (int?)789;
    bool result1 = ValueTypeHelper.IsNullable(a); // false
    bool result2 = ValueTypeHelper.IsNullable(b); // true
    bool result3 = ValueTypeHelper.IsNullable(c); // false
    bool result4 = ValueTypeHelper.IsNullable(d); // false
    bool result5 = ValueTypeHelper.IsNullable(e); // true
    bool result6 = ValueTypeHelper.IsNullable(f); // true

9
给您点赞,因为您增加了测试用例。我已经使用这些测试用例来检查所有其他答案。更多人应该做这个额外的工作。 - Marty Neal
4
说句实话,在VB.NET中这行代码是不起作用的。在所有返回True的情况下,它都会导致编译器错误"Overload resolution failed because no accessible 'IsNullable' is most specific for these arguments"。 - ckittel
1
我真的很喜欢这个解决方案,但很遗憾VB无法处理它。我尝试使用ValueType进行解决,但是在VB编译器中遇到了问题,因为根据调用方式(作为共享方法还是扩展方法),编译器不一致地选择要使用哪个重载。我甚至提出了一个问题,因为这似乎很奇怪:https://dev59.com/z2jWa4cB1Zd3GeqPoC-l - James Close
26
你正在检查编译时类型,但如果编译时类型是可空(System.Nullable <>),则已经明显(从智能感知)。 如果你说 object g = e; 然后 ValueTypeHelper.IsNullable(g),你期望得到什么? - Jeppe Stig Nielsen
20
我刚刚验证了,就像Jeppe说的那样,这行不通。如果将变量转换为对象,它将始终返回false。因此,您无法以这种方式在运行时确定未知对象的类型。唯一能行的情况是类型在编译时已固定,而在这种情况下,根本不需要运行时检查。 - HugoRune
显示剩余3条评论

45
这对我来说很有效,而且似乎很简单:

static bool IsNullable<T>(T obj)
{
    return default(T) == null;
}

对于值类型:

static bool IsNullableValueType<T>(T obj)
{
    return default(T) == null && typeof(T).BaseType != null && "ValueType".Equals(typeof(T).BaseType.Name);
}

9
就这个事情而言,这也是微软所使用的测试。 - canton7
2
不错啊...这不是因为它回答得晚所以没成为最佳答案吗?我觉得最佳答案很令人困惑。 - Vincent Buscarello
2
这应该是最佳答案。经过多天尝试不同的方法后,我随机想到了这个解决方案,尝试了一下,似乎与最高评分的答案相比完美地运行着。 - user3163495
4
这是一个很好的解决方案,可以找出任何实例是否可以设置为NULL,但它会为所有可以设置为null的内容,包括普通对象返回true。重要的是要意识到原始问题特别想检测可空值类型(Nullable ValueTypes)。 - JamesHoux
这是所选答案。 - Desolator
显示剩余2条评论

30
“如何检查类型是否可为空?”实际上是“如何检查类型是否为 Nullable<>?”,这可以推广到“如何检查类型是否为某个泛型类型的构造类型?”,从而不仅回答了问题“Nullable<int>Nullable<> 吗?”,还回答了问题“List<int>List<> 吗?”。
大多数提供的解决方案使用 Nullable.GetUnderlyingType() 方法,这显然只能用于 Nullable<> 的情况。我没有看到通用的反射解决方案,它将适用于任何泛型类型,因此我决定在这里为后人添加它,即使这个问题早已被回答。
使用反射检查一个类型是否为某种形式的 Nullable<>,你首先必须将你的构造泛型类型,例如 Nullable<int>,转换为泛型类型定义,即 Nullable<>。你可以使用 Type 类的 GetGenericTypeDefinition() 方法来实现。然后,你可以将结果类型与 Nullable<> 进行比较:
Type typeToTest = typeof(Nullable<int>);
bool isNullable = typeToTest.GetGenericTypeDefinition() == typeof(Nullable<>);
// isNullable == true

同样的方法也适用于任何通用类型:
Type typeToTest = typeof(List<int>);
bool isList = typeToTest.GetGenericTypeDefinition() == typeof(List<>);
// isList == true

有些类型看起来可能相同,但不同数量的类型参数意味着它是完全不同的类型。

Type typeToTest = typeof(Action<DateTime, float>);
bool isAction1 = typeToTest.GetGenericTypeDefinition() == typeof(Action<>);
bool isAction2 = typeToTest.GetGenericTypeDefinition() == typeof(Action<,>);
bool isAction3 = typeToTest.GetGenericTypeDefinition() == typeof(Action<,,>);
// isAction1 == false
// isAction2 == true
// isAction3 == false

Type对象每个类型只实例化一次,因此您可以在它们之间检查引用相等性。因此,如果您想检查两个对象是否为相同的通用类型定义,则可以编写以下代码:

var listOfInts = new List<int>();
var listOfStrings = new List<string>();

bool areSameGenericType =
    listOfInts.GetType().GetGenericTypeDefinition() ==
    listOfStrings.GetType().GetGenericTypeDefinition();
// areSameGenericType == true

如果您想检查一个对象是否可为空,而不是检查一个Type类型的对象,那么您可以使用上述技巧和Marc Gravell的解决方案来创建一个非常简单的方法:
static bool IsNullable<T>(T obj)
{
    if (!typeof(T).IsGenericType)
        return false;

    return typeof(T).GetGenericTypeDefinition() == typeof(Nullable<>);
}

@ AllonGuralnek 在我的回答中有一个简化版本。我想将其作为编辑,并且由于我的声望不及您的水平,它将作为编辑而没有我的名字出现在您的回答中,即使如此,似乎评论总是在攻击我,即使它并不是针对作者的。奇怪的世界,有些人不理解定义 :)。 - ipavlu
@ipavlu:你的版本并不是简化版,事实上它更加复杂。我想你的意思是它被优化了,因为你缓存了结果。这使得它更难理解。 - Allon Guralnek
@AllonGuralnek 静态泛型类和静态一次性初始化字段,这是很复杂的吗?天哪,我犯了可怕的罪孽 :)。 - ipavlu
@ipavku:是的,因为它与问题“如何检查对象是否可为空?”无关。我尽量保持简单明了,避免引入不必要和无关的概念。 - Allon Guralnek
1
@nawfal:如果我理解正确的话,你在质疑我的实现方式,因为框架已经提供了Nullable.GetUnderlyingType()方法。为什么不直接使用框架中的方法呢?嗯,你应该使用它。它更清晰、更简洁、更经过测试。但是在我的帖子中,我试图教授如何使用反射来获取所需的信息,以便某人可以将其应用于任何类型(通过将typeof(Nullable<>)替换为任何其他类型)。如果你查看GetUnderlyingType()的源代码(原始或反编译),你会发现它与我的代码非常相似。 - Allon Guralnek
显示剩余7条评论

20

好的,你可以使用:

return !(o is ValueType);

...但是对象本身并不可为空或其他 - 只有类型可以为空。您打算如何使用它?


2
这让我有点困惑。例如,int?i = 5; typeof(i)返回System.Int32而不是Nullable<Int32> - typeof(int?)返回Nullable<Int32>.. 我在哪里可以获得关于这个主题的一些清晰度? - Gishu
3
typeof(i)会导致编译错误——你不能在变量上使用typeof。你实际上做了什么? - Jon Skeet
15
i.GetType() 会先装箱为 Object 类型,而且没有所谓的可空值类型的装箱 - Nullable<int> 装箱后会成为一个 null 引用或者一个装箱的 int 类型。 - Jon Skeet
@Kiquenet:我们这里没有类型,只有值。 - Jon Skeet
标记的答案有:Type type = typeof(T); 关于它,哪个更好的解决方案? - Kiquenet
显示剩余4条评论

14

我想到的最简单的解决方案是实现 Microsoft 的解决方案 (如何:识别可空类型(C# 编程指南)) 作为扩展方法:

public static bool IsNullable(this Type type)
{
    return Nullable.GetUnderlyingType(type) != null;
}

然后可以这样调用:

bool isNullable = typeof(int).IsNullable();

这似乎也是访问IsNullable()的一种合理方式,因为它符合Type类的所有其他IsXxxx()方法。


1
你不是想用"=="而不是"!="吗? - vkelman
很好的发现 @vkelman。我已经更新了答案,使用了Microsoft的当前建议,因为自从我写这篇文章以来,这个建议已经改变了。 - sclarke81

11
我能想到最简单的方法是:
public bool IsNullable(object obj)
{
    Type t = obj.GetType();
    return t.IsGenericType 
        && t.GetGenericTypeDefinition() == typeof(Nullable<>);
}

+1。对于可空类型的优秀解决方案。我还没有进行过专门的测试。所以如果有其他人可以验证,将不胜感激。 - TamusJRoyce
我已经测试过了。我不得不创建一种类似于“Nullable”的类型,但语义不同。在我的情况下,我应该支持null作为有效值,同时也支持没有任何值。因此,我创建了一个“Optional”类型。由于需要支持null值,我还必须实现处理“Nullable”值的代码作为我的实现的一部分。这就是这段代码的来源。 - CARLOS LOTH
9
我认为这个解决方案是错误的。将可空值类型作为参数传递给期望对象类型参数的方法应该会导致装箱发生。可空是一个值类型,装箱转换的结果是引用类型。没有装箱的可空类型。我认为这个方法总是返回false? - Mishax
1
有没有像其他答案一样的测试? - Kiquenet
5
由于对值进行装箱,它无法正常工作。它会始终返回FALSE。 - N Rocking

11
这里涉及两个问题:1) 测试Type类型是否可空;2)测试对象是否表示可为空的Type。
对于第一个问题(测试Type),我在自己的系统中使用了以下解决方案:TypeIsNullable-check solution
对于第二个问题(测试对象),Dean Chalk提供的解决方案适用于值类型,但并不适用于引用类型,因为使用重载总是返回false。由于引用类型本质上是可为空的,因此测试引用类型应始终返回true。有关这些语义的说明,请参见下面的“nullability”说明。因此,这是我的修改意见:
    public static bool IsObjectNullable<T>(T obj)
    {
        // If the parameter-Type is a reference type, or if the parameter is null, then the object is always nullable
        if (!typeof(T).IsValueType || obj == null)
            return true;

        // Since the object passed is a ValueType, and it is not null, it cannot be a nullable object
        return false; 
    }

    public static bool IsObjectNullable<T>(T? obj) where T : struct
    {
        // Always return true, since the object-type passed is guaranteed by the compiler to always be nullable
        return true;
    }

以下是关于上述解决方案的客户端测试代码的修改:

这里是我对客户端测试代码的修改:

    int a = 123;
    int? b = null;
    object c = new object();
    object d = null;
    int? e = 456;
    var f = (int?)789;
    string g = "something";

    bool isnullable = IsObjectNullable(a); // false 
    isnullable = IsObjectNullable(b); // true 
    isnullable = IsObjectNullable(c); // true 
    isnullable = IsObjectNullable(d); // true 
    isnullable = IsObjectNullable(e); // true 
    isnullable = IsObjectNullable(f); // true 
    isnullable = IsObjectNullable(g); // true

我修改了Dean在IsObjectNullable<T>(T t)中的方法,因为他原来的方法对于引用类型总是返回false。由于像IsObjectNullable这样的方法应该能够处理引用类型的值,并且所有引用类型本质上都是可空的,因此如果传递任何一个引用类型或null,则该方法应始终返回true。
可以用以下单个方法替换上述两个方法并实现相同的输出:
    public static bool IsObjectNullable<T>(T obj)
    {
        Type argType = typeof(T);
        if (!argType.IsValueType || obj == null)
            return true;
        return argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(Nullable<>);
    }

然而,使用这种最后一种单一方法的问题在于,当使用Nullable<T>参数时性能会受到影响。与允许编译器选择先前显示的第二种方法重载相比,在使用IsObjectNullable调用Nullable<T>类型参数时,执行此单个方法的最后一行需要更多的处理器时间。因此,最佳解决方案是使用此处所示的两种方法。
注意:只有在使用原始对象引用或精确副本的情况下,该方法才可靠地工作,如示例所示。然而,如果将可空对象装箱到另一个类型(如对象等)中,而不是保持其原始Nullable<>形式,则该方法将无法可靠地工作。如果调用此方法的代码未使用原始的、未装箱的对象引用或精确副本,则无法可靠地使用此方法确定对象的可空性。
在大多数编码场景中,要确定可空性,必须依赖于测试原始对象的类型,而不是其引用(例如,代码必须访问对象的原始类型以确定可空性)。在这些更常见的情况下,IsTypeNullable(请参见链接)是一种可靠的确定可空性的方法。
附言 - 关于“可空性”
我应该重申一下我在另一篇帖子中关于可空性的声明,它直接适用于正确处理此主题。也就是说,我认为这里的讨论重点不应该是如何检查对象是否为通用Nullable类型,而应该是是否可以将null值赋给其类型的对象。换句话说,我认为我们应该确定一个对象类型是否可空,而不是它是否为Nullable。区别在于语义,即确定可空性的实际原因,这通常是最重要的。
在使用可能在运行时之前未知的类型的对象的系统中(Web服务、远程调用、数据库、提要等),一个常见的要求是确定是否可以将null分配给该对象,或者该对象是否可能包含null。在非可空类型上执行此类操作通常会产生错误,通常是异常,这些错误在性能和编码要求方面非常昂贵。为了采取高度优先的方法,以预防此类问题,有必要确定任意类型的对象是否能够包含null,即是否一般“可空”。
在.NET术语中,从非常实际和典型的意义上来看,可空性根本不一定意味着对象的类型是Nullable形式的。事实上,在许多情况下,对象具有引用类型,可以包含null值,因此都是可空的;其中没有一个具有Nullable类型。因此,在大多数情况下,应该针对一般概念的可空性进行测试,而不是针对实现相关的Nullable概念。因此,我们不应该只关注.NET Nullable类型,而是在关注一般实际可空性概念的过程中融入我们对其要求和行为的理解。

6

注意,当对可空类型(例如Nullable<int>或int?)进行装箱时要小心:

int? nullValue = null;
object boxedNullValue = (object)nullValue;
Debug.Assert(boxedNullValue == null);

int? value = 10;
object boxedValue = (object)value;
Debug.Assert( boxedValue.GetType() == typeof(int))

它变成了真正的引用类型,因此您失去了它是可空的事实。


3
也许有点离题,但仍然有一些有趣的信息。我发现很多人使用 Nullable.GetUnderlyingType() != null 来判断一个类型是否可空。这显然是有效的,但是微软建议使用以下方式 type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) (请参见http://msdn.microsoft.com/en-us/library/ms366789.aspx)。
我从性能方面考虑了这个问题。下面测试的结论(一百万次尝试)是:当一个类型是可空时,微软的方法提供了最佳性能。 Nullable.GetUnderlyingType(): 1335ms (慢3倍) GetGenericTypeDefinition() == typeof(Nullable<>): 500ms 我知道我们在谈论一个小小的时间差,但每个人都喜欢微调毫秒!所以如果你的老板想要减少一些毫秒,那么这就是你的救星...
/// <summary>Method for testing the performance of several options to determine if a type is     nullable</summary>
[TestMethod]
public void IdentityNullablePerformanceTest()
{
    int attempts = 1000000;

    Type nullableType = typeof(Nullable<int>);

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();
    for (int attemptIndex = 0; attemptIndex < attempts; attemptIndex++)
    {
        Assert.IsTrue(Nullable.GetUnderlyingType(nullableType) != null, "Expected to be a nullable"); 
    }

    Console.WriteLine("Nullable.GetUnderlyingType(): {0} ms", stopwatch.ElapsedMilliseconds);

    stopwatch.Restart();

    for (int attemptIndex = 0; attemptIndex < attempts; attemptIndex++)
   {
       Assert.IsTrue(nullableType.IsGenericType && nullableType.GetGenericTypeDefinition() == typeof(Nullable<>), "Expected to be a nullable");
   }

   Console.WriteLine("GetGenericTypeDefinition() == typeof(Nullable<>): {0} ms", stopwatch.ElapsedMilliseconds);
   stopwatch.Stop();
}

1
嗨,测量时间可能存在一个问题,Assert 可能会影响结果。你试过不用 Assert 进行测试吗?另外,Console.WriteLine 应该在计时区域外。加一赞,因为尝试量化性能问题 :)。 - ipavlu
@ipavlu Console.WriteLine 确实在计量区域之外 ;) - nawfal
Roel,正如ipavlu所提到的,Assert应该在循环外部。其次,您还应该对非空值进行测试以测试错误情况。我进行了类似的测试(2个可空和4个非空),并且GetUnderlyingType需要约2秒钟,而GetGenericTypeDefinition需要约1秒钟,即GetGenericTypeDefinition快两倍(而不是三倍)。 - nawfal
又进行了一轮包含 2 个可为空类型和 2 个非空类型的测试 - 这次 GetUnderlyingType 函数要慢 2.5 倍。但只测试非空类型时,两者速度几乎相同。 - nawfal
但更重要的是,当您必须检查可空性并获取其基础类型时,“GetUnderlyingType”很有用。这非常实用,您经常会看到类似于“Activator.CreateInstance(Nullable.GetUnderlyingType(type)??type)”的模式。它就像“as”关键字一样,既检查转换又执行它并返回结果。如果您想要获取可为空类型的基础类型,则进行“GetGenericTypeDefinition”检查,然后获取通用类型将是一个坏主意,而“GetUnderlyingType”则更加易读和易记。如果我只做1000次,那我不介意使用它。 - nawfal

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