it-swarm-ru.tech

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

Когда для конструктора правильно выбрасывать исключение? (Или в случае с Целью C: когда правомерно вернуть ноль?)

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

195
Mark R Lindsey

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

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

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

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

256
Sebastian Redl

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

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

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

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

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

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

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

56
Jacob Krall

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

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

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

30
Alhazred

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

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

6
Michael L Perry

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

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

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

5
Denice

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

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

4
blowdart

Разумно, чтобы конструктор выбрасывал исключение, если он правильно очищается. Если вы следуете парадигме RAII (Resource Acquisition Is Initialization), тогда довольно часто конструктор делает значимые действия Работа; хорошо написанный конструктор, в свою очередь, очистит себя, если не может быть полностью инициализирован.

4
Matt Dillard

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

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

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

Один этап

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

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

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

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

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 через приватный конструктор

Помимо вышеупомянутых преимуществ валидации и предотвращения выделения кучи, предыдущая методология дает нам еще одно замечательное преимущество: асинхронная поддержка. Это удобно при работе с многоэтапной аутентификацией, например, когда вам нужно получить токен-носитель перед использованием вашего 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.

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

4
cwharris

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

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

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

3
stevex

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

3
Nick

Смотрите разделы C++ FAQ 17.2 и 17.4 .

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

3
moonshadow

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

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

2
user14070

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

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

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

2
Luke Halliwell

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

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

1
mlbrock

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

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

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

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

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

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

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

1
VonC

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

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

1
Don Neufeld

Обычный контракт в OO заключается в том, что методы объекта действительно работают.

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

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

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

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

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

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

1
Tim Williscroft

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

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

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

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

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

class B : A, IDisposable
{
    public B()
    {
        Console.WriteLine("Initialize B's 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's resources.");
        base.Dispose();
    }
}
class C : B, IDisposable
{
    public C()
    {
        Console.WriteLine("Initialize C's resources. Not called because B throws during construction. C's resources not a worry.");
    }

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


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

        // Resource's allocated by c's "A" not explicitly disposed.
    }
}
1
Ashley

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

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

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

0
Scott Swezey

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

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

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

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

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

0
Raedwald

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

0
scubabbl

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

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

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

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

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

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

Приложение

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

0
nsanders

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

0
Tegan Mulholland