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

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

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

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


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

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

不幸的是,我只能选择其中一个作为我接受的答案,所以如果你正在访问这个页面,你应该花些时间来投票给所有这三个以及给出的任何其他优秀答案。

如何正确的面向对象解决方案? 这正是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(...); } 

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

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

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

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

你应该保证它不是null,通过在调用“IsAuthorized”或之前的事件之前检查。

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

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

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

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

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

您可以从调用该类的另一种语言(F#或IL)添加一些内容,或者使用Reflection.Emit生成一个代理,这样做并且可以正常工作。 例如,以下代码将调用object定义的GetHashCode版本(即,即使重写了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 RootHashCode = (Func)dynM.CreateDelegate(typeof(Func)); 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 CheckAuthorized = (Func)dynM.CreateDelegate(typeof(Func)); //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未定义的。

你可以使用结构吗? 然后它不应该是null。

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

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

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

if(thisUser.IsAuthorized)

做:

if(User.IsAuthorized(thisUser))

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

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

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

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

 public static TOut ValueOrDefault(this TIn input, Func 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) //Nullables with no value throw these { return defaultValue; } } 

用法:

 class MyClass {public MyClass2 MyField;} class MyClass2 {public List 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(); //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()”方法,和/或具有包含抛出exception(如果有)的输出参数。 但是,如果你要遇到那种麻烦,为什么不尝试自己抓住这件事呢?