Мога ли да принудя собственото си късо съединение в извикване на метод?

Да предположим, че искам да проверя куп обекти, за да се уверя, че никой не е null:

if (obj != null &&
    obj.Parameters != null &&
    obj.Parameters.UserSettings != null) {

    // do something with obj.Parameters.UserSettings
}

Примамлива е перспективата да напишете помощна функция, която да приема променлив брой аргументи и да опрости този вид проверка:

static bool NoNulls(params object[] objects) {
    for (int i = 0; i < objects.Length; i++)
        if (objects[i] == null) return false;

    return true;
}

Тогава горният код може да стане:

if (NoNulls(obj, obj.Parameters, obj.Parameters.UserSettings)) {
    // do something
}

нали Грешно. Ако obj е null, тогава ще получа NullReferenceException, когато се опитам да предам obj.Parameters на NoNulls.

Така че горният подход е очевидно погрешен. Но операторът if, използващ оператора &&, работи добре, тъй като има късо съединение. И така: има ли някакъв начин да направите метод на късо, така че аргументите му да не се оценяват, докато не бъдат изрично посочени в метода?


person Dan Tao    schedule 16.12.2009    source източник
comment
Това, от което всъщност се нуждаете, за да разрешите този проблем, не е мързелива оценка на аргументите на метода -- въпреки че, както забелязвате, това би свършило работа. Какво би било по-добре е оператор x.?y, който има семантиката на (x == null ? null : x.y) -- по този начин можете да кажете if (obj.?Parameters.?UserSettings == null). Обмисляхме такъв оператор за C# 4, но никога не го внедрихме. Може би за хипотетична бъдеща версия.   -  person Eric Lippert    schedule 16.12.2009
comment
Това е наистина интересно. Има ли други езици, които прилагат еквивалентен оператор?   -  person Chris Clark    schedule 17.12.2009
comment
@Chris: Groovy го прави. Нарича се оператор за дерефериране с нулева защита. Радвам се да чуя, че екипът на C# го е обмислил и може да го обмисли отново :)   -  person Jon Skeet    schedule 17.12.2009
comment
Имайте предвид, че всички реализации тук (с изключение на тази, използваща отражения, wink wink) все още трябва да пишат: obj, obj.Parameters, obj.Parameters.UserSettings. Наистина, без оператора, който Ерик споменава, оригиналното късо съединение && е най-малко объркващо и най-ефективно.   -  person Merlyn Morgan-Graham    schedule 17.12.2009
comment
Ruby gem andand предоставя този вид „извикване на защитен метод“: andand.rubyforge.org „фонът“ има връзка към оригиналната публикация на raganwald за него   -  person AakashM    schedule 17.12.2009
comment
Това по някаква причина ми напомня за цитиране на код в Lisp за по-късна оценка.   -  person Sarah Vessels    schedule 17.12.2009
comment
Напомня ви за това, защото това е! Както обикновено се случва, дизайнерите на Lisp са предвидили необходимостта и са я приложили още през 50-те години на миналия век.   -  person Eric Lippert    schedule 17.12.2009


Отговори (3)


Е, това е грозно, но...

static bool NoNulls(params Func<object>[] funcs) {
    for (int i = 0; i < funcs.Length; i++)
        if (funcs[i]() == null) return false;

    return true;
}

След това го извикайте с:

if (NoNulls(() => obj,
            () => obj.Parameters,
            () => obj.Parameters.UserSettings)) {
    // do something
}

По принцип вие предоставяте делегати да оценяват лениво стойностите, а не самите стойности (тъй като оценяването на тези стойности е това, което причинява изключение).

Не казвам, че е хубаво, но го има като опция...

РЕДАКТИРАНЕ: Това всъщност (и случайно) стига до сърцето на това, което преследваше Дан, мисля. Всички аргументи на метода се оценяват преди самият метод да бъде изпълнен. Ефективното използване на делегати ви позволява да забавите тази оценка, докато методът трябва да извика делегата, за да извлече стойността.

person Jon Skeet    schedule 16.12.2009
comment
Това е грозно и объркващо, но операторът за нулево обединяване не върши ли работа: if( (object z = a ?? b ?? c) != null ) DoSomething(); - person LBushkin; 16.12.2009
comment
Друга идея би била да се предаде единичен ламбда израз като изразно дърво към метод, който може да разглоби изразното дърво и да извърши оценката на късо съединение. - person LBushkin; 16.12.2009
comment
Всъщност си помислих за това, вярвате или не. Разбира се, за дадената цел - проверка на нули - това ще изисква приблизително същото количество код като оригиналния подход и следователно не е толкова полезно. Естествено, за други цели може да е по-добре. Основната причина, поради която не искам да го използвам обаче, е, че изисква използването на ламбда, които не са достъпни за VB (поне преди VB 10, мисля?), и искам нещо, което би било полезно от както C#, така и VB, тъй като използваме и двата езика на работа. - person Dan Tao; 16.12.2009
comment
@LBushkin: ?? ще работи само ако всички изрази са от един и същи тип (недействителност по модул) или един тип може да бъде преобразуван в друг. Не мисля, че би работило в дадения тук случай, например. - person Jon Skeet; 16.12.2009
comment
@Jon: Да, прав си за ??. Човек трябва да преобразува всеки елемент в обект, за да работи, което не го прави непременно по-прост: if( (object z = (object)a ?? (object)b ?? (object)c ) != null ) DoSomething(); - person LBushkin; 16.12.2009
comment
@LBushkin: Мисля, че по-уместното нещо, което трябва да се отбележи тук, е, че използването на оператора за нулево сливане всъщност е (вероятно) по-малко четимо от използването на if с куп && оператори, което би i> дори да е ОК, ако беше приложимо за повече сценарии, отколкото просто проверка за нули. Но това, за което всъщност се чудех, е дали писането на метод с късо съединение като цяло е възможно (въпреки че моят пример включваше проверка на нула). - person Dan Tao; 17.12.2009
comment
@Jon: Какво мислите за идеята за трансформация на изразно дърво? Изхвърлих го там, но дори не съм 100% сигурен, че е осъществимо предвид някои от ограниченията за това, което е позволено в дървото на израза. - person LBushkin; 17.12.2009
comment
@Jon: Освен ако не греша, това е конвенцията за извикване по име на Алгол. Не? - person Mike Dunlavey; 17.12.2009
comment
@Mike: Страхувам се, че не мога да кажа. @LBushkin: Да, трябва да е осъществимо. Изпълнението може да е доста грозно, но би било осъществимо :) - person Jon Skeet; 17.12.2009

Можете да напишете функция, която приема дърво на изрази и трансформира това дърво във формуляр, който ще проверява за нули и ще върне Func<bool>, което може безопасно да бъде оценено, за да се определи дали има нула.

Подозирам, че докато полученият код може да е готин, той би бил объркващ и много по-малко ефективен от простото писане на куп a != null && a.b != null... проверки на късо. Всъщност вероятно би било по-малко ефективно от просто проверка на всички стойности и улавяне на NullReferenceException (не че препоръчвам обработката на изключения като механизъм за контрол на потока).

Сигнатурата за такава функция би била нещо като:

public static Func<bool> NoNulls( Expression<Func<object>> expr )

и използването му ще изглежда нещо като:

NoNulls( () => new { a = obj, 
                     b = obj.Parameters, 
                     c = obj.Parameters.UserSettings } )();

Ако имам малко свободно време, ще напиша функция, която прави точно такава трансформация на изразно дърво и ще актуализирам публикацията си. Сигурен съм обаче, че Джон Скийт или Марк Гравел биха могли да напишат такава функция с едно затворено око и една ръка зад гърба си.

Също така бих искал да видя C# да прилага оператора .?, за който споменава Ерик. Както може да каже един различен Ерик (Картман), това би "сритало задника".

person LBushkin    schedule 16.12.2009
comment
Не е необходимо да го правите три отделни аргумента - просто използвайте () => obj.Parameters.UserSettings и вземете дървото на израза, за да проверите резултата за нула между всяко извикване на свойство. - person Jon Skeet; 17.12.2009

Бихте могли да използвате отражения, ако нямате нищо против да загубите безопасността на статичния тип. Имам нещо против, така че просто използвам първата ви конструкция за късо съединение. Функция като споменатата от Ерик би била добре дошла :)

Мислил съм за този проблем няколко пъти. Lisp има макроси, които решават проблема по начина, който споменахте, тъй като ви позволяват да персонализирате оценката си.

Също така се опитах да използвам методи за разширение, за да разреша този проблем, но нищо не е по-малко грозно от оригиналния код.

Редактиране: (Отговорите не ми позволяват да вмъквам кодови блокове, така че редактирам публикацията си)

Упс, не продължих с това. Съжалявам за това :)

Можете да използвате отражения, за да търсите и оценявате член или свойство чрез низ. Клас, който един от моите приятели написа, приема синтаксис като:

new ReflectionHelper(obj)["Parameters"]["UserSettings"]

Работеше чрез верига на методите, връщайки ReflectionHelper на всяко ниво. Знам, че NullReferenceException е проблем в този пример. Просто исках да демонстрирам как оценката може да бъде отложена до време на изпълнение.

Пример, малко по-близък до това да бъде полезен:

public class Something
{
  public static object ResultOrDefault(object baseObject, params string[] chainedFields)
  {
    // ...
  }
}

Отново, този синтаксис смърди. Но това демонстрира използването на низове + отражения за отлагане на оценката до време на изпълнение.

person Merlyn Morgan-Graham    schedule 16.12.2009
comment
От любопитство, как може да се използва отражение, за да се постигне това? Проблемът изглежда е, че след като сте въвели метода, всичките му аргументи вече са оценени. Не съм наясно как размисълът може да поправи това; но може би пропускам нещо. - person Dan Tao; 17.12.2009
comment
Редактира публикацията, за да отговори на вашия коментар - person Merlyn Morgan-Graham; 04.02.2010