«Легко рассуждать» - что это значит? [закрыто]


49

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

Фраза «Легко рассуждать» использовалась как есть, без каких-либо объяснений или примеров кода. Так что для меня это похоже на очередное «модное» слово, которое более «опытные» разработчики используют в своих выступлениях.

Вопрос: Можете ли вы привести несколько примеров «Не легко рассуждать», чтобы их можно было сравнить с примерами «Легко рассуждать»?


4
@MartinMaat Более точная фраза, которая широко используется - это эквациональные рассуждения, я бы предположил, что это может быть то, что преследует Фабио
jk.

3
Мне нравится использовать фразу «когнитивная нагрузка» для такого рода вещей.
Baldrickk

16
Знаете ли вы, что такое рассуждения о программах ?
Берги

5
В неформальном смысле я использую это, чтобы означать, что решение достаточно простое, чтобы понять (в общем), каковы будут результаты для любого данного ввода без его проверки. Это означает, что для любого набора входных данных результаты будут неудивительными. Решения, которые имеют неочевидные угловые случаи, например, трудно рассуждать. В основном я использую это в отношении надежности.
JimmyJames

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

Ответы:


58

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

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

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


29
С небольшим предостережением, что независимо от того, как хорошо вы называете свои переменные, программе, которая пытается опровергнуть гипотезу Гольдбаха, по своей сути трудно «выполнить», в вашей голове или где-либо еще. Но все еще может быть легко рассуждать, в том смысле, что легко убедить себя в том, что, если он утверждает, что нашел контрпример, то он говорит правду ;-)
Стив Джессоп,

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

1
Как можно ответить на вопрос о рассуждениях о коде, даже не упоминая формальную проверку ? Этот ответ предполагает, что рассуждения о коде неформальные и специальные. его нет, его обычно делают с очень большой осторожностью и математическими подходами. Существуют определенные математические свойства, которые делают код «легким для рассуждения» в объективном смысле (чистые функции, чтобы привести очень простой пример). Имена переменных не имеют ничего общего с тем, насколько легко «рассуждать» о коде, по крайней мере, в формальном смысле.
Полигном

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

2
@Polygnome "Code easy to reason about" almost exclusively alludes to its mathematical properties and formal verification- это звучит примерно как ответ на вопрос. Возможно, вы захотите опубликовать это как ответ вместо того, чтобы не соглашаться с тем, что (субъективный) ответ содержится в комментариях.
Dukeling

47

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

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

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

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

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


9
Я согласен с этим ответом, но даже с чистыми функциями декларативные подходы (такие как CSS, или XSLT, makeили даже специализация шаблона C ++ и перегрузка функций) могут вернуть вас в положение рассмотрения всей программы. Даже если вы думаете, что нашли определение чего-либо, язык позволяет более конкретному объявлению где-либо в программе переопределять его. Ваша IDE может помочь с этим.
Стив Джессоп

4
Я бы добавил, что в многопоточном сценарии вы также должны иметь достаточно глубокое понимание того, к каким инструкциям более низкого уровня обращается ваш код: операция, которая выглядит как атомарная в источнике, может иметь неожиданные точки прерывания в реальном выполнении.
Джаред Смит

6
@ SteveJessop: Действительно, этот момент часто упускается из виду. Есть причина, по которой C # заставляет вас говорить, когда вы хотите, чтобы метод был перезаписываемым, а не тихо устанавливая переопределение по умолчанию; мы хотим пометить флажок, говорящий, что «правильность вашей программы может зависеть от кода, который вы не можете найти во время компиляции» на этом этапе. (Тем не менее, я также хотел бы, чтобы «запечатанный» был по умолчанию для классов в C #.)
Эрик Липперт

@EricLippert Каковы были последние причины sealedне быть по умолчанию?
Зев Шпиц

@ZevSpitz: Это решение было принято задолго до моего времени; Я не знаю.
Эрик Липперт

9

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

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

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

Тем не менее, есть много других вещей, о которых сложнее рассуждать, и я согласен с @Kilian, что параллелизм является ярким примером. Распределенные системы тоже.


5

Избежание более широкого обсуждения и решение конкретного вопроса:

Можете ли вы привести примеры «Не легко рассуждать», чтобы их можно было сравнить с примерами «Легко рассуждать»?

Я отсылаю вас к «Истории Мела, настоящего программиста» , куска фольклора программистов, который датируется 1983 годом и поэтому считается нашей легендой для нашей профессии.

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

кажущийся бесконечный цикл фактически был закодирован таким образом, чтобы воспользоваться ошибкой переноса-переполнения. Добавление 1 к инструкции, которая расшифровывается как «Загрузить с адреса x», обычно дает «Загрузить с адреса x + 1». Но когда x уже был максимально возможным адресом, адрес не только обернулся до нуля, но и в биты, из которых будет считываться код операции, была перенесена единица, изменив код операции с «загрузить с» на «перейти к», чтобы что полная инструкция изменилась с «загрузить с последнего адреса» на «перейти к нулевому адресу».

Это пример кода, который «трудно рассуждать».

Конечно, Мел не согласится ...


1
+1 за ссылку на историю Мела, одного из моих постоянных фаворитов.
Джон Боллинджер

3
Прочитайте «Историю Мела» здесь, так как статья в Википедии на нее не ссылается.
TRiG

@ TRiG сноска 3 на странице, нет?
AakashM

@AakashM Как-то удалось это пропустить.
TRiG

5

Я могу привести пример, и очень распространенный.

Рассмотрим следующий код C #.

// items is List<Item>
var names = new List<string>();
for (var i = 0; i < items.Count; i++)
{
    var item = items[i];
    var mangled = MyMangleFunction(item.Name);
    if (mangled.StartsWith("foo"))
    {
        names.Add(mangled);
    }
}

Теперь рассмотрим эту альтернативу.

// items is List<Item>
var names = items
    .Select(item => MyMangleFunction(item.Name))
    .Where(s => s.StartsWith("foo"))
    .ToList();

Во втором примере я точно знаю, что этот код делает с первого взгляда. Когда я вижу Select, я знаю, что список предметов превращается в список чего-то еще. Когда я вижу Where, я знаю, что некоторые элементы отфильтровываются. С namesпервого взгляда я могу понять, что это такое, и эффективно использовать его.

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

В конечном счете, легкость рассуждений здесь также зависит от понимания методов Selectи LINQ Where. Если вы их не знаете, то второй код будет сложнее рассуждать изначально. Но вы платите только за то, чтобы понять их один раз. Вы платите за понимание forцикла каждый раз, когда используете его, и каждый раз, когда он меняется. Иногда стоимость того стоит, но обычно «легче рассуждать» гораздо важнее.


2

Связанная фраза (я перефразирую),

Недостаточно, чтобы в коде не было « никаких очевидных ошибок »: вместо этого в нем должно быть « явно никаких ошибок ».

Примером относительно "легкого рассуждения" может быть RAII .

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


1
Хм. Я не уверен, что RAII так легко рассуждать. Конечно, это легко понять концептуально , но на самом деле все труднее на самом деле рассуждать (то есть предсказывать) поведение кода, который широко использует RAII. Я имею в виду, это в основном невидимые вызовы функций на уровне области видимости. Тот факт, что у многих людей возникают проблемы с рассуждениями об этом, очень прост, если вы когда-либо занимались программированием COM .
Коди Грей

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

Этот пример на основе COM проблематичен, потому что он смешивает стили, то есть интеллектуальный указатель в стиле C ++ ( CComPtr<>) с функцией в стиле C ( CoUninitialize()). Я также нахожу это странным примером, насколько я помню, вы вызываете CoInitialize / CoUninitialize в области видимости модуля и для всего времени жизни модуля, например, внутри mainили внутри DllMain, а не в какой-то крошечной недолговечной локальной области действия функции, как показано в примере ,
ChrisW

Это слишком упрощенный пример для иллюстративных целей. Вы совершенно правы, что COM инициализируется в области видимости модуля, но представьте, что пример Рэймонда (как пример Ларри) является mainфункцией точки входа ( ) для приложения. Вы инициализируете COM при запуске, а затем неинициализируете его прямо перед выходом. За исключением того, что у вас есть глобальные объекты, такие как смарт-указатели COM, использующие парадигму RAII. Что касается стилей микширования: глобальный объект, который инициализирует COM в своем ctor и неинициализирован в своем dtor, является работоспособным, и то, что предлагает Рэймонд, но это тонкий и непростой аргумент.
Коди Грей

Я бы сказал, что во многих отношениях программирование COM легче рассуждать на C, потому что все это явный вызов функции. Там нет ничего скрытого или невидимого за вашей спиной. Это немного больше работы (то есть, более утомительно), потому что вы должны вручную написать все эти вызовы функций и вернуться и проверить свою работу, чтобы увидеть, что вы сделали это правильно, но все это обнажено, что является ключевым чтобы было легко рассуждать о. Другими словами, «иногда умные указатели слишком умны» .
Коди Грей

2

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

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

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

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

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


0

Конечно. Возьми параллелизм:

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

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

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


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

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

0

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

Несмотря на это, выражение определено слабо и не может быть задано строго. Но это не значит, что он должен оставаться таким тусклым для нас. Давайте представим, что структура проходит некоторый тест и получает оценки за разные точки. Хорошие оценки для КАЖДОГО пункта означают, что структура удобна в каждом аспекте и, таким образом, «легко рассуждать».

Структура «Легко рассуждать» должна получить хорошие оценки за следующее:

  • Внутренние термины имеют разумные, легко различимые и определенные имена. Если элементы имеют некоторую иерархию, разница между родительскими и дочерними именами должна отличаться от разницы между именами братьев и сестер.
  • Количество типов конструктивных элементов невелико
  • Используемые типы конструктивных элементов - это простые вещи, к которым мы привыкли.
  • Труднопонятные элементы (рекурсии, мета-шаги, 4+-мерная геометрия ...) изолированы друг от друга и не связаны напрямую. (например, если вы попытаетесь подумать об изменении рекурсивного правила для 1,2,3,4..n..мерных кубов, это будет очень сложно. Но если вы перенесете каждое из этих правил в некоторую формулу в зависимости от n у вас будет отдельно формула для каждого n-куба и отдельно правило рекурсии для такой формулы. И об этих двух структурах можно легко подумать)
  • Типы структурных элементов явно различаются (например, не используются смешанные массивы, начиная с 0 и с 1)

Является ли тест субъективным? Да, естественно, это так. Но само выражение тоже субъективно. То, что легко для одного человека, не легко для другого. Итак, тесты должны быть разными для разных доменов.


0

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

Для примера системы рассуждений в pi-исчислении каждое изменяемое место в памяти в императивном языке должно быть представлено как отдельный параллельный процесс, тогда как последовательность функциональных операций - это отдельный процесс. Сорок лет спустя из программы проверки теорем LFC мы работаем с ГБ ОЗУ, поэтому наличие сотен процессов не представляет особой проблемы - я использовал pi-исчисление для удаления потенциальных тупиков из нескольких сотен строк C ++, несмотря на то, что представление имеет сотни Процессы, которые рассуждал, исчерпали пространство состояний примерно в 3 ГБ и вылечили прерывистую ошибку. Это было бы невозможно в 70-х годах или потребовался бы суперкомпьютер в начале 90-х годов, тогда как пространство состояний программы функционального языка аналогичного размера было достаточно маленьким, чтобы рассуждать об этом тогда.

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


-2

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

«Легко рассуждать» на самом деле очень самоописательная фраза. Если кто-то смотрит на код и хочет рассуждать о том, что он делает, это легко =)

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

Наихудший случай для «легкого рассуждения» - это когда единственный способ понять, что делает код, - это построчно пройти код, как машина Тьюринга, для всех входных данных. В этом случае единственный способ что-либо объяснить в коде - это превратить себя в компьютер и выполнить его в своей голове. Эти наихудшие примеры легко увидеть в запутанных соревнованиях по программированию, таких как эти 3 строки PERL, которые расшифровывают RSA:

#!/bin/perl -sp0777i<X+d*lMLa^*lN%0]dsXx++lMlN/dsM0<j]dsj
$/=unpack('H*',$_);$_=`echo 16dio\U$k"SK$/SM$n\EsN0p[lN*1
lK[d2%Sa2/d0$^Ixp"|dc`;s/\W//g;$_=pack('H*',/((..)*)$/)

Что касается простоты рассуждений, опять же, термин очень культурный. Вы должны рассмотреть:

  • Какие навыки есть у мыслителя? Сколько опыта?
  • Какие вопросы могут быть у рассуждения о коде?
  • Насколько уверенным должен быть разум?

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

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

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

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


12
Код Perl трудно читать ; не повод о. Если бы я был заинтересован в том, чтобы понять это, я бы де-обфускировал код. Код, который на самом деле трудно рассуждать, это тот, о котором все еще трудно рассуждать, когда он красиво отформатирован с четкими идентификаторами для всего, и без хитростей в коде.
Каз
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.