Почему «&&», а не «&»?


135

Почему &&предпочтительнее& и ||предпочтительнее |?

Я спросил кого-то, кто программировал годами, и он объяснил:

Например, in if (bool1 && bool2 && bool3) { /*DoSomething*/ }, bool1должно быть истинным, чтобы он мог проверить, bool2который должен быть истинным, прежде чем перейти к bool3и т. Д. Если бы я использовал один& вместо этого нет никакого порядка для теста, даже если все они должны быть истинными для перейти к следующей строке, так почему это все равно имеет значение?

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


55
& и && | и || совершенно разные операторы
Феличе Поллано

3
Повторно помечен, так как это относится не только к C #
Джонатан Дикинсон

12
Отменено, потому что ответы уже специфичны для C #, и внутренняя работа может немного отличаться в других языках, которые обычно имеют ту же концепцию.
Дэниел Хилгарт,

8
@Felice: они разные, но вряд ли совершенно разные. На самом деле они очень похожи: x & yи x && yвсегда будут давать одинаковый результат, если x и y являются выражениями логического типа. На самом деле единственное различие в этом случае заключается в том x & y, что y всегда вычисляется.
Джорен

1
@slawekin: Я бы предложил прочитать ответы. Некоторые подробно пишут о различиях в производительности. Ответ может вас удивить, хотя.
Авель

Ответы:


183

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

Пример:

if(CanExecute() && CanSave())
{
}

Если CanExecuteвозвращается false, полное выражение будет false, независимо от возвращаемого значения CanSave. Из-за этого CanSaveне выполняется.

Это очень удобно при следующих обстоятельствах:

string value;
if(dict.TryGetValue(key, out value) && value.Contains("test"))
{
    // Do Something
}

TryGetValueвозвращает, falseесли указанный ключ не найден в словаре. Из-за короткого замыкания &&, value.Contains("test")выполняется только, когда TryGetValueвозвращается trueи, следовательно value, нет null. Если бы вместо этого вы использовали побитовый оператор AND& , вы бы получилиNullReferenceException если ключ не найден в словаре, потому что вторая часть выражения выполняется в любом случае.

Аналогичным, но более простым примером этого является следующий код (как упомянуто TJHeuvel):

if(op != null && op.CanExecute())
{
    // Do Something
}

CanExecuteвыполняется только если opнет null. Если opесть null, первая часть выражения ( op != null) оценивается, falseа оценка остальных ( op.CanExecute()) пропускается.

Помимо этого, технически, они тоже разные:
&&и ||могут быть использованы только boolтогда &и |может быть использован на любой интегрального типа ( bool, int, long, sbyte, ...), потому что они являются битовые операторы. &является побитовым оператором И и |является побитовым оператором ИЛИ .

Точнее говоря, в C # эти операторы ( &, |^]) называются «логическими операторами» (см. Спецификацию C # , глава 7.11). Существует несколько реализаций этих операторов:

  1. Для целых чисел ( int, uint, longи ulong, глава 7.11.1):
    Они реализуются , чтобы вычислить результат побитового операндов и оператора, то есть &это реализовать , чтобы вычислить побитовое логическое и ANDт.д.
  2. Для перечислений (глава 7.11.2):
    они реализованы для выполнения логической операции базового типа перечисления.
  3. Для bools и nullable bools (главы 7.11.3 и 7.11.4):
    результат не вычисляется с использованием побитовых вычислений. Результат в основном ищется на основе значений двух операндов, потому что количество возможностей очень мало.
    Поскольку оба значения используются для поиска, эта реализация не имеет короткого замыкания.

31
Это также может быть полезно для проверки, если что-то имеет значение null. Например: if(op != null && op.CanExecute()). Поскольку вторая причина не оценивается, когда первая не истинна, это допустимо.
TJHeuvel

2
@TJHeuvel: Это в основном то же использование, которое я описал на своем TryGetValueпримере. Но да, это еще один хороший пример этого.
Дэниел Хилгарт,

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

81

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

Следующий код:

if (a && b)
{
   Foo();
}

Действительно скомпилировано в это:

if (a)
{
    if (b)
    {
        Foo();
    }
}

Где следующий код скомпилирован именно так, как он представлен:

if (a & b)
{
   Foo();
}

Это называется коротким замыканием. В общем, вы всегда должны использовать &&и ||в ваших условиях.

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

Diatribe / Monologue : Относительно неправильного предсказания ветви, которое большинство блаженно игнорируют. Цитируя Энди Ферта (который работает над играми в течение 13 лет): «Это может быть более низкий уровень, когда люди думают, что им нужно идти ... но они ошибаются. Понимание того, как аппаратное обеспечение, которое вы программируете, относится к веткам, может влияет на производительность в огромной степени ... гораздо больше, чем большинство программистов могут оценить re: death на тысячу сокращений ".

  • Разработчики игр (и другие, работающие в экстремальных условиях реального времени) зашли настолько далеко, что реструктурировали свою логику, чтобы лучше соответствовать предиктору. Это также подтверждается декомпилированным кодом mscorlib.
  • То, что .NET защищает вас от подобных вещей, не означает, что это не важно. Неправильное предсказание ветвления ужасно дорого при 60 Гц; или на 10000 запросов в секунду.
  • У Intel не было бы инструментов для определения местоположения неправильных предсказаний, и у Windows не было бы счетчика производительности для этого, и не было бы ни слова, чтобы описать это, если бы это не было проблемой.
  • Незнание о нижних уровнях и архитектуре не делает кого-то, кто знает о них неправильно.
  • Всегда пытайтесь понять ограничения оборудования, на котором вы работаете.

Вот эталон для неверующих. Лучше всего запустить процесс в RealTime / High, чтобы смягчить эффект планировщика: https://gist.github.com/1200737


7
О «бонусных оценках»: мы все знаем, что хорошо получается от преждевременной оптимизации. :)
CVN

6
@ Майкл - вот почему «решающее значение наносекунды» выделено жирным шрифтом :). Разработчики игр ААА обычно беспокоятся о таких вещах - и вы никогда не знаете, кто будет читать ответы; поэтому всегда лучше документировать даже пограничные / экстремальные случаи.
Джонатан Дикинсон

1
Этот бонусный знак действителен для C #? Я бы не подумал, что MSIL интерпретируется, если только выражение не скомпилировано вплоть до машинного кода.
Джереми МакГи

7
@ Джереми MSIL не интерпретируется.
Джонатан Дикинсон

2
@TheD перепроверьте ответ - я добавил монолог о том, почему вы ДОЛЖНЫ быть обеспокоены этим. И, к вашему сведению, (x && y)переводит туда, LOAD x; BRANCH_FALSE; LOAD y; BRANCH_FALSE;где (x & y)переводится LOAD x; LOAD y; AND; BRANCH_FALSE;. Одна ветка против двух.
Джонатан Дикинсон

68

Логический оператор ( ||и &&) против побитового оператора ( |и &).

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

Чтобы быть педантичным, побитовый оператор берет битовую комбинацию (например, 01101011) и выполняет побитовое И / ИЛИ для каждого бита. Так, например, если у вас есть два 8-битных целых числа:

a     = 00110010 (in decimal:    32+16+2   = 50)
b     = 01010011 (in decimal: 64+   16+2+1 = 83)
----------------
a & b = 00010010 (in decimal:       16+2   = 18)
a | b = 01110011 (in decimal: 64+32+16+2+1 = 115)

в то время как логический оператор работает только в bool:

a      = true
b      = false
--------------
a && b = false
a || b = true

Во-вторых, часто можно использовать побитовый оператор в bool, поскольку true и false эквивалентны 1 и 0 соответственно, и бывает, что если вы переводите true в 1 и false в 0, то выполняете битовую операцию, а затем преобразуете ненулевое значение на истину и ноль на ложь; бывает, что результат будет таким же, если вы только что использовали логический оператор (проверьте это для упражнения).

Другое важное отличие состоит также в том, что логический оператор закорочен . Таким образом, в некоторых кругах [1] вы часто видите людей, делающих что-то вроде этого:

if (person && person.punch()) {
    person.doVictoryDance()
}

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

Если бы вы использовали вместо этого побитовый оператор, это:

if (person & person.punch()) {
    person.doVictoryDance()
}

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

Обратите внимание, что в короткозамкнутом логическом операторе person.punch()код может вообще не выполняться, если он personравен нулю. Фактически, в этом конкретном случае второй код выдаст нулевую ссылку, если он personравен нулю, так как он пытается вызвать person.punch()независимо от того, является ли пользователь нулевым или нет. Такое поведение не оценки правильного операнда называется коротким замыканием .

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

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

int CAN_PUNCH = 1 << 0, CAN_KICK = 1 << 1, CAN_DRINK = 1 << 2, CAN_SIT = 1 << 3,
    CAN_SHOOT_GUNS = 1 << 4, CAN_TALK = 1 << 5, CAN_SHOOT_CANNONS = 1 << 6;

Person person;
person.abilities = CAN_PUNCH | CAN_KICK | CAN_DRINK | CAN_SIT | CAN_SHOOT_GUNS;

Place bar;
bar.rules = CAN_DRINK | CAN_SIT | CAN_TALK;

Place military;
military.rules = CAN_SHOOT_CANNONS | CAN_PUNCH | CAN_KICK | CAN_SHOOT_GUNS | CAN_SIT;

CurrentLocation cloc1, cloc2;
cloc1.usable_abilities = person_abilities & bar_rules;
cloc2.usable_abilities = person_abilities & military_rules;

// cloc1.usable_abilities will contain the bit pattern that matches `CAN_DRINK | CAN_SIT`
// while cloc2.usable_abilities will contain the bit pattern that matches `CAN_PUNCH | CAN_KICK | CAN_SHOOT_GUNS | CAN_SIT`

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

Person person;
person.can_punch = person.can_kick = person.can_drink = person.can_sit = person.can_shoot_guns = true;
person.can_shoot_cannons = false;

Place bar;
bar.rules.can_drink = bar.rules.can_sit = bar.rules.can_talk = true;
bar.rules.can_punch = bar.rules.can_kick = bar.rules.can_shoot_guns = bar.rules.can_shoot_cannons = false;

Place military;
military.rules.can_punch = military.rules.can_kick = military.rules.can_shoot_guns = military.rules.can_shoot_cannons = military.rules.can_sit = true;
military.rules.can_drink = military.rules.can_talk = false;

CurrentLocation cloc1;
bool cloc1.usable_abilities.can_punch         = bar.rules.can_punch         && person.can_punch,
     cloc1.usable_abilities.can_kick          = bar.rules.can_kick          && person.can_kick,
     cloc1.usable_abilities.can_drink         = bar.rules.can_drink         && person.can_drink,
     cloc1.usable_abilities.can_sit           = bar.rules.can_sit           && person.can_sit,
     cloc1.usable_abilities.can_shoot_guns    = bar.rules.can_shoot_guns    && person.can_shoot_guns,
     cloc1.usable_abilities.can_shoot_cannons = bar.rules.can_shoot_cannons && person.can_shoot_cannons
     cloc1.usable_abilities.can_talk          = bar.rules.can_talk          && person.can_talk;

bool cloc2.usable_abilities.can_punch         = military.rules.can_punch         && person.can_punch,
     cloc2.usable_abilities.can_kick          = military.rules.can_kick          && person.can_kick,
     cloc2.usable_abilities.can_drink         = military.rules.can_drink         && person.can_drink,
     cloc2.usable_abilities.can_sit           = military.rules.can_sit           && person.can_sit,
     cloc2.usable_abilities.can_shoot_guns    = military.rules.can_shoot_guns    && person.can_shoot_guns,
     cloc2.usable_abilities.can_talk          = military.rules.can_talk          && person.can_talk,
     cloc2.usable_abilities.can_shoot_cannons = military.rules.can_shoot_cannons && person.can_shoot_cannons;

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


3
+1 для стороны влияет на проблему. Удивило, что это не было упомянуто ранее
Конрад Фрикс

3
пример кажется немного жестоким, но кажется, что другие ответы слишком сосредоточены на коротком замыкании и недостаточно на разнице между операциями над целыми числами и логическими значениями.
R0MANARMY

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

@ROMANARMY - жестокий, я люблю иронию, данную твоему монкикеру. Хорошая работа
brumScouse

8

В случае:

if (obj != null && obj.Property == true) { }

будет работать как ожидалось.

Но:

if (obj != null & obj.Property == true) { }

потенциально может выдать исключение нулевой ссылки.


2

Коротко и просто:

1 && 2= true,
потому что
1 = true (не ноль) в C
2 = true (не ноль) в C

trueANDS логически, trueчтобы дать true.

Но

1 & 2= 0 = ложь,
потому что
1 = 0001 в двоичном формате
2 = 0010 в двоичном виде

0001 И поразрядно с 0010, чтобы дать 0000 = 0 в десятичном виде.

Аналогично для || и | операторы тоже ...!


2
-1: мы говорим о C # здесь ... 1 && 2это незаконно в C #
Даниэль Хилгарт

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

1

&&версия короткого замыкания &.

Если мы оцениваем false & true, мы уже знаем по первому аргументу, что результат будет ложным. &&Версия оператора будет возвращать результат , как только он может, а не оценивать целое выражение. Существует также аналогичная версия |оператора ||.


1
if (list.Count() > 14 && list[14] == "foo")

безопасно

if (list.Count() > 14 & list[14] == "foo")

произойдет сбой, если список не имеет нужного размера.


Я не могу себе представить, что кто-то может написать «if (list.Count ()> 14 & list [14] ==« foo »)» вместо «if (list.Count ()> 14 && list [14] ==» Foo ")". & просто и естественно не может использоваться для && в этом случае, даже если & s, безусловно, безопасно (список [1], например).
Тиен До

1

Операторы C # должны объяснить, почему:

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

& Оператор имеет пример использования одного&.


Обе ссылки (эффективно) не работают (перенаправляет на «Документацию по Visual Studio 2005 Retired» ).
Питер Мортенсен

1

Хорошо, по номиналу

    Boolean a = true;
    Boolean b = false;

    Console.WriteLine("a({0}) && b({1}) =  {2}", a, b, a && b);
    Console.WriteLine("a({0}) || b({1}) =  {2}", a, b, a || b);
    Console.WriteLine("a({0}) == b({1}) =  {2}", a, b, a == b);

    Console.WriteLine("a({0}) & b({1}) =  {2}", a, b, a & b);
    Console.WriteLine("a({0}) | b({1}) =  {2}", a, b, a | b);
    Console.WriteLine("a({0}) = b({1}) =  {2}", a, b, a = b);

дать тот же ответ. Однако, как вы показали, если у вас есть более сложный вопрос, то:

if (a and b and c and d) ..

Если aэто не так, и, возможно b, это функция, где она должна отключиться, подключиться к чему-то, получить это, сделать это, принять решение ... зачем? Пустая трата времени, вы знаете, что это уже не удалось. Зачем заставлять машину выключаться и делать лишнюю бессмысленную работу?

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

if (limit && !MyDictionary.ContainsKey("name")) 
    continue;

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


1

При использовании в логическом выражении, например, в операторе if, &&предпочтительнее, поскольку он прекращает вычислять выражения, как только встречается первый ложный результат. Это возможно, потому что ложное значение приведет к тому, что все выражение будет ложным. Аналогично (и снова в логических выражениях)|| предпочтительнее, потому что он прекратит вычислять выражения, как только встретит истинное выражение, потому что любое истинное значение приведет к тому, что все выражение будет истинным.

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

Операторы &and |также могут использоваться с целыми числами, и в этом случае они дают целочисленный результат, который представляет собой два операнда и-ed или or-ed вместе на уровне битов. Это может быть полезно, когда двоичные биты целочисленного значения используются в качестве массива значений true и false. Чтобы проверить, включен или выключен определенный бит, битовая маска битовая добавляется со значением. Чтобы включить немного, одну и ту же маску можно поразрядно добавить к значению. Наконец, чтобы отключить немного, побитовое дополнение (использование ~) маски побитово и отредактировано со значением.

int a = 0; // 0 means all bits off
a = a | 4; // set a to binary 100
if ((a & 4) != 0) {
    // will do something
}
a = a & (~4) // turn bit off again, a is now 000

В языках, отличных от C #, следует соблюдать осторожность при использовании логических и побитовых режимов & и |. В приведенном выше коде ifусловное выражение оператора (a & 4) != 0является безопасным способом выражения этого условия, но во многих языках, подобных С, условные операторы могут просто обрабатывать нулевые целые значения как ложные, а ненулевые целые значения - как истинные. (Причина этого связана с доступными инструкциями процессора условных переходов и их отношением к нулевому флагу, который обновляется после каждой целочисленной операции.) Таким образом ìf, проверка оператора на ноль может быть удалена, а условие может быть сокращено до (a & 4).

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

if (foo() & bar()) {
    // do something
}

В C, если foo()возвращает 1 и bar()возвращает 2, «что-то» не будет сделано, потому что 1 & 2это ноль.

C # требует условных операторов, таких как ifналичие логического oeprand, а язык не позволяет приводить целочисленное значение к логическому значению. Таким образом, приведенный выше код будет генерировать ошибки компилятора. Правильнее было бы выразиться следующим образом:

if (foo() != 0 & bar() != 0) {
    // do something
}

1

Если вы программист на старом таймере, будьте осторожны . C # действительно удивил меня.

MSDN говорит для |оператора:

Двоичный | операторы предопределены для целочисленных типов и bool . Для целочисленных типов | вычисляет побитовое ИЛИ его операндов. Для булевых операндов, | вычисляет логическое ИЛИ его операндов; то есть результат ложен тогда и только тогда, когда оба его операнда ложны.

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

&&и ||короткозамкнуты. &и |оцените оба операнда.

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

Причина в том, что я могу рассуждать так: поскольку в C нет реального логического типа, вы можете использовать побитовый оператор, |и его результат оценивается как истинный или ложный в условии if. Но это неправильное отношение к C #, потому что для булевых типов уже есть особый случай.


0

Это важно, потому что, если стоимость оценки bool2 (например) высока, а bool1 ложна, вы сэкономили немало вычислений, используя && over &


0

Потому &&и ||используются для управления потоком, как и if/elseесть. Это не всегда об условностях. Вполне разумно написать как утверждение, а не как ifили whileусловно, следующее:

 a() && b() && c() && d();

или даже

 w() || x() || y() || z();

Дело не только в том, что их легче набирать, чем эквивалентные if/elseверсии; они также намного легче читать и понимать.


0

&& и & означают две совершенно разные вещи и дают вам два разных ответа.

1 && 2доходность 1 («истина»)
1 & 2доходность 0 («ложь»)

&&является логическим оператором - это означает, что «истина, если оба операнда истинны»
&- это побитовое сравнение. Это означает «скажите мне, какие биты установлены в обоих операндах»


2
Вопрос о C #. В C # нет способа привести число к логическому значению, поэтому 0 не является «ложным», а ненулевое не является «истинным»; просто нет эквивалентности.
Nate CK

Чтобы преобразовать число в bool, таким образом, что 1 означает истину, а 0 означает ложь, произнесите «n! = 0» (я полагаю ... я не очень знаком с C #). На самом деле я хотел отозвать этот комментарий, так как он не очень хорошо изучен, и я не думаю, что он полезен или действительно имеет отношение к предыдущему комментарию, теперь, когда я думаю об этом больше, но я случайно нажал Enter и теперь я не думаю, что могу отмените это так, вот и вы, во что бы то ни стало :-)
Дон Хэтч

1 && 2выдает ошибку компилятора: «Ошибка 4 Оператор« && »не может быть применена к операндам типа« int »и« int »»
Питер Мортенсен

0

Самый быстрый (и слегка тупой) способ объяснить это людям, которым НЕ НУЖНО знать точные операции кода при этом,

&& выполняет проверку каждого из этих условий, пока не обнаружит ложное и не вернет весь результат как ложное

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

& выполняет условия на основе MATHS ОБА / ВСЕХ условий и имеет дело с результатом.

| выполняет на основе MATHS апо ОБА / ВСЕ условия и имеет дело с результатом.

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

НАПРИМЕР:

r = fullvalue >> 0xFF & 0xFF;
g = fullvalue >> 0xF & 0xFF;
b = fullvalue & 0xFF;

В рамках этой операции "& 0xFF" заставляет смотреть только двоичное значение. Я лично не нашел применение для | все же хотя.


0

Просто,

if exp1 && exp2

если exp1 flase не проверять exp2

но

if exp1 & exp2

если exp1 равен falseor true проверить exp2

и редко люди используют, &потому что они редко хотят проверить exp2, если exp1false

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.