有没有办法让C#类处理自己的空引用异常

时间:2012-08-15 15:26:46

标签: c#

问题

我希望有一个能够处理自身空引用的类。我怎样才能做到这一点?扩展方法是我能想到的唯一方法,但我想如果有一些我不了解的关于C#的好东西,我会问。

示例

我有一个名为User的类,其中包含一个名为IsAuthorized的属性。

正确实例化UserIsAuthorized有一个实现。但是,当我的User引用包含null时,我希望调用IsAuthorized来返回false而不是爆炸。


解决方案

很多很好的答案。我最终使用其中三个来解决我的问题。

  1. 我使用了Zaid Masud建议的Null对象设计模式。
  2. 我将这与Belmiris建议使用struct相结合,所以我没有空引用
  3. 得到了一个很好的解释,为什么C#以这种方式工作以及如何从Jon Hanna真正解决它
  4. 不幸的是,我只能选择其中一个作为我接受的答案,所以如果你正在访问这个页面,你应该花时间投票给所有这三个以及给出的任何其他优秀答案。

7 个答案:

答案 0 :(得分:29)

正确的面向对象解决方案怎么样?这正是Null Object设计模式的用途。

您可以提取IUser接口,让User对象实现此接口,然后创建NullUser对象(也实现IUser)并始终在IsAuthorized属性上返回false。

现在,修改消费代码以依赖IUser而不是User。客户端代码将不再需要空检查。

以下代码示例:

public interface IUser
{
    // ... other required User method/property signatures

    bool IsAuthorized { get; }
}

public class User : IUser
{
    // other method/property implementations

    public bool IsAuthorized
    {
        get { // implementation logic here }
    }
}

public class NullUser : IUser
{
    public bool IsAuthorized
    {
        get { return false; }
    }
}

现在,您的代码将返回IUser而不是用户,客户端代码将仅依赖于IUser:

public IUser GetUser()
{
    if (condition)
    {
        return new NullUser(); // never return null anymore, replace with NullUser instead
    }
    return new User(...);
}

答案 1 :(得分:18)

  

但是,当我的用户引用包含null时,我希望IsAuthorized始终返回false而不是爆炸。

只有在IsAuthorized是静态方法时才能执行此操作,在这种情况下,您可以检查null。这就是扩展方法可以做到这一点的原因 - 它们实际上只是用于调用静态方法的不同语法。

调用方法或属性(例如IsAuthorized)作为实例方法需要一个实例。只需在null上调用实例方法(包括属性获取器)的操作就会触发异常。您的类不会引发异常,但是当您尝试使用(null)引用时,运行时本身不会引发异常。在C#中没有办法解决这个问题。

答案 2 :(得分:11)

如果变量为null,则表示它不引用任何对象,因此在类方法中处理null引用是没有意义的(我认为这在技术上是不可能的)。

您应该通过在调用“IsAuthorized”或之前的事件之前进行检查来保证它不为空。

编辑:找到一个解决方法来做这件事会是一件坏事:让某人理解这种行为会让人感到困惑,因为它不是编程语言的“预期”行为。它还可能导致您的代码隐藏一些问题(一个空值,它应该是一个对象)并创建一个很难找到的错误。那说:肯定是一个坏主意。

答案 3 :(得分:7)

问题不在于创建这样的方法。这是调用方法。如果您在代码中对if(this == null)进行了测试,则完全有效。我想它可以被编译器根据它“不可能”被击中进行优化,但我还是幸运的是它不是。

但是,当您调用该方法时,它将通过callvirt完成,因此它不会直接调用该方法,而是会找到要调用该方法的方法的版本。特殊实例就像使用虚方法一样。因为对于空引用会失败,所以你的完美自我测试方法在调用之前就会失败。

C#故意这样做。 According to Eric Gunnerson这是因为他们认为让你这样做会有点奇怪。

我从来没有理解为什么让一个以C ++为模型的.NET语言在.NET和同一家公司生产的C ++编译器中做了完全允许的事情,*被认为有点奇怪。我一直认为它不被允许有点奇怪。

您可以添加来自调用该类的其他语言(F#或IL)的内容,或使用Reflection.Emit生成一个代理,这样做并且可以正常工作。例如,以下代码将调用GetHashCode中定义的object版本(即,即使GetHashCode被覆盖,也不会调用覆盖),这是一个示例一个可以安全地调用null实例的方法:

DynamicMethod dynM = new DynamicMethod(string.Empty, typeof(int), new Type[]{typeof(object)}, typeof(object));
ILGenerator ilGen = dynM.GetILGenerator(7);
ilGen.Emit(OpCodes.Ldarg_0);
ilGen.Emit(OpCodes.Call, typeof(object).GetMethod("GetHashCode"));
ilGen.Emit(OpCodes.Ret);
Func<object, int> RootHashCode = (Func<object, int>)dynM.CreateDelegate(typeof(Func<object, int>));
Console.WriteLine(RootHashCode(null));

关于这一点的一个好处是,你可以抓住RootHashCode,这样你只需要构建一次(比如在静态构造函数中)然后你可以重复使用它。

这当然让其他代码通过空引用调用您的方法没有价值,因为您建议的扩展方法是您唯一的选择。

当然值得注意的是,如果您使用的语言没有C#的这个怪癖,那么您应该提供一些替代方法来获取调用空引用的“默认”结果,因为C#人们无法得到它。就像C#一样,人们应该避免使用公共名称之间仅存在案例的差异,因为某些语言无法解决这个问题。

编辑:您的问题IsAuthorized被调用的完整示例,因为投票显示有些人不相信它可以完成(!)

using System;
using System.Reflection.Emit;
using System.Security;

/*We need to either have User allow partially-trusted
 callers, or we need to have Program be fully-trusted.
 The former is the quicker to do, though the latter is
 more likely to be what one would want for real*/ 
[assembly:AllowPartiallyTrustedCallers]

namespace AllowCallsOnNull
{
  public class User
  {
    public bool IsAuthorized
    {
      get
      {
        //Perverse because someone writing in C# should be expected to be friendly to
        //C#! This though doesn't apply to someone writing in another language who may
        //not know C# has difficulties calling this.
        //Still, don't do this:
        if(this == null)
        {
          Console.Error.WriteLine("I don't exist!");
          return false;
        }
        /*Real code to work out if the user is authorised
        would go here. We're just going to return true
        to demonstrate the point*/
        Console.Error.WriteLine("I'm a real boy! I mean, user!");
        return true;
      }
    }
  }
  class Program
  {
    public static void Main(string[] args)
    {
      //Set-up the helper that calls IsAuthorized on a
      //User, that may be null.
      DynamicMethod dynM = new DynamicMethod(string.Empty, typeof(bool), new Type[]{typeof(User)}, typeof(object));
      ILGenerator ilGen = dynM.GetILGenerator(7);
      ilGen.Emit(OpCodes.Ldarg_0);
      ilGen.Emit(OpCodes.Call, typeof(User).GetProperty("IsAuthorized").GetGetMethod());
      ilGen.Emit(OpCodes.Ret);
      Func<User, bool> CheckAuthorized = (Func<User, bool>)dynM.CreateDelegate(typeof(Func<User, bool>));

      //Now call it, first on null, then on an object
      Console.WriteLine(CheckAuthorized(null));    //false
      Console.WriteLine(CheckAuthorized(new User()));//true
      //Wait for input so the user will actually see this.
      Console.ReadKey(true);
    }
  }
}

哦,这是一个真实的实际问题。关于C#行为的好处是,它会导致对null引用的调用失败,因为它们无论如何都会失败,因为它们访问中间的某个字段或虚拟区域。这意味着我们不必担心在编写调用时我们是否处于null实例中。但是,如果您希望在完全公共方法(即公共类的公共方法)中防弹,那么您就不能依赖于此。如果方法的第1步始终跟随第2步至关重要,而第2步只有在空实例上调用时才会失败,那么应该进行自动空检查。这很少会发生,但它可能会导致非C#用户的错误,如果不使用上述技术,你将永远无法在C#中重现。

*虽然,这是特定于他们的编译器 - 根据C ++标准IIRC,它是未定义的。

答案 4 :(得分:5)

您可以使用结构吗?那么它不应该是空的。

答案 5 :(得分:3)

如果您没有有效的实例引用,则无法引用属性。如果您希望能够引用属性,即使使用空引用而不是在调用者上放置空值检查,一种方法是User中的静态方法:

static bool IsAuthorized(User user)
{
    if(user!=null)
    {
        return user.IsAuthorized;
    }
    else
    {
        return false;
    }
}

然后,当您想要检查您的授权时,而不是:

if(thisUser.IsAuthorized)

执行:

if(User.IsAuthorized(thisUser))

答案 6 :(得分:2)

唯一可行的方法是使用扩展方法或其他静态方法来处理空引用。

当告诉代码调用属于实际不存在的对象实例的方法时,会发生NullReferenceExceptions(Javaheads的NullPointerExceptions;大致是同义词)。你必须记住的事情是null实际上并不是任何对象。类型的变量可以设置为null,但这只是意味着变量不引用实例。

存在问题;如果一个变量,无论其类型(只要是可空类型)都为null,那么就没有一个实例可以调用该方法,实例方法需要一个实例,因为这就是程序如何确定该方法可访问的成员的状态。如果MyClass有一个MyField和MyMethod(),并且你在MyClass的空引用上调用了MyMethod,那么MyField的值是多少?

解决方案通常是移动到静态范围。静态成员(和类)保证具有状态,因为它们在运行时被实例化一次(通常是即时的,如在第一次引用之前)。因为它们总是具有状态,所以它们总是可以被调用,因此可以提供可能无法在实例级别完成的事情。这是一个可以在C#中用来从对象成员链返回值的方法,否则可能会导致NRE:

public static TOut ValueOrDefault<TIn, TOut>(this TIn input, Func<TIn, TOut> projection, 
       TOut defaultValue = default(TOut))
    {
        try
        {
            var result = projection(input);
            if (result == null) result = defaultValue;
            return result;
        }
        catch (NullReferenceException) //most nulls result in one of these.
        {
            return defaultValue;
        }
        catch (InvalidOperationException) //Nullable<T>s with no value throw these
        {
            return defaultValue;
        }
    }

用法:

class MyClass {public MyClass2 MyField;}
class MyClass2 {public List<string> MyItems; public int? MyNullableField;}

...
var myClass = null;
//returns 0; myClass is null
var result = myClass.ValueOrDefault(x=>x.MyField.MyItems.Count);
myClass = new MyClass();
//returns 0; MyField is null
result = myClass.ValueOrDefault(x=>x.MyField.MyItems.Count);
myClass.MyField = new MyClass2();
//returns 0; MyItems is null
result = myClass.ValueOrDefault(x=>x.MyField.MyItems.Count);
myClass.MyField.MyItems = new List<string>();
//returns 0, but now that's the actual result of the Count property; 
//the entire chain is valid
result = myClass.ValueOrDefault(x=>x.MyField.MyItems.Count);
//returns null, because FirstOrDefault() returns null
var myString = myClass.ValueOrDefault(x=>x.MyField.MyItems.FirstOrDefault());
myClass.MyField.MyItems.Add("A string");
//returns "A string"
myString = myClass.ValueOrDefault(x=>x.MyField.MyItems.FirstOrDefault());
//returns 0, because MyNullableField is null; the exception caught here is not an NRE,
//but an InvalidOperationException
var myValue = myClass.ValueOrDefault(x=>x.MyField.MyNullableField.Value);

虽然这种方法在某些情况下具有价值,否则这些情况会要求长嵌套三元运算符产生某些东西(任何东西)以显示用户或在计算中使用,我不建议使用此模式来执行操作(void方法)。因为不会抛弃任何NRE或IOE,所以你永远不会知道你要求它做的是否真的完成了。您可以使用返回true或false的“TryPerformAction()”方法,和/或具有包含抛出异常(如果有)的输出参数。但是,如果你要遇到那种麻烦,为什么不自己尝试/抓住它呢?