Лямбда-выражения для рефакторинга ArgumentException

Обновление. Это больше не проблема с С# 6, которая внедрила оператор nameof для решения таких сценариев (см. MSDN).

Примечание. Обратитесь к разделу Получение имен локальных переменных (и параметров) во время выполнения через лямбда-выражения" для обобщения этот вопрос, а также некоторые ответы.

Мне нравится идея использования лямбда-выражений для создания рефакторизуемых реализаций интерфейса INotifyPropertyChanged, используя код, аналогичный тому, который предоставляется Eric De Carufel.

Im экспериментирует с реализацией чего-то подобного для предоставления имени параметра ArgumentException (или его производных классов) в режиме рефакторинга.

Я определил следующий метод утилиты для выполнения проверок null:

public static void CheckNotNull<T>(Expression<Func<T>> parameterAccessExpression)
{
    Func<T> parameterAccess = parameterAccessExpression.Compile();
    T parameterValue = parameterAccess();
    CheckNotNull(parameterValue, parameterAccessExpression);
}

public static void CheckNotNull<T>(T parameterValue, 
    Expression<Func<T>> parameterAccessExpression)
{
    if (parameterValue == null)
    {
        Expression bodyExpression = parameterAccessExpression.Body;
        MemberExpression memberExpression = bodyExpression as MemberExpression;
        string parameterName = memberExpression.Member.Name;
        throw new ArgumentNullException(parameterName);
    }
}

Проверка правильности аргумента может быть выполнена в режиме рефакторинга с использованием следующего синтаксиса:

CheckNotNull(() => arg);           // most concise
CheckNotNull(arg, () => args);     // equivalent, but more efficient

Моя забота заключается в следующих строках:

Expression bodyExpression = parameterAccessExpression.Body;
MemberExpression memberExpression = bodyExpression as MemberExpression;

A MemberExpression представляет "доступ к полю или свойству". Он гарантированно работает корректно в случае INotifyPropertyChanged, поскольку выражение лямбда будет доступностью свойств.

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

Мой вопрос: Является ли это поведение (продвижение захваченных параметров в переменные экземпляра) документированным в спецификации .NET, или это просто деталь реализации, которая может измениться в альтернативных реализациях или будущих версиях структуры? В частности, могут быть среды, в которых parameterAccessExpression.Body is MemberExpression возвращает false?

Ответ 1

Закрытие: как вы сказали, для доступа к параметрам компилятор С# (да, в частности, компилятор) создает класс закрытия, который содержит поля экземпляра для хранения значения вашей захваченной переменной параметра. Может ли это измениться в будущих версиях компилятора С#? Конечно. Возможно, в будущей версии С# генерируемые классы замыкания будут иметь случайные имена переменных, поскольку имя не имеет большого значения во время выполнения. Кроме того, код, который у вас есть, может не работать для других языков .NET. Вы заметите, что VB.NET генерирует деревья выражений и классы закрытия немного иначе, чем С# иногда...

Я не уверен, будет ли ваша текущая реализация работать и для структур (хотя я мог бы неправильно помнить... ситуация, о которой я думаю, касающаяся бокса, может применяться только для Expression<Func<T, object>> (читайте, пожалуйста, попробуйте сам).

В любом случае... все это говорит... изменится ли он в будущих версиях С#? Возможно нет. Если это так, вы можете изменить свою внутреннюю реализацию, чтобы справиться с ней, вероятно,...

Что касается производительности: пожалуйста, будьте очень осторожны. Вы уже сказали, что было бы более эффективно передавать два аргумента, поэтому вам не нужно компилировать и оценивать лямбда... но просто чтобы быть ясными, вы говорите об ударе от 15 до 30 мс каждый раз, когда вы компилируете и оценить.