Возможные ошибки использования этой (основанной на расширении) стенографии

Обновление С# 6

В С# 6 ?. теперь является языковой функцией:

// C#1-5
propertyValue1 = myObject != null ? myObject.StringProperty : null; 

// C#6
propertyValue1 = myObject?.StringProperty;

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

Оригинальный вопрос:

Я регулярно хочу получить доступ к свойствам, возможно, для нулевых объектов:

string propertyValue1 = null;
if( myObject1 != null )
    propertyValue1 = myObject1.StringProperty;

int propertyValue2 = 0;
if( myObject2 != null )
    propertyValue2 = myObject2.IntProperty;

И так далее...

Я использую это так часто, что у меня есть фрагмент для него.

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

propertyValue1 = myObject != null ? myObject.StringProperty : null;

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

propertyValue1 = myObject != null ? 
    (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null : null;

Чего я действительно хочу, это синтаксис стиля ??, который отлично подходит для непосредственных нулевых типов:

int? i = SomeFunctionWhichMightReturnNull();
propertyValue2 = i ?? 0;

Итак, я придумал следующее:

public static TResult IfNotNull<T, TResult>( this T input, Func<T, TResult> action, TResult valueIfNull )
    where T : class
{
    if ( input != null ) return action( input );
    else return valueIfNull;
}

//lets us have a null default if the type is nullable
public static TResult IfNotNull<T, TResult>( this T input, Func<T, TResult> action )
    where T : class
    where TResult : class
{ return input.IfNotNull( action, null ); }

Это позволяет мне использовать этот синтаксис:

propertyValue1 = myObject1.IfNotNull( x => x.StringProperty );
propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0);

//or one with multiple levels
propertyValue1 = myObject.IfNotNull( 
    o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );

Это упрощает эти вызовы, но я не уверен в проверке такого метода расширения - он делает код немного легче читать, но за счет расширения объекта. Это будет отображаться во всем, хотя я могу поместить его в специально указанное пространство имен.

Этот пример довольно простой, немного более сложный - это сравнение двух свойств объекта с возможностью сбрасывания:

if( ( obj1 == null && obj2 == null ) || 
    ( obj1 != null && obj2 != null && obj1.Property == obj2.Property ) )
    ...

//becomes
if( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property ) 
    ...

Каковы подводные камни использования расширений таким образом? Можно ли смутить других кодировщиков? Это просто злоупотребление расширениями?


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

propertyValue1 = myObject != null ? myObject.StringProperty : null;

//becomes
propertyValue1 = myObject?StringProperty;

Это упростит сложный случай:

propertyValue1 = myObject != null ? 
    (myObject.ObjectProp != null ? myObject.ObjectProp.StringProperty) : null

//becomes
propertyValue1 = myObject?ObjectProp?StringProperty;

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

int? propertyValue2 = myObject?ObjectProp?IntProperty;

//or

int propertyValue3 = myObject?ObjectProp?IntProperty ?? 0;

Ответы

Ответ 1

Мы самостоятельно придумали то же самое имя и реализацию метода расширения: Метод распространения с расширением Null. Поэтому мы не считаем это запутанным или злоупотреблением методами расширения.

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

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp).IfNotNull(p => p.StringProperty);

Там теперь закрытая ошибка в Microsoft Connect, которая предложила "?". как новый оператор С#, который будет выполнять это нулевое распространение. Mads Torgersen (от команды языка С#) кратко объяснил, почему они не будут его реализовывать.

Ответ 2

Здесь другое решение для цепочечных элементов, включая методы расширения:

public static U PropagateNulls<T,U> ( this T obj
                                     ,Expression<Func<T,U>> expr) 
{  if (obj==null) return default(U);

   //uses a stack to reverse Member1(Member2(obj)) to obj.Member1.Member2 
   var members = new Stack<MemberInfo>();

   bool       searchingForMembers = true;
   Expression currentExpression   = expr.Body;

   while (searchingForMembers) switch (currentExpression.NodeType)
    { case ExpressionType.Parameter: searchingForMembers = false; break;

           case ExpressionType.MemberAccess:    
           { var ma= (MemberExpression) currentExpression;
             members.Push(ma.Member);
             currentExpression = ma.Expression;         
           } break;     

          case ExpressionType.Call:
          { var mc = (MethodCallExpression) currentExpression;
            members.Push(mc.Method);

           //only supports 1-arg static methods and 0-arg instance methods
           if (   (mc.Method.IsStatic && mc.Arguments.Count == 1) 
               || (mc.Arguments.Count == 0))
            { currentExpression = mc.Method.IsStatic ? mc.Arguments[0]
                                                     : mc.Object; 
              break;
            }

           throw new NotSupportedException(mc.Method+" is not supported");
         } 

        default: throw new NotSupportedException
                        (currentExpression.GetType()+" not supported");
  }

   object currValue = obj;
   while(members.Count > 0)
    { var m = members.Pop();

      switch(m.MemberType)
       { case MemberTypes.Field:
           currValue = ((FieldInfo) m).GetValue(currValue); 
           break;

         case MemberTypes.Method:
           var method = (MethodBase) m;
           currValue = method.IsStatic
                              ? method.Invoke(null,new[]{currValue})
                              : method.Invoke(currValue,null); 
           break;

         case MemberTypes.Property:
           var method = ((PropertyInfo) m).GetGetMethod(true);
                currValue = method.Invoke(currValue,null);
           break;

       }     

      if (currValue==null) return default(U);   
    }

   return (U) currValue;    
}

Затем вы можете сделать это, если любой из них может быть нулевым, или none:

foo.PropagateNulls(x => x.ExtensionMethod().Property.Field.Method());

Ответ 3

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

Ответ 4

Как

propertyValue1 = myObject.IfNotNull(o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );

легче читать и писать, чем

if(myObject != null && myObject.ObjectProp != null)
    propertyValue1 = myObject.ObjectProp.StringProperty;

Jafar Husain опубликовал образец использования деревьев выражений, чтобы проверить нуль в цепочке, Макросы времени выполнения в С# 3.

Это, очевидно, имеет последствия для производительности. Теперь, если бы у нас был способ сделать это во время компиляции.

Ответ 5

Я просто должен сказать, что мне нравится этот хак!

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

class Class1
{
    public static readonly Class1 Empty = new Class1();
.
.
x = (obj1 ?? Class1.Empty).X;

Ответ 6

он делает код немного легче читать, но за счет расширения объекта. Это будет отображаться во всем,

Обратите внимание, что вы фактически не расширяете ничего (кроме теоретически).

propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0);

будет генерировать IL-код точно так, как если бы он был написан:

ExtentionClass::IfNotNull(myObject2,  x => x.IntProperty, 0);

К объектам не добавляется "накладные расходы".

Ответ 7

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


propertyValue1 = Util.IfNotNull(myObject1, x => x.StringProperty );
propertyValue2 = Util.IfNotNull(myObject2, x => x.IntProperty, 0);

"Утилита". но это ИМО - меньшее синтаксическое зло.

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

Ответ 8

Хотя методы расширения обычно вызывают недоразумения при вызове из нулевых экземпляров, я думаю, что в этом случае намерение довольно простое.

string x = null;
int len = x.IfNotNull(y => y.Length, 0);

Я хочу быть уверенным, что этот статический метод работает с типами значений, которые могут быть нулевыми, например int?

Изменить: компилятор говорит, что ни одно из них не является допустимым:

    public void Test()
    {
        int? x = null;
        int a = x.IfNotNull(z => z.Value + 1, 3);
        int b = x.IfNotNull(z => z.Value + 1);
    }

Кроме этого, перейдите к нему.

Ответ 9

Не ответ на заданный вопрос, но в С# 6.0 есть Null-Conditional Operator . Я могу утверждать, что это будет плохой выбор для использования опции в OP с С# 6.0:)

Итак, ваше выражение проще,

string propertyValue = myObject?.StringProperty;

В случае, если myObject имеет значение null, он возвращает null. Если свойство является типом значения, вы должны использовать эквивалентный тип NULL, например,

int? propertyValue = myObject?.IntProperty;

Или иначе вы можете объединить с нулевым коалесцирующим оператором, чтобы дать значение по умолчанию в случае null. Например,

int propertyValue = myObject?.IntProperty ?? 0;

?. не единственный доступный синтаксис. Для индексированных свойств вы можете использовать ?[..]. Например,

string propertyValue = myObject?[index]; //returns null in case myObject is null

Одно удивительное поведение оператора ?. состоит в том, что он может разумно обходить последующие вызовы .Member, если объект имеет значение null. Один из таких примеров приведен в ссылке:

var result = value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);

В этом случае result имеет значение null, если value равно null, а выражение value.Length не приведет к NullReferenceException.

Ответ 10

Лично, даже после всех ваших объяснений, я не могу вспомнить, как это работает:

if( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property ) 

Это может быть потому, что у меня нет опыта С#; однако я мог читать и понимать все остальное в вашем коде. Я предпочитаю, чтобы код языка не был агностиком (особенно для тривиальных вещей), так что завтра другой разработчик мог бы изменить его на совершенно новый язык без слишком большой информации о существующем языке.

Ответ 11

Вот еще одно решение, использующее myObject.NullSafe(x = > x.SomeProperty.NullSafe(x = > x.SomeMethod)), объясненный в http://www.epitka.blogspot.com/