Выстрел себе в ногу литералом С# по умолчанию и нулевым значением

  • Михаил
  • 12 мин. на прочтение
  • 62
  • 25 Nov 2022
  • 25 Nov 2022

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

Однако я хотел бы выделить то, что меня зацепило: использование литерала по умолчанию в сочетании со значениями, допускающими значение NULL.

Это не вина самой языковой функции, а скорее случай, когда я применяю ее вслепую, не задумываясь об этом сначала, что всегда опасно!

defaultБуквальный _

У нас есть выражения значений по умолчанию , например default(int), уже давно*. Вот надуманный пример, который присваивает значение по умолчанию для int( 0) переменной, если логическое значение setValueравно false:

var setValue = false;
var intValue = 123;
var valueOrDefault = setValue ? intValue : default(int);

Начиная с C# 7.1 литерал по умолчанию может определять тип:

var valueOrDefault = setValue ? intValue : default;

Обратите внимание, что тип выводится из типа, возвращаемого другой ветвью условия ( value), а не из типа переменной, которую вы присваиваете ; в любом случае это не может быть в этом случае, поскольку это объявлено с помощью var.

Как я выстрелил себе в ногу...

Я создавал очень простую систему аутентификации на основе файлов cookie в веб-приложении .NET Core. В Loginметод контроллера я добавил следующее:

// Preceding code omitted for brevity

var principal = new ClaimsPrincipal(identity);

var authenticationProperties = new AuthenticationProperties {
    IsPersistent = model.RememberMe,
    ExpiresUtc = model.RememberMe 
        ? DateTimeOffset.UtcNow.AddDays(7) 
        : default
};

await HttpContext.SignInAsync(principal, authenticationProperties);

Я ожидаю, что некоторые из вас уже заметили ошибку... но если нет, читайте дальше!

AuthenticationProperties.ExpiresUtcопределяет, как долго файл cookie аутентификации висит в браузере клиента. если пользователь установит флажок «Запомнить меня», я хочу, чтобы срок его действия истек через 7 дней; если нет, время истечения следует оставить по умолчанию.

ExpiresUtcявляется DateTimeOffset?, поэтому defaultзначение будет null. Это приведет к тому, что файл cookie будет установлен как файл cookie сеанса, срок действия которого истекает при закрытии браузера.

Однако, несмотря на то, что код читается так, он ведет себя иначе !

Как я уже упоминал, defaultлитерал выводит тип из значения, возвращаемого другой ветвью троичного условного оператора, в данном случае это DateTimeOffset.UtcNow.AddDays(7).

Это не обнуляемый DateTimeOffset? , а базовый DateTimeOffset: значение по умолчанию равно DateTimeOffset.MinValue.

Итак... если пользователь не отмечает "Запомнить меня", мой код устанавливается ExpiresUtcна 01/01/0001 00:00:00 +00:00. Это означает, что срок действия куки-файла аутентификации немедленно истекает, и даже несмотря на то, что их вход в систему был действительно успешным, пользователь перенаправляется прямо обратно на страницу входа.

Правильный путь

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

var authenticationProperties = new AuthenticationProperties {
    IsPersistent = model.RememberMe,
    ExpiresUtc = model.RememberMe 
        ? DateTimeOffset.UtcNow.AddDays(7) 
        : default(DateTimeOffset?)
};

Теперь мы используем DateTimeOffset?значение по умолчанию, что означает, что значение по умолчанию на самом деле null... не значение смещения тысячи лет назад. 

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