Информация об изменениях

Сообщение Re[90]: Когда это наконец станет defined behavior? от 04.09.2023 12:53

Изменено 04.09.2023 13:03 vdimas

Re[90]: Когда это наконец станет defined behavior?
Здравствуйте, ·, Вы писали:

v>> ·>покажи пример кода как обеспечить иммутабельность через view мутабельного объекта.

v>> Легко — достаточно обеспечить отсутствие протекание ссылки на мутабельный объект.
v>> (например, создавать приватный мутабельный объект в конструкторе view)
·>Не очень понял что ты имеешь в виду, продемонстрируй кодом.

Берёшь достаточно сложный мутабельный объект и прикручиваешь сверху иммутабельный view.
Один из популярных сценариев — какой-нить глобальный справочник.
На шарпе пара способов:
////////////////////////////////
public struct ReadOnlyDictionary<TKey, TValue> where TKey : notnull 
{
    private Dictionary<TKey, TValue> _dict;
  
    public int Count => _dict.Count;

    public TValue this[TKey key] => _dict[key];

    // наполняем приватный словарь в конструкторе
    public ReadOnlyDictionary((TKey key, TValue value)[] data)
        => _dict = data.ToDictionary(item => item.key, item => item.value);

    // обёртка над существующим словарём -
    //   потенциально небезопасная конструкция, поэтому приватная
    private ReadOnlyDictionary(Dictionary<TKey, TValue> dict) 
        =>_dict = dict;

    // пусть программист выражает намерения явно
    public static ReadOnlyDictionary<TKey, TValue> Wrap(Dictionary<TKey, TValue> dict) 
        => new(dict);
}

////////////////////////////////
public static class SomeSubsystem 
{
    private static (int key, string value)[] GetData() => new (int, string)[] {
        (42, "42"), 
        (43, "43")
    };

    // храним и возвращаем иммутабельный view
    public static ReadOnlyDictionary<int, string> Dict1 { get; } = new(GetData());

    // храним приватный мутабельный словарь
    private static Dictionary<int, string> dict2 = GetData().ToDictionary(item => item.key, item => item.value);

    // возвращаем публичную иммутабельную обертку над приватным мутабельным объектом
    public static ReadOnlyDictionary<int, string> Dict2 => ReadOnlyDictionary<int, string>.Wrap(dict2);
}

////////////////////////////////
class Program
{
    static void Main() {
         Console.WriteLine(SomeSubsystem.Dict1[42]);
         Console.WriteLine(SomeSubsystem.Dict2[43]);
    }
}



v>> ·>Вопрос в том, как же компилятор проверяет/помогает гарантировать отстуствие протечек с помощью const?

v>> Достаточно просто — объявляешь данные как const и они автоматом становятся иммутабельными.
·>А дальше что? В лучшем случае ты их можешь такими сделать в пределах локальной переменной или поля класса.

Для глобальных переменных аналогично.


·>В другом скопе он уже теряет иммутабельность.


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


v>> Иммутабельность автоматически распространяется при владении другими объектами по-значению.

·>По значению это и есть defensive copy.

Э, нет. ))
Это в джаве такая техника, связанная с тем, что все объекты имеют ссылочную семантику.
Тогда, если объект не предоставляет явного read-only АПИ, необходимо создавать его копию при подаче такого объекта в кач-ве аргументов, чтобы вызываемый код не изменил состояние вызывающего кода.

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

В дотнете такие вещи обыгрываются автоматически через value-type, где при передаче по-значению копия создаётся автоматом.
Второй способ — это передача readonly-ссылки на value-type значение.

Первый способ является defensive copy, второй способ заимствован у плюсов (причём, относительно недавно).

Ну и, для GC-объектов в дотнете ситуация ровно как в джаве и решается точно так же.


v>> ·>Ответ — да никак.

v>> Для этого необходимо показать хотя бы минимальный пример этого "никак".
·>Пример чего? Пример кода, который никак не написать??!

Пример нарушения иммутабельности константного объекта.


v>> ·>Важное свойство иммутабельных объектов в том, что их копии никак не отличимы от оригинала и копирование можно избегать.

v>> Это лишь один из трюков, достижимых при иммутабельности.
v>> Так же как шаренье иммутабельных данных без блокировки м/у потоками.
·>Угу. А константные объекты даже пошарить нельзя.

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


v>> Но иммутабельные данные необходимо как-то создавать.

v>> И наиболее эффективно их создавать в мутабельной манере.
v>> Я уже отсылал к паттерну mutable_builder => immutable_object.
·>Да, но там создаётся иммутабельная копия из данных мутабельного билдера. Как и в шарпах всяческих.

Есть еще несколько вариантов:

  • Данные не пересоздаются в конце работы builder, а меняют своего владельца, сам builder помечает этот факт в данных и, если построение данных продолжится после выдачи иммутабельного объекта, то builder создаст себе новую копию данных для дальнейшего над ними измывательства;

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

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

    Тут происходит экономия на флагах в самих данных (флаги помечают — рассматриваются ли данные уже как иммутабельные или еще нет).

    Например, для иммутабельных деревьев в шарпе такая пометка живёт в каждом узле, что для некоторых сценариев накладно (дерево большое и является более init-only, чем CRUD). В этом случае заруливает всё и вся read-only view над данными, как показал в примере выше.


    v>> Вот после создания иммутабельного объекта, все эти трюки можно использовать.

    ·>С иммутабельным можно. С константным — не всегда, только компилятор гарантии не даёт.

    Я попросил пример, где компилятор не даёт гарантии.

    v>> ·>Как и в Плюсах.

    v>> В плюсах компилятор больно бъёт тебе по пальцам.
    ·>Компилятор ничего про иммутабельность не знает, поэтому по пальцам бить тупо не может. Нет такого понятия в стандарте.

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


    ·>ИЧСХ, ты это и так прекрасно понимаешь (когда, например, ты скромно "забыл" назвать объект типа const SomeObj иммутабельным). Я не понимаю зачем ты тут этот цирк устраиваешь?


    На самом деле я нифига не понимаю твою логику.
    Смешались в кучу люди, кони...

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

    А вот это твоё переливание из пустого в порожнее чиста на словах/эмоциях...


    v>> Собсно, новички в плюсах зачастую маются с компиллированием программ именно из-за попыток нарушения константности.

    ·>Это, кстати, тоже минус.

    Этот минус живёт пару дней обычно у новичка.
    Затем сплошной плюс плюс. ))


    ·>С интерфейсами же всё проще — есть метод, можно дёрнуть, нет метода — нельзя дёрнуть.


    Ну... если бы существовал идеально сбалансированный ЯП, не было бы так много других. ))

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


    v>> ·>Ровно твои слова же: "Если протечек ссылок для мутабельности нет, то автоматом получается иммутабельность". К чему сарказм-то?

    v>> К тому, что компилятор в плюсах отслеживает попытки "протечек" и отказывается компилировать такой код.
    ·>Не отслеживает. Сам же ниже пишешь, например, о сценарии утечки this из конструктора.

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

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

    Тогда строгая иммутабельность объекта будет обеспечена даже без ключевого слова const перед переменной его типа. ))

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


    v>> ·>Вопрос в том, как собственно обеспечить отсутствие протечек.

    v>> result[{42, 43}] = "42, 43";
    ·>Здесь формально копиктор struct срабатывает.

    Здесь сам словарь мутабельный, ключ словаря иммутабельный, данные по ключу перезаписываемы.
    result[index] возвращает non-const ссылку на значение, по этой ссылке происходит присвоение нового значения.

    Кстате, operator[] для map в плюсах имеет non-const сигнатуру и это порой неудобно.
    Можно было добавить рядом аналогичную const-сигатуру и возвращать константную ссылку, потому как это сделано для метода at.

    Т.е. можно было так:
    value & operator[](const key & index) { ... }
    
    const value & operator[](const key & index) const { ... }

    Второй вариант вызывается для константных объектов, соответственно, присвоить по константной ссылке ничего нельзя (это не может быть left-value), можно только прочитать (right-value).


    v>> // но здесь иммутабельный объект

    v>> const SomeDictionary dict = buildDictionary();
    ·>И здесь формально копия.

    Формально копия, верно.
    А на деле никакой копии — дополнительные временные объекты не создаются.


    ·>Но допустим у тебя есть этот самый dict офигенного размера и он тут на стеке вроде как иммутабельный. Как его теперь обработать из другого треда так, чтобы этот самый другой тред был уверен, что ему передают только иммутабельные объекты?


    Это зависит от взаимной времени жизни — поток не должен пережить время жизни такой переменной (например, создали nfreтаку переменную в main и передале программе дальше).
    Или же такая переменная может быть глобальной/статической, как я и показал в примере.


    v>> В данном примере гарантируется, что переменная dict иммутабельная, хотя на этапе построения мы пошагово мутировали значение.

    ·>Это немного враньё. Значение мы мутировали у result, а dict это копия.

    Формально да.
    А на деле происходили операции в одной области памяти, т.е. получился классический кейз иммутабельных данных — init-only.

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

    Работает этот трюк за счёт того, что время жизни объекта не обязательно равно времени жизни переменной.
    Именно поэтому описание init-only сценариев настолько простое/элегантное — здравствуй выразительность! ))


    v>> Но даже в процессе мутации ключи таблицы были гарантированно иммутабельны.

    ·>Потому что копии.

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


    v>> ·>Допустим, у тебя есть где-то const SomeThing someValue(42); — ты гарантируешь, что someValue — иммутабельно?

    v>> Без хаков если, то да.
    ·>И если из конструктора this не утекает, например.

    Выше отписывался по этому кейзу.


    v>> Данные/значения — это термин из механики происходящего, где by ref или by value имеют однозначную интерпретацию.

    ·>by value это скучный случай, но т.к. там копирование, то не всегда лучший.

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


    v>> Здесь оператор new возвращает указатель на неконстантный объект, но мы его сохраняем в указатель на константный объект.

    v>> Если конструктор SomeObject не имеет побочных эффектов (не сохраняет указатель на себя где-нить в глобальной переменной, т.е. если нет протечек), то гарантии получаются железобетонные.
    ·>Это тавтология какая-то. "Если гарантии есть, то гарантии есть". Вопрос и состоит в том — откуда собственно берутся эти гарантии отсутствия побочек и протечек? Ответ — обеспечиваются программистом. Вывод —
    ·>ты словоблудишь, т.к. речь идёт о гарантиях, даваемых компилятором.

    Я уже упомянул вариант сделать все поля объекта const.
    Т.е. возможность получить железобетонные гарантии тоже есть.
    Тогда, даже если this протечёт, через этот this невозможно будет изменить init-only данные.

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


    ·>Более того, в реальном коде могут быть и чуть менее тривиальные куски кода, стоит чуток порефакторить и вместо const SomeObject * obj = new SomeObject(); сделать const SomeObject * obj = makeSomeObject(); — так компилятору становится вообще плевать на все твои const-старания. А в том же шарпе ImmutableList означает именно что иммутабельный список и ничего более, со 100% гарантией компилятора, без всяких допущений и недосказок.


    Тогда обернуть объект в read-only view.


    ·>Угу. Верно. И в данном случае в java компилятор помогает больше. Наличие record тебе даёт гарантированно иммутабельный тип, и протечек никаких быть не может.


    Угу, теперь построй на своих record реализацию иммутабельного словаря (в т.ч. хеш-таблицы) или приоритетной очереди.


    v>> Да, в плюсах можно разрабатывать достоверно иммутабельные типы данных:

    v>> автоматом унаследовав всю инфраструктуру вокруг исходного типа, например, вычисление хеш-функции.
    ·>В java вместо этой всей колбасы в коде будет только @Builder record Index(int a, int b){} — создастся два типа — сам иммутабельный record и builder для него. Может не так красиво, зато билдер делается не в спеке ЯП, а либой.

    Это всё понятно, если самому всё с 0-ля писать (и то, вопрос выше в силе).
    Непонятно как использовать тысячи уже готовых типов в имммутабельных сценариях.

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


    v>> По константной ссылке/указателю ты можешь ссылаться на мутабельные и иммутабельные объекты, делая код ортогональным для данных.

    ·>Ага. Но не получается сделать ссылку на иммутабельный объект.

    Абсолютно верно.
    Эта ссылка может ссылаться как на мутабельный, так и на иммутабельный объект.

    Следовательно, гарантии происходят от того, кто эту ссылку передаёт во внешний мир — ведь только владелец данных "знает" характер своих данных.
    А выполняемый read-only код единообразен (например, банальное логирование), отсюда ортогональность кода к данным.


    v>> ·>Это ложится на программиста, что в плюсах, что в джаве, что в шарпах. const для иммутабельности не нужен. Нужен record.

    v>> Это вам в джаве нужен, т.к. иммутабельность обеспечивается прикладной семантикой, где record — лишь синтаксический сахар для такой семантики.
    ·>Ну не совсем. В java обычно не делают что-то ради одной цели. records ввели в яп ещё и для реализации pattern matching.

    Это, скорее, следствие.
    И вообще, отдельная тема.
    В какую-то версию шарпа ввели Deconstruct для паттерн-матчинга (который матчил только туплы и рекорды, на манер которых позже сделали джавовские)... А потом допилили паттерн-матчинг до спецификации полей через is { Field1 == X }, что теперь Deconstruct стал не нужным, бгг...


    v>> Но в типе SomeDictionary const index резко становится иммутабельным.

    ·>Этот случай, кстати, поинтереснее. Т.к. это не голая структура, а есть куча всяких методов. И, внезапно, выясняется, что хоть и "резко", но уже не нахаляву — интерфейс SomeDictionary уже надо внимательно проектировать с разделением const/nonconst методов.

    Ес-но.
    Но это всё еще одно описание типа, где ты просто помечаешь read-only методы через const.

    У плюсовиков этот рефлекс на уровне мозжечка, бо является еще дополнительным документированием/аннотированием методов — профит! ))
    "Читать" такие типы потом проще.


    v>> — ссылки на константные данные не гарантируют константность тех данных, они накладывают ограничения — позволяют использовать лишь иммутабельное АПИ объектов.

    ·>константное, а не иммутабельное. Не жонглируй терминами.

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


    v>> Как именно данные случаются неизменяемыми — да как угодно.

    v>> Можно и безо-всяких const, record, final и прочих приблуд, достаточно того, чтобы в момент работы таких алгоритмов данные никто не изменял.
    ·>Гарантию "никто не изменял" компилятор может дать лишь для иммутабельных типов.

    Это всё до некоторой степени, пока не пошли хаки, например, через реинтерпретацию данных или рефлексию.

    Лишь малое кол-во языков способно обеспечить доказуемость корректности своего кода.
    И ведь не просто так эти языки нифига не мейнстримовые?

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


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

    ·>Так это константность называется, а не иммутабельность, опять термины поменяешь.

    Разве?

    Неизменяемым (англ. immutable) называется объект, состояние которого не может быть изменено после создания.

    В языках программирования C, C++, C# и D const является квалификатором типа: ключевое слово применяется к типу данных, показывая, что данные константны (неизменяемы).

    Если бы ключевое слово было не const, а immutable — тебе было бы легче? ))


    ·>Если же прекратить черри-пикать факты, то надо рассуждать не лишь о том, что тебе по душе, но и об остальных аспектах, и ещё порассуждать о том не как "хотим рассматривать", а как иметь иммутабельность с гарантией компилятора.


    Гарантии в любом случае недоказуемы в обсуждаемых языках.

    В джаве можно допустить ошибку — изменив данные через некий метод интерфейса, который (интерфейс) согласно прикладной семантики должен был давать read-only view.
    В плюсах можно забыть приписать полю const и однажды наступить в это, и т.д. до бесконечности.

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

    В этих вариантах останется обсуждать лишь выразительность (т.е. трудоёмкость готового решения) и ничего более.


    v>> В общем, подход плюсов оказался настолько удобным, что его перетянули и в C# (но только для value-типов) и даже в последние версии FreePascal.

    v>> Ранее в этих языках был доступен только подход джавы.
    ·>Не очень понял, что ты имеешь в виду перетянулы в шарп?

    В шарпе появились константные ссылки — "readonly ref", или кратко "in" для параметров (применяется для value-типов и для переменных-ссылок на GC-типы), а так же полные аналоги const-методов из плюсов — это "readonly" методы у value-типов.

    Для борьбы с потенциальными протечками локальных ссылок ввели ref struct — это такие value-типы, которые могут жить только на стеке.
    А так же ввели инструмент контроля за протечками — ключевое слово scope.
    Более подробно расписано в доке, там достаточно интересно. ))

    Всё вместе служит той же цели — выразительности кода при достижении тех или иных гарантий.


    v>> ·>Нет, не подмножеством. read-only view для мутабельного объекта — не подмножество иммутабельного сценария, ну никак.

    v>> Еще как подмножество.
    ·>Я привёл явный пример когда это не так.

    Не видел.
    И оно не может быть "не так" согласно определению иммутабельности и семантики кода, получающего read-only доступ к данным.


    v>> Наоборот, упрощение, бо комбинаторный подход позволяет упростить базу — меньше писать.

    ·>Даже по количеству буковок не сильно отличается.

    Не скажи.))

    Я привёл примёр read-only view на шарпе (на джаве можно аналогично, разве что +1 к косвенности), а в плюсах достаточно было простого ключевого слова const безо всей этой возни.


    v>> Одним и тем же кодом в плюсах ты можешь проходиться по мутабельному и иммутабельному дереву, например.

    v>> В джаве тебе пришлось бы делать два типа дерева и два кода для их обхода (или выкручиваться через интерфйесы, что утяжеляет/тормозит).
    ·>Я уже об этом упоминал. Нет никакого утяжеления. Интерфейсы в яве практически бесплатны.

    Вранье. ))
    Интерфейсы в джаве намного тяжелее прямых методов.
    Бесплатны они только если компилятор или JIT "видят" жизненный цикл ссылки — тогда может заменить вызов метода через интерфейс прямым вызовом.
    В плюсах аналогично.

    Стоимость же честного вызова интерфейса в плюсах дешевле, бо в плюсах объект ссылается на свою vtable явно, а в джаве и шарпе — через промежуточный дескриптор типа, т.е. +1 к косвенности, в сравнении с плюсами.


    v>> ·>константность — не является необходимостью

    v>> Разумеется, константность не является необходимостью для иммутабельных сценариев.
    v>> Она лишь дико экономит труд програмиста и размер конечного бинаря, а так-то фигня полная.
    ·>Не экономит она труд.

    Выше пример, который не нужен в плюсах.


    ·>Насчёт размера тут тоже сложно всё. Неясно как можно адекватно сравнивать бинари плюсов и явы... слишком много нюансов.


    Надо сравнивать бинари в рамках одной техонлогии.
    Например, можно сравнивать бинари в плюсах при применении подхода джавы vs родного плюсового подхода.
    Или можно сравнивать бинари в шарпе, где доступны оба подхода для value-типов.

    В любом случае, лишний код есть лишний код.


    v>> ·>и можно нафиг выкинуть, что собственно и сделали в шарпах-явах.

    v>> Наоборот, в шарпе вот относительно недавно добавили.
    v>> А потом опять добавили.
    v>> Ключевое слово чуть другое — readonly, но смысл и сценарии использования те же.
    ·>Если я правильно помню, то это аналог final-поля, который был в java с рождения.

    Это изначально применительно к полям объектов.
    Затем readonly стало можно применять к аргументам методов и возвращаемым значениям.
    Затем через readonly стало можно помечать методы, где этот метод не может изменять внутренние поля — компилятор не даст.


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

    ·>Не понимаю что ты понимаешь под сложностью.

    Лишний код.
    Тот же твой Defensive copying как малая часть этой сложности.
    Например, в дотнете достаточно передавать DateTime по значению или по константной ссылке, а в джаве надо делать GC-копию объекта на каждый чих.
    Да там уже просто рука устаёт всё это внимательно описывать. ))
  • Re[90]: Когда это наконец станет defined behavior?
    Здравствуйте, ·, Вы писали:

    v>> ·>покажи пример кода как обеспечить иммутабельность через view мутабельного объекта.

    v>> Легко — достаточно обеспечить отсутствие протекание ссылки на мутабельный объект.
    v>> (например, создавать приватный мутабельный объект в конструкторе view)
    ·>Не очень понял что ты имеешь в виду, продемонстрируй кодом.

    Берёшь достаточно сложный мутабельный объект и прикручиваешь сверху иммутабельный view.
    Один из популярных сценариев — какой-нить глобальный справочник.
    На шарпе пара способов:
    ////////////////////////////////
    public struct ReadOnlyDictionary<TKey, TValue> where TKey : notnull 
    {
        private Dictionary<TKey, TValue> _dict;
      
        public int Count => _dict.Count;
    
        public TValue this[TKey key] => _dict[key];
    
        // наполняем приватный словарь в конструкторе
        public ReadOnlyDictionary((TKey key, TValue value)[] data)
            => _dict = data.ToDictionary(item => item.key, item => item.value);
    
        // обёртка над существующим словарём -
        //   потенциально небезопасная конструкция, поэтому приватная
        private ReadOnlyDictionary(Dictionary<TKey, TValue> dict) 
            =>_dict = dict;
    
        // пусть программист выражает намерения явно
        public static ReadOnlyDictionary<TKey, TValue> Wrap(Dictionary<TKey, TValue> dict) 
            => new(dict);
    }
    
    ////////////////////////////////
    public static class SomeSubsystem 
    {
        private static (int key, string value)[] GetData() => new (int, string)[] {
            (42, "42"), 
            (43, "43")
        };
    
        // храним и возвращаем иммутабельный view
        public static ReadOnlyDictionary<int, string> Dict1 { get; } = new(GetData());
    
        // храним приватный мутабельный словарь
        private static Dictionary<int, string> dict2 = GetData().ToDictionary(item => item.key, item => item.value);
    
        // возвращаем публичную иммутабельную обертку над приватным мутабельным объектом
        public static ReadOnlyDictionary<int, string> Dict2 => ReadOnlyDictionary<int, string>.Wrap(dict2);
    }
    
    ////////////////////////////////
    class Program
    {
        static void Main() {
             Console.WriteLine(SomeSubsystem.Dict1[42]);
             Console.WriteLine(SomeSubsystem.Dict2[43]);
        }
    }



    v>> ·>Вопрос в том, как же компилятор проверяет/помогает гарантировать отстуствие протечек с помощью const?

    v>> Достаточно просто — объявляешь данные как const и они автоматом становятся иммутабельными.
    ·>А дальше что? В лучшем случае ты их можешь такими сделать в пределах локальной переменной или поля класса.

    Для глобальных переменных аналогично.


    ·>В другом скопе он уже теряет иммутабельность.


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


    v>> Иммутабельность автоматически распространяется при владении другими объектами по-значению.

    ·>По значению это и есть defensive copy.

    Э, нет. ))
    Это в джаве такая техника, связанная с тем, что все объекты имеют ссылочную семантику.
    Тогда, если объект не предоставляет явного read-only АПИ, необходимо создавать его копию при подаче такого объекта в кач-ве аргументов, чтобы вызываемый код не изменил состояние вызывающего кода.

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

    В дотнете такие вещи обыгрываются автоматически через value-type, где при передаче по-значению копия создаётся автоматом.
    Второй способ — это передача readonly-ссылки на value-type значение.

    Первый способ является defensive copy, второй способ заимствован у плюсов (причём, относительно недавно).

    Ну и, для GC-объектов в дотнете ситуация ровно как в джаве и решается точно так же.


    v>> ·>Ответ — да никак.

    v>> Для этого необходимо показать хотя бы минимальный пример этого "никак".
    ·>Пример чего? Пример кода, который никак не написать??!

    Пример нарушения иммутабельности константного объекта.


    v>> ·>Важное свойство иммутабельных объектов в том, что их копии никак не отличимы от оригинала и копирование можно избегать.

    v>> Это лишь один из трюков, достижимых при иммутабельности.
    v>> Так же как шаренье иммутабельных данных без блокировки м/у потоками.
    ·>Угу. А константные объекты даже пошарить нельзя.

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


    v>> Но иммутабельные данные необходимо как-то создавать.

    v>> И наиболее эффективно их создавать в мутабельной манере.
    v>> Я уже отсылал к паттерну mutable_builder => immutable_object.
    ·>Да, но там создаётся иммутабельная копия из данных мутабельного билдера. Как и в шарпах всяческих.

    Есть еще несколько вариантов:

  • Данные не пересоздаются в конце работы builder, а меняют своего владельца, сам builder помечает этот факт в данных и, если построение данных продолжится после выдачи иммутабельного объекта, то builder создаст себе новую копию данных для дальнейшего над ними измывательства;

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

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

    Тут происходит экономия на флагах в самих данных (флаги помечают — рассматриваются ли данные уже как иммутабельные или еще нет).

    Например, для иммутабельных деревьев в шарпе такая пометка живёт в каждом узле, что для некоторых сценариев накладно (дерево большое и является более init-only, чем CRUD). В этом случае заруливает всё и вся read-only view над данными, как показал в примере выше.


    v>> Вот после создания иммутабельного объекта, все эти трюки можно использовать.

    ·>С иммутабельным можно. С константным — не всегда, только компилятор гарантии не даёт.

    Я попросил пример, где компилятор не даёт гарантии.

    v>> ·>Как и в Плюсах.

    v>> В плюсах компилятор больно бъёт тебе по пальцам.
    ·>Компилятор ничего про иммутабельность не знает, поэтому по пальцам бить тупо не может. Нет такого понятия в стандарте.

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


    ·>ИЧСХ, ты это и так прекрасно понимаешь (когда, например, ты скромно "забыл" назвать объект типа const SomeObj иммутабельным). Я не понимаю зачем ты тут этот цирк устраиваешь?


    На самом деле я нифига не понимаю твою логику.
    Смешались в кучу люди, кони...

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

    А вот это твоё переливание из пустого в порожнее чиста на словах/эмоциях...


    v>> Собсно, новички в плюсах зачастую маются с компиллированием программ именно из-за попыток нарушения константности.

    ·>Это, кстати, тоже минус.

    Этот минус живёт пару дней обычно у новичка.
    Затем сплошной плюс плюс. ))


    ·>С интерфейсами же всё проще — есть метод, можно дёрнуть, нет метода — нельзя дёрнуть.


    Ну... если бы существовал идеально сбалансированный ЯП, не было бы так много других. ))

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


    v>> ·>Ровно твои слова же: "Если протечек ссылок для мутабельности нет, то автоматом получается иммутабельность". К чему сарказм-то?

    v>> К тому, что компилятор в плюсах отслеживает попытки "протечек" и отказывается компилировать такой код.
    ·>Не отслеживает. Сам же ниже пишешь, например, о сценарии утечки this из конструктора.

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

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

    Тогда строгая иммутабельность объекта будет обеспечена даже без ключевого слова const перед переменной его типа. ))

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


    v>> ·>Вопрос в том, как собственно обеспечить отсутствие протечек.

    v>> result[{42, 43}] = "42, 43";
    ·>Здесь формально копиктор struct срабатывает.

    Здесь сам словарь мутабельный, ключ словаря иммутабельный, данные по ключу перезаписываемы.
    result[index] возвращает non-const ссылку на значение, по этой ссылке происходит присвоение нового значения.

    Кстате, operator[] для map в плюсах имеет non-const сигнатуру и это порой неудобно.
    Можно было добавить рядом аналогичную const-сигатуру и возвращать константную ссылку, потому как это сделано для метода at.

    Т.е. можно было так:
    value & operator[](const key & index) { ... }
    
    const value & operator[](const key & index) const { ... }

    Второй вариант вызывается для константных объектов, соответственно, присвоить по константной ссылке ничего нельзя (это не может быть left-value), можно только прочитать (right-value).


    v>> // но здесь иммутабельный объект

    v>> const SomeDictionary dict = buildDictionary();
    ·>И здесь формально копия.

    Формально копия, верно.
    А на деле никакой копии — дополнительные временные объекты не создаются.


    ·>Но допустим у тебя есть этот самый dict офигенного размера и он тут на стеке вроде как иммутабельный. Как его теперь обработать из другого треда так, чтобы этот самый другой тред был уверен, что ему передают только иммутабельные объекты?


    Это зависит от взаимного времени жизни — поток не должен пережить время жизни такой переменной (например, создали такую переменную в main и передали программе дальше).
    Или же такая переменная может быть глобальной/статической, как я и показал в примере.


    v>> В данном примере гарантируется, что переменная dict иммутабельная, хотя на этапе построения мы пошагово мутировали значение.

    ·>Это немного враньё. Значение мы мутировали у result, а dict это копия.

    Формально да.
    А на деле происходили операции в одной области памяти, т.е. получился классический кейз иммутабельных данных — init-only.

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

    Работает этот трюк за счёт того, что время жизни объекта не обязательно равно времени жизни переменной.
    Именно поэтому описание init-only сценариев настолько простое/элегантное — здравствуй выразительность! ))


    v>> Но даже в процессе мутации ключи таблицы были гарантированно иммутабельны.

    ·>Потому что копии.

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


    v>> ·>Допустим, у тебя есть где-то const SomeThing someValue(42); — ты гарантируешь, что someValue — иммутабельно?

    v>> Без хаков если, то да.
    ·>И если из конструктора this не утекает, например.

    Выше отписывался по этому кейзу.


    v>> Данные/значения — это термин из механики происходящего, где by ref или by value имеют однозначную интерпретацию.

    ·>by value это скучный случай, но т.к. там копирование, то не всегда лучший.

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


    v>> Здесь оператор new возвращает указатель на неконстантный объект, но мы его сохраняем в указатель на константный объект.

    v>> Если конструктор SomeObject не имеет побочных эффектов (не сохраняет указатель на себя где-нить в глобальной переменной, т.е. если нет протечек), то гарантии получаются железобетонные.
    ·>Это тавтология какая-то. "Если гарантии есть, то гарантии есть". Вопрос и состоит в том — откуда собственно берутся эти гарантии отсутствия побочек и протечек? Ответ — обеспечиваются программистом. Вывод —
    ·>ты словоблудишь, т.к. речь идёт о гарантиях, даваемых компилятором.

    Я уже упомянул вариант сделать все поля объекта const.
    Т.е. возможность получить железобетонные гарантии тоже есть.
    Тогда, даже если this протечёт, через этот this невозможно будет изменить init-only данные.

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


    ·>Более того, в реальном коде могут быть и чуть менее тривиальные куски кода, стоит чуток порефакторить и вместо const SomeObject * obj = new SomeObject(); сделать const SomeObject * obj = makeSomeObject(); — так компилятору становится вообще плевать на все твои const-старания. А в том же шарпе ImmutableList означает именно что иммутабельный список и ничего более, со 100% гарантией компилятора, без всяких допущений и недосказок.


    Тогда обернуть объект в read-only view.


    ·>Угу. Верно. И в данном случае в java компилятор помогает больше. Наличие record тебе даёт гарантированно иммутабельный тип, и протечек никаких быть не может.


    Угу, теперь построй на своих record реализацию иммутабельного словаря (в т.ч. хеш-таблицы) или приоритетной очереди.


    v>> Да, в плюсах можно разрабатывать достоверно иммутабельные типы данных:

    v>> автоматом унаследовав всю инфраструктуру вокруг исходного типа, например, вычисление хеш-функции.
    ·>В java вместо этой всей колбасы в коде будет только @Builder record Index(int a, int b){} — создастся два типа — сам иммутабельный record и builder для него. Может не так красиво, зато билдер делается не в спеке ЯП, а либой.

    Это всё понятно, если самому всё с 0-ля писать (и то, вопрос выше в силе).
    Непонятно как использовать тысячи уже готовых типов в имммутабельных сценариях.

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


    v>> По константной ссылке/указателю ты можешь ссылаться на мутабельные и иммутабельные объекты, делая код ортогональным для данных.

    ·>Ага. Но не получается сделать ссылку на иммутабельный объект.

    Абсолютно верно.
    Эта ссылка может ссылаться как на мутабельный, так и на иммутабельный объект.

    Следовательно, гарантии происходят от того, кто эту ссылку передаёт во внешний мир — ведь только владелец данных "знает" характер своих данных.
    А выполняемый read-only код единообразен (например, банальное логирование), отсюда ортогональность кода к данным.


    v>> ·>Это ложится на программиста, что в плюсах, что в джаве, что в шарпах. const для иммутабельности не нужен. Нужен record.

    v>> Это вам в джаве нужен, т.к. иммутабельность обеспечивается прикладной семантикой, где record — лишь синтаксический сахар для такой семантики.
    ·>Ну не совсем. В java обычно не делают что-то ради одной цели. records ввели в яп ещё и для реализации pattern matching.

    Это, скорее, следствие.
    И вообще, отдельная тема.
    В какую-то версию шарпа ввели Deconstruct для паттерн-матчинга (который матчил только туплы и рекорды, на манер которых позже сделали джавовские)... А потом допилили паттерн-матчинг до спецификации полей через is { Field1 == X }, что теперь Deconstruct стал не нужным, бгг...


    v>> Но в типе SomeDictionary const index резко становится иммутабельным.

    ·>Этот случай, кстати, поинтереснее. Т.к. это не голая структура, а есть куча всяких методов. И, внезапно, выясняется, что хоть и "резко", но уже не нахаляву — интерфейс SomeDictionary уже надо внимательно проектировать с разделением const/nonconst методов.

    Ес-но.
    Но это всё еще одно описание типа, где ты просто помечаешь read-only методы через const.

    У плюсовиков этот рефлекс на уровне мозжечка, бо является еще дополнительным документированием/аннотированием методов — профит! ))
    "Читать" такие типы потом проще.


    v>> — ссылки на константные данные не гарантируют константность тех данных, они накладывают ограничения — позволяют использовать лишь иммутабельное АПИ объектов.

    ·>константное, а не иммутабельное. Не жонглируй терминами.

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


    v>> Как именно данные случаются неизменяемыми — да как угодно.

    v>> Можно и безо-всяких const, record, final и прочих приблуд, достаточно того, чтобы в момент работы таких алгоритмов данные никто не изменял.
    ·>Гарантию "никто не изменял" компилятор может дать лишь для иммутабельных типов.

    Это всё до некоторой степени, пока не пошли хаки, например, через реинтерпретацию данных или рефлексию.

    Лишь малое кол-во языков способно обеспечить доказуемость корректности своего кода.
    И ведь не просто так эти языки нифига не мейнстримовые?

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


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

    ·>Так это константность называется, а не иммутабельность, опять термины поменяешь.

    Разве?

    Неизменяемым (англ. immutable) называется объект, состояние которого не может быть изменено после создания.

    В языках программирования C, C++, C# и D const является квалификатором типа: ключевое слово применяется к типу данных, показывая, что данные константны (неизменяемы).

    Если бы ключевое слово было не const, а immutable — тебе было бы легче? ))


    ·>Если же прекратить черри-пикать факты, то надо рассуждать не лишь о том, что тебе по душе, но и об остальных аспектах, и ещё порассуждать о том не как "хотим рассматривать", а как иметь иммутабельность с гарантией компилятора.


    Гарантии в любом случае недоказуемы в обсуждаемых языках.

    В джаве можно допустить ошибку — изменив данные через некий метод интерфейса, который (интерфейс) согласно прикладной семантики должен был давать read-only view.
    В плюсах можно забыть приписать полю const и однажды наступить в это, и т.д. до бесконечности.

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

    В этих вариантах останется обсуждать лишь выразительность (т.е. трудоёмкость готового решения) и ничего более.


    v>> В общем, подход плюсов оказался настолько удобным, что его перетянули и в C# (но только для value-типов) и даже в последние версии FreePascal.

    v>> Ранее в этих языках был доступен только подход джавы.
    ·>Не очень понял, что ты имеешь в виду перетянулы в шарп?

    В шарпе появились константные ссылки — "readonly ref", или кратко "in" для параметров (применяется для value-типов и для переменных-ссылок на GC-типы), а так же полные аналоги const-методов из плюсов — это "readonly" методы у value-типов.

    Для борьбы с потенциальными протечками локальных ссылок ввели ref struct — это такие value-типы, которые могут жить только на стеке.
    А так же ввели инструмент контроля за протечками — ключевое слово scope.
    Более подробно расписано в доке, там достаточно интересно. ))

    Всё вместе служит той же цели — выразительности кода при достижении тех или иных гарантий.


    v>> ·>Нет, не подмножеством. read-only view для мутабельного объекта — не подмножество иммутабельного сценария, ну никак.

    v>> Еще как подмножество.
    ·>Я привёл явный пример когда это не так.

    Не видел.
    И оно не может быть "не так" согласно определению иммутабельности и семантики кода, получающего read-only доступ к данным.


    v>> Наоборот, упрощение, бо комбинаторный подход позволяет упростить базу — меньше писать.

    ·>Даже по количеству буковок не сильно отличается.

    Не скажи.))

    Я привёл примёр read-only view на шарпе (на джаве можно аналогично, разве что +1 к косвенности), а в плюсах достаточно было простого ключевого слова const безо всей этой возни.


    v>> Одним и тем же кодом в плюсах ты можешь проходиться по мутабельному и иммутабельному дереву, например.

    v>> В джаве тебе пришлось бы делать два типа дерева и два кода для их обхода (или выкручиваться через интерфйесы, что утяжеляет/тормозит).
    ·>Я уже об этом упоминал. Нет никакого утяжеления. Интерфейсы в яве практически бесплатны.

    Вранье. ))
    Интерфейсы в джаве намного тяжелее прямых методов.
    Бесплатны они только если компилятор или JIT "видят" жизненный цикл ссылки — тогда может заменить вызов метода через интерфейс прямым вызовом.
    В плюсах аналогично.

    Стоимость же честного вызова интерфейса в плюсах дешевле, бо в плюсах объект ссылается на свою vtable явно, а в джаве и шарпе — через промежуточный дескриптор типа, т.е. +1 к косвенности, в сравнении с плюсами.


    v>> ·>константность — не является необходимостью

    v>> Разумеется, константность не является необходимостью для иммутабельных сценариев.
    v>> Она лишь дико экономит труд програмиста и размер конечного бинаря, а так-то фигня полная.
    ·>Не экономит она труд.

    Выше пример, который не нужен в плюсах.


    ·>Насчёт размера тут тоже сложно всё. Неясно как можно адекватно сравнивать бинари плюсов и явы... слишком много нюансов.


    Надо сравнивать бинари в рамках одной техонлогии.
    Например, можно сравнивать бинари в плюсах при применении подхода джавы vs родного плюсового подхода.
    Или можно сравнивать бинари в шарпе, где доступны оба подхода для value-типов.

    В любом случае, лишний код есть лишний код.


    v>> ·>и можно нафиг выкинуть, что собственно и сделали в шарпах-явах.

    v>> Наоборот, в шарпе вот относительно недавно добавили.
    v>> А потом опять добавили.
    v>> Ключевое слово чуть другое — readonly, но смысл и сценарии использования те же.
    ·>Если я правильно помню, то это аналог final-поля, который был в java с рождения.

    Это изначально применительно к полям объектов.
    Затем readonly стало можно применять к аргументам методов и возвращаемым значениям.
    Затем через readonly стало можно помечать методы, где этот метод не может изменять внутренние поля — компилятор не даст.


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

    ·>Не понимаю что ты понимаешь под сложностью.

    Лишний код.
    Тот же твой Defensive copying как малая часть этой сложности.
    Например, в дотнете достаточно передавать DateTime по значению или по константной ссылке, а в джаве надо делать GC-копию объекта на каждый чих.
    Да там уже просто рука устаёт всё это внимательно описывать. ))