25

问题

我想要一个能够处理自身空引用的类。我怎样才能做到这一点?扩展方法是我能想到的唯一方法,但我想我会问,以防有一些我不知道的关于 C# 的漂亮事情。

例子

我有一个名为的类User,其属性名为IsAuthorized.

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


解决方案

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

  1. 我使用了 Zaid Masud 建议的 Null Object 设计模式。
  2. 我将它与 Belmiris 的使用 struct 的建议结合起来,所以我不能有空引用
  3. 并从 Jon Hanna 那里得到了一个很好的解释,解释了为什么 C# 以这种方式工作以及我如何才能真正解决它

不幸的是,我只能选择其中一个作为我接受的答案,因此如果您正在访问此页面,您应该花时间对所有这三个以及任何其他出色的答案进行投票。

4

7 回答 7

29

一个合适的面向对象的解决方案怎么样?这正是空对象设计模式的用途。

您可以提取一个 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 而不是 User 并且客户端代码将仅依赖于 IUser:

public IUser GetUser()
{
    if (condition)
    {
        return new NullUser(); // never return null anymore, replace with NullUser instead
    }
    return new User(...);
}
于 2012-08-15T15:43:37.267 回答
18

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

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

调用方法或属性,例如IsAuthorized实例方法需要实例。仅调用实例方法(包括属性 getter)的行为null就会触发异常。当您尝试使用 (null) 引用时,该异常不是由您的类引发,而是由运行时本身引发。在 C# 中没有办法解决这个问题。

于 2012-08-15T15:31:23.190 回答
11

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

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

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

于 2012-08-15T15:32:03.267 回答
7

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

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

C# 故意这样做。根据 Eric Gunnerson的说法,这是因为他们认为让你这样做会有点奇怪。

我一直不明白为什么让以 C++ 为模型的 .NET 语言在 .NET 和同一家公司生产的 C++ 编译器中完全允许做一些事情,* 被认为有点奇怪。我一直认为不允许这样做有点奇怪。

您可以从调用该类的另一种语言(F# 或 IL)中添加一些内容,或者使用它Reflection.Emit来生成执行此操作的委托,这样就可以正常工作。例如,以下代码将调用GetHashCode定义在的版本object(即,即使GetHashCode被覆盖,this 也不会调用覆盖),这是可以安全调用空实例的方法的示例:

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# 行为的好处在于,它会导致对空引用的调用快速失败,因为它们访问中间某处的字段或虚拟,所以无论如何都会失败。这意味着我们在编写调用时不必担心我们是否处于空实例中。但是,如果您想在完全公共的方法(即公共类的公共方法)中防弹,那么您不能依赖于此。如果方法的第 1 步始终跟在第 2 步之后很重要,并且只有在空实例上调用第 2 步时才会失败,那么应该进行自空检查。这很少会发生,但它可能会导致非 C# 用户的错误,如果不使用上述技术,您将永远无法在 C# 中重现这些错误。

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

于 2012-08-15T16:19:44.303 回答
5

你可以改用结构吗?那么它不应该为空。

于 2012-08-15T15:37:42.933 回答
3

如果您没有有效的实例引用,则无法引用属性。如果您希望即使使用空引用也能够引用属性并且不将空检查的责任放在调用者身上,一种方法是使用静态方法User

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

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

if(thisUser.IsAuthorized)

做:

if(User.IsAuthorized(thisUser))

于 2012-08-15T15:35:13.113 回答
2

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

NullReferenceExceptions(Javaheads 的 NullPointerExceptions;大致同义)发生在代码被告知调用属于实际上不存在的对象实例的方法时。您必须记住的是 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()”方法,和/或具有包含抛出异常(如果有)的输出参数。但是,如果您要遇到那种麻烦,为什么不自己尝试/抓住它呢?

于 2012-08-15T17:43:52.237 回答