В C #, почему переменные, объявленные внутри блока try, ограничены в области видимости?


23

Я хочу добавить обработку ошибок в:

var firstVariable = 1;
var secondVariable = firstVariable;

Ниже не будет компилироваться:

try
{
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Почему блок try catch должен влиять на область видимости переменных, как это делают другие блоки кода? Не говоря уже о согласованности, разве не имеет смысла иметь возможность обернуть наш код обработкой ошибок без необходимости рефакторинга?


14
A try.. catch- это определенный тип кодового блока, и, поскольку все кодовые блоки идут, вы не можете объявить переменную в одном и использовать эту же переменную в другом как область видимости.
Нил

msgstr "это определенный тип кодового блока". Конкретно каким образом? Спасибо
JᴀʏMᴇᴇ

7
Я имею в виду что-либо между фигурными скобками является блоком кода. Вы видите это после оператора if и после оператора for, хотя концепция та же самая. Содержимое находится в расширенной области по отношению к родительской области. Я уверен, что это было бы проблематично, если бы вы использовали фигурные скобки {}без попытки.
Нил

Совет: обратите внимание, что использование (IDisposable) {} и просто {} также применимо. Когда вы используете использование с IDisposable, оно автоматически очищает ресурсы независимо от успеха или неудачи. Есть несколько исключений из этого, например, не все классы, которые вы ожидаете, реализуют IDisposable ...
Джулия Макгиган

1
Много дискуссий по этому же вопросу о StackOverflow, здесь: stackoverflow.com/questions/94977/…
Джон Шнайдер

Ответы:


90

Что если ваш код был:

try
{
   MethodThatMightThrow();
   var firstVariable = 1;
}
catch {}

try
{
   var secondVariable = firstVariable;
}
catch {}

Теперь вы будете пытаться использовать необъявленную переменную ( firstVariable), если вызов вашего метода бросает.

Примечание . Приведенный выше пример конкретно отвечает на исходный вопрос, в котором говорится «в сторону согласованности». Это показывает, что есть и другие причины, чем последовательность. Но, как показывает ответ Питера, есть и весомый аргумент в пользу последовательности, которая наверняка была бы очень важным фактором в принятии решения.


Ааа, это именно то, что я был после. Я знал, что были некоторые языковые особенности, которые делали то, что я предлагал, невозможным, но я не мог придумать никаких сценариев. Большое спасибо.
J17Mᴇᴇ

1
«Теперь вы пытаетесь использовать необъявленную переменную, если вызов вашего метода сгенерирован». Кроме того, предположим, что этого удалось избежать, обрабатывая переменную, как если бы она была объявлена, но не инициализирована, перед кодом, который может выдать. Тогда он не был бы необъявленным, но он все равно был бы потенциально неназначенным, и анализ определенных назначений запретил бы чтение его значения (без промежуточного присвоения, которое могло бы доказать, что оно произошло).
Элия ​​Каган

3
Для такого статического языка, как C #, объявление действительно актуально только во время компиляции. Компилятор может легко переместить объявление ранее в области видимости. Более важным фактом во время выполнения является то, что переменная не может быть инициализирована .
jpmc26

3
Я не согласен с этим ответом. В C # уже есть правило, что неинициализированные переменные не могут быть прочитаны из-за некоторого понимания потока данных. (Попробуйте объявить переменные в случаях a switchи получить к ним доступ в других.) Это правило может легко применяться здесь и предотвратить компиляцию этого кода в любом случае. Я думаю, что ответ Питера ниже более правдоподобен.
Себастьян Редл

2
Существует разница между необъявленными неинициализированными и C # отслеживает их отдельно. Если бы вам было разрешено использовать переменную вне блока, в котором она была объявлена, это означало бы, что вы сможете назначить ей переменную в первом catchблоке, а затем она определенно будет назначена во втором tryблоке.
svick

64

Я знаю, что на это хорошо ответил Бен, но я хотел обратиться к последовательности POV, которую было удобно отодвинуть в сторону. Предполагая, что try/catchблоки не влияют на область действия, вы получите:

{
    // new scope here
}

try
{
   // Not new scope
}

И для меня это врезается в принцип наименьшего удивления (POLA), потому что теперь у вас есть двойная обязанность {и }выполнение в зависимости от контекста того, что им предшествовало.

Единственный выход из этого беспорядка - назначить какой-то другой маркер для разграничения try/catchблоков. Который начинает добавлять запах кода. Таким образом, к тому времени, когда у вас не будет видимости try/catchв языке, это будет такой беспорядок, что вам будет лучше с версией с ограниченным доступом.


Еще один отличный ответ. И я никогда не слышал о POLA, так приятно читать дальше. Большое спасибо, приятель.
JᴀʏMᴇᴇ

«Единственный выход из этого беспорядка - назначить какой-то другой маркер для разграничения try/ catchблоков». - Вы имеете в виду try { { // scope } }:? :)
CompuChip

@CompuChip, который будет иметь {}двойную обязанность в качестве области видимости, а не создание области видимости в зависимости от контекста. try^ //no-scope ^будет примером другого маркера.
Лелиэль

1
На мой взгляд, это гораздо более фундаментальная причина, и ближе к «реальному» ответу.
Джек Эйдли,

@JackAidley, тем более что вы уже можете написать код, в котором вы используете переменную, которая может быть не назначена. Поэтому, хотя в ответе Бена и говорится о том, как это полезно, я не вижу его в том, почему такое поведение существует. В ответе Бена отмечается, что ОП говорит «ради консистенции в стороне», но последовательность - совершенно веская причина! Узкий охват имеет множество других преимуществ.
Кат

21

Не говоря уже о согласованности, разве не имеет смысла иметь возможность обернуть наш код обработкой ошибок без необходимости рефакторинга?

Чтобы ответить на этот вопрос, необходимо взглянуть не только на область видимости переменной .

Даже если переменная останется в области видимости, она не будет определенно назначена .

Объявление переменной в блоке try выражает - как компилятору, так и читателям - что она имеет смысл только внутри этого блока. Компилятору полезно применять это.

Если вы хотите, чтобы переменная находилась в области видимости после блока try, вы можете объявить ее вне блока:

var zerothVariable = 1_000_000_000_000L;
int firstVariable;

try {
    // Change checked to unchecked to allow the overflow without throwing.
    firstVariable = checked((int)zerothVariable);
}
catch (OverflowException e) {
    Console.Error.WriteLine(e.Message);
    Environment.Exit(1);
}

Это говорит о том, что переменная может иметь смысл вне блока try. Компилятор позволит это.

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

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

Console.WriteLine(firstVariable);

Это даст ошибку во время компиляции :

CS0165 Использование неназначенной локальной переменной firstVariable

Я вызвал Environment.Exit в блоке catch, так что я знаю, что переменная была назначена до вызова Console.WriteLine. Но компилятор не выводит это.

Почему компилятор такой строгий?

Я даже не могу сделать это:

int n;

try {
    n = 10; // I know this won't throw an IOException.
}
catch (IOException) {
}

Console.WriteLine(n);

Один из способов взглянуть на это ограничение - сказать, что анализ определенных назначений в C # не очень сложен. Но другой способ взглянуть на это заключается в том, что когда вы пишете код в блоке try с предложениями catch, вы говорите и компилятору, и всем читателям-людям, что его следует обрабатывать так, как будто не все могут работать.

Чтобы проиллюстрировать, что я имею в виду, представьте, что компилятор разрешил приведенный выше код, но затем вы добавили вызов в блоке try в функцию, которая, как вы знаете, не вызовет исключения . Не имея возможности гарантировать, что вызываемая функция не выдает IOException, компилятор не может знать, что nбыло назначено, и тогда вам придется выполнить рефакторинг.

Это означает, что, отказавшись от сложного анализа при определении того, была ли переменная, назначенная в блоке try с предложениями catch, определенно назначена впоследствии, компилятор помогает вам избежать написания кода, который может позже сломаться. (В конце концов, поймать исключение обычно означает, что вы думаете, что его могут выбросить.)

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

Вы можете скомпилировать код, передав переменной значение перед блоком try или блоком catch. Таким образом, он все равно будет инициализирован или назначен, даже если назначение в блоке try не выполняется. Например:

var n = 0; // But is this meaningful, or just covering a bug?

try {
    n = 10;
}
catch (IOException) {
}

Console.WriteLine(n);

Или:

int n;

try {
    n = 10;
}
catch (IOException) {
    n = 0; // But is this meaningful, or just covering a bug?
}

Console.WriteLine(n);

Те компилируют. Но лучше всего делать что-то подобное, только если заданное вами значение по умолчанию имеет смысл * и дает правильное поведение.

Обратите внимание, что во втором случае, когда вы назначаете переменную в блоке try и во всех блоках catch, хотя вы можете прочитать переменную после try-catch, вы все равно не сможете прочитать переменную внутри присоединенного finallyблока , поскольку выполнение может оставить блок try в большем количестве ситуаций, чем мы часто думаем .

* Кстати, некоторые языки, такие как C и C ++, допускают неинициализированные переменные и не имеют определенного анализа присваивания, чтобы предотвратить чтение из них. Поскольку чтение неинициализированной памяти приводит к тому, что программы ведут себя недетерминированным и ошибочным образом, обычно рекомендуется избегать введения переменных в этих языках без предоставления инициализатора. В языках с определенным анализом присваивания, таких как C # и Java, компилятор избавляет вас от чтения неинициализированных переменных, а также от меньшего зла - инициализации их бессмысленными значениями, которые впоследствии могут быть неверно истолкованы как значимые.

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

Если вы планируете выполнить какое-либо действие (например, ведение журнала) и перебросить исключение или сгенерировать другое исключение, и это происходит в любых предложениях catch, где переменная не назначена, тогда компилятор будет знать, что переменная была назначена:

int n;

try {
    n = 10;
}
catch (IOException e) {
    Console.Error.WriteLine(e.Message);
    throw;
}

Console.WriteLine(n);

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

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

* Например, во многих приложениях нет предложения catch, которое обрабатывает исключение OutOfMemoryException, потому что все, что они могут с этим поделать, может быть, по крайней мере, таким же плохим, как и сбой .

Может быть , вы на самом деле действительно хотите , чтобы реорганизовать код.

В вашем примере вы вводите firstVariableи secondVariableв блоки try. Как я уже сказал, вы можете определить их до блоков try, в которых они назначены, чтобы они впоследствии оставались в области видимости, и вы можете удовлетворить / заставить компилятор позволить вам читать из них, убедившись, что они всегда назначены.

Но код, который появляется после этих блоков, по-видимому, зависит от их правильного назначения. Если это так, то ваш код должен отражать и обеспечивать это.

Во-первых, можете ли вы (и должны) справиться с ошибкой там? Одна из причин, по которой существует обработка исключений, состоит в том, чтобы упростить обработку ошибок, когда они могут эффективно обрабатываться , даже если это не близко к месту их возникновения.

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

Другой подход - поместить код, который использует переменные, в блок try. Это часто разумно. Опять же, если те же самые исключения, которые вы ловите из их инициализаторов, также могут быть выброшены из окружающего кода, вы должны убедиться, что вы не пренебрегаете этой возможностью при обработке их.

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

Третий способ - извлечь код, который может дать сбой, и try-catch, который его обрабатывает, в свой собственный метод. Это полезно, если вы хотите сначала полностью разобраться с ошибками, а затем не беспокоиться о непреднамеренном перехвате исключения, которое вместо этого должно быть обработано где-то еще.

Предположим, например, что вы хотите немедленно выйти из приложения в случае неудачи при назначении любой переменной. (Очевидно, что не вся обработка исключений относится к фатальным ошибкам; это всего лишь пример, и может быть, а может и не быть так, как вы хотите, чтобы ваше приложение реагировало на проблему.) Вы можете сделать что-то вроде этого:

// In real life, this should be named more descriptively.
private static (int firstValue, int secondValue) GetFirstAndSecondValues()
{
    try {
        // This code is contrived. The idea here is that obtaining the values
        // could actually fail, and throw a SomeSpecificException.
        var firstVariable = 1;
        var secondVariable = firstVariable;
        return (firstVariable, secondVariable);
    }
    catch (SomeSpecificException e) {
        Console.Error.WriteLine(e.Message);
        Environment.Exit(1);
        throw new InvalidOperationException(); // unreachable
    }
}

// ...and of course so should this.
internal static void MethodThatUsesTheValues()
{
    var (firstVariable, secondVariable) = GetFirstAndSecondValues();

    // Code that does something with them...
}

Этот код возвращает и восстанавливает ValueTuple с синтаксисом C # 7.0 для возврата нескольких значений, но если вы все еще используете более раннюю версию C #, вы все равно можете использовать эту технику; например, вы можете использовать параметры или вернуть пользовательский объект, который предоставляет оба значения . Кроме того, если две переменные на самом деле не связаны между собой, в любом случае, вероятно, лучше иметь два отдельных метода.

Особенно, если у вас есть несколько таких методов, вам следует подумать о централизации своего кода для уведомления пользователя о фатальных ошибках и выхода из него. (Например, вы могли бы написать Dieметод с messageпараметром.) Линия фактически никогда не выполняются , так что вы не нужны (и не должна) написать статью на вылов для него.throw new InvalidOperationException();

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

Вывод: сфера является лишь частью картины.

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


«когда вы пишете код в блоке try с предложениями catch, вы говорите и компилятору, и всем читателям-людям, что его следует обрабатывать так, как будто не все могут работать». Что беспокоит компилятор, так это то, что элемент управления может достигать более поздних операторов, даже если предыдущие операторы выдают исключение. Компилятор обычно предполагает, что если один оператор выдает исключение, следующий оператор не будет выполнен, поэтому неназначенная переменная не будет прочитана. Добавление 'catch' позволит элементу управления обращаться к более поздним операторам - важен только тот улов, а не выбрасывает ли код в блоке try.
Пит Киркхэм
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.