Почему .NET-исключения не работают против интерфейса, а не базового класса?
Реализация try-catch framework.Net позволяет вам захватывать типы, которые наследуют базовый класс "System.Exception". Почему это не было таким интерфейсом, как "System.IException"?
Использовать регистр
Мы используем собственный базовый класс в каждом API, который наследует System.Exception. Это генерируется только после регистрации исключения, и поэтому мы можем легко избежать повторной регистрации с помощью чего-то вроде:
try
{
// Do something.
}
catch (LoggedException)
{
// Already logged so just rethrow.
throw;
}
catch (Exception ex)
{
// TODO: Log exception.
throw new LoggedException("Failed doing something.", ex);
}
Это здорово, пока вы не захотите создать настраиваемое исключение, которое наследует другой системный тип исключения, например System.FormatException
Единственный способ обработать оба из них - иметь два настраиваемых базовых типа и дублировать каждый оператор catch.
Рефакторинг
Если инфраструктура .net просто искала что-то вроде System.IException, тогда вы могли бы просто создать собственный интерфейс исключений, такой как CompanyName.ILoggedException, наследующий System.IException, который реализует все ваши настраиваемые типы исключений. Поэтому ваш новый код catch выглядит примерно так:
try
{
// Do something.
}
catch (ILoggedException)
{
// Already logged so just rethrow.
throw;
}
catch (IException ex)
{
// TODO: Log exception.
throw new CustomException("Failed doing something.", ex);
}
Существует ли практическая причина, по которой эта структура реализована? Или это будет что-то запрашивать в будущей версии .Net framework?
Ответы
Ответ 1
Один возможный способ обхода, который еще не упоминался, - использовать методы расширения. Путем использования поля Exception.Data вы можете аккуратно обнаружить из одного блока catch, было ли существующее исключение уже зарегистрировано, и предпринимать действия по мере необходимости. Это позволит вам создать множество различных специфических для конкретной компании исключений (которые неявно уже зарегистрированы).
Необходимые методы расширения:
private const string ExceptionLoggedKey = "IsExceptionLogged";
public static bool IsLogged(this Exception exception)
{
if (exception.Data.Contains(ExceptionLoggedKey))
{
return (bool)exception.Data[ExceptionLoggedKey];
}
return false;
}
public static void SetLogged(this Exception exception)
{
exception.Data.Add(ExceptionLoggedKey, true);
}
Исключения компании принимают следующий формат, устанавливая флаг IsLogged в конструкторе:
public class CompanysLoggedException : InvalidOperationException //Could be any Exception
{
public CompanysLoggedException()
{
this.SetLogged();
}
}
попробуйте/поймайте использование:
try
{
throw new ArgumentException("There aren't any arguments.");
}
catch (Exception ex)
{
if (ex.IsLogged())
//Nothing additional to do - simply throw the exception
throw;
else
//TODO Log the exception
throw new CompanysLoggedException();
}
Я бы согласился, что это определенно не так просто, как возможность сопоставить исключение, основанное на реализованном интерфейсе, но я думаю, что шаблон довольно краткий и читабельный. Не забудьте добавить вызов SetLogged() в каждую новую компанию. Исключение определяется, однако, немного ошибкой.
Ответ 2
Как вы, наверное, знаете, что в случае базового класса у нас есть только одно наследование, но в случае интерфейса класс может реализовать множество интерфейсов, поэтому, если у вас есть что-то вроде:
class MyException : IExceptionB, IExceptionA
{
}
try
{
throw new MyException();
}
catch(IExceptionB b){}
catch(IExceptionA b){}
Теперь это создает двусмысленность в отношении того, как решить, какой обработчик catch вызывать, поскольку исключение реализует оба интерфейса, то есть в терминах иерархии оба уровня находятся на одном уровне, в отличие от базового класса, где не будет двух классов на одном уровне.
Код гипотетический, показывающий проблему в случае, если улов на основе интерфейса был разрешен
Ответ 3
Я действительно не знаю причину, но я предполагаю, что это касается производительности. В случае исключения каждый блок catch должен проверять, обрабатывает ли оно исключение. Если вы разрешаете только типы, это довольно просто в случае .Net, потому что у вас есть только одно наследование. Деревья наследования реализованных интерфейсов могут стать гораздо более сложными.
Ответ 4
В VB можно поймать интерфейсы, используя что-то вроде:
' Doesn't require external function, but will will require typecast
' if it needs to actually use Ex as IMyExceptionInterface
Catch Ex As Exception When TypeOf(Ex) Is IMyExceptionInterface
' Alternate form: requires pre-declared variable and a function:
' Function TryCastIntoSecondParam(Of TSource As Class, TDest As Class) _
' (ByVal Thing As TSource, ByRef Dest As TDest)
' Dim Result As TDest
' Result = TryCast(Thing, TDest)
' Dest = Result
' Return Dest IsNot Nothing
' End Function
Catch Ex As Exception When TryCastIntoSecondParam(Ex, myIMyException)
Если разработчики компилятора VB или С# захотели сделать это, они могли бы позволить использовать синтаксис
Catch Ex As IMyExceptionInterface ' vb
catch IExceptionInterface ex ' C#
и реализовать его с помощью вышеуказанного кода. Даже без поддержки компилятора, пользователи vb могут получить правильную семантику с вышеуказанным кодом. В С# было бы необходимо поймать исключение, проверить, является ли он желаемым типом, и ретронировать, если это не так; семантика которых отличается от семантики использования фильтра, чтобы избежать исключения в первую очередь. Обратите внимание, что для компилятора С# для реализации вышеприведенных конструктов ему необходимо будет использовать блоки фильтров, но ему не нужно было бы подвергать все возможности блоков фильтров программистам - то, что разработчики С# намеренно отказались делать.
Все, что было сказано, я подозреваю, что ответ на исходный вопрос может заключаться в том, что "дизайнеры не могли придумать какие-либо хорошие варианты использования для такой вещи", или это может быть "Интерфейсы требуют более сложного разрешения типа, чем делают классы, создавая возможность того, что простой акт принятия решения о том, следует ли поймать исключение, может завершиться неудачей, за исключением его собственных."
На самом деле мне не нравится использование типов классов в качестве средства решения каких исключений для улова, поскольку вопрос о том, следует ли поймать исключение, часто в значительной степени ортогонален вопросу о том, что именно произошло, чтобы вызвать его. Если попытка загрузить документ не удалась, меня почти не интересует вопрос о том, был ли какой-то аргумент вне диапазона или какой-то индекс был вне диапазона, так как я нахожусь в вопросе о том, могу ли я спокойно восстановиться после попытки притворяясь, что я никогда этого не делал. То, что действительно необходимо, - это "серьезность" меры для исключений, которые могут быть увеличены или уменьшены, поскольку исключение пузырится вверх по цепочке вызовов. Такое может быть несколько практичным в vb.net(у которого есть фильтры исключений), но, вероятно, не в С# (это не так), но в любом случае будет ограничено отсутствием поддержки внутри встроенных исключений.
Edit/Добавление
В проекте С# можно использовать фильтры исключений, если вы используете DLL, написанную в vb, для реализации оболочки try/filter/catch/finally, которая вызывает некоторых делегатов. К сожалению, использование такой DLL потребует некоторых компромиссов между эффективностью во время выполнения и четкостью кода. Я не думал о внедрении такой DLL для конкретной оптимизированной цели улавливания произвольного количества интерфейсов; Я не знаю, есть ли какие-либо преимущества для включения таких функций в DLL, а не для передачи им лямбда-выражения или анонимного метода для проверки того, следует ли исключать исключение.
BTW, еще одна особенность, которую может предоставить оболочка, которая в противном случае отсутствует в С#, - это возможность сообщать о состоянии с двойной ошибкой (исключение встречается в магистрали, а другое исключение возникает во время последующего "окончательного" блока) без необходимости уловить начальное исключение. Когда исключение происходит в блоке finally, это, как правило, большая проблема, чем исключение, которое встречается в основной строке, и поэтому не должно быть задушенным, но исключение "окончательного блока" для перколяции стека вызовов обычно уничтожает любые доказательства первоначального исключения. В то время как окружающий код, скорее всего, будет больше заинтересован в сбое очистки, чем исходное исключение, регистрация обоих исключений может быть гораздо более полезной, чем удушение оригинала.
Ответ 5
Исключение - это нечто большее, чем просто способ сообщить вашему коду, что что-то пошло не так. Он содержит все виды отладочной информации (трассировка стека, цель, внутреннее исключение, hresult, ведра Watson и т.д.), И это нужно собирать SOMEWHERE. Самое логичное и простое решение - позволить базовому классу собрать его.
Ответ 6
Это почти вы упомянули, без какого-то "кода сахара":
try
{
}
catch(LoggerException ex)
{
ex.WriteLog();
}
catch(Exception ex)
{
ILoggerException1 l1 = ex as ILoggerException1;
if (l1 != null)
{
l1.WriteLog1();
}
else
{
ILoggerException2 l2 = ex as ILoggerException2;
if (l2 != null)
{
l2.WriteLog2();
}
else
{
ILoggerException3 l3 = ex as ILoggerException3;
if (l3 != null)
{
l3.WriteLog3();
}
else
{
throw ex;
}
}
}
}
С поддержкой формы компилятора он записывается как:
try
{
}
catch(LoggerException ex)
{
ex.WriteLog();
}
// no more classes is allowed by the compiler be here, only interfaces in a tail catch recursion
catch(ILoggerException1 ex1)
{
ex1.WriteLog();
}
catch(ILoggerException2 ex2)
{
ex2.WriteLog();
}
catch(ILoggerException3 ex3)
{
ex3.WriteLog();
}
Ответ 7
Оказывается, есть интерфейс с момента создания рамки 2.0:
System.Runtime.InteropServices._Exception
Для получения дополнительной информации об этом интерфейсе перейдите здесь.
Я использовал обходной путь и использовал абстрактный класс и реализовал класс. Чувствую себя грязным для меня, но на самом деле это была более простая реализация для того, что мне нужно.
Ответ 8
В С# 6 введена фильтрация исключений, поэтому теперь вы можете запросить С# (это уже давно возможно в VB.Net).
Вот ваш код, реорганизованный для использования нового синтаксиса:
void Main()
{
try
{
// Do something.
throw new CustomLoggedException();
}
catch (Exception ex) when (ex is ILoggedException)
{
// Already logged so just rethrow.
throw;
}
catch (Exception ex)
{
// TODO: Log exception.
throw new Exception("Failed doing something.", ex);
}
}
Определения класса и интерфейса:
public interface ILoggedException
{
}
public class CustomLoggedException : Exception, ILoggedException
{
}