Не говоря уже о согласованности, разве не имеет смысла иметь возможность обернуть наш код обработкой ошибок без необходимости рефакторинга?
Чтобы ответить на этот вопрос, необходимо взглянуть не только на область видимости переменной .
Даже если переменная останется в области видимости, она не будет определенно назначена .
Объявление переменной в блоке 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
- это определенный тип кодового блока, и, поскольку все кодовые блоки идут, вы не можете объявить переменную в одном и использовать эту же переменную в другом как область видимости.