Почему они решили сделать String
неизменяемыми в Java и .NET (и некоторых других языках)? Почему они не сделали его изменчивым?
String
на самом деле изменчиво внутри. StringBuilder
в .NET 2.0 мутирует строку . Я просто оставлю это здесь.
Почему они решили сделать String
неизменяемыми в Java и .NET (и некоторых других языках)? Почему они не сделали его изменчивым?
String
на самом деле изменчиво внутри. StringBuilder
в .NET 2.0 мутирует строку . Я просто оставлю это здесь.
Ответы:
Согласно Эффективной Java , глава 4, стр. 73, 2-е издание:
«Для этого есть много веских причин: неизменяемые классы легче проектировать, реализовывать и использовать, чем изменяемые классы. Они менее подвержены ошибкам и более безопасны.
[...]
« Неизменяемые объекты просты. Неизменяемый объект может находиться точно в одном состоянии, в том состоянии, в котором он был создан. Если вы убедитесь, что все конструкторы устанавливают инварианты классов, то гарантируется, что эти инварианты будут оставаться верными в течение всего времени, с никаких усилий с вашей стороны.
[...]
Неизменяемые объекты по своей природе поточно-ориентированы; они не требуют синхронизации. Они не могут быть повреждены несколькими потоками, обращающимися к ним одновременно. Это, безусловно, самый простой подход к обеспечению безопасности потоков. Фактически, ни один поток не может наблюдать какое-либо влияние другого потока на неизменный объект. Следовательно, неизменяемые объекты могут свободно использоваться совместно.
[...]
Другие маленькие пункты из той же главы:
Вы можете не только делиться неизменными объектами, но и делиться своими внутренними объектами.
[...]
Неизменяемые объекты создают отличные строительные блоки для других объектов, как изменяемых, так и неизменных.
[...]
Единственным реальным недостатком неизменяемых классов является то, что им требуется отдельный объект для каждого отдельного значения.
report2.Text = report1.Text;
. Затем, где - то еще, изменяя текст: report2.Text.Replace(someWord, someOtherWord);
. Это изменило бы первый отчет, а также второй.
Есть как минимум две причины.
Первый - безопасность 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, если кто-то изменит значение, это повлияет на все ссылки. Итак, солнце решило сделать его неизменным.
На самом деле, строка с причинами неизменяемости в 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), поскольку операция не зависит ни от длины исходной строки, ни от длины подстроки, которую мы должны извлечь. Такое поведение также имеет некоторые преимущества памяти, так как многие строки могут использовать свой основной символ [].
char[]
является довольно сомнительным дизайнерским решением. Если вы читаете весь файл в одну строку и сохраняете ссылку только на 1-символьную подстроку, весь файл должен храниться в памяти.
String.substring()
выполняет полную копию, чтобы предотвратить проблемы, упомянутые в комментариях выше. В Java 8 два поля, обеспечивающие char[]
совместное использование, а именно, count
и offset
, удалены, тем самым уменьшая объем памяти экземпляров String.
Поток безопасности и производительности. Если строка не может быть изменена, безопасно и быстро передать ссылку между несколькими потоками. Если бы строки были изменяемыми, вам всегда приходилось бы копировать все байты строки в новый экземпляр или обеспечивать синхронизацию. Типичное приложение будет читать строку 100 раз за каждый раз, когда необходимо изменить строку. Смотрите википедию об неизменности .
Нужно спросить: «Почему Х должен быть изменчивым?» По умолчанию лучше использовать неизменяемость из-за преимуществ, уже упомянутых принцессой Пух . Должно быть исключение, что что-то изменчивое.
К сожалению, большинство современных языков программирования по умолчанию имеют изменчивость, но, надеюсь, в будущем по умолчанию будет больше неизменности (см . Список пожеланий для следующего основного языка программирования ).
Вот Это Да! Я не могу поверить дезинформации здесь. String
Быть неизменным не имеет ничего общего с безопасностью. Если у кого-то уже есть доступ к объектам в работающем приложении (что можно предположить, если вы пытаетесь защититься от того, что кто-то «взломал») String
в вашем приложении, у него наверняка будет много других возможностей для взлома.
Это довольно новая идея, что неизменность String
заключается в решении проблем потоков. Хммм ... У меня есть объект, который изменяется двумя разными потоками. Как мне решить это? синхронизировать доступ к объекту? Naawww ... давайте не будем никого менять объект - это решит все наши проблемы с параллельным выполнением! Фактически, давайте сделаем все объекты неизменяемыми, а затем мы можем удалить синхронизированный конструкт из языка Java.
Реальная причина (указанная другими выше) - это оптимизация памяти. В любом приложении достаточно часто использовать один и тот же строковый литерал. Фактически, это настолько распространено, что десятилетия назад многие компиляторы оптимизировали хранение только одного экземпляра String
литерала. Недостаток этой оптимизации заключается в том, что код времени выполнения, который модифицирует String
литерал, создает проблему, поскольку он модифицирует экземпляр для всего другого кода, который разделяет его. Например, было бы неправильно, чтобы функция где-то в приложении изменяла String
литерал "dog"
на "cat"
. А printf("dog")
приведет "cat"
к записи в стандартный вывод. По этой причине должен быть способ защиты от кода, который пытается изменитьString
литералы (т. е. сделать их неизменяемыми). Некоторые компиляторы (при поддержке со стороны ОС) могли бы выполнить это, поместив String
литерал в специальный сегмент памяти только для чтения, который мог вызвать ошибку памяти, если была предпринята попытка записи.
В Java это называется интернированием. Компилятор Java здесь просто следует стандартной оптимизации памяти, проводимой компиляторами в течение десятилетий. И для решения той же проблемы, String
связанной с изменением этих литералов во время выполнения, Java просто делает String
класс неизменным (т. Е. Не дает вам установщиков, которые позволили бы вам изменять String
содержимое). String
s не должен был бы быть неизменным, если интернирование String
литералов не происходило.
String
и StringBuffer
, но, к сожалению, немногие другие типы следуют этой модели.
String
не является примитивным типом, но обычно вы хотите использовать его с семантикой значения, то есть как значение.
Ценность, которой вы можете доверять, не изменится за вашей спиной. Если вы пишете: String str = someExpr();
вы не хотите, чтобы это изменилось, если вы не сделаете что-то с str
.
String
Так как an Object
имеет естественную семантику указателя, для получения семантики значения она также должна быть неизменной.
Одним из факторов является то, что, если бы String
s был изменяемым, объекты, хранящие String
s, должны были бы соблюдать осторожность при хранении копий, чтобы их внутренние данные не изменились без уведомления. Учитывая, что String
s - довольно примитивный тип, такой как числа, хорошо, когда можно обращаться с ними так, как будто они были переданы по значению, даже если они передаются по ссылке (что также помогает экономить память).
Я знаю, что это удар, но ... они действительно неизменны? Учтите следующее.
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 # не полностью неизменяемыми.
Для большинства целей «строка» - это (используется / рассматривается как / считается / считается) значимая атомная единица, как и число .
Ты должен знать почему. Просто подумай об этом.
Мне неприятно это говорить, но, к сожалению, мы обсуждаем это, потому что наш язык отстой, и мы пытаемся использовать одно слово, строку , чтобы описать сложную, контекстуально расположенную концепцию или класс объекта.
Мы выполняем вычисления и сравнения с «строками», аналогично тому, как мы делаем с числами. Если бы строки (или целые числа) были изменяемыми, мы должны были бы написать специальный код, чтобы зафиксировать их значения в неизменяемых локальных формах, чтобы надежно выполнять любые вычисления. Поэтому лучше всего рассматривать строку как числовой идентификатор, но вместо длины в 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 );
}
}
Безопасность - это не только «контроль доступа», но и «безопасность» и «обеспечение правильности». Если метод не может быть легко написан и зависит от надежного выполнения простого вычисления или сравнения, тогда его небезопасно вызывать, но было бы безопасно подвергать сомнению сам язык программирования.
unsafe
) или просто посредством отражения (вы можете легко получить базовое поле). Это лишает смысла безопасность, поскольку любой, кто намеренно хочет изменить строку, может сделать это довольно легко. Тем не менее, он обеспечивает безопасность для программистов: если вы не сделаете что-то особенное, строка гарантированно неизменна (но это не потокобезопасно!).
Неизменность не так тесно связана с безопасностью. Для этого, по крайней мере, в .NET, вы получите SecureString
класс.
Позже редактирование: в Java вы найдете GuardedString
аналогичную реализацию.
Решение иметь изменяемую строку в C ++ вызывает много проблем, см. Эту превосходную статью Келвина Хенни о Mad COW Disease .
COW = Копировать при записи.
Это компромисс. String
s входят в String
пул, и когда вы создаете несколько одинаковых String
s, они совместно используют одну и ту же память. Разработчики полагали, что этот метод экономии памяти будет хорошо работать в общем случае, поскольку программы имеют тенденцию многократно перебирать одни и те же строки.
Недостатком является то, что конкатенации производят много дополнительных функций, String
которые являются только переходными и просто становятся мусором, фактически нанося ущерб производительности памяти. Вы должны StringBuffer
и StringBuilder
(в Java, StringBuilder
также в .NET) использовать для сохранения памяти в этих случаях.
String
в Java не являются по-настоящему неизменными, вы можете изменить их значение, используя отражение и / или загрузку классов. Вы не должны зависеть от этого свойства в целях безопасности. Примеры см .: Магический трюк в Java
Неизменность это хорошо. Смотрите Эффективная Java. Если бы вам приходилось копировать строку каждый раз, когда вы ее передавали, то это было бы очень подверженным ошибкам кодом. У вас также есть путаница относительно того, какие модификации влияют на какие ссылки. Точно так же, как Integer должен быть неизменным, чтобы вести себя как int, строки должны вести себя как неизменные, чтобы действовать как примитивы. В C ++ передача строк по значению делает это без явного упоминания в исходном коде.
Существует исключение почти для каждого правила:
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();
}
}
}
}
Во многом по соображениям безопасности. Гораздо сложнее защитить систему, если вы не можете поверить, что ваши системы защищены от String
взлома.