Вызов метода, если не null в C #

106

Можно ли как-то сократить это утверждение?

if (obj != null)
    obj.SomeMethod();

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

И аналогичная проблема с событиями, где

public event Func<string> MyEvent;

а затем вызвать

if (MyEvent != null)
    MyEvent.Invoke();
Якуб Арнольд
источник
41
Мы рассмотрели добавление нового оператора в C # 4: «obj.?SomeMethod ()» будет означать «вызвать SomeMethod, если obj не равен нулю, в противном случае вернуть ноль». К сожалению, это не укладывалось в наш бюджет, поэтому мы не реализовали его.
Эрик Липперт
@Eric: Этот комментарий еще в силе? Я где-то видел, что это доступно с 4.0?
CharithJ
@CharithJ: Нет. Это так и не было реализовано.
Эрик Липперт
3
@CharithJ: Мне известно о существовании оператора объединения с нулевым значением. Он не делает того, чего хочет Дарт; ему нужен оператор доступа к члену с поднятым до нулевого значения. (И, кстати, ваш предыдущий комментарий дает неверную характеристику оператора объединения null. Вы хотели сказать «v = m == null? Y: m.Value может быть записано v = m ?? y».)
Эрик Lippert
4
Для более новых читателей: C # 6.0 реализует?., Поэтому x? .Y? .Z? .ToString () вернет null, если x, y или z равны нулю, или вернет z.ToString (), если ни один из них не равен нулю.
Дэвид

Ответы:

162

Начиная с C # 6, вы можете просто использовать:

MyEvent?.Invoke();

или:

obj?.SomeMethod();

Это ?.оператор с нулевым распространением и вызовет .Invoke()короткое замыкание, когда операнд null. Доступ к операнду осуществляется только один раз, поэтому нет риска возникновения проблемы «изменение значения между проверкой и вызовом».

===

До C # 6 нет: не существовало нулевого безопасного волшебства, за одним исключением; методы расширения - например:

public static void SafeInvoke(this Action action) {
    if(action != null) action();
}

теперь это действительно:

Action act = null;
act.SafeInvoke(); // does nothing
act = delegate {Console.WriteLine("hi");}
act.SafeInvoke(); // writes "hi"

В случае событий это имеет то преимущество, что также удаляет условие гонки, то есть вам не нужна временная переменная. Обычно вам понадобятся:

var handler = SomeEvent;
if(handler != null) handler(this, EventArgs.Empty);

но с:

public static void SafeInvoke(this EventHandler handler, object sender) {
    if(handler != null) handler(sender, EventArgs.Empty);
}

мы можем просто использовать:

SomeEvent.SafeInvoke(this); // no race condition, no null risk
Марк Гравелл
источник
1
Я немного противоречу этому. В случае действия или обработчика событий, которые обычно более независимы, это имеет смысл. Однако я бы не стал нарушать инкапсуляцию для обычного метода. Это подразумевает создание метода в отдельном статическом классе, и я не думаю, что потеря инкапсуляции и ухудшение читабельности / организации кода в целом стоит незначительного улучшения локальной читаемости
tvanfosson
@tvanfosson - действительно; но я хочу сказать, что это единственный случай, когда я знаю, где это будет работать. Да и сам вопрос поднимает тему делегатов / мероприятий.
Марк Гравелл
Этот код заканчивает тем, что где-то генерирует анонимный метод, что действительно портит трассировку стека любого исключения. Можно ли давать имена анонимным методам? ;)
сисве
Неправильно согласно 2015 / C # 6.0 ...? он решение. ReferenceTHatMayBeNull? .CallMethod () не будет вызывать метод при значении null.
TomTom
1
@mercu это должно быть ?.- в VB14 и выше
Marc Gravell
27

Что вы ищете является Null Условный (не «сливаясь») Оператор: ?.. Он доступен с C # 6.

Ваш пример был бы obj?.SomeMethod();. Если obj имеет значение null, ничего не происходит. Когда метод имеет аргументы, например, obj?.SomeMethod(new Foo(), GetBar());аргументы не оцениваются, если objимеет значение null, что имеет значение, если оценка аргументов будет иметь побочные эффекты.

И возможна цепочка: myObject?.Items?[0]?.DoSomething()

Vimes
источник
1
Это фантастика. Стоит отметить, что это функция C # 6 ... (что подразумевается из вашего заявления VS2015, но все же стоит отметить). :)
Кайл Гуд
10

Метод быстрого расширения:

    public static void IfNotNull<T>(this T obj, Action<T> action, Action actionIfNull = null) where T : class {
        if(obj != null) {
            action(obj);
        } else if ( actionIfNull != null ) {
            actionIfNull();
        }
    }

пример:

  string str = null;
  str.IfNotNull(s => Console.Write(s.Length));
  str.IfNotNull(s => Console.Write(s.Length), () => Console.Write("null"));

или альтернативно:

    public static TR IfNotNull<T, TR>(this T obj, Func<T, TR> func, Func<TR> ifNull = null) where T : class {
        return obj != null ? func(obj) : (ifNull != null ? ifNull() : default(TR));
    }

пример:

    string str = null;
    Console.Write(str.IfNotNull(s => s.Length.ToString());
    Console.Write(str.IfNotNull(s => s.Length.ToString(), () =>  "null"));
катбайт
источник
У меня была попытка сделать это с помощью методов расширения, и в итоге я получил почти такой же код. Однако проблема этой реализации в том, что она не будет работать с выражениями, возвращающими тип значения. Поэтому для этого нужен был второй метод.
orad
4

События могут быть инициализированы с помощью пустого делегата по умолчанию, который никогда не удаляется:

public event EventHandler MyEvent = delegate { };

Нет необходимости в нулевой проверке.

[ Обновление , спасибо Бевану за указание на это]

Однако имейте в виду возможное влияние на производительность. Быстрый микротест, который я провел, показывает, что обработка события без подписчиков происходит в 2-3 раза медленнее при использовании шаблона «делегат по умолчанию». (На моем двухъядерном ноутбуке с частотой 2,5 ГГц это означает 279 мс: 785 мс для сбора 50 миллионов событий без подписки.) Это может быть проблемой для горячих точек приложений.

Свен Кюнцлер
источник
1
Итак, вы избегаете нулевой проверки, вызывая пустой делегат ... измеримая жертва как памяти, так и времени, чтобы сэкономить несколько нажатий клавиш? YMMV, но для меня это плохой компромисс.
Беван
Я также видел тесты, которые показывают, что вызов события с несколькими подписанными на него делегатами значительно дороже, чем с одним.
Грег Д.
2

В этой статье Яна Гриффитса представлены два разных решения проблемы, которые, по его мнению, представляют собой изящные уловки, которые вам не следует использовать.

Даррел Миллер
источник
3
И, как автор этой статьи, я бы добавил, что вам определенно не следует использовать его сейчас, когда C # 6 решает эту проблему из коробки с помощью условных операторов NULL (?. И. []).
Ян Гриффитс,
2

Предлагаемый метод расширения Cerating на самом деле не решает проблемы с условиями гонки, а скорее скрывает их.

public static void SafeInvoke(this EventHandler handler, object sender)
{
    if (handler != null) handler(sender, EventArgs.Empty);
}

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

Проблема с обоими заключается в том, что возможно, что подписчик события может быть вызван ПОСЛЕ того, как он отписался от события . Это возможно, потому что отмена подписки может произойти после того, как экземпляр делегата будет скопирован во временную переменную (или передан как параметр в методе выше), но до вызова делегата.

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

Единственный известный способ обеспечить безопасность потока - использовать инструкцию блокировки для отправителя события. Это гарантирует, что все подписки \ отписки \ вызовы будут сериализованы.

Чтобы быть более точным, блокировку следует применить к тому же объекту синхронизации, который используется в методах доступа к событию add \ remove, который по умолчанию является this.

андрей.цыкунов
источник
Это не состояние гонки, о котором идет речь. Условие гонки: if (MyEvent! = Null) // MyEvent теперь не равно null MyEvent.Invoke (); // MyEvent теперь имеет значение null, происходят плохие вещи Итак, с этим состоянием гонки я не могу написать обработчик асинхронных событий, который гарантированно работает. Однако с вашим «состоянием гонки» я могу написать обработчик асинхронных событий, который гарантированно будет работать. Конечно, звонки на абонента и абонента должны быть синхронными, иначе там потребуется код блокировки.
jyoung
На самом деле. Мой анализ этой проблемы размещен здесь: blogs.msdn.com/ericlippert/archive/2009/04/29/…
Эрик Липперт
1

Может быть, не лучше, но, на мой взгляд, более читабельным будет создание метода расширения

public static bool IsNull(this object obj) {
 return obj == null;
}
Кенни Элиассон
источник
1
что return obj == nullзначит. Что он вернет
Хаммад Хан
1
Это значит, что если objесть nullметод вернется true, я думаю.
Joel
1
Как это вообще отвечает на вопрос?
Kugel
Поздний ответ, но вы уверены, что это сработает? Можете ли вы вызвать метод расширения для объекта null? Я почти уверен, что это не работает с собственными методами типа, поэтому сомневаюсь, что это будет работать и с методами расширения. Я считаю , что лучший способ проверить , если объект находится null вне obj is null. К сожалению, проверить , если объект не не nullтребует упаковки в скобках, что является неудачным.
natiiix