Виклик методу, якщо не нульовий у 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 не має значення null, інакше повернути null". На жаль, це не входило до нашого бюджету, тому ми ніколи його не застосовували.
Ерік Ліпперт,

@Eric: Чи залишається цей коментар дійсним? Я десь бачив, що він доступний з 4.0?
CharithJ,

@CharithJ: Ні. Він ніколи не був реалізований.
Ерік Ліпперт,

3
@CharithJ: Мені відомо про існування нульового оператора злиття. Це не робить те, що хоче Дарт; він хоче оператора доступу, який може бути анульований. (І, до речі, ваш попередній коментар дає неправильну характеристику нульового оператора злиття. Ви мали на увазі сказати "v = m == null? Y: m. Значення можна записати v = m ?? y".)
Ерік Ліпперт

4
Для нових читачів: C # 6.0 реалізує?., Тож x? .Y? .Z? .ToString () поверне null, якщо x, y або z є нульовими, або поверне z.ToString (), якщо жоден з них не має значення null.
Девід

Відповіді:


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 - справді; але я хочу сказати, що це єдиний випадок, коли я знаю, де це буде працювати. І саме питання піднімає тему делегатів / подій.
Марк Гравелл

Цей код в кінцевому підсумку генерує десь анонімний метод, який дійсно псує трасування стеку будь-якого винятку. Чи можна давати імена анонімним методам? ;)
sisve

Неправильно згідно з 2015 / C # 6.0 ...? це рішення. ReferenceTHatMayBeNull? .CallMethod () не буде викликати метод, коли значення null.
TomTom

1
@mercu це повинно бути ?.- у VB14 і вище
Марк Гравелл

27

Те, що ви шукаєте, - це оператор Null Conditional (не "злиття"):?. . Це доступно з C # 6.

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

І ланцюжок можливий: myObject?.Items?[0]?.DoSomething()


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 вирішує це нестандартно за допомогою нульових умовних операторів (?. Та. []).
Ян Гріффітс,

2

Отриманий спосіб продовження, як запропонований, насправді не вирішує проблем з умовами перегонів, а навпаки, приховує їх.

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

Як зазначено, цей код є елегантним еквівалентом рішення з тимчасовою змінною, але ...

Проблема обох, що цілком можливо, що субцибер події може бути названий ПІСЛЯ відмови від події . Це можливо, оскільки скасування підписки може відбутися після того, як екземпляр делегата буде скопійовано у змінну temp (або передано як параметр у наведеному вище методі), але до того, як буде викликаний делегат.

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

Єдиний відомий спосіб забезпечити безпеку потоку - використовувати оператор блокування для відправника події. Це гарантує, що всі підписки \ відписки \ виклик серіалізовані.

Щоб бути точнішим, блокування слід застосовувати до того самого об'єкта синхронізації, що використовується в методах додавання / видалення події, який за замовчуванням є "цим".


Це не умова перегони, про яку йдеться. Умова гонки - якщо (MyEvent! = Null) // MyEvent зараз не null MyEvent.Invoke (); // MyEvent зараз нульовий, трапляються погані речі. Отже, з цією умовою перегони я не можу написати асинхронний обробник подій, який гарантовано працює. Однак з вашими "умовами перегонів" я можу написати асинхронний обробник подій, який гарантовано спрацює. Звичайно, дзвінки до абонента та відписки повинні бути синхронними, або там потрібен код блокування.
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, я думаю.
Джоель

1
Як це взагалі відповідає на питання?
Kugel

Пізня відповідь, але ви впевнені, що це навіть спрацює? Чи можете ви викликати метод розширення для об'єкта, який є null? Я майже впевнений, що це не працює з власними методами типу, тому я сумніваюся, що це буде працювати і з методами розширення. Я вважаю, що найкращим способом перевірити, чи є об’єкт , null є obj is null. На жаль, перевірити , якщо об'єкт НЕ НЕ nullвимагає упаковки в дужках, що є невдалим.
natiiix

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.