Is there a way to have a C# class handle its own null reference exceptions

前端 未结 7 1152
青春惊慌失措
青春惊慌失措 2021-02-05 05:05

Question

I\'d like to have a class that is able to handle a null reference of itself. How can I do this? Extension methods are the only way I can think

相关标签:
7条回答
  • 2021-02-05 05:34

    The problem isn't with creating such a method at all. It's with calling the method. If you put a test of if(this == null) in your code, that's perfectly valid. I suppose it could be optimised away by the compiler on the basis of it being "impossible" to be hit, but I thankfully it isn't.

    However, when you call the method, it'll be done via callvirt, so rather than call the method directly, it will find the version of the method to call for the particular instance just as it would with a virtual method. Since that will fail for null references, your perfectly good self-null-testing method will fail before it is even called.

    C# deliberately does this. According to Eric Gunnerson this was because they thought letting you do so would be a bit weird.

    I've never understood why letting a .NET language modelled upon C++ do something that is perfectly allowable in both .NET and the C++ compiler produced by the same company,* was considered a bit weird. I've always considered it a bit weird that it wasn't allowed.

    You can add something from another language (F# or IL) that calls the class, or use Reflection.Emit to generate a delegate that does so and that'll work fine. For example, the following code will call the version of GetHashCode defined in object (that is, even if GetHashCode was overridden, this doesn't call the override) which is an example of a method that is safe to call on a null instance:

    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));
    

    The one good thing about this, is that you can hold onto RootHashCode so you only need to build it once (say in a static constructor) and then you can use it repeatedly.

    This of course is of no value in letting other code call your method through a null reference, for that extension methods like you suggest are your only bet.

    It's also worth noting of course, that if you are writing in a language that doesn't have this quirk of C#, that you should offer some alternative means of getting the "default" result for calling on a null reference because C# people can't get it. Much like C# people should avoid case-only differences between public names because some languages can't deal with that.

    Edit: A full example of your question's IsAuthorized being called, since votes suggest some people don't believe it can be done (!)

    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);
        }
      }
    }
    

    Oh, and a real-life practical concern in this. The good thing about C#'s behaviour is that it causes fail-fast on calls on null-references that would fail anyway because they access a field or virtual somewhere in the middle. This means we don't have to worry about whether we're in a null instance when writing calls. If however you want to be bullet-proof in a fully public method (that is, a public method of a public class), then you can't depend on this. If it's vital that step 1 of a method is always followed by step 2, and step 2 would only fail if called on a null instance, then there should be a self-null check. This is rarely going to happen, but it could cause bugs for non-C# users that you'll never be able to reproduce in C# without using the above technique.

    *Though, that is specific to their compiler - it's undefined per the C++ standard IIRC.

    0 讨论(0)
提交回复
热议问题