Обновление C# 6
В C# 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;
Однако это немного неуклюже, особенно если задано много свойств или если более одного уровня могут иметь значение 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 ) );
Это упрощает эти вызовы, но я не уверен, стоит ли проверять такой метод расширения - он делает код немного более читаемым, но за счет расширения объекта. Это будет отображаться на всем, хотя я мог бы поместить это в специально указанное пространство имен.
Этот пример довольно простой, немного более сложный - сравнение двух свойств объекта, допускающих значение NULL:
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;
Это будет работать только для типов значений, но вы можете вернуть эквиваленты, допускающие значение NULL:
int? propertyValue2 = myObject?ObjectProp?IntProperty;
//or
int propertyValue3 = myObject?ObjectProp?IntProperty ?? 0;





Лично, даже после всех ваших объяснений, я не могу вспомнить, как это, черт возьми, работает:
if ( obj1.NullCompare( obj2, (x,y) => x.Property == y.Property )
Это могло быть потому, что у меня нет опыта работы с C#; однако я мог прочитать и понять все остальное в вашем коде. Я предпочитаю сохранять независимость от языка кода (особенно для тривиальных вещей), чтобы завтра другой разработчик мог изменить его на совершенно новый язык без излишней информации о существующем языке.
Если вам нужно очень часто проверять, является ли ссылка на объект нулевой, возможно, вам следует использовать Шаблон пустого объекта. В этом шаблоне вместо использования null для случая, когда у вас нет объекта, вы реализуете новый класс с тем же интерфейсом, но с методами и свойствами, которые возвращают адекватные значения по умолчанию.
it does make the code a little easier to read, but at the cost of extending object. This would appear on everything,
Обратите внимание, что вы на самом деле ничего не расширяете (кроме теоретических).
propertyValue2 = myObject2.IfNotNull( x => x.IntProperty, 0);
сгенерирует IL-код точно так же, как если бы он был написан:
ExtentionClass::IfNotNull(myObject2, x => x.IntProperty, 0);
К объектам не добавляются "накладные расходы", чтобы поддержать это.
Как
propertyValue1 = myObject.IfNotNull(o => o.ObjectProp.IfNotNull( p => p.StringProperty ) );
легче читать и писать, чем
if (myObject != null && myObject.ObjectProp != null)
propertyValue1 = myObject.ObjectProp.StringProperty;
Джафар Хусейн опубликовал образец использования деревьев выражений для проверки нулевого значения в цепочке Макросы времени выполнения в C# 3.
Однако это, очевидно, влияет на производительность. Вот если бы у нас был способ сделать это во время компиляции.
Я просто должен сказать, что мне нравится этот хак!
Я не понимал, что методы расширения не предполагают нулевой проверки, но это имеет смысл. Как отметил Джеймс, сам вызов метода расширения не дороже обычного метода, однако, если вы делаете много этого, то имеет смысл следовать шаблону нулевого объекта, который предложил ljorquera. Или использовать нулевой объект и ?? все вместе.
class Class1
{
public static readonly Class1 Empty = new Class1();
.
.
x = (obj1 ?? Class1.Empty).X;
Сами по себе методы расширения не дороже, однако этот конкретный метод расширения дороже, потому что для него требуется лямбда / анонимный метод. Лямбда-выражения компилируются до распределения классов в фоновом режиме. Таким образом, это дороже, потому что требует выделения.
@JudahHimango Lamdas подчиняются распределению классов только в том случае, если они захватывают переменную (становясь закрытием). В противном случае они превращаются в статический метод, созданный компилятором ... Вы можете проверить это, посмотрев на скомпилированную DLL с чем-то вроде dotPeek (jetbrains.com/decompiler)
Для незнакомого читателя это выглядит так, как будто вы вызываете метод по нулевой ссылке. Если вы этого хотите, я бы предложил поместить его в служебный класс, а не использовать метод расширения:
propertyValue1 = Util.IfNotNull(myObject1, x => x.StringProperty );
propertyValue2 = Util.IfNotNull(myObject2, x => x.IntProperty, 0);
Файл "Утиль". решает, но является ИМО меньшим синтаксическим злом.
Кроме того, если вы разрабатываете это как часть команды, мягко спросите, что думают и делают другие. Согласованность кодовой базы для часто используемых шаблонов важна.
Хотя методы расширения обычно вызывают недоразумения при вызове из нулевых экземпляров, я думаю, что в этом случае целью является довольно просто.
string x = null;
int len = x.IfNotNull(y => y.Length, 0);
Я хотел бы быть уверен, что этот статический метод работает с типами значений, которые могут иметь значение null, например int?
Обновлено: компилятор говорит, что ни один из них не действителен:
public void Test()
{
int? x = null;
int a = x.IfNotNull(z => z.Value + 1, 3);
int b = x.IfNotNull(z => z.Value + 1);
}
Кроме этого, дерзайте.
Вот почему есть две перегрузки: одна требует значения по умолчанию без ограничения типа результата, а другая не требует значения по умолчанию, но ограничивает результат ссылочными типами.
Это потому что int? фактически компилируется в Nullable <int>, который на самом деле является структурой. Только магия компилятора позволяет сравнивать его с нулевым значением (оно не соответствует ограничению where TResult: class). Мне может потребоваться добавить еще одну перегрузку, специфичную для Nullable <T>
Если подумать - Nullable <int> имеет только два свойства: HasValue и Value - оба используются в ?? синтаксис. Это не нужно для int?
Вот еще одно решение для связанных членов, включая методы расширения:
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;
}
Затем вы можете сделать это, где any может быть null или none:
foo.PropagateNulls(x => x.ExtensionMethod().Property.Field.Method());
Обожаю эту идею! Однако он (как и все другие решения здесь), похоже, не работает в операторе LINQ. Например, при выполнении .Select в новый анонимный тип, например .Select (s => new {MyNewProperty = s.PropogateNulls (p => p.Thing)}). Это не работает. По-прежнему придется использовать старую нулевую проверку.
Вы должны изменить код, чтобы принимать статические методы с двумя аргументами, для Enumerable.Select(src, lambda)
Мы независимо друг от друга придумали одно и то же имя и реализацию метода расширения: Метод расширения с нулевым распространением. Поэтому мы не думаем, что это сбивает с толку или злоупотребление методами расширения.
Я бы написал ваш "многоуровневый" пример с цепочкой следующим образом:
propertyValue1 = myObject.IfNotNull(o => o.ObjectProp).IfNotNull(p => p.StringProperty);
Есть теперь закрытая ошибка в Microsoft Connect, предлагающий "?" как новый оператор C#, который будет выполнять это нулевое распространение. Мадс Торгерсен (из группы разработчиков языка C#) кратко объяснил, почему они не будут его реализовывать.
Да, я спросил Мэдса прямо в TechEd - в основном эта функция отсутствует, они все еще могут добавить ее в будущую версию C#.
@Keith Спасибо за обновление! (Не помешает постоянно напоминать команде C#, что клиенты сочтут это полезным.)
Сейчас они серьезно его рассматривают: blogs.msdn.com/b/jerrynixon/archive/2014/02/26/…
Статус функции «Нулевое распространение» - «Готово». См. Roslyn Статус реализации языковой функции
?. реализован в C# 6 - я обновил вопрос, чтобы отразить это. Ответы здесь все еще применимы к C# 1-5
Вот еще одно решение с использованием myObject.NullSafe (x => x.SomeProperty.NullSafe (x => x.SomeMethod)), объясненное на http://www.epitka.blogspot.com/
Спасибо, но это намного больше кода, чтобы сделать то же самое. Также ваш класс Maybe очень похож на Nullable <T> фреймворка, и с помощью Invoke вы добавляете ненужное снижение производительности. Тем не менее - приятно видеть альтернативный подход к той же проблеме.
Не ответ на точный вопрос, но есть Нуль-условный операторв C# 6.0. Я могу поспорить, что использовать эту опцию в OP, начиная с C# 6.0, будет плохим выбором :)
Так что ваше выражение проще,
string propertyValue = myObject?.StringProperty;
Если myObject равен нулю, он возвращает ноль. Если свойство является типом значения, вы должны использовать эквивалентный тип, допускающий значение NULL, например,
int? propertyValue = myObject?.IntProperty;
В противном случае вы можете объединиться с оператором объединения с нулевым значением, чтобы получить значение по умолчанию в случае нулевого значения. Например,
int propertyValue = myObject?.IntProperty ?? 0;
?. - не единственный доступный синтаксис. Для проиндексированных свойств вы можете использовать ?[..]. Например,
string propertyValue = myObject?[index]; //returns null in case myObject is null
Одно удивительное поведение оператора ?. заключается в том, что он может разумно обходить последующие вызовы .Member, если объект оказывается нулевым. Один из таких примеров приведен по ссылке:
var result = value?.Substring(0, Math.Min(value.Length, length)).PadRight(length);
В этом случае result равен нулю, если value равен нулю, и выражение value.Length не приведет к NullReferenceException.
Я согласен с вашим первым комментарием. Мне кажется, проблема в том, что из кода не сразу видно, что он делает. Он менее загроможден, но как только вы узнаете, что он делает легче понять.