Сколько работы нужно сделать в конструкторе?

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

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

Какое изящное решение этой проблемы?

конструктор похож на мастер настройки приложения, в котором вы только настраиваете.

Ramiz Uddin 26.05.2011 16:00

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

Ramiz Uddin 26.05.2011 16:15
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Принцип подстановки Лискова
Принцип подстановки Лискова
Принцип подстановки Лискова (LSP) - это принцип объектно-ориентированного программирования, который гласит, что объекты суперкласса должны иметь...
51
2
14 584
18
Перейти к ответу Данный вопрос помечен как решенный

Ответы 18

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

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

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

Greg Eremeev 04.01.2021 16:56

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

В вашем объекте структуры каталогов: я недавно реализовал браузер samba (общие ресурсы Windows) для своего HTPC, и, поскольку это было невероятно медленно, я решил фактически инициализировать каталог только при касании. например Сначала дерево будет состоять только из списка машин, затем всякий раз, когда вы переходите в каталог, система автоматически инициализирует дерево с этой машины и получает список каталогов на один уровень глубже, и так далее.

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

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

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

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

Foredecker 16.11.2008 20:19

Foredecker: Речь идет о семантике класса: MutexLocker может ждать произвольно долгое время, пока ресурс не станет доступным, поскольку это то, что составляет инвариант MutexLocker (время вызова деструктора = ресурс принадлежит).

Johannes Schaub - litb 16.11.2008 20:25

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

Teej 19.11.2008 04:44

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

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

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

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

Существует огромная разница между «предварительной оптимизацией» и архитектурным или дизайнерским выбором, который является ошибкой производительности. Производительность - это такая же особенность, как и привлекательный пользовательский интерфейс, правильность, ремонтопригодность и надежность. Архитектор и дизайн для него.

Foredecker 16.11.2008 20:17

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

KeithB 17.11.2008 18:23

Подвести итоги:

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

  • Ваш выбор инвариантов может повлиять на ваших клиентов. (Обещает ли объект быть готовым к доступу в любое время? Или только в определенных состояниях?) Конструктор, который позаботится обо всех предварительных настройках, может упростить жизнь для клиентов класса.

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

  • Для систем, предполагающих взаимодействие с пользователем, длительные методы любого типа могут привести к плохой реакции, и их следует избегать.

  • Отсрочка вычислений до тех пор, пока конструктор не станет эффективной оптимизацией; может оказаться ненужным выполнять всю работу. Это зависит от приложения и не должно определяться преждевременно.

  • В общем, это зависит от обстоятельств.

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

Самая важная задача конструктора - дать объекту начальное допустимое состояние. На мой взгляд, наиболее важным ожиданием от конструктора является то, что конструктор не должен иметь ПОБОЧНЫХ ЭФФЕКТОВ.

«Без побочных эффектов» - это слишком сильно. Например: конструктор семафора может блокировать другие экземпляры от завершения их построения. Этот побочный эффект - его работа. Другой конструктор может автоматически присвоить объекту уникальный идентификатор из увеличивающегося счетчика (глобального или принадлежащего классу). Это тоже законный побочный эффект.

Oddthinking 27.12.2009 03:18

согласен с @Oddthinking. Другой пример: если у вас есть конструктор для чего-то вроде ofstream, ожидаете ли вы «БЕЗ ПОБОЧНЫХ ЭФФЕКТОВ»?

João Portela 05.07.2010 15:26

Я не согласен с ответом. Самая важная задача конструктора - предоставить мне пригодный для использования объект (RAII).

Emily L. 27.09.2013 17:49

Столько, сколько нужно, и не более того.

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

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

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

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

Другой способ реализовать это - сохранить значения в текстовом файле и использовать N в качестве основы для смещения в файле, из которого будет извлекаться значение. В этом случае конструктор откроет файл, а деструктор закроет файл, а метод выполнит своего рода fseek / fread и вернет значение.

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

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

Убедитесь, что ctor не делает ничего, что могло бы вызвать исключение.

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

EricSchaefer 16.11.2008 19:33

При использовании C++ с моделью RAII ожидается, что ctors будут генерировать исключения (например, если файл не существует). Обычно именно dtors не должны вызывать исключения. C++ определяет порядок конструкторов, поэтому он четко определен.

Procedural Throwback 16.11.2008 20:10

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

Aydya 16.11.2008 20:54

Так что мой ответ неверен для одного шаблона и все еще верен для остальных. Создание исключений в ctors обычно не является хорошей идеей и считается плохой практикой.

EricSchaefer 17.11.2008 00:35

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

MSalters 17.11.2008 13:02

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

KeithB 18.11.2008 20:27

Ctor предназначен для инициализации вашего объекта. Выделите память для ваших структур, установите начальные значения и тому подобное. Нет особых причин для исключения исключения, кроме OOM. Считается плохой практикой помещать в ctor какую-либо логику.

EricSchaefer 22.11.2008 12:32

Эрик Шефер - не могли бы вы предоставить свои источники для вашего комментария «считается плохой практикой»?

Len Holgate 21.12.2009 14:08

@Len Holgate: grassutter.wordpress.com/2008/07/25/… Если ваш ctor выполняет больше, чем просто инициализацию, возможно, вы не сможете создать экземпляр класса изолированно (например, подумайте о модульном тестировании и возможности повторного использования).

EricSchaefer 21.12.2009 21:49

Я согласен с тем, что долго работающие конструкторы по своей сути неплохи. Но я бы сказал, что ты почти всегда поступаешь неправильно. Мой совет аналогичен совету Хьюго, Рича и Литба:

  1. сведите к минимуму работу, которую вы выполняете в конструкторах - сосредоточьтесь на состоянии инициализации.
  2. Не бросайте из конструкторов, если вы не можете этого избежать. Я пытаюсь бросить только std :: bad_alloc.
  3. Не вызывайте API-интерфейсы ОС или библиотек, если не знаете, что они делают - большинство из них может блокировать. Они будут работать быстро на вашем компьютере для разработчиков и на тестовых машинах, но в полевых условиях они могут быть заблокированы на длительное время, поскольку система занята чем-то другим.
  4. Никогда, никогда не выполняйте операции ввода-вывода в конструкторе - любого рода. Ввод-вывод обычно подвержен всевозможным очень долгим задержкам (от 100 миллисекунд до секунд). Ввод / вывод включает
    • Дисковый ввод / вывод
    • Все, что использует сеть (даже косвенно). Помните, что большинство ресурсов может быть отключено.

I/O problem example: Many hard disks have a problem where they get into a state where they do not service reads or writes for 100's or even thousands of milliseconds. The first and generation solid state drives do this often. The user has now way of knowing that your program jus hung for a bit - they just think it is your buggy software.

Конечно, вредность долго работающего конструктора зависит от двух вещей:

  1. Что значит "долго"
  2. Как часто в данный период строятся объекты с «длинными» конструкторами.

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

Частота - важный фактор, процессор в 500 мкс - не проблема, если вы создаете только несколько из них: но создание миллиона из них может создать значительную проблему с производительностью.

Давайте поговорим о вашем примере: заполнение дерева объектов каталога внутри объекта "class Directory". (обратите внимание, я предполагаю, что это программа с графическим интерфейсом пользователя). Здесь продолжительность вашего CTOR не зависит от кода, который вы пишете - его ответчик от времени, необходимого для перечисления произвольно большого дерева каталогов. Это плохо для локального жесткого диска. Еще более проблематично это при удаленном (сетевом) возрождении.

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

Зависание пользовательского интерфейса может заставить людей по-настоящему ненавидеть ваше программное обеспечение.

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

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

KeithB 18.11.2008 20:31

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

Foredecker 22.11.2008 08:00

Массивы объектов всегда будут использовать конструктор по умолчанию (без аргументов). Нет никакого способа обойти это.

Существуют «специальные» конструкторы: конструктор копирования и оператор = ().

Конструкторов может быть много! Или в конце концов перейдем к большому количеству конструкторов. Время от времени Билл в стране ла-ла хочет новый конструктор с числами с плавающей запятой, а не с удвоениями, чтобы сохранить эти 4 паршивых байта. (Купите немного RAM Bill!)

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

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

.

Все это создает много проблем, когда в конструкторе присутствует значимая логика. (Или хотя бы дублирование кода.)

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

В зависимости от обстоятельств initialize () может быть закрытым. Или он может быть общедоступным и поддерживать множественные вызовы (например, повторную инициализацию).

.

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

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

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

RAII является основой управления ресурсами C++, поэтому получите необходимые ресурсы в конструкторе и освободите их в деструкторе.

Это когда вы устанавливаете свои инварианты класса. Если на это нужно время, на это нужно время. Чем меньше у вас конструкций типа «если X существует, сделайте Y», тем проще будет спроектировать остальную часть класса. Позже, если профилирование покажет, что это проблема, рассмотрите возможность оптимизации, например, отложенной инициализации (получение ресурсов, когда они вам впервые понадобятся).

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

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

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

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

mabraham 29.11.2016 19:33

Отличный вопрос: приведенный вами пример, в котором объект «Каталог» имеет ссылки на другие объекты «Каталог», также является отличным примером.

В этом конкретном случае я бы переместил код для создания подчиненных объектов из конструктора (или, возможно, сделал бы первый уровень [непосредственные дочерние элементы], как рекомендует другой пост здесь), и имел бы отдельный механизм «инициализации» или «сборки»).

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

Ответ принят как подходящий

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

Например, предположим, что мне нужно отобразить следующий класс компании в подробном представлении:

public class Company
{
    public int Company_ID { get; set; }
    public string CompanyName { get; set; }
    public Address MailingAddress { get; set; }
    public Phones CompanyPhones { get; set; }
    public Contact ContactPerson { get; set; }
}

Поскольку я хочу отображать всю имеющуюся у меня информацию о компании в подробном представлении, мой конструктор будет содержать весь код, необходимый для заполнения каждого свойства. Учитывая, что это сложный тип, конструктор Company также инициирует выполнение конструктора Address, Phones и Contact.

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

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

Другие вопросы по теме