Как проверить наличие нулей в глубоком лямбда-выражении?

Как я могу проверить наличие нулей в глубоком лямда-выражении?

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

x => x.Two.Three.Four.Foo

Я хочу, чтобы он возвращал null, если Two, Three или Four были null, а не генерировал System.NullReferenceException.

public class Tests
{
    // This test will succeed
    [Fact]
    public void ReturnsValueWhenClass2NotNull()
    {
        var one = new One();
        one.Two = new Two();
        one.Two.Three = new Three();
        one.Two.Three.Four = new Four();
        one.Two.Three.Four.Foo = "blah";

        var result = GetValue(one, x => x.Two.Three.Four.Foo);

        Assert.Equal("blah", result);
    }

    // This test will fail
    [Fact]
    public void ReturnsNullWhenClass2IsNull()
    {
        var one = new One();

        var result = GetValue(one, x => x.Two.Three.Four.Foo);

        Assert.Equal(null, result);
    }

    private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression)
    {
        var func = expression.Compile();
        var value = func(model);
        return value;
    }

    public class One
    {
        public Two Two { get; set; }
    }

    public class Two
    {
        public Three Three { get; set; }
    }

    public class Three
    {
        public Four Four { get; set; }
    }

    public class Four
    {
        public string Foo { get; set; }
        public string Bar { get; set; }
    }
}

ОБНОВИТЬ:

Одним из решений было бы поймать исключение NullReferenceException следующим образом:

    private TResult GetValue<TModel, TResult>(TModel model, Expression<Func<TModel, TResult>> expression)
    {
        TResult value;
        try
        {
            var func = expression.Compile();
            value = func(model);
        }
        catch (NullReferenceException)
        {
            value = default(TResult);
        }
        return value;
    }

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

ОБНОВЛЕНИЕ 2:

Другое решение - изменить получатели свойств следующим образом:

    public class One
    {
        private Two two;
        public Two Two
        {
            get
            {
                return two ?? new Two();
            }
            set
            {
                two = value;
            }
        }
    }

Что в основном подходит для моего домена, но бывают случаи, когда я действительно ожидаю, что свойство вернет значение null. Я проверил ответ Джоша Э. как полезный, поскольку в некоторых случаях он очень близок к тому, что мне нужно.


person JohnRudolfLewis    schedule 12.05.2009    source источник


Ответы (10)


Вы не можете сделать это кратко. Вы можете сделать лямбда-выражение несколькими строками или использовать вложенные тернарные операторы:

var result = GetValue(one, x => x.Two == null ? null :
                                x.Two.Three == null ? null :
                                x.Two.Three.Four == null ? null :
                                x.Two.Three.Four.Foo;

Я знаю, некрасиво.

person Gabe Moothart    schedule 12.05.2009
comment
это гораздо более лаконично. Может быть (x = ›x.Two.Three.Four.Foo); см. might.codeplex.com - person Maslow; 03.06.2010

Вы можете сделать это с помощью универсального вспомогательного метода расширения, например:

public static class Get {
    public static T IfNotNull<T, U>(this U item, Func<U, T> lambda) where U: class {
        if (item == null) {
            return default(T);
        }
        return lambda(item);
    }
}

var one = new One();
string fooIfNotNull = one.IfNotNull(x => x.Two).IfNotNull(x => x.Three).IfNotNull(x => x.Four).IfNotNull(x => x.Foo);
person Lucero    schedule 12.05.2009
comment
В этом случае я бы хотел вернуть значение по умолчанию для любого типа Foo или Bar. Чего я действительно хочу избежать, так это исключения, если что-то выше в дереве выражений имело нулевое значение. - person JohnRudolfLewis; 13.05.2009
comment
Я отредактировал свой ответ и добавил образец кода, который компилируется нормально и должен помочь. - person Lucero; 13.05.2009
comment
Это просто? Это элегантно? +1 И никакого падения производительности из-за рефлексии. Кто-нибудь сравнивал это с решением Гейба или «нормальным» подходом? - person Simon D.; 12.03.2011

Чтобы сделать это кратко, требуется еще не реализованный оператор. Мы рассмотрели возможность добавления оператора ".?" на C # 4.0, который имел бы желаемую семантику, но, к сожалению, не укладывался в наш бюджет. Мы рассмотрим это в качестве гипотетической будущей версии языка.

person Eric Lippert    schedule 12.05.2009
comment
Было бы здорово! Призма Delphi делает то же самое со своим оператором:: prismwiki.codegear.com/en/Colon_Operator - person Gabe Moothart; 13.05.2009
comment
Я поддерживаю это. Это сделало бы код намного чище! - person mdonatas; 24.05.2012
comment
Эта функция теперь есть в C # 6! - person Gabe Moothart; 22.02.2016
comment
Я пробовал использовать ?. в этом случае вопроса, но это не разрешено ... Это вызывает ошибку Error CS8072 An expression tree lambda may not contain a null propagating operator. Некоторые обсуждения здесь: Null- оператор распространения?. в деревьях выражений - person Alielson Piffer; 18.10.2017

Теперь вы можете использовать проект Maybe на codeplex.

Синтаксис:

string result = One.Maybe(o => o.Two.Three.Four.Foo);

string cityName = Employee.Maybe(e => e.Person.Address.CityName);
person Maslow    schedule 19.05.2010
comment
Влияет ли использование дерева выражений на производительность? - person Michael Freidgeim; 02.06.2012
comment
влияет ли что-то другое на характеристики производительности? да. Достаточно ли этого на самом деле, чтобы вы или пользователь заметили? Я не знаю, как вы пользуетесь, профилируйте это. - person Maslow; 03.06.2012
comment
Есть ли у вас статистика производительности вашего (или кого-то еще) использования? - person Michael Freidgeim; 03.06.2012
comment
Я не. Если вы попробуете, возможно, вы поделитесь со мной результатами? знак равно - person Maslow; 03.06.2012

Я написал метод расширения, который позволяет вам это делать:

blah.GetValueOrDefault(x => x.Two.Three.Four.Foo);

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

Вы также можете передать значение по умолчанию для возврата, если хотите:

blah.GetValueOrDefault(x => x.Two.Three.Four.Foo, Foo.Empty);

Я написал об этом блог здесь.

person Steve Wilkes    schedule 10.01.2013

Я не разбираюсь в C #, но, возможно, есть способ реализовать шаблон «andand» из Ruby, который решает именно эту проблему, не загрязняя реализацию.

Эта концепция также известна как «Может быть монада» в Haskell.

Название этой статьи кажется многообещающим.

person krusty.ar    schedule 12.05.2009
comment
Интересно, что статья почти идентична решению, которое я придумал, подумав об этом, см. Мой пост ... - person Lucero; 13.05.2009
comment
Вау, совсем пропустил, наверное, искал слово может быть - person krusty.ar; 13.05.2009
comment
Это может сделать might.codeplex.com. - person Maslow; 03.06.2010

Всегда инициализируйте свои свойства перед их использованием. Добавьте конструктор к классам One, Two, Three и Four. В конструкторе инициализируйте свои свойства, чтобы они не были нулевыми.

person Wallstreet Programmer    schedule 12.05.2009
comment
Обычно я это делаю, но в этом домене для свойств иногда может быть установлено значение null. Это допустимое поведение. - person JohnRudolfLewis; 13.05.2009

Вы можете изменить свои геттеры, чтобы читать что-то вроде:

private Two _two;
public Two Two
{
     get 
     {
       if (null == _two)
         return new Two();
       else
         return _two;
      }
}
person Josh E    schedule 12.05.2009
comment
Изменение реализации для сохранения некоторых строк в клиентском коде должно вызывать всевозможные аварийные сигналы. - person krusty.ar; 13.05.2009
comment
Я склонен не соглашаться с тем, что это проблема: я бы назвал это защитным кодированием. Приведенный выше код гарантирует, что значение свойства никогда не будет нулевым без обмена этими знаниями с любым потребителем этого свойства / объекта. - person Josh E; 13.05.2009
comment
Если я продолжаю вызывать Two, пока _two имеет значение null, я продолжаю получать новые экземпляры Two ... ew - person Lucas; 13.05.2009
comment
хорошая точка зрения. В этом случае вы можете изменить его, чтобы установить _two в новый экземпляр Two () перед его возвратом, например если (null == _two) _two = new Two (); return _two; - person Josh E; 13.05.2009

Иногда для этого мне может быть полезен оператор coalesce. Это помогает только в том случае, если есть версия объекта, эквивалентная по умолчанию / нулю, которую вы можете добавить.

Например, иногда, когда я взламываю XML ...

IEnumeratable<XElement> sample;
sample.Where(S => (S.Attribute["name"] ?? new XAttribute("name","")).Value.StartsWith("Hello"))...

В зависимости от того, как извлекаются объекты по умолчанию, это может быть подробным, и приведенный выше пример не очень полезен, но вы поняли идею. Для частного случая чтения атрибутов XML у меня есть метод расширения, который возвращает значение атрибута или пустую строку.

person Frank Schwieterman    schedule 12.05.2009

Я преобразовал функцию, которая использовала множество операторов if, чтобы избежать нулей, в метод .IFNotNull для классов, преобразованных мной из XSD, которые имеют глубину 4 и 5 уровней.

Вот несколько строк преобразованного кода:

ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_VALUE = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.VALUE).ToDouble();  
ProdYear.PRIOR_CUMULATIVE_CARBON_DIOXIDE_UOM = year.IfNotNull(x => x.PRIOR_CUMULATIVE).IfNotNull(y => y.CARBON_DIOXIDE).IfNotNull(z => z.UOM);  

Вот интересная статистика по этому поводу:

1) Этот новый метод работал в 3,7409 раз дольше, чем вариант с операторами If.
2) Я уменьшил количество строк моей функции с 157 до 59.
3) CodeRush от DevExpress имеет оценку« Сложность обслуживания ». Когда я перешел на лямбда-выражения, оно увеличилось с 984 до 2076, что теоретически намного сложнее поддерживать.

person M Akin    schedule 16.08.2012