Автор: Sergey Teplyakov
Я как-то не особо лезу со своими комментариями к другим людям, а уж тем более к постам на хабре. Но вчера вот листал RSS-ленту и увидел интригующее название поста 'Занимательный C#. Пять примеров для кофе-брейка'.
Итить, думаю, дай-ка зайду, посмотрю, что да как.
И вот первая загадка что выдаст следующий код: using System,public struct SDummy : IDisposable{ private bool _dispose, public void Dispose() =>, _dispose = true, public bool GetDispose() =>, _dispose, private static void Main(string[] args) { var d = new SDummy(), using (d) { Console.WriteLine(d.GetDispose()), } Console.WriteLine(d.GetDispose()), }}
Ну, думаю, ок. Странно начинать с изменяемых структур и особенностей блока using, ну, ничего.
Открыл объяснение, а в нем говорится, что причина странного поведения в упаковке, дескать. Компилятор зовет Dispose метод через каст: ((IDisposable)myStruct).Dispose(), ну а каст структуры к интерфейсу, как известно, приводит к упаковке.
Вот те на, подумалось мне. Мало того, что черти дают достаточно невменяемые и не практичные загадки, так еще и ответы у них неверные.
Я добавил комментарий, после чего началось небольшое обсуждение. Дескать, сам Эрик 'уже давно в фейсбуке работает' Липперт писал, что упаковка в блоке using быть должна и компилятор нарушает спеку и все такое... (хотя сегодня это и не так).
Меня смутило две вещи: то, как авторы загадки пытались выкрутиться и найти оправдание своей ошибке. Ну, и главное, что они полезли в дебри, не выяснив, что же в этих дебрях происходит. Да не просто полезли, они этим дебрям еще и учат.
Что в этом плохого?
Структуры в C# имеют две особенности они являются 'значениями', и могут располагаться напрямую в памяти контейнера (в стеке, регистрах и напрямую в других объектах).
Первое говорит о том, что в рантайме структуры по умолчанию копируются и то, что компилятор старается обеспечить семантику значения (т.е. неизменяемость) путем встраивания туда-сюда создание защитных копий (чтобы вызов метода или свойства, например, на неизменяемом поле ни в коем разе значение этого поля не поменял).
Второе же (место жизни структуры) может привести к упаковке т.е. к созданию копии структуры в куче.
Два этих отличия очень важны и в голове они, по-хорошему, должны лежать на разных полочках. Ибо каждый из них достаточно сложен, может меняться и развиваться по мере развития языка, да и проявляются эти особенности по-разному.
Вот, например, семантика значения и защитные копии стали гораздо более распространенной бедой с выходом C# 7x с их модификаторами in и возвратом по неизменяемой ссылке (readonly refs) (вот, например, много буков по этому поводу - The in-modifier and the readonly structs in C#).
А самая большая беда проявляется с изменяемыми структурами, когда скрытая копия прячет изменения состояния, поскольку произойти они могут на временной копии. Не столь серьезное последствие заключается в некоторой потери производительности за счет создания копии, что решается путем использования readonly структур.
Упаковка же происходит совсем в других местах, при кастах к объектам/интерфейсам и в более экзотических случаях, типа при вызове методов из System.Object или System.ValueType (когда, например, Equals/GetHashCode не переопределены). А проявляется она путем увеличения давления на сборку мусора, что может аукнуться за счет тормозов сборщика мусора.
Так это я все к чему: учить других это хорошо. Это просто здорово! Но касаясь всяких закоулков языка и рантайма, хорошо бы понимать, что, да как на самом деле происходит под капотом, да и желательно разбираться, почему происходит все именно так, а не иначе.