Когда для конструктора правильно выбрасывать исключение? - PullRequest
195 голосов
/ 17 сентября 2008

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

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

Ответы [ 25 ]

256 голосов
/ 17 сентября 2008

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

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

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

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

55 голосов
/ 17 сентября 2008

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

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

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

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

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

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

30 голосов
/ 17 сентября 2008

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

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

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

6 голосов
/ 17 сентября 2008

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

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

5 голосов
/ 17 сентября 2008

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

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

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

4 голосов
/ 20 марта 2017

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

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

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

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

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

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

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 голосов
/ 17 сентября 2008

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

4 голосов
/ 17 сентября 2008

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

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

3 голосов
/ 17 сентября 2008

См. Разделы часто задаваемых вопросов по C ++ 17,2 и 17,4 .

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

3 голосов
/ 15 февраля 2011

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

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

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

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