问题
我希望有一个能够处理自身空引用的类。我怎样才能做到这一点?扩展方法是我能想到的唯一方法,但我想如果有一些我不了解的关于C#的好东西,我会问。
示例
我有一个名为User
的类,其中包含一个名为IsAuthorized
的属性。
正确实例化User
时IsAuthorized
有一个实现。但是,当我的User
引用包含null时,我希望调用IsAuthorized
来返回false而不是爆炸。
解决方案
很多很好的答案。我最终使用其中三个来解决我的问题。
不幸的是,我只能选择其中一个作为我接受的答案,所以如果你正在访问这个页面,你应该花时间投票给所有这三个以及给出的任何其他优秀答案。
答案 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()”方法,和/或具有包含抛出异常(如果有)的输出参数。但是,如果你要遇到那种麻烦,为什么不自己尝试/抓住它呢?