Подтвердить что ты не робот

Есть ли способ, чтобы класс С# обрабатывал свои собственные нулевые ссылочные исключения

Вопрос

Я хотел бы иметь класс, способный обрабатывать нулевую ссылку. Как я могу это сделать? Методы расширения - единственный способ, которым я могу это сделать, но подумал, что попрошу, если бы была какая-то отличная вещь, о которой я не знал о С#.

Пример

У меня есть класс под названием User со свойством IsAuthorized.

Если User правильно создан IsAuthorized имеет реализацию. Однако, когда моя ссылка User содержит null, я бы хотел, чтобы вызов IsAuthorized возвращал false вместо взрыва.


Решение

Много хороших ответов. Я решил использовать три из них, чтобы решить мою проблему.

  • Я использовал шаблон дизайна Null Object, предложенный Заидом Масудом.
  • Я объединил это с предложением Белмира по использованию структуры, поэтому я не мог иметь нулевую ссылку
  • И получилось отличное объяснение, почему С# работает таким образом и как я могу действительно решить его от Jon Hanna.

К сожалению, я могу выбрать только один из них в качестве моего принятого ответа, поэтому, если вы посещаете эту страницу, вы должны потратить время на голосование по всем трем из них и любым другим отличным ответам, которые были даны.

4b9b3361

Ответ 1

Как насчет правильного объектно-ориентированного решения? Это именно то, что для шаблона дизайна Null Object для.

Вы можете извлечь интерфейс IUser, создать объект User, реализующий этот интерфейс, а затем создать объект NullUser (который также реализует IUser) и всегда возвращает false в свойстве IsAuthorized.

Теперь измените код потребления, чтобы зависеть от IUser, а не от пользователя. Клиентскому коду больше не потребуется нулевая проверка.

Пример кода ниже:

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

Ответ 2

Однако, когда моя ссылка на пользователя содержит null, я бы хотел, чтобы IsAuthorized всегда возвращал false вместо взрыва.

Это можно сделать, только если IsAuthorized является статическим методом, и в этом случае вы можете проверить значение null. Вот почему методы расширения могут это сделать - они действительно просто другой синтаксис для вызова статического метода.

Вызов метода или свойства, например IsAuthorized в качестве метода экземпляра, требует экземпляра. Только действие вызова метода экземпляра (включая свойство getter) на null вызовет исключение. Исключение не возбуждается вашим классом, а самим временем выполнения, когда вы пытаетесь использовать (нулевую) ссылку. В С# нет никакого способа обойти это.

Ответ 3

Если переменная имеет значение null, это означает, что она не ссылается на объект, поэтому нет смысла (и я думаю, что это не технически возможно) для обработки нулевой ссылки внутри метода класса.

Вы должны гарантировать, что он не является нулевым, либо путем проверки непосредственно перед вызовом "IsAuthorized" или события раньше.

Изменить: найти обходной путь для этого было бы плохо: для кого-то было бы непонятно понимать поведение, поскольку это не "ожидаемое" поведение для языка программирования. Это также может привести к тому, что ваш код скроет некоторую проблему (нулевое значение, где должен быть объект), и создайте ошибку, которую трудно найти. Тем не менее: это плохая идея.

Ответ 4

Проблема заключается не в создании такого метода вообще. Он с вызывает метод. Если вы проверите тест if(this == null) в своем коде, это совершенно верно. Полагаю, что это может быть оптимизировано компилятором на основе того, что его невозможно "поразить", но, к счастью, это не так.

Однако, когда вы вызывают метод, это будет сделано через callvirt, поэтому вместо того, чтобы напрямую обращаться к методу, он найдет версию метода для вызова конкретного экземпляра как это было бы с виртуальным методом. Так как это приведет к ошибкам для нулевых ссылок, ваш совершенно хороший метод самовыполнения результатов завершится неудачей до того, как он будет даже вызван.

С# намеренно делает это. По словам Эрика Гуннерсона, это было потому, что они думали, что позволить вам сделать это было бы немного странно.

Я никогда не понимал, почему использование языка .NET, смоделированного на С++, делает что-то, что вполне допустимо как в .NET, так и в компиляторе С++, выпущенном той же компанией, * считалось немного странным. Я всегда считал это немного странным, что это было запрещено.

Вы можете добавить что-то с другого языка (F # или IL), который вызывает класс, или использовать Reflection.Emit для создания делегата, который делает это, и это будет работать нормально. Например, следующий код будет вызывать версию GetHashCode, определенную в object (то есть, даже если GetHashCode был переопределен, это не вызывает переопределение), который является примером метода, который безопасен для вызвать нулевой экземпляр:

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, поэтому вам нужно только построить ее один раз (скажем, в статическом конструкторе), а затем вы можете использовать ее повторно.

Это, конечно, не имеет значения, если другой код вызовет ваш метод через нулевую ссылку, поскольку эти методы расширения, как вы предлагаете, являются вашей единственной ставкой.

Также стоит отметить, что если вы пишете на языке, который не имеет этого причуда С#, вам следует предложить альтернативные способы получения результата по умолчанию для вызова нулевой ссылки, поскольку С# люди не могут это получить. Подобно тому, как люди С# должны избегать различий между общедоступными именами только для случая, поскольку некоторые языки не могут с этим справиться.

Изменить: вызывается полный пример вашего вопроса 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);
    }
  }
}

О, и реальная практическая проблема в этом. Хорошо, что поведение С# заключается в том, что он вызывает сбои при вызове нулевых ссылок, которые в любом случае потерпят неудачу, потому что они получают доступ к полю или виртуальному где-то посередине. Это означает, что нам не нужно беспокоиться о том, находимся ли мы в нулевом экземпляре при написании вызовов. Если вы хотите быть пуленепробиваемым в полностью открытом методе (то есть публичном методе открытого класса), то вы не можете зависеть от этого. Если жизненно важно, чтобы на шаг 1 метода всегда следовал шаг 2, а шаг 2 просто терпел неудачу, если был вызван нулевой экземпляр, тогда должна быть проверка с нулевым значением. Это редко случается, но это может привести к ошибкам для пользователей, не относящихся к С#, которые вы никогда не сможете воспроизвести на С# без использования вышеупомянутой техники.

* Хотя это специфично для их компилятора - это undefined для стандарта С++ С++.

Ответ 5

Можете ли вы использовать структуру вместо этого? Тогда он никогда не должен быть нулевым.

Ответ 6

Вы не можете ссылаться на свойство, если у вас нет действительной ссылки на экземпляр. Если вы хотите иметь возможность ссылаться на свойство даже с нулевой ссылкой, а не накладывать бремя нулевой проверки на вызывающий объект, одним из способов является статический метод в User:

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

Затем, когда вы хотите проверить свою авторизацию, а не:

if(thisUser.IsAuthorized)

делать:

if(User.IsAuthorized(thisUser))

Ответ 7

Единственный способ, которым это может работать, - использовать метод расширения или другой статический метод для обработки нулевой ссылки.

NullReferenceExceptions (NullPointerExceptions для Javaheads, примерно синонимичные) возникают, когда при вызове кода вызов метода, принадлежащего экземпляру объекта, который фактически не существует. То, что вы должны иметь в виду, это то, что null на самом деле не является объектом. Переменная типа может быть установлена ​​в нуль, но это просто означает, что переменная не ссылается на экземпляр.

В этом и заключается проблема; если переменная, независимо от ее типа (до тех пор, как этот тип с нулевым типом), имеет значение NULL, то нет экземпляра, для которого следует вызвать метод, и экземпляр необходим для методов экземпляра, поскольку тот способ, которым программа определяет состояние участников, доступных для метода. Если у MyClass были MyField и MyMethod(), и вы вызвали MyMethod по нулевой ссылке MyClass, какое значение MyField?

Решение, как правило, относится к статической области. Статические члены (и классы) гарантированно имеют состояние, потому что они создаются один раз во время выполнения (как правило, точно в срок, как и до первой ссылки). Поскольку у них всегда есть состояние, они всегда могут быть вызваны, и поэтому могут быть даны вещи, которые могут быть выполнены не на уровне экземпляра. Здесь метод, который вы можете использовать в С#, чтобы вернуть значение из цепочки объектов объекта, которое в противном случае может привести к 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 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 methods). Поскольку никакие NRE или IOE не будут выброшены, вы никогда не узнаете, действительно ли то, что вы попросили сделать. Вы можете уйти с помощью метода TryPerformAction(), который возвращает true или false, и/или имеет выходной параметр, содержащий вызванное исключение (если оно есть). Но если вы собираетесь пойти на такие проблемы, почему бы просто не попробовать/не поймать вещь?