Почему исключения .NET не работают с интерфейсом, а не с базовым классом? - PullRequest
17 голосов
/ 15 сентября 2011

. Реализация try-catch платформы .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?

Ответы [ 8 ]

17 голосов
/ 15 сентября 2011

Как вы, наверное, знаете, в случае базового класса у нас есть только одно наследование, но в случае интерфейса класс может реализовать много интерфейсов, поэтому, если у вас есть что-то вроде:

class MyException : IExceptionB, IExceptionA
{
}

try
{
 throw new MyException();
}
catch(IExceptionB b){}
catch(IExceptionA b){}

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

Код является гипотетическим, показывая проблему в случае, если разрешен интерфейсный улов

7 голосов
/ 29 ноября 2011

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

Необходимые методы расширения:

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 () в каждое новое определенное исключение компании является недостатком.

5 голосов
/ 15 сентября 2011

Я действительно не знаю причину, но я предполагаю, что речь идет о производительности. В случае исключения каждый блок catch должен проверять, обрабатывает ли он исключение. Если вы разрешаете только типы, это довольно просто в случае .Net, потому что у вас есть только одно наследование. Деревья наследования реализованных интерфейсов могут стать намного более сложными.

3 голосов
/ 28 октября 2016

В C6 введено фильтрация исключений , поэтому то, что вы просите, теперь возможно в C # ( это давно возможно в VB.Net ). Теперь мы можем использовать ключевое слово when.

Вот ваш код с рефакторингом для использования нового синтаксиса:

try
{
      ...
}
catch (Exception ex) when (!(ex is ILoggedException))
{
    // TODO: Log exception.
    throw new Exception("Failed doing something.", ex);
}

Обратите внимание, что нам больше не нужен первый блок catch, так как он по сути был просто фильтром, и все, что он делал, это throw.

Определения классов и интерфейсов:

public interface ILoggedException { }

public class CustomLoggedException : Exception, ILoggedException { ... }
2 голосов
/ 19 сентября 2011

В 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 или C # хотят сделать это, они могут позволить использовать синтаксис

  Catch Ex As IMyExceptionInterface  ' vb
  catch IExceptionInterface ex       ' C#

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

Все, что было сказано, я подозреваю, что ответ на первоначальный вопрос может быть следующим: «дизайнеры не могли придумать ни одного хорошего варианта использования для такой вещи», или это может быть «Интерфейсы требуют более сложного разрешения типов, чем классы, создавая возможность того, что простое действие принятия решения о том, следует ли ловить исключение, может потерпеть неудачу за исключением своего собственного. "

На самом деле, мне не нравится использование типов классов в качестве средства решения, какие исключения отлавливать, поскольку вопрос о том, отлавливать ли исключение, часто в значительной степени ортогональн к вопросу о том, что именно произошло, вызвавшее его. Если попытка загрузить документ не удалась, меня почти не интересует вопрос о том, был ли какой-либо аргумент вне допустимого диапазона, или какой-либо индекс вышел за пределы допустимого диапазона, поскольку меня интересует, могу ли я безопасно восстановиться после попытки притворяясь, что я никогда этого не сделал. Что действительно необходимо, так это мера «серьезности» для исключений, которая может увеличиваться или уменьшаться по мере того, как исключение всплывает в цепочке вызовов. Такая вещь может быть несколько практичной в vb.net (которая имеет фильтры исключений), но, вероятно, не в C # (который не имеет), но в любом случае будет ограничена отсутствием какой-либо поддержки во встроенных исключениях.

Редактировать / Добавление

Можно использовать фильтры исключений в проекте C #, если кто-либо использует DLL, написанную в vb, для реализации оболочки try / filter / catch / finally, которая вызывает некоторые делегаты. К сожалению, использование такой DLL потребует некоторых компромиссов между эффективностью выполнения и удобочитаемостью кода. Я не думал о реализации такой DLL для конкретной оптимизированной цели - перехватить произвольное количество интерфейсов; Я не знаю, будет ли какое-либо преимущество в том, чтобы включать такую ​​функциональность в DLL, вместо того, чтобы передавать ей лямбда-выражение или анонимный метод, чтобы проверить, должно ли быть поймано исключение.

Кстати, еще одна функция, которую может предоставить оболочка, которая в C # отсутствует, - это возможность сообщать о состоянии двойной ошибки (исключение происходит в основной линии, а другое исключение возникает во время последующего блока finally) без необходимости поймать начальное исключение. Когда исключение возникает в блоке finally, это, как правило, более серьезная проблема, чем исключение, которое возникает в основной строке, и поэтому его не следует подавлять, но если исключение «окончательного блока» просачивается вверх по стеку вызовов, как правило, уничтожаются любые доказательства. оригинального исключения. Хотя окружающий код, скорее всего, будет более заинтересован в сбое очистки, чем исходное исключение, регистрация обоих исключений может оказаться гораздо более полезной, чем удушение оригинала.

1 голос
/ 15 сентября 2011

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

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;
            }
        }
    }
}

При поддержке компилятора это можно записать так:

1 голос
/ 15 сентября 2011

Исключение - это гораздо больше, чем просто способ сообщить вашему коду, что что-то пошло не так. Он содержит все виды отладочной информации (трассировка стека, цель, внутреннее исключение, hresult, корзины Ватсона и т. Д.), И его нужно собирать НЕКОТОРЫМ Наиболее логичное и простое решение - позволить базовому классу собрать его.

0 голосов
/ 14 августа 2015

Оказывается, существует интерфейс со времени фреймворка 2.0:

System.Runtime.InteropServices._Exception

Для получения дополнительной информации об этом интерфейсе перейдите здесь .

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...