Почему бы мне не обернуть каждый блок в «попробовать» - «поймать»?


430

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

Я только что написал: « Вы должны ВСЕГДА оборачивать вызовы, которые могут бросать попытки, ловить блоки. на этот вопрос и мне сказали, что это «удивительно плохой совет» - я хотел бы понять, почему.


4
Если бы я знал, что метод вызовет исключение, я бы исправил его в первую очередь. Это потому, что я не знаю, где и почему код будет генерировать исключения, что я ловлю их в источнике (каждый метод - один универсальный try-catch). Например, команда дала мне кодовую базу с более ранней базой данных. Многие столбцы отсутствовали и перехватывались только после добавления try catch в слой SQL. Во-вторых, я могу записать имя метода и сообщение для автономной отладки, иначе я не знаю, откуда он возник.
Чакра

Ответы:


340

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

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

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


12
Стоит также отметить, что существуют затраты (с точки зрения генерируемого кода) на tryблоки. Хорошая дискуссия Скотта Мейерса «Более эффективный C ++».
Ник Мейер,

28
На самом деле tryблоки бесплатны в любом современном C-компиляторе, эта информация датируется Ником. Я также не согласен с наличием обработчика исключений верхнего уровня, потому что вы теряете информацию о местонахождении (фактическое место, где инструкция не выполнена).
Блинди

31
@Blindly: верхний обработчик исключений предназначен не для обработки исключения, а для того, чтобы громко выкрикнуть, что было необработанное исключение, передать его сообщение и завершить программу изящным способом (вернуть 1 вместо вызова terminate) , Это скорее механизм безопасности. Кроме того, try/catchболее или менее бесплатны, когда нет никаких исключений. Когда есть один размножающийся, он потребляет время каждый раз, когда его бросают и ловят, так что цепочка try/catchэтого единственного повторного броска не является бесплатной.
Матье М.

17
Я не согласен с тем, что вы должны всегда терпеть неудачу при необъяснимом исключении. Современный дизайн программного обеспечения очень разделен, так почему вы должны наказать остальную часть приложения (и, что более важно, пользователя!) Только потому, что была одна ошибка? Отказ от абсолютно последнего, что вы хотите сделать, по крайней мере, попытаться дать пользователю небольшое окно кода, которое позволит ему сохранить работу, даже если к остальной части приложения нет доступа.
Кендалл Хельмштеттер Гелнер

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

136

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

Есть несколько связанных запахов кода, которые вы определенно хотите избежать, в дополнение к запаху "лови все везде" .

  1. «catch, log, rethrow» : если вы хотите вести журнал на основе областей , то напишите класс, который выдает оператор log в своем деструкторе, когда стек разворачивается из-за исключения (ala std::uncaught_exception()). Все, что вам нужно сделать, это объявить экземпляр журнала в интересующей вас области, и, вуаля, у вас есть журналирование и нет ненужных try/ catchлогики.

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

  3. «лови, убирай, отбрасывай» : это одна из моих любимых мозолей. Если вы видите много это, то вам следует обратиться Получение ресурса есть инициализация методы и поместить часть очистки в деструкторе дворника экземпляра объекта.

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


6
№ 1 является новым для меня. +1 за это. Кроме того, я хотел бы отметить распространенное исключение для # 2, которое заключается в том, что если вы часто разрабатываете библиотеку, вам нужно будет преобразовать внутренние исключения во что-то, заданное интерфейсом библиотеки, чтобы уменьшить связь (это может быть то, что вы имеете в виду "федеративным решением", но я не знаком с этим термином).
rmeador

3
В основном то, что вы сказали: parashift.com/c++-faq-lite/exceptions.html#faq-17.13
Björn Pollex,

1
№2, где он не является запахом кода, но имеет смысл, может быть улучшен путем сохранения старого исключения как вложенного.
Дедупликатор

1
Что касается # 1: std :: uncaught_exception () говорит вам, что в полете есть необработанное исключение, но AFAIK только предложение catch () позволяет вам определить, что это за исключение на самом деле. Таким образом, хотя вы можете регистрировать тот факт, что вы выходите из области действия из-за неперехваченного исключения, только включающая попытка / отловка позволяет регистрировать любые детали. Правильный?
Джереми

@ Джереми - ты прав. Я обычно регистрирую детали исключения, когда обрабатываю исключение. Отслеживание промежуточных кадров очень полезно. Как правило, вам необходимо регистрировать идентификатор потока или некоторый идентифицирующий контекст, а также сопоставлять строки журнала. Я использовал Loggerкласс, аналогичный тому, log4j.Loggerкоторый включает идентификатор потока в каждой строке журнала, и выдавал предупреждение в деструкторе, когда исключение было активным.
Д.Шоули

48

Потому что следующий вопрос: «Я поймал исключение, что мне делать дальше?» Что ты бы сделал? Если вы ничего не делаете - это скрытие ошибок, и программа может «просто не работать» без какой-либо возможности выяснить, что произошло. Вы должны понимать, что именно вы будете делать, как только вы поймали исключение, и поймать только, если вы знаете.


29

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

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

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

Три, все ваши подчиненные методы могут предполагать, что каждый вызов успешен . Если вызов не удался, выполнение перейдет к блоку catch, а последующий код никогда не будет выполнен. Это может сделать ваш код намного чище. Например, вот с кодами ошибок:

int ret = SaveFirstSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveSecondSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

ret = SaveThirdSection();

if (ret == FAILED)
{
    /* some diagnostic */
    return;
}

Вот как это может быть написано с исключениями:

// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();

Теперь стало намного понятнее, что происходит.

Заметьте, что безопасный код исключения может быть сложнее написать другими способами: вы не хотите терять память при возникновении исключения. Убедитесь, что вы знаете о RAII , STL-контейнерах, интеллектуальных указателях и других объектах, которые освобождают свои ресурсы в деструкторах, поскольку объекты всегда уничтожаются перед исключениями.


2
Великолепные примеры. Да, ловите как можно выше, в логических единицах, например, вокруг какой-нибудь «транзакционной» операции, такой как загрузка / сохранение / и т. Д. Ничего выглядит хуже , чем код приправлен с повторяющимися, избыточно try- catchблоки , которые пытаются флаг до каждой несколько иной перестановки некоторой ошибки с немного другим сообщением, когда в действительности все они должны закончить то же самое: сделки или программы отказа и выхода! Если происходит отказ, достойный исключительной ситуации, я держу пари, что большинство пользователей просто хотят спасти то, что они могут, или, по крайней мере, оставить в покое, не имея дело с 10 уровнями сообщений об этом.
underscore_d

Просто хотел сказать, что это одно из лучших объяснений «брось рано, поймай поздно», которые я когда-либо читал: сжатые и примеры прекрасно иллюстрируют твои идеи. Спасибо!
corderazo00

27

Херб Саттер писал об этой проблеме здесь . Наверняка стоит прочесть.
Тизер:

«Написание кода, исключающего исключительные ситуации, в основном сводится к написанию« try »и« catch »в правильных местах». Обсудить.

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

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


15

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

Например, в вопросе, который породил ваш вопрос, спрашивающий спрашивает, безопасно ли игнорировать исключения для целого lexical_castчисла в строку. Такой актерский состав никогда не должен подвести. Если это не помогло, в программе что-то пошло не так. Что вы могли бы сделать, чтобы восстановиться в этой ситуации? Вероятно, лучше просто позволить программе умереть, поскольку она находится в состоянии, которому нельзя доверять. Поэтому не обрабатывать исключение может быть самым безопасным.


12

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

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


9

Лучший совет, который я услышал, заключается в том, что вы должны ловить исключения только в тех местах, где вы можете разумно что-то делать с исключительным условием, и что «ловить, регистрировать и освобождать» не является хорошей стратегией (если ее иногда можно избежать в библиотеках).


2
@KeithB: я бы посчитал это второй лучшей стратегией. Лучше, если вы сможете записать журнал другим способом.
Дэвид Торнли

1
@KeithB: Это стратегия «лучше, чем ничего в библиотеке». «Лови, регистрируйся, разбирайся с этим правильно» лучше, где это возможно. (Да, я знаю, что это не всегда возможно.)
Donal Fellows

6

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

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

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


5

Мой профессор компьютерных наук однажды дал мне совет: «Используйте блоки Try и Catch только тогда, когда невозможно обработать ошибку стандартными средствами».

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

int f()
{
    // Do stuff

    if (condition == false)
        return -1;
    return 0;
}

int condition = f();

if (f != 0)
{
    // handle error
}

Тогда вы должны использовать try, catch блоков. Хотя вы можете использовать исключения для обработки этого, как правило, это не рекомендуется, потому что исключения являются дорогостоящими с точки зрения производительности.


7
Это одна из стратегий, но многие люди рекомендуют никогда не возвращать коды ошибок или состояния неудачи / успеха из функций, используя вместо этого исключения. Обработка ошибок на основе исключений часто легче читать, чем код на основе кода ошибки. (См. Ответ AshleysBrain на этот вопрос для примера.) Кроме того, всегда помните, что у многих профессоров информатики очень мало опыта написания реального кода.
Кристофер Джонсон

1
-1 @Sagelika Ваш ответ состоит в том, чтобы избегать исключений, поэтому не нужно пытаться поймать.
Висенте Ботет Эрибли

3
@ Кристофер: Другими большими недостатками кода возврата является то, что очень легко забыть проверить код возврата, и сразу после вызова это не обязательно лучшее место для решения проблемы.
Дэвид Торнли

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

3

Если вы хотите проверить результат каждой функции, используйте коды возврата.

Цель исключений состоит в том, что вы можете часто проверять результаты МЕНЬШЕ. Идея состоит в том, чтобы отделить исключительные (необычные, более редкие) условия от вашего более обычного кода. Это делает обычный код чище и проще - но все же способен справляться с этими исключительными условиями.

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


2

Я хотел бы добавить к этому обсуждению, что, начиная с C ++ 11 , это имеет большой смысл, если каждый catchблок rethrowявляется исключением вплоть до того момента, когда он может / должен быть обработан. Таким образом, можно создать обратную трассировку . Поэтому я считаю, что предыдущие мнения частично устарели.

Используйте std::nested_exceptionиstd::throw_with_nested

Здесь и здесь описано, как этого добиться, в StackOverflow .

Поскольку вы можете сделать это с любым производным классом исключений, вы можете добавить много информации к такой обратной трассировке! Вы также можете взглянуть на мой MWE на GitHub , где обратная трассировка будет выглядеть примерно так:

Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"

2

Мне дали «возможность» спасти несколько проектов, и руководители заменили всю команду разработчиков, потому что в приложении было слишком много ошибок, а пользователи устали от проблем и ошибок. Все эти кодовые базы имели централизованную обработку ошибок на уровне приложения, как описано в ответе с наибольшим количеством голосов. Если этот ответ - лучшая практика, почему он не сработал и не позволил предыдущей команде разработчиков решить проблемы? Возможно, иногда это не работает? В ответах выше не указано, сколько времени разработчики тратят на исправление отдельных проблем. Если время для решения проблем является ключевым показателем, лучше использовать инструментальный код с блоками try..catch.

Как моя команда исправила проблемы без значительного изменения пользовательского интерфейса? Все просто, каждый метод был снабжен блокировкой try..catch, и все было зарегистрировано в точке сбоя с именем метода, значения параметров метода объединены в строку, переданную вместе с сообщением об ошибке, сообщением об ошибке, именем приложения, датой, и версия. С помощью этой информации разработчики могут запускать аналитику на наличие ошибок, чтобы выявить исключение, которое возникает чаще всего! Или пространство имен с наибольшим количеством ошибок. Он также может проверить, что ошибка, возникающая в модуле, правильно обрабатывается и не вызвана несколькими причинами.

Еще одним преимуществом этого является то, что разработчики могут установить одну точку останова в методе регистрации ошибок, и с одной точкой останова и одним нажатием кнопки отладки «выйти» они находятся в методе, который не удался с полным доступом к фактическому объекты в точке сбоя, удобно доступны в ближайшем окне. Это делает его очень простым для отладки и позволяет перетаскивать выполнение обратно в начало метода, чтобы дублировать проблему, чтобы найти точную строку. Позволяет ли централизованная обработка исключений разработчику воспроизвести исключение за 30 секунд? Нет.

Утверждение «Метод должен перехватывать исключение только тогда, когда он может обрабатывать его каким-либо разумным способом». Это подразумевает, что разработчики могут предсказать или столкнутся с каждой ошибкой, которая может произойти до выпуска. Если бы это был действительно верхний уровень, обработчик исключений приложения не понадобился бы, и не было бы рынка для Elastic Search и logstash.

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

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

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

Этот метод использовался для быстрой стабилизации приложения, которое каждый час давало сбой большинству пользователей в компании Fortune 500, разработанной 12 разработчиками в течение 2 лет. Используя эти 3000 различных исключений, были выявлены, исправлены, протестированы и развернуты в течение 4 месяцев. Это усредняется для исправления каждые 15 минут в среднем в течение 4 месяцев.

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


1
Заворачивание каждого блока кажется излишним. Это быстро делает ваш код раздутым и болезненным для чтения. Запись трассировки стека из исключения на более высоких уровнях показывает вам, где произошла проблема, и что в сочетании с самой ошибкой, как правило, достаточно для продолжения. Мне было бы любопытно, где вы нашли, что этого недостаточно. Просто так я могу получить чужой опыт.
user441521

1
«Исключения в 100-1000 раз медленнее, чем обычный код, и их никогда не следует перебрасывать» - это утверждение неверно для большинства современных компиляторов и аппаратного обеспечения.
Митч Уит

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

Нет, исключения очень медленные. Альтернатива - коды возврата, объекты или переменные. См. Этот пост переполнения стека ... "исключения по крайней мере в 30 000 раз медленнее, чем коды возврата" stackoverflow.com/questions/891217/…
user2502917

1

Помимо приведенного выше совета, лично я использую некоторые try + catch + throw; по следующей причине:

  1. На границе другого кодера я использую try + catch + throw в написанном мной коде, перед тем как исключение выдается вызывающей программе, которая пишется другими, это дает мне возможность узнать о некоторой ошибке, возникшей в моем коде, и это место намного ближе к коду, который изначально выдает исключение, чем ближе, тем легче найти причину.
  2. На границе модулей, хотя разные модули могут быть написаны мои же лица.
  3. Цель обучения + отладки, в этом случае я использую catch (...) в C ++ и catch (Exception ex) в C #, для C ++ стандартная библиотека не выдает слишком много исключений, поэтому этот случай встречается редко в C ++. Но общее место в C #, C # имеет огромную библиотеку и развитую иерархию исключений, код библиотеки C # генерирует тонны исключений, в теории я (и вы) должны знать все исключения из функции, которую вы вызывали, и знать причину / случай, почему эти исключения выдают, и знают, как с ними справиться (пройти мимо или поймать и обработать его на месте) изящно. К сожалению, на самом деле очень сложно узнать все о потенциальных исключениях, прежде чем я напишу одну строку кода. Поэтому я ловлю все и позволяю своему коду говорить вслух, регистрируя (в среде продукта) / диалог подтверждения (в среде разработки), когда действительно происходит какое-либо исключение. Таким образом, я постепенно добавляю код обработки исключений. Я знаю, что это совпало с хорошим советом, но на самом деле это работает для меня, и я не знаю лучшего способа решения этой проблемы.

1

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

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

Экспоненциально, потому что если каждый метод разветвляется двумя различными способами, то каждый раз, когда вы вызываете другой метод, вы возводите в квадрат предыдущее число потенциальных результатов. К тому времени, как вы вызвали пять методов, вы получите до 256 возможных результатов как минимум. Сравните это с тем, что в каждом отдельном методе не выполняется try / catch, и у вас есть только один путь.

Вот как я на это смотрю. Вы можете быть склонны возразить, что любой тип ветвления делает то же самое, но try / catches - особый случай, потому что состояние приложения в основном становится неопределенным.

Короче говоря, try / catches усложняет понимание кода.


-2

Вам не нужно скрывать каждую часть вашего кода внутри try-catch. Основное использование try-catchблока - обработка ошибок и получение ошибок / исключений в вашей программе. Некоторое использование try-catch-

  1. Вы можете использовать этот блок там, где хотите обработать исключение, или просто можете сказать, что блок написанного кода может вызвать исключение.
  2. Если вы хотите избавиться от своих объектов сразу после их использования, вы можете использовать try-catchблок.

1
«Если вы хотите избавиться от своих объектов сразу после их использования, вы можете использовать блок try-catch». Вы намеревались продвинуть RAII / минимальное время жизни объекта? Если так, то try/ catchполностью отделен / ортогональн от этого. Если вы хотите расположить объекты в меньшем объеме, вы можете просто открыть новый { Block likeThis; /* <- that object is destroyed here -> */ }- не нужно оборачивать это в try/, catchесли catch, конечно, вам ничего не нужно .
underscore_d

# 2 - Удаление объектов (которые были созданы вручную) в исключении мне кажется странным, это может быть полезно в некоторых языках, без сомнения, но обычно вы делаете это в try / finally «в блоке try / Кроме», а не в частности, в самом блоке исключений - поскольку сам объект мог быть причиной исключения в первую очередь и, таким образом, вызывать другое исключение и, возможно, сбой.
TS
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.