Когда правильно для конструктора выбрасывать исключение?

Когда правильно, чтобы конструктор выбрал исключение? (Или в случае Objective C: когда правильно для init'er возвращать нуль?)

Мне кажется, что конструктор должен потерпеть неудачу - и таким образом отказаться от создания объекта - если объект не завершен. I.e., конструктор должен иметь контракт со своим абонентом, чтобы обеспечить функциональный и рабочий объект, по которому методы можно назвать значимо? Это разумно?

Ответ 1

Задача конструктора - привести объект в полезное состояние. В этом есть две школы мысли.

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

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

Одноэтапные конструкторы должны бросать, если они не могут полностью инициализировать объект. Если объект не может быть инициализирован, его нельзя допускать к существованию, поэтому конструктор должен бросить.

Ответ 2

Эрик Липперт говорит, что есть 4 вида исключений.

  • Фатальные исключения - не ваша вина, вы не можете предотвратить их, и вы не можете разумно очистить их.
  • Исключения из головы - это ваша собственная ошибка, вы могли бы их предотвратить, и поэтому они являются ошибками в вашем коде.
  • Vexing исключения являются результатом неудачных дизайнерских решений. Vexing исключения создаются в совершенно не исключительных обстоятельствах, и поэтому должны быть пойманы и обрабатываться все время.
  • И, наконец, экзогенные исключения выглядят, как досадные исключения, за исключением того, что они не являются результатом неудачного выбора дизайна. Скорее, они являются результатом неопрятных внешних реалий, влияющих на вашу красивую, четкую программную логику.

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

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

Исключения Vexing (например, Int32.Parse()) не должны создаваться конструкторами, поскольку они не имеют исключительных обстоятельств.

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

Ссылочная ссылка: https://blogs.msdn.microsoft.com/ericlippert/2008/09/10/vexing-exceptions/

Ответ 3

ничего не может быть достигнуто путем разводки инициализации объекта из конструкции. RAII правильно, успешный вызов конструктору должен либо привести к полностью инициализированному живому объекту, либо он должен потерпеть неудачу, а ошибки ALL в любой точке любого пути кода должны всегда вызывать исключение. Вы ничего не получаете с помощью отдельного метода init(), кроме дополнительной сложности на некотором уровне. Контракт ctor должен быть либо он возвращает функциональный действительный объект, либо он очищается после себя и бросает.

Рассмотрим, если вы реализуете отдельный метод init, вам нужно еще вызвать его. Он все равно будет иметь возможность генерировать исключения, их все равно придется обрабатывать, и их практически всегда нужно вызывать сразу же после конструктора, но теперь у вас есть 4 возможных состояния объекта вместо 2 (IE, построенный, инициализированный, неинициализированный, и не удалось vs просто действительный и несуществующий).

В любом случае, я столкнулся в течение 25 лет с примерами разработки OO, где, похоже, отдельный метод инициализации "решает некоторые проблемы" - это недостатки дизайна. Если вам не нужен объект "СЕЙЧАС", вы не должны его строить сейчас, и если вам это нужно сейчас, вам нужно его инициализировать. KISS всегда должен следовать принципу, а также простому понятию, что поведение, состояние и API любого интерфейса должны отражать ЧТО, что делает объект, а не КАК он это делает, клиентский код не должен даже знать, что объект имеет какой-либо вид внутреннего состояния, которое требует инициализации, поэтому init после шаблона нарушает этот принцип.

Ответ 4

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

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

Ответ 5

Конструктор должен генерировать исключение, если он не может завершить построение указанного объекта.

Например, если конструктор должен выделять 1024 КБ оперативной памяти, и он не может этого сделать, он должен генерировать исключение, таким образом, вызывающий объект конструктора знает, что объект не готов к использованию, и там где-то ошибка должна быть исправлена.

Объекты, которые являются полуинициализированными и полумертвыми, просто вызывают проблемы и проблемы, поскольку для вызывающего не существует никакого способа узнать. Я предпочел бы, чтобы мой конструктор ошибся, когда что-то пошло не так, как нужно полагаться на программирование для запуска вызова функции isOK(), которая возвращает true или false.

Ответ 6

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

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

Ответ 7

Для конструктора разумно исключить исключение, если оно правильно очистится. Если вы выполните парадокс RAII (инициализация ресурсов - это инициализация), то довольно часто используется конструктором для создания значимых Работа; хорошо написанный конструктор, в свою очередь, очистит сам по себе, если он не может быть полностью инициализирован.

Ответ 8

Насколько я могу судить, никто не представляет довольно очевидное решение, которое воплощает лучшее как одностадийное, так и двухступенчатое построение.

Примечание: Этот ответ предполагает С#, но принципы могут применяться на большинстве языков.

Во-первых, преимущества обоих:

One-Stage

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

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(dateOfBirth));
        }

        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }
}

Двухэтапный метод проверки

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

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    public Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public void Validate()
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(Name));
        }

        if (DateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }
    }
}

Одноступенчатый с помощью частного конструктора

Итак, как мы можем сохранить исключения из наших конструкторов и не позволить себе выполнять распределение кучи на объектах, которые будут немедленно отброшены? Это довольно просто: мы создаем конструктор private и создаем экземпляры через статический метод, предназначенный для выполнения экземпляра, и, следовательно, для распределения кучи, только после проверки.

public class Person
{
    public string Name { get; }
    public DateTime DateOfBirth { get; }

    private Person(string name, DateTime dateOfBirth)
    {
        this.Name = name;
        this.DateOfBirth = dateOfBirth;
    }

    public static Person Create(
        string name,
        DateTime dateOfBirth)
    {
        if (string.IsNullOrWhitespace(Name))
        {
            throw new ArgumentException(nameof(name));
        }

        if (dateOfBirth > DateTime.UtcNow) // side note: bad use of DateTime.UtcNow
        {
            throw new ArgumentOutOfRangeException(nameof(DateOfBirth));
        }

        return new Person(name, dateOfBirth);
    }
}

Async Single-Stage через частный конструктор

Помимо вышеупомянутых преимуществ по профилактике и распределению кучи, предыдущая методология предоставляет нам еще одно отличное преимущество: поддержка async. Это пригодится при работе с многоэтапной аутентификацией, например, когда вам нужно извлечь токен-носитель перед использованием вашего API. Таким образом, вы не получаете недействительный "клиент" API-интерфейса, и вместо этого вы можете просто повторно создать клиент API, если при попытке выполнить запрос вы получите ошибку авторизации.

public class RestApiClient
{
    public RestApiClient(HttpClient httpClient)
    {
        this.httpClient = new httpClient;
    }

    public async Task<RestApiClient> Create(string username, string password)
    {
        if (username == null)
        {
            throw new ArgumentNullException(nameof(username));
        }

        if (password == null)
        {
            throw new ArgumentNullException(nameof(password));
        }

        var basicAuthBytes = Encoding.ASCII.GetBytes($"{username}:{password}");
        var basicAuthValue = Convert.ToBase64String(basicAuthBytes);

        var authenticationHttpClient = new HttpClient
        {
            BaseUri = new Uri("https://auth.example.io"),
            DefaultRequestHeaders = {
                Authentication = new AuthenticationHeaderValue("Basic", basicAuthValue)
            }
        };

        using (authenticationHttpClient)
        {
            var response = await httpClient.GetAsync("login");
            var content = response.Content.ReadAsStringAsync();
            var authToken = content;
            var restApiHttpClient = new HttpClient
            {
                BaseUri = new Uri("https://api.example.io"), // notice this differs from the auth uri
                DefaultRequestHeaders = {
                    Authentication = new AuthenticationHeaderValue("Bearer", authToken)
                }
            };

            return new RestApiClient(restApiHttpClient);
        }
    }
}

Недостатки этого метода в моем опыте мало.

Как правило, использование этой методологии означает, что вы больше не можете использовать класс как DTO, потому что десериализация объекта без конструктора с открытым положением в лучшем случае сложна. Однако, если вы использовали объект в качестве DTO, вы не должны действительно проверять сам объект, а скорее неактивны значения объекта, поскольку вы пытаетесь их использовать, поскольку технически значения не являются "недействительными" в отношении к DTO.

Это также означает, что вы создадите методы или классы factory, когда вам нужно разрешить контейнеру IOC создавать объект, поскольку в противном случае контейнер не будет знать, как создать экземпляр объекта. Однако во многих случаях методы factory оказываются одним из методов Create.

Ответ 9

См. разделы часто задаваемых вопросов С++ 17.2 и 17.4.

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

Ответ 10

Если вы пишете UI-Controls (ASPX, WinForms, WPF,...), вам следует избегать бросать исключения в конструкторе, потому что дизайнер (Visual Studio) не может справиться с ними при создании своих элементов управления. Знайте свой жизненный цикл управления (управляйте событиями) и используйте ленивую инициализацию, где это возможно.

Ответ 11

Обратите внимание, что если вы выбрали исключение в инициализаторе, вы закончите утечку, если какой-либо код использует шаблон [[[MyObj alloc] init] autorelease], поскольку исключение пропустит авторекламу.

Смотрите этот вопрос:

Как предотвратить утечку при создании исключения в init?

Ответ 12

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

На практике вам может быть очень осторожно. Помните, что в С++ деструктор не будет вызываться, поэтому, если вы выбросите его после выделения ресурсов, вам необходимо проявить большую осторожность, чтобы правильно его обработать!

На этой странице подробно обсуждается ситуация на С++.

Ответ 13

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

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

Ответ 14

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

Это не единственный вариант: он может завершить конструктор и построить объект, но с помощью метода isCoherent(), возвращающего false, чтобы иметь возможность сигнализировать некогерентное состояние (что может быть предпочтительным в определенном случае, чтобы избежать жестокого прерывания рабочего процесса из-за исключения)
Предупреждение: как сказал EricSchaefer в своем комментарии, это может принести некоторую сложность для модульного тестирования (бросок может увеличить циклическую сложность функции из-за условия, которое вызывает его)

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

Ответ 15

Бросок исключения во время строительства - отличный способ сделать ваш код более сложным. Вещи, которые кажутся простыми, внезапно становятся тяжелыми. Например, скажем, у вас есть стек. Как вы поместите стек и вернете верхнее значение? Ну, если объекты в стеке могут вставлять свои конструкторы (создавая временное, чтобы вернуться к вызывающему), вы не можете гарантировать, что вы не потеряете данные (уменьшите указатель стека, постройте возвращаемое значение, используя конструктор копирования значения в стек, который бросает, и теперь есть стек, который просто потерял элемент)! Вот почему std:: stack:: pop не возвращает значение, и вам нужно вызвать std:: stack:: top.

Эта проблема хорошо описана здесь, проверьте пункт 10, написав безопасный код.

Ответ 16

Обычный договор в OO состоит в том, что фактически действуют функции объектов.

Итак, как средство, чтобы никогда не возвращать объект-зомби, создайте конструктор /init.

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

Я впервые создал зомби в Objective C, много лет назад.

Как и все эмпирические правила, существует "исключение".

Вполне возможно, что у конкретного интерфейса может быть контракт, в котором говорится, что существует метод "инициализировать", который разрешен для исключения. То, что объект, реализующий этот интерфейс, может некорректно реагировать на любые вызовы, кроме средств определения свойств, до тех пор, пока не будет вызвана инициализация. Я использовал это для драйверов устройств в операционной системе OO во время процесса загрузки и был работоспособен.

В общем, вам не нужны объекты зомби. В таких языках, как Smalltalk с стать, все становится немного шипучим, но чрезмерное использование становится плохим. Становится возможным изменение объекта в другой объект in-situ, поэтому нет необходимости в оболочке-оболочке (Advanced С++) или шаблоне стратегии (GOF).

Ответ 17

Я не могу обратиться к передовой практике в Objective-C, но в С++ это прекрасно для конструктора, который генерирует исключение. Тем более, что нет другого способа гарантировать, что исключительное условие, возникающее при построении, сообщается, не прибегая к вызову метода isOK().

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

Ответ 18

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

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

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

  • Ваша языковая поддержка исключений не очень хороша.
  • У вас есть актуальная причина дизайна, чтобы по-прежнему использовать new и delete
  • Ваша инициализация требует интенсивной работы процессора и должна выполняться асинхронно с потоком, создавшим объект.
  • Вы создаете библиотеку DLL, которая может генерировать исключения вне ее интерфейса для приложения, использующего другой язык. В этом случае проблема может быть не в том, чтобы не выдавать исключения, а в том, чтобы они перехватывались перед общедоступным интерфейсом. (Вы можете ловить исключения C++ в С#, но есть обходы, через которые можно перепрыгнуть.)
  • Статические конструкторы (С#)

Ответ 19

У вопроса ОП есть тег "независимый от языка"... на этот вопрос нельзя ответить одинаково для всех языков/ситуаций.

В следующей примере С# иерархия классов IDisposeable.Dispose конструктор класса B, пропуская немедленный вызов класса A IDisposeable.Dispose при выходе из основного using, пропуская явное удаление ресурсов класса A.

Если, например, класс A создал Socket при создании, подключенный к сетевому ресурсу, это, вероятно, все равно будет иметь место после using блока (относительно скрытая аномалия).

class A : IDisposable
{
    public A()
    {
        Console.WriteLine("Initialize A resources.");
    }

    public void Dispose()
    {
        Console.WriteLine("Dispose A resources.");
    }
}

class B : A, IDisposable
{
    public B()
    {
        Console.WriteLine("Initialize B resources.");
        throw new Exception("B construction failure: B can cleanup anything before throwing so this is not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose B resources.");
        base.Dispose();
    }
}
class C : B, IDisposable
{
    public C()
    {
        Console.WriteLine("Initialize C resources. Not called because B throws during construction. C resources not a worry.");
    }

    public new void Dispose()
    {
        Console.WriteLine("Dispose C resources.");
        base.Dispose();
    }
}


class Program
{
    static void Main(string[] args)
    {
        try
        {
            using (C c = new C())
            {
            }
        }
        catch
        {           
        }

        // Resource allocated by c "A" not explicitly disposed.
    }
}

Ответ 20

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

Ответ 21

Для меня это несколько философское дизайнерское решение.

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

Некоторые другие подходы - это метод init(), который связан с некоторыми проблемами. Одним из них является обеспечение вызова init().

Вариант использует ленивый подход для автоматического вызова init() при первом вызове accessor/mutator, но для этого требуется, чтобы любой потенциальный вызывающий пользователь беспокоился о том, что объект действителен. (В отличие от "он существует, следовательно, это действительная философия" ).

Я видел различные предлагаемые шаблоны проектирования для решения этой проблемы. Например, чтобы создать исходный объект через ctor, но нужно вызвать init(), чтобы получить доступ к содержащемуся инициализированному объекту с помощью accesors/mutators.

Каждый подход имеет свои взлеты и падения; Я использовал все это успешно. Если вы не делаете готовые к использованию объекты с момента их создания, я рекомендую использовать большую дозу утверждений или исключений, чтобы убедиться, что пользователи не взаимодействуют до init().

Добавление

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

Ответ 22

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

http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaFundamentals/CocoaObjects/chapter_3_section_6.html

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

Ответ 23

Используя фабрики или методы factory для создания всех объектов, вы можете избежать недопустимых объектов, не бросая исключения из конструкторов. Метод создания должен возвращать запрошенный объект, если он может его создать, или null, если он не является. Вы теряете немного гибкости при обработке ошибок конструкции в пользователе класса, потому что возврат null не говорит вам, что пошло не так в создании объекта. Но он также избегает добавления сложности нескольких обработчиков исключений при каждом запросе объекта и риска исключения исключений, которые вы не должны обрабатывать.

Ответ 24

Лучший совет, который я видел об исключениях, - это исключение, если и только если альтернативой является неспособность выполнить условие сообщения или поддерживать инвариант.

Этот совет заменяет нечеткое субъективное решение (это хорошая идея) с техническим, точным вопросом, основанным на дизайнерских решениях (инвариантных и почтовых условиях), которые вы уже должны были сделать.

Конструкторы - это особый, но не особый случай для этого совета. Итак, возникает вопрос, какие инварианты должны иметь класс? Адвокаты отдельного метода инициализации, которые должны быть вызваны после построения, предполагают, что класс имеет два или более режима работы, с неживым режимом после построения и по меньшей мере одним режимом готовности, введенным после инициализации. Это дополнительное усложнение, но приемлемо, если класс имеет несколько режимов работы. Трудно понять, как это осложнение стоит, если класс иначе не имел бы рабочих режимов.

Обратите внимание, что нажатие, заданное в отдельный метод инициализации, не позволяет избежать исключений. Исключения, которые мог бы вызвать ваш конструктор, теперь будут вызваны методом инициализации. Все полезные методы вашего класса должны будут генерировать исключения, если они вызываются для неинициализированного объекта.

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

Ответ 25

ctors не должны делать никаких "умных" вещей, поэтому бросать исключение в любом случае не нужно. Используйте метод Init() или Setup(), если вы хотите выполнить более сложную настройку объекта.