Почему этот (null ||! TryParse) условный результат приводит к «использованию неназначенной локальной переменной»?

98

Следующий код приводит к использованию неназначенной локальной переменной numberOfGroups :

int numberOfGroups;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Однако этот код работает нормально (хотя ReSharper говорит, что = 10он избыточен):

int numberOfGroups = 10;
if(options.NumberOfGroups == null || !int.TryParse(options.NumberOfGroups, out numberOfGroups))
{
    numberOfGroups = 10;
}

Мне что-то не хватает или компилятору не нравится мой ||?

Я сузил это до dynamicпричинения проблем ( optionsв моем приведенном выше коде была динамическая переменная). Остается вопрос, почему я не могу этого сделать ?

Этот код не компилируется:

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        dynamic myString = args[0];

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Однако этот код делает :

internal class Program
{
    #region Static Methods

    private static void Main(string[] args)
    {
        var myString = args[0]; // var would be string

        int myInt;
        if(myString == null || !int.TryParse(myString, out myInt))
        {
            myInt = 10;
        }

        Console.WriteLine(myInt);
    }

    #endregion
}

Я не понимал, dynamicчто это может быть фактором.

Брэндон Мартинес
источник
Не думайте, что достаточно умен, чтобы знать, что вы не используете значение, переданное вашему outпараметру, в качестве входных данных
Чарльз
3
Приведенный здесь код не демонстрирует описанного поведения; он работает нормально. Пожалуйста, разместите код, который действительно демонстрирует описываемое вами поведение, которое мы можем скомпилировать сами. Дайте нам весь файл.
Эрик Липперт
8
Ах, теперь у нас есть кое-что интересное!
Эрик Липперт
1
Неудивительно, что компилятор смущен этим. Вспомогательный код для сайта динамического вызова, вероятно, имеет некоторый поток управления, который не гарантирует назначения outпараметру. Конечно, интересно подумать, какой вспомогательный код компилятор должен создать, чтобы избежать проблемы, или если это вообще возможно.
CodesInChaos
1
На первый взгляд это действительно похоже на ошибку.
Эрик Липперт

Ответы:

73

Я почти уверен, что это ошибка компилятора. Хорошая находка!

Изменить: это не ошибка, как демонстрирует Quartermeister; dynamic может реализовывать странный trueоператор, который может yникогда не инициализироваться.

Вот минимальное воспроизведение:

class Program
{
    static bool M(out int x) 
    { 
        x = 123; 
        return true; 
    }
    static int N(dynamic d)
    {
        int y;
        if(d || M(out y))
            y = 10;
        return y; 
    }
}

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

Я действительно встречаюсь с командой C # завтра; Я скажу им об этом. Приносим извинения за ошибку!

Эрик Липперт
источник
6
Я просто рад узнать, что я не сойду с ума :) С тех пор я обновил свой код, чтобы полагаться только на TryParse, так что сейчас я настроен. Спасибо за понимание!
Брэндон Мартинес
4
@NominSim: предположим, что анализ времени выполнения завершился неудачно: тогда перед чтением локального будет выброшено исключение. Предположим, что анализ времени выполнения завершился успешно: тогда во время выполнения либо d истинно и установлено y, либо d равно false и M устанавливает y. В любом случае y установлено. Тот факт, что анализ откладывается до времени выполнения, ничего не меняет.
Эрик Липперт
2
На случай, если кому-то интересно: я только что проверил, и компилятор Mono все правильно понял. imgur.com/g47oquT
Дэн Тао
17
Я думаю, что поведение компилятора действительно правильное, поскольку значение dможет быть типа с перегруженным trueоператором. Я отправил ответ с примером, в котором ни одна ветка не взята.
Quartermeister
2
@Quartermeister, и в этом случае компилятор Mono ошибается :)
porges
52

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

||Оператор будет вызывать trueоператор , чтобы решить , следует ли оценивать правую, а затем ifоператор будет вызывать trueоператор , чтобы решить , следует ли оценивать свое тело. В нормальном режиме boolони всегда будут возвращать один и тот же результат, поэтому будет оцениваться ровно один результат, но для пользовательского оператора такой гарантии нет!

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

using System;

class Program
{
    static bool M(out int x)
    {
        x = 123;
        return true;
    }

    static int N(dynamic d)
    {
        int y = 3;
        if (d || M(out y))
            y = 10;
        return y;
    }

    static void Main(string[] args)
    {
        var result = N(new EvilBool());
        // Prints 3!
        Console.WriteLine(result);
    }
}

class EvilBool
{
    private bool value;

    public static bool operator true(EvilBool b)
    {
        // Return true the first time this is called
        // and false the second time
        b.value = !b.value;
        return b.value;
    }

    public static bool operator false(EvilBool b)
    {
        throw new NotImplementedException();
    }
}
Квартирмейстер
источник
8
Хорошая работа здесь. Я передал это командам разработчиков и разработчиков C #; Я посмотрю, есть ли у них какие-нибудь комментарии, когда увижу их завтра.
Эрик Липперт
3
Для меня это очень странно. Почему нужно dоценивать дважды? (Я не спорю , что она ясно это , как вы показали.) Я ожидал бы оценочный результата true(от первого оператора вызова, причины по ||) , чтобы быть «прошел вдоль» в ifзаявление. Это определенно произойдет, если, например, вы поместите туда вызов функции.
Дэн Тао
3
@DanTao: выражение dоценивается только один раз, как и следовало ожидать. Это trueоператор, который вызывается дважды, один раз ||и один раз if.
Quartermeister
2
@DanTao: Было бы более ясно, если бы мы поместили их в отдельные утверждения как var cond = d || M(out y); if (cond) { ... }. Сначала мы оцениваем, dчтобы получить EvilBoolссылку на объект. Чтобы оценить ||, мы сначала вызываем EvilBool.trueс этой ссылкой. Это возвращает истину, поэтому мы закорачиваем и не вызываем M, а затем присваиваем ссылку cond. Затем мы переходим к ifутверждению. ifОператор оценивает его состояние путем вызова EvilBool.true.
Quartermeister
2
Теперь это действительно круто. Я понятия не имел, есть ли оператор true или false.
IllidanS4 хочет вернуть Монику
7

Из MSDN (выделено мной):

Динамический тип позволяет операциям, в которых он происходит, обойти проверку типа во время компиляции . Вместо этого эти операции разрешаются во время выполнения . Динамический тип упрощает доступ к COM API, таким как API автоматизации Office, а также к динамическим API, таким как библиотеки IronPython, и к объектной модели документа HTML (DOM).

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

Поскольку компилятор не проверяет типы и не разрешает какие-либо операции, содержащие выражения динамического типа, он не может гарантировать, что переменная будет назначена с помощью TryParse().

NominSim
источник
Если первое условие выполнено, numberGroupsприсваивается (в if trueблоке), если нет, второе условие гарантирует присваивание (через out).
leppie
1
Это интересная мысль, но код отлично компилируется без myString == null(полагаясь только на TryParse).
Брэндон Мартинес
1
@leppie Дело в том, что поскольку первое условие (а значит, и все ifвыражение) включает в себя dynamicпеременную, оно не разрешается во время компиляции (поэтому компилятор не может сделать эти предположения).
NominSim
@NominSim: Я понимаю вашу точку зрения :) +1 Может быть, компилятор принес в жертву (нарушение правил C #), но другие предложения, похоже, подразумевают ошибку. Фрагмент Эрика показывает, что это не жертва, а ошибка.
leppie
@NominSim Это не может быть правдой; то, что некоторые функции компилятора отложены, не означает, что все они отложены. Существует множество свидетельств того, что при несколько иных обстоятельствах компилятор без проблем выполняет анализ определенного присваивания, несмотря на наличие динамического выражения.
dlev