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

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

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

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

Я хочу, чтобы он возвращал значение 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; }
    }
}

UPDATE:

Одним из решений было бы уловить исключение 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:

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

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

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

Ответ 1

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

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;

Уродливо, я знаю.

Ответ 2

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

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);

Ответ 3

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

Ответ 4

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

Синтаксис:

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

string cityName = Employee.Maybe(e => e.Person.Address.CityName);

Ответ 5

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

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

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

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

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

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

Ответ 6

Я не квалифицирован в С#, но, возможно, есть способ реализовать шаблон "andand" из ruby, который решает именно эту проблему, не загрязняя реализацию.

Эта концепция также известна как Модификация Maybe в Haskell.

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

Ответ 7

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

Ответ 8

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

private Two _two;
public Two Two
{
     get 
     {
       if (null == _two)
         return new Two();
       else
         return _two;
      }
}

Ответ 9

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

Например, иногда, когда я запускаю открытый XML...

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

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

Ответ 10

Я преобразовал функцию, которая использовала множество операторов 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, что теоретически гораздо сложнее поддерживать.