Почему компилятор жалуется, что «не все пути кода возвращают значение», хотя я ясно вижу, что это так?

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

public static Boolean Foo(Boolean x)
{
    Boolean a = false;
    if( x )
    {
        a = true;
    }
    else
    {
        try
        {
            SomethingThatMightThrow();
            Assert.IsFalse( a );
            return a;
        }
        catch(Exception)
        {
            a = true;
        }
    }

    if( a )
    {
        return x;
    }
}

Немедленное исправление состоит в том, чтобы просто полностью удалить оператор защиты if( a ) и немедленно return x, но почему компилятор жалуется, даже если он должен быть в состоянии статически доказать, что все возможные пути кода попадут в оператор return? Важно отметить, что нет петель, которые часто являются основной причиной невозможности доказать return-ность.

Я использую VS2015 Update 3.


person Dai    schedule 21.01.2017    source источник
comment
Что, если в конце этого кода printUsage равно false? Помните, что компилятор не может угадать, всегда ли во время выполнения ваша переменная имеет значение true, когда достигает точки выхода.   -  person Steve    schedule 21.01.2017
comment
@Steve Но printUsage никогда не будет ложным к концу функции Main - если это когда-либо будет false, то последует оператор return внутри try{}.   -  person Dai    schedule 21.01.2017
comment
Если для переменных всегда установлено значение true, зачем вообще использовать переменную? Удалите условие и всегда печатайте использование, если вы еще не вернулись из метода.   -  person knittl    schedule 21.01.2017
comment
А что, если по какой-то причине через месяц или год кто-то изменит ваш код и printUsage станет ложным, когда вы его проверите?   -  person Martin Verjans    schedule 21.01.2017
comment
@MartinVerjans В предложенном вами случае да, компилятор был бы прав, утверждая, что существует путь кода без оператора return. Но мой вопрос заключается в том, почему компилятору не удается статически доказать правильность моего метода Main. Я знаю, что текущая структура кода неоптимальна, но она по-прежнему верна.   -  person Dai    schedule 21.01.2017
comment
Я думаю, вы могли бы найти эту статью интересной blog.coverity.com/ 06.11.2013 c-reachability/#.WIMgWIWcGdI   -  person Steve    schedule 21.01.2017
comment
Если вы знаете, что a всегда true, почему бы вообще не удалить if?   -  person Niyoko    schedule 21.01.2017
comment
Спецификация C# 8.1. Конечные точки и достижимость. Чтобы определить, достижимы ли конкретный оператор или конечная точка, компилятор выполняет анализ потока в соответствии с правилами достижимости, определенными для каждого оператора. Анализ потока учитывает значения константных выражений (§7.19), управляющих поведением операторов, но возможные значения непостоянных выражений не учитываются. Другими словами, в целях анализа потока управления рассматривается, что непостоянное выражение данного типа имеет любое возможное значение этого типа.   -  person user4003407    schedule 21.01.2017
comment
Просто комментарий Style. Вы усложняете свой код, чтобы поставить единую точку выхода. Возможно, это обескураживает. См. softwareengineering.stackexchange.com/questions/104551/ и softwareengineering.stackexchange.com/questions/118703/   -  person borjab    schedule 21.01.2017
comment
Могу ли я указать, что путь кода не совпадает с путь потока управления...   -  person user541686    schedule 22.01.2017
comment
@Mehrdad В чем разница?   -  person Dai    schedule 22.01.2017
comment
@Dai: Код относится к тому, что вы написали, поток управления (также известный как поток управления) относится к тому, что на самом деле выполняется. Не все, что вы написали, может быть выполнено. Таким образом, не каждый путь в вашем коде является путем в потоке управления. Вы думаете, что все потоки управления здесь возвращаются, тогда как компилятор говорит, что возвращаются не все пути кода... так что вы говорите о разных вещах. (Хотя бывают ситуации, когда компилятор игнорирует некоторые пути кода, которые он может тривиально доказать, что они не являются путями потока управления, но это противоположно заданному вами вопросу и требует другого примера.)   -  person user541686    schedule 22.01.2017
comment
Это тоже результат теоремы Райса Я думаю: всегда будут случаи, когда определенное условие всегда будет выполняться, но найти их во всех сценариях — неразрешимая проблема. .   -  person Willem Van Onsem    schedule 26.01.2017


Ответы (3)


Существует поддерживаемый сценарий, когда a становится false, когда вы достигаете конца своей функции. Это сценарий, когда вы отлаживаете свой код и используете отладчик для установки a в false.

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

Одна часть этих простых правил заключается в том, что значения переменных не учитываются. Даже если a статически гарантировано равно true, компилятор по своей конструкции не может использовать этот факт.

@PetSerAl уже процитировал соответствующую формулировку в спецификации языка C#:

8.1 Конечные точки и доступность

[...]

Чтобы определить, достижим ли конкретный оператор или конечная точка, компилятор выполняет анализ потока в соответствии с правилами достижимости, определенными для каждого оператора. Анализ потока учитывает значения постоянных выражений (§7.19), управляющих поведением операторов, но возможные значения непостоянных выражений не учитываются. Другими словами, для целей анализа потока управления считается, что непостоянное выражение данного типа имеет любое возможное значение этого типа.

Это часть последней опубликованной спецификации языка для C# 5.0. Версия, которую вы используете, C# 6.0 (это то, что предлагает VS2015), еще не имеет опубликованной спецификации, поэтому возможно, что формулировка будет немного отличаться, но, как показал ваш компилятор, фактически то же правило все еще применяется.

person Community    schedule 21.01.2017
comment
Это отличный ответ, но, поскольку этот вопрос стал настолько популярным, вы можете рассмотреть возможность его улучшения, сославшись на соответствующую часть спецификации языка C#. PetSerAl уже процитировал это в комментарии. - person Cody Gray; 21.01.2017
comment
@CodyGray OP использует VS2015, который реализует C # 6, который, насколько мне известно, до сих пор не имеет спецификации языка. - person ; 21.01.2017
comment
@hvd VS2015 использует любую версию C#, которую вы выберете для своего проекта, и, похоже, в вопросе нет ничего конкретного для C#6. - person BartoszKP; 22.01.2017
comment
@BartoszKP Нет, на сегодняшний день каждый компилятор C # реализует только одну версию языка. У них есть возможность выдавать ошибки для новых функций языка, но нет возможности реализовать более раннюю версию спецификации. (Наиболее известным примером, вероятно, является изменение переменной foreach в C# 5: -sharp-5" title="использование переменных foreachs было изменено в c Sharp 5"> stackoverflow.com/questions/12112881/) И хотя в вопросе может не быть ничего конкретного для С# 6, я подозреваю этот конкретный фрагмент спецификации необходимо изменить для методов async C # 6, хотя я не могу быть уверен на 100%. - person ; 22.01.2017
comment
Ой, подождите, async была функцией C# 5, просто немного улучшенной в C# 6. И ничто другое в C# 6 не должно требовать изменений в этой части спецификации... - person ; 22.01.2017
comment
У вас действительно хорошая идея установить значение false с помощью отладчика. +1 - person Matty; 22.01.2017
comment
Я сомневаюсь, что изменение вещей в отладчике является поддерживаемым сценарием для разработки языка. Ведь в принципе любую гарантию спецификации языка C# можно нарушить отладчиком (может и не VS, но все же). Если вы измените локальный логический элемент на постоянный член, вы можете фактически написать код, как показано, и вы все еще можете перепрыгнуть через возврат даже в VS. Происходят шутки. - person Voo; 22.01.2017
comment
@Voo Неуправляемый код может нарушить все гарантии, а не только отладчики. Но отладчики, работающие с управляемым кодом и использующие поддерживаемые методы для изменения переменных, должны быть хотя бы в какой-то мере безопасными. Вы привели очень конкретный сценарий, где вы говорите, что это не так. Я проверю это, когда у меня будет возможность, и если вы правы, я исправлю свой ответ. Спасибо за комментарий. - person ; 22.01.2017
comment
@Voo Проверено сейчас, в VS2017RC. Он не позволяет изменять константы. - person ; 22.01.2017
comment
@hvd Я не сказал изменить константы (что было бы невозможно сделать без исходного кода, если подумать - константы в конце концов встроены), но перепрыгнул через возврат, который легко возможен (щелкните правой кнопкой мыши по строке и выберите что-то вроде set next адрес) и имеет те же последствия, что и изменение переменной. - person Voo; 22.01.2017
comment
@Voo А, неправильно понял, что ты хотел сказать. Это возможно даже без каких-либо констант: static int Main() { return 0; } позволяет переместить точку выполнения в закрывающую } до того, как return 0; также выполнится. Интересный. - person ; 22.01.2017
comment
@hvd Да, это было излишне запутанно. Я был очень удивлен поддержкой этой функции, когда впервые узнал о ней... хотя она на удивление полезна. - person Voo; 22.01.2017

Время выполнения и время компиляции

Ваш пример слишком сложен. Это тоже не скомпилируется:

static int Test()
{
    bool f = true;
    if (f)
    {
        return 1;
    }
    else
    {
        //Not all code paths return a value
    }
}

С другой стороны, это будет:

static int Test()
{
    if (true)
    {
        return 1;
    }
    else
    {
        //No error
    }
}

Я предполагаю, что любые механизмы проверки не имеют достаточной логики, чтобы вывести содержимое переменной времени выполнения. Переменные времени компиляции не проблема.

person John Wu    schedule 21.01.2017

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

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

Ваш пример можно сократить до минимума следующим образом:

public static Int32 Main(String[] args)
{
    var printUsage = true;
    if (printUsage)
    {
        return 0;
    }

    // return nothing, so compiler is not happy
}

при этом все еще получая ошибку.

ПРИМЕЧАНИЕ. если вы используете Resharper, он выполнит нужный вам анализ и предупредит вас соответствующим образом:

if (printUsage)         // Warning: expression is always true 
person Alexei - check Codidact    schedule 21.01.2017
comment
Интересно - хотя, согласно статье, на которую ссылался @steve (не меньше, чем Эрик Липперт!), В ней говорится, что компилятор вычисляет достижимость с использованием констант времени компиляции, поэтому, согласно этой статье, компилятор также не должен сообщать о каких-либо проблемах с вашим методом Main ( blog.coverity.com/2013/11/06/c -reachability/#.WIMgWIWcGdI ) - person Dai; 21.01.2017
comment
@Dai printUsage не является константой времени компиляции. - person ; 21.01.2017
comment
Более того, даже если printUsage объявлен членом класса static readonly, он все равно будет жаловаться. - person Niyoko; 21.01.2017