Почему строки не могут быть изменяемыми в Java и .NET?


191

Почему они решили сделать Stringнеизменяемыми в Java и .NET (и некоторых других языках)? Почему они не сделали его изменчивым?


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

8
Спасибо, белугабоб, но я не она, я он. Видимо люди не учитывают культурные различия.
chrissie1

7
Мои извинения - Крисси (как правило), имя девушки в Великобритании, - что делает меня жертвой другого культурного различия :-)
belugabob

Просто обратите внимание, что в .NET Stringна самом деле изменчиво внутри. StringBuilderв .NET 2.0 мутирует строку . Я просто оставлю это здесь.
Элвин Вонг

На самом деле строки .NET являются изменяемыми. И это даже не чертовски хак.
Bitterblue

Ответы:


205

Согласно Эффективной Java , глава 4, стр. 73, 2-е издание:

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

[...]

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

[...]

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

[...]

Другие маленькие пункты из той же главы:

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

[...]

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

[...]

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


22
Прочитайте второе предложение моего ответа: неизменяемые классы легче проектировать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны.
ПРИНЦЕСС ФЛУФ

5
@PRINCESSFLUFF Я бы добавил, что совместное использование изменяемых строк опасно даже для одного потока. Например, копирование отчета: report2.Text = report1.Text;. Затем, где - то еще, изменяя текст: report2.Text.Replace(someWord, someOtherWord);. Это изменило бы первый отчет, а также второй.
фото

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

1
@PRINCESSFLUFF Этот ответ не относится конкретно к строкам. Это был вопрос ОП. Это так расстраивает - это постоянно происходит с SO, а также с вопросами неизменяемости String. Ответ здесь говорит об общих преимуществах неизменности. Так почему же не все типы неизменяемы? Можете ли вы вернуться и обратиться к String?
Howiecamp

@Howiecamp Я думаю, что ответ подразумевает, что строки могли быть изменяемыми (ничто не препятствует существованию гипотетического изменяемого класса строк). Они просто решили не делать это таким образом для простоты, и потому что это покрывало 99% случаев использования. Они все еще предоставили StringBuilder для других 1% случаев.
Даниэль Гарсия Рубио

102

Есть как минимум две причины.

Первый - безопасность http://www.javafaq.nu/java-article1060.html

Основной причиной, по которой String стал неизменным, была безопасность. Посмотрите на этот пример: у нас есть метод открытия файла с проверкой входа. Мы передаем String этому методу для обработки аутентификации, которая необходима перед передачей вызова в ОС. Если строка была изменяемой, то можно было каким-то образом изменить ее содержимое после проверки подлинности, прежде чем ОС получит запрос от программы, тогда можно запросить любой файл. Поэтому, если у вас есть право открывать текстовый файл в пользовательском каталоге, но на лету, когда вам как-то удастся изменить имя файла, вы можете запросить открыть файл «passwd» или любой другой. Затем файл может быть изменен, и можно будет войти непосредственно в ОС.

Второе - эффективность памяти http://hikrish.blogspot.com/2006/07/why-string-class-is-immutable.html

JVM поддерживает внутренний пул строк. Для достижения эффективности памяти JVM будет ссылаться на объект String из пула. Он не будет создавать новые объекты String. Таким образом, всякий раз, когда вы создаете новый строковый литерал, JVM проверит в пуле, существует ли он уже или нет. Если он уже присутствует в пуле, просто укажите ссылку на тот же объект или создайте новый объект в пуле. Будет много ссылок, указывающих на одни и те же объекты String, если кто-то изменит значение, это повлияет на все ссылки. Итак, солнце решило сделать его неизменным.


Это хороший момент для повторного использования, и особенно верно, если вы используете String.intern (). Можно было бы использовать повторно, не делая все строки неизменяемыми, но жизнь в этот момент усложняется.
jsight

3
Ни одна из этих причин не кажется мне очень веской причиной в наше время.
Брайан Кноблаух

1
Я не слишком убежден аргументом эффективности памяти (то есть, когда два или более объекта String совместно используют одни и те же данные, и один изменяется, тогда оба изменяются). Объекты CString в MFC позволяют обойти это с помощью подсчета ссылок.
RobH

7
Безопасность на самом деле не является частью Raison d'être для неизменяемых строк - ваша ОС будет копировать строки в буферы режима ядра и выполнять там проверку доступа, чтобы избежать временных атак. Это действительно все о безопасности потоков и производительности :)
snemarch

1
Аргумент эффективности памяти тоже не работает. В родном языке, таком как C, строковые константы - это просто указатели на данные в секции инициализированных данных - они в любом случае доступны только для чтения / неизменны. «если кто-то изменяет значение» - опять же, строки из пула в любом случае доступны только для чтения.
wj32

57

На самом деле, строка с причинами неизменяемости в Java не имеет большого отношения к безопасности. Две основные причины следующие:

Теад Безопасность:

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

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

Производительность:

Хотя было упомянуто интернирование String, оно представляет лишь небольшой выигрыш в эффективности памяти для программ Java. Только строковые литералы интернированы. Это означает, что только строки, одинаковые в вашем исходном коде, будут использовать один и тот же объект String. Если ваша программа динамически создает одинаковые строки, они будут представлены в разных объектах.

Что еще более важно, неизменяемые строки позволяют им обмениваться своими внутренними данными. Для многих строковых операций это означает, что базовый массив символов не нужно копировать. Например, скажем, вы хотите взять пять первых символов строки. В Java вы бы вызвали myString.substring (0,5). В этом случае метод substring () просто создает новый объект String, который разделяет лежащий в основе myString символ char [], но кто знает, что он начинается с индекса 0 и заканчивается индексом 5 этого char []. Чтобы поместить это в графическую форму, вы должны получить следующее:

 |               myString                  |
 v                                         v
"The quick brown fox jumps over the lazy dog"   <-- shared char[]
 ^   ^
 |   |  myString.substring(0,5)

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


6
Реализация подстрок в качестве ссылок, которые разделяют базовый уровень, char[]является довольно сомнительным дизайнерским решением. Если вы читаете весь файл в одну строку и сохраняете ссылку только на 1-символьную подстроку, весь файл должен храниться в памяти.
Гейб

5
Точно, я столкнулся с этой конкретной ошибкой при создании сканера веб-сайта, который должен был извлечь только несколько слов со всей страницы. Весь HTML-код страницы находился в памяти, и благодаря подстроке, разделяющей символ [], я сохранил весь HTML-код, хотя мне потребовалось всего несколько байтов. Обходным путем для этого является использование новой String (original.substring (.., ..)), конструктор String (String) создает копию соответствующего диапазона базового массива.
LordOfThePigs

1
Приложение, чтобы покрыть последующие изменения: начиная с Jave 7, String.substring()выполняет полную копию, чтобы предотвратить проблемы, упомянутые в комментариях выше. В Java 8 два поля, обеспечивающие char[]совместное использование, а именно, countи offset, удалены, тем самым уменьшая объем памяти экземпляров String.
Кристиан Семрау

Я согласен с частью безопасности Thead, но сомневаюсь в подстроке.
Gqqnbig

@LoveRight: Затем проверьте исходный код java.lang.String ( grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/… ), так было до Java 6 (который был актуален, когда этот ответ был написан). Я, видимо, изменился в Java 7.
LordOfThePigs

28

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


11

Нужно спросить: «Почему Х должен быть изменчивым?» По умолчанию лучше использовать неизменяемость из-за преимуществ, уже упомянутых принцессой Пух . Должно быть исключение, что что-то изменчивое.

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


7

Вот Это Да! Я не могу поверить дезинформации здесь. StringБыть неизменным не имеет ничего общего с безопасностью. Если у кого-то уже есть доступ к объектам в работающем приложении (что можно предположить, если вы пытаетесь защититься от того, что кто-то «взломал») Stringв вашем приложении, у него наверняка будет много других возможностей для взлома.

Это довольно новая идея, что неизменность Stringзаключается в решении проблем потоков. Хммм ... У меня есть объект, который изменяется двумя разными потоками. Как мне решить это? синхронизировать доступ к объекту? Naawww ... давайте не будем никого менять объект - это решит все наши проблемы с параллельным выполнением! Фактически, давайте сделаем все объекты неизменяемыми, а затем мы можем удалить синхронизированный конструкт из языка Java.

Реальная причина (указанная другими выше) - это оптимизация памяти. В любом приложении достаточно часто использовать один и тот же строковый литерал. Фактически, это настолько распространено, что десятилетия назад многие компиляторы оптимизировали хранение только одного экземпляра Stringлитерала. Недостаток этой оптимизации заключается в том, что код времени выполнения, который модифицирует Stringлитерал, создает проблему, поскольку он модифицирует экземпляр для всего другого кода, который разделяет его. Например, было бы неправильно, чтобы функция где-то в приложении изменяла Stringлитерал "dog"на "cat". А printf("dog")приведет "cat"к записи в стандартный вывод. По этой причине должен быть способ защиты от кода, который пытается изменитьStringлитералы (т. е. сделать их неизменяемыми). Некоторые компиляторы (при поддержке со стороны ОС) могли бы выполнить это, поместив Stringлитерал в специальный сегмент памяти только для чтения, который мог вызвать ошибку памяти, если была предпринята попытка записи.

В Java это называется интернированием. Компилятор Java здесь просто следует стандартной оптимизации памяти, проводимой компиляторами в течение десятилетий. И для решения той же проблемы, Stringсвязанной с изменением этих литералов во время выполнения, Java просто делает Stringкласс неизменным (т. Е. Не дает вам установщиков, которые позволили бы вам изменять Stringсодержимое). Strings не должен был бы быть неизменным, если интернирование Stringлитералов не происходило.


3
Я категорически не согласен с неизменяемостью и многопоточностью комментариев, мне кажется, вы не совсем поняли смысл. И если Джош Блох, один из разработчиков Java, говорит, что это была одна из проблем проектирования, как это может быть дезинформацией?
javashlook

1
Синхронизация стоит дорого. Ссылки на изменяемые объекты должны быть синхронизированы, а не неизменяемыми. Это причина сделать все объекты неизменяемыми, если они не должны быть изменяемыми. Строки могут быть неизменяемыми, что делает их более эффективными в нескольких потоках.
Дэвид Торнли

5
@Jim: Оптимизация памяти - это не причина, а причина. Потокобезопасность также является причиной «А», поскольку неизменяемые объекты по своей природе поточно-ориентированы и не требуют дорогой синхронизации, как отметил Дэвид. Безопасность потока на самом деле является побочным эффектом неизменяемости объекта. Вы можете думать о синхронизации как о способе сделать объект «временно» неизменным (ReaderWriterLock сделает его доступным только для чтения, а обычная блокировка сделает его недоступным вообще, что, конечно, также сделает его неизменным).
Triynko

1
@DavidThornley: создание нескольких независимых ссылочных путей к изменяемому держателю значений фактически превращает его в сущность и значительно усложняет решение даже в стороне от проблем с многопоточностью. Как правило, изменяемые объекты более эффективны, чем неизменяемые, в тех случаях, когда для каждого существует ровно один ссылочный путь, но неизменяемые объекты позволяют эффективно обмениваться содержимым объектов путем совместного использования ссылок. Лучший образец иллюстрируется Stringи StringBuffer, но, к сожалению, немногие другие типы следуют этой модели.
Суперкат

7

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

Ценность, которой вы можете доверять, не изменится за вашей спиной. Если вы пишете: String str = someExpr(); вы не хотите, чтобы это изменилось, если вы не сделаете что-то с str.

StringТак как an Objectимеет естественную семантику указателя, для получения семантики значения она также должна быть неизменной.


7

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


6

Я знаю, что это удар, но ... они действительно неизменны? Учтите следующее.

public static unsafe void MutableReplaceIndex(string s, char c, int i)
{
    fixed (char* ptr = s)
    {
        *((char*)(ptr + i)) = c;
    }
}

...

string s = "abc";
MutableReplaceIndex(s, '1', 0);
MutableReplaceIndex(s, '2', 1);
MutableReplaceIndex(s, '3', 2);
Console.WriteLine(s); // Prints 1 2 3

Вы даже можете сделать это методом расширения.

public static class Extensions
{
    public static unsafe void MutableReplaceIndex(this string s, char c, int i)
    {
        fixed (char* ptr = s)
        {
            *((char*)(ptr + i)) = c;
        }
    }
}

Что делает следующую работу

s.MutableReplaceIndex('1', 0);
s.MutableReplaceIndex('2', 1);
s.MutableReplaceIndex('3', 2);

Вывод: они находятся в неизменном состоянии, известном компилятору. Конечно, вышесказанное относится только к строкам .NET, поскольку в Java нет указателей. Однако строка может быть полностью изменяемой, используя указатели в C #. Это не то, как указатели предназначены для использования, практического использования или безопасного использования; однако это возможно, таким образом, изгибая все «изменяемое» правило. Обычно вы не можете изменять индекс непосредственно для строки, и это единственный способ. Есть способ, которым это может быть предотвращено путем запрета экземпляров указателей на строки или создания копии, когда на указатель указана строка, но это не делается, что делает строки в C # не полностью неизменяемыми.


1
+1. Строки .NET на самом деле не являются неизменяемыми; на самом деле, это делается постоянно в классах String и StringBuilder по соображениям производительности.
Джеймс Ко

3

Для большинства целей «строка» - это (используется / рассматривается как / считается / считается) значимая атомная единица, как и число .

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

Ты должен знать почему. Просто подумай об этом.

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

Мы выполняем вычисления и сравнения с «строками», аналогично тому, как мы делаем с числами. Если бы строки (или целые числа) были изменяемыми, мы должны были бы написать специальный код, чтобы зафиксировать их значения в неизменяемых локальных формах, чтобы надежно выполнять любые вычисления. Поэтому лучше всего рассматривать строку как числовой идентификатор, но вместо длины в 16, 32 или 64 бита она может быть длиной в сотни бит.

Когда кто-то говорит «струна», мы все думаем о разных вещах. Те, кто думает об этом просто как о наборе персонажей, не имея особой цели, конечно, будут потрясены, что кто-то просто решил, что он не сможет манипулировать этими персонажами. Но класс "string" - это не просто массив символов. Это, а STRINGне char[]. Есть некоторые основные предположения о концепции, которую мы называем «строкой», и ее обычно можно описать как значимую атомарную единицу кодированных данных, например число. Когда люди говорят о «манипулировании строками», возможно, они на самом деле говорят о манипулировании символами для создания строк , и StringBuilder отлично подходит для этого.

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

string GetPersonalInfo( string username, string password )
{
    string stored_password = DBQuery.GetPasswordFor( username );
    if (password == stored_password)
    {
        //another thread modifies the mutable 'username' string
        return DBQuery.GetPersonalInfoFor( username );
    }
}

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


В C # строка является изменяемой по своему указателю (использование unsafe) или просто посредством отражения (вы можете легко получить базовое поле). Это лишает смысла безопасность, поскольку любой, кто намеренно хочет изменить строку, может сделать это довольно легко. Тем не менее, он обеспечивает безопасность для программистов: если вы не сделаете что-то особенное, строка гарантированно неизменна (но это не потокобезопасно!).
Авель

Да, вы можете изменить байты любого объекта данных (строки, int и т. Д.) С помощью указателей. Однако мы говорим о том, почему строковый класс является неизменным в том смысле, что в него не встроены открытые методы для изменения его символов. Я говорил, что строка очень похожа на число, поскольку манипулирование отдельными символами не имеет больше смысла, чем манипулирование отдельными битами числа (когда вы рассматриваете строку как весь токен (а не как байтовый массив), а число как числовое значение (не как битовое поле). Мы говорим на уровне концептуальных объектов, а не на уровне
субобъектов

2
И просто чтобы уточнить, указатели в объектно-ориентированном коде по своей сути небезопасны, именно потому, что они обходят общедоступные интерфейсы, определенные для класса. Я говорил о том, что функцию можно легко обмануть, если открытый интерфейс для строки позволяет ее изменять другим потокам. Конечно, его всегда можно обмануть, получая доступ к данным напрямую с помощью указателей, но не так легко или непреднамеренно.
Triynko

1
«Указатели в объектно-ориентированном коде небезопасны», если вы не называете их ссылками . Ссылки в Java не отличаются от указателей в C ++ (отключена только арифметика указателей). Другая концепция - это управление памятью, которое может быть управляемым или ручным, но это совсем другое. Вы можете иметь эталонную семантику (указатели без арифметики) без GC (наоборот будет сложнее в том смысле, что семантику достижимости будет сложнее сделать чистой, но не невыполнимой)
Дэвид Родригес - dribeas

Другое дело, что если строки почти неизменны, но не совсем так (я не знаю достаточно CLI здесь), это может быть очень плохо по соображениям безопасности. В некоторых более старых реализациях Java вы могли сделать это, и я нашел фрагмент кода, который использовал это для интернализации строк (попробуйте найти другую внутреннюю строку с таким же значением, поделить указатель, удалить старый блок памяти) и использовал бэкдор переписать содержимое строки, вызывая некорректное поведение в другом классе. (Попробуйте переписать «SELECT *» в «DELETE»)
Дэвид Родригес - dribeas

3

Неизменность не так тесно связана с безопасностью. Для этого, по крайней мере, в .NET, вы получите SecureStringкласс.

Позже редактирование: в Java вы найдете GuardedStringаналогичную реализацию.


2

Решение иметь изменяемую строку в C ++ вызывает много проблем, см. Эту превосходную статью Келвина Хенни о Mad COW Disease .

COW = Копировать при записи.


2

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

Недостатком является то, что конкатенации производят много дополнительных функций, Stringкоторые являются только переходными и просто становятся мусором, фактически нанося ущерб производительности памяти. Вы должны StringBufferи StringBuilder(в Java, StringBuilderтакже в .NET) использовать для сохранения памяти в этих случаях.


1
Имейте в виду, что «пул строк» ​​не используется автоматически для ВСЕХ строк, если вы явно не используете строки «inter ()».
jsight

2

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


1
Я полагаю, что вы сможете делать такие трюки только в том случае, если ваш код работает с полным доверием, поэтому нет потери безопасности. Вы также можете использовать JNI для записи непосредственно в область памяти, где хранятся строки.
Антуан Обри

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

0

Неизменность это хорошо. Смотрите Эффективная Java. Если бы вам приходилось копировать строку каждый раз, когда вы ее передавали, то это было бы очень подверженным ошибкам кодом. У вас также есть путаница относительно того, какие модификации влияют на какие ссылки. Точно так же, как Integer должен быть неизменным, чтобы вести себя как int, строки должны вести себя как неизменные, чтобы действовать как примитивы. В C ++ передача строк по значению делает это без явного упоминания в исходном коде.


0

Существует исключение почти для каждого правила:

using System;
using System.Runtime.InteropServices;

namespace Guess
{
    class Program
    {
        static void Main(string[] args)
        {
            const string str = "ABC";

            Console.WriteLine(str);
            Console.WriteLine(str.GetHashCode());

            var handle = GCHandle.Alloc(str, GCHandleType.Pinned);

            try
            {
                Marshal.WriteInt16(handle.AddrOfPinnedObject(), 4, 'Z');

                Console.WriteLine(str);
                Console.WriteLine(str.GetHashCode());
            }
            finally
            {
                handle.Free();
            }
        }
    }
}

-1

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


1
Можете ли вы привести пример того, что вы подразумеваете под «взломом». Этот ответ чувствует себя действительно вне контекста.
Гергели Орос
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.