Чому саме вони вирішили зробити 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 цьому методу для обробки автентифікації, яка необхідна до того, як виклик буде переданий ОС. Якщо 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 Object. Якщо ваша програма динамічно створює однакові рядки, вони будуть представлені в різних об'єктах.
Що ще важливіше, незмінні рядки дозволяють їм ділитися внутрішніми даними. Для багатьох рядкових операцій це означає, що основний масив символів не потрібно копіювати. Наприклад, скажіть, що ви хочете взяти п’ять перших символів String. На Java ви б назвали myString.substring (0,5). У цьому випадку те, що робить метод substring () - це просто створити новий об'єкт String, який розділяє основний char myString [], але хто знає, що він починається з індексу 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 разів за кожен раз, коли рядок потрібно змінювати. Дивіться вікіпедію про незмінність .
Справді слід запитати: "чому X повинен бути змінним?" Краще за замовчуванням незмінність через переваги, про які вже згадувала принцеса Фуфф . Має бути винятком, що щось змінюється.
На жаль, більшість сучасних мов програмування за замовчуванням змінюється, але, сподіваємось, у майбутньому дефолт буде більше на незмінність (див . Список бажань для наступної основної мови програмування ).
Оце Так! Я не можу повірити дезінформації тут. String
s незмінні нічого не мають із захистом. Якщо хтось уже має доступ до об'єктів у запущеній програмі (про що варто було б припустити, якщо ви намагаєтесь захистити від того, щоб хтось "зламав" 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
як це 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 .
COW = Копіювати при записі.
Це торг. String
s перейдіть до String
пулу, і коли ви створите кілька однакових String
s, вони поділяють однакову пам'ять. Дизайнери вважали, що ця технологія збереження пам’яті буде добре працювати в загальній справі, оскільки програми, як правило, багато перемелюють одні й ті ж рядки.
Мінус полягає в тому, що конкатенації роблять багато зайвих String
, які є лише перехідними і просто стають сміттям, фактично шкодячи пам'яті. Ви повинні StringBuffer
і StringBuilder
(у Java StringBuilder
також є в .NET) використовувати для збереження пам'яті в цих випадках.
String
s на Java не справді незмінні, ви можете змінити їх значення за допомогою відображення та завантаження класу. Ви не повинні залежати від цього властивості для безпеки. Приклади див .: Чарівний трюк на Java
Незмінність - це добре. Див. Ефективна Java. Якщо вам довелося копіювати рядок щоразу, коли ви передавали її навколо, тоді це було б багато схильного до помилок коду. Ви також маєте плутанину щодо того, які зміни впливають на посилання. Таким же чином, як Integer повинен бути незмінним, щоб поводитись як int, Strings повинен поводитися як непорушний, щоб діяти як примітиви. У 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
s захищені неправомірно.