Занимательный C#. Пять примеров для кофе-брейка

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

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

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

    image

    Пример 1


    Структуры в C#. С ними нередко возникают вопросы даже у опытных разработчиков, чем так часто любят пользоваться всевозможные онлайн-тесты.

    Наш первый пример – пример на внимательность и на знание того, во что разворачивается блок using. А также вполне себе тема для общения во время собеседования.

    Рассмотрим код:

        public struct SDummy : IDisposable
        {
            private bool _dispose;
            
            public void Dispose()
            {
                _dispose = true;
            }
    
            public bool GetDispose()
            {
                return _dispose;
            }
    
            static void Main(string[] args)
            {
                var d = new SDummy();
    
                using (d)
                {
                    Console.WriteLine(d.GetDispose());
                }
    
                Console.WriteLine(d.GetDispose());
            }
        }
    

    Что выведет на консоль метод Main?
    Обратим внимание, что SDummy – это структура, реализующая интерфейс IDisposable, благодаря чему переменные типа SDummy можно использовать в блоке using.

    Согласно спецификации языка C# using statement для значимых типов во время компиляции разворачивается в try-finally блок:

        try
        {
            Console.WriteLine(d.GetDispose());
        }
        finally
        {
            ((IDisposable)d).Dispose();
        }
    

    Итак, в нашем коде внутри блока using вызывается метод GetDispose(), который возвращает булевское поле _dispose, значение которого для объекта d еще нигде не было задано (оно задается только в методе Dispose(), который еще не был вызван) и поэтому возвращается значение по умолчанию, равное False. Что дальше?

    А дальше самое интересное.
    Выполнение строки в блоке finally
     ((IDisposable)d).Dispose();
    

    в обычном случае приводит к упаковке (boxing). Это нетрудно увидеть, например, тут (справа вверху в Results сначала выберите C#, а потом IL):

    image

    В этом случае метод Dispose вызывается уже для другого объекта, а вовсе не для объекта d.
    Запустим нашу программу и увидим, что программа действительно выводит на консоль «False False». Но все ли так просто? :)

    На самом деле никакой УПАКОВКИ НЕ ПРОИСХОДИТ. Что, по словам Эрика Липперта, сделано ради оптимизации (см. тут и тут).
    Но, если нет упаковки (что само по себе может показаться удивительным), почему на экране «False False», а не «False True», ведь Dispose теперь должен применяться к тому же объекту?!?

    А вот и не к тому же!
    Заглянем, во что разворачивает нашу программу компилятор C#:

    public struct SDummy : IDisposable
    {
        private bool _dispose;
    
        public void Dispose()
        {
            _dispose = true;
        }
    
        public bool GetDispose()
        {
            return _dispose;
        }
    
        private static void Main(string[] args)
        {
            SDummy sDummy = default(SDummy);
            SDummy sDummy2 = sDummy;
            try
            {
                Console.WriteLine(sDummy.GetDispose());
            }
            finally
            {
                ((IDisposable)sDummy2).Dispose();
            }
            Console.WriteLine(sDummy.GetDispose());
        }
    }
    


    Появилась новая переменная sDummy2, к которой применяется метод Dispose()!
    Откуда взялась эта скрытая переменная?
    Снова обратимся к спеке:
    A using statement of the form 'using (expression) statement' has the same three possible expansions. In this case ResourceType is implicitly the compile-time type of the expression… The 'resource' variable is inaccessible in, and invisible to, the embedded statement.

    Т.о. переменная sDummy оказывается невидимой и недоступной для встроенного выражения (embedded statement) блока using, а все операции внутри этого выражения производятся с другой переменной sDummy2.

    В итоге метод Main выводит на консоль «False False», а не «False True», как считают многие из тех, кто столкнулся с этим примером впервые. При этом обязательно имейте в виду, что тут не происходит упаковки, но создается дополнительная скрытая переменная.

    Общий вывод такой: изменяемые значимые типы (mutable value types) – это зло, которое лучше избегать.

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

    Хочется сказать отдельное спасибо SergeyT за ценные замечания к этому примеру.



    Пример 2


    Конструкторы и последовательность их вызовов – одна из основных тем любого объектно-ориентированного языка программирования. Иногда такая последовательность вызовов может удивить и, что гораздо хуже, даже «завалить» программу в самый неожиданный момент.

    Итак, рассмотрим класс MyLogger:

    class MyLogger
    {
        static MyLogger innerInstance = new MyLogger();
    
        static MyLogger()
        {
            Console.WriteLine("Static Logger Constructor");
        }
    
        private MyLogger()
        {
            Console.WriteLine("Instance Logger Constructor");
        }
    
        public static MyLogger Instance { get { return innerInstance; } }
    }
    

    Предположим, что этот класс обладает некоторой бизнес-логикой, необходимой нам для поддержки логирования (функциональность сейчас не так важна).

    Посмотрим, что есть в нашем классе MyLogger:

    1. Задан статический конструктор
    2. Есть приватный конструктор без параметров
    3. Определена закрытая статическая переменная innerInstance
    4. И есть открытое статическое свойство Instance для общения с внешним миром

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

    Снаружи класса (без использования ухищрений типа reflection) мы можем использовать только открытое статическое свойство Instance, которое можем вызвать так:

    class Program
    {
        public static void Main()
        {
            var logger = MyLogger.Instance;
        }
    }
    

    Что выведет эта программа?
    Все мы знаем, что статический конструктор вызывается перед доступом к любому члену класса (за исключением констант). При этом запускается он единственный раз в рамках application domain.

    В нашем случае мы обращаемся к члену класса — свойству Instance, что должно вызвать сначала запуск статического конструктора, а потом вызовет конструктор экземпляра класса. Т.е. программа выведет:

    Static Logger Constructor
    Instance Logger Constructor


    Однако после запуска программы мы получим на консоли:

    Instance Logger Constructor
    Static Logger Constructor


    Как так? Инстанс конструктор отработал раньше статического конструктора?!?
    Ответ: Да!

    И вот почему.

    В стандарте ECMA-334 языка C# на счет статических классов указано следующее:

    17.4.5.1: «If a static constructor (§17.11) exists in the class, execution of the static field initializers occurs immediately prior to executing that static constructor.

    17.11: … If a class contains any static fields with initializers, those initializers are executed in textual order immediately prior to executing the static constructor


    (Что в вольном переводе значит: если в классе есть статический конструктор, то запуск инициализации статических полей происходит непосредственно ПЕРЕД запуском статического конструктора.

    Если класс содержит какие-либо статические поля с инициализаторами, то такие инициализаторы запускаются в порядке следования в тексте программы непосредственно ПЕРЕД запуском статического конструктора.)

    В нашем случае статическое поле innerInstance объявлено вместе с инициализатором, в качестве которого выступает конструктор экземпляра класса. Согласно стандарту ECMA инициализатор должен быть вызван ПЕРЕД вызовом статического конструктора. Что и происходит в нашей программе: конструктор экземпляра, являясь инициализатором статического поля, вызывается ДО статического конструктора. Согласитесь, довольно неожиданно.

    Обратим внимание, что это верно только для инициализаторов статических полей. В общем случае статический конструктор вызывается ПЕРЕД вызовом конструктора экземпляра класса.

    Как, например, тут:

    class MyLogger
    {
        static MyLogger()
        {
            Console.WriteLine("Static Logger Constructor");
        }
    
        public MyLogger()
        {
            Console.WriteLine("Instance Logger Constructor");
        }
    }
    
    class Program
    {
        public static void Main()
        {
            var logger = new MyLogger();
        }
    }
    

    И программа ожидаемо выведет на консоль:

    Static Logger Constructor
    Instance Logger Constructor


    image

    Пример 3


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

    Пусть нам необходимо реализовать такую функцию, которая проверяет число на нечетность (т.е. что число не делится на 2 без остатка).

    Реализация может выглядеть так:

    static bool isOddNumber(int i)
    {
        return (i % 2 == 1);
    }
    

    На первый взгляд все хорошо и, например, для чисел 5,7 и 11 мы ожидаемо получаем True.

    А что вернет функция isOddNumber(-5)?
    -5 нечетное число, но в качестве ответа нашей функции мы получим False!
    Разберемся, в чем причина.

    Согласно MSDN про оператор остатка от деления % написано следующее:
    «Для целочисленных операндов результатом a % b является значение, произведенное a — (a / b) * b»
    В нашем случае для a=-5, b=2 мы получаем:
    -5 % 2 = (-5) — ((-5) / 2) * 2 = -5 + 4 = -1
    Но -1 всегда не равно 1, что объясняет наш результат False.

    Оператор % чувствителен к знаку операндов. Поэтому, чтобы не получать таких «сюрпризов», лучше результат сравнивать с нулем, у которого нет знака:

    static bool isOddNumber(int i)
    {
        return (i % 2 != 0);
    }
    

    Или завести отдельную функцию проверки на четность и реализовать логику через нее:

    static bool isEvenNumber(int i)
    {
        return (i % 2 == 0);
    }
    
    static bool isOddNumber(int i)
    {
        return !isEvenNumber(i);
    }
    


    Пример 4


    Все, кто программировал на C#, наверняка встречался с LINQ, на котором так удобно работать с коллекциями, создавая запросы, фильтруя и агрегируя данные…

    Мы не будем заглядывать под капот LINQ. Возможно, сделаем это в другой раз.

    А пока рассмотрим небольшой пример:

    int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };
    
    int summResult = 0;
    var selectedData = dataArray.Select(
        x =>
        {
            summResult += x;
            return x;
        });
    
    Console.WriteLine(summResult);
    

    Что выведет этот код?
    Мы получим на экране значение переменной summResult, которое равно начальному значению, т.е. 0.

    Почему так произошло?

    А потому, что определение LINQ запроса и запуск этого запроса – это две операции, которые выполняются отдельно. Таким образом, определение запроса еще не означает его запуск/выполнение.

    Переменная summResult используется внутри анонимного делегата в методе Select: последовательно перебираются элементы массива dataArray и прибавляются к переменной summResult.

    Можно предположить, что наш код напечатает сумму элементов массива dataArray. Но LINQ работает не так.

    Рассмотрим переменную selectedData. Ключевое слово var – это «синтаксический сахар», который во многих случаях сокращает размер кода программы и улучшает ее читабельность. А настоящий тип переменной selectedData реализует интерфейс IEnumerable. Т.е. наш код выглядит так:

        IEnumerable<int> selectedData = dataArray.Select(
        x =>
        {
            summResult += x;
            return x;
        }); 
    

    Здесь мы определяем запрос (Query), но сам запрос не запускаем. Схожим образом можно работать с базой данных, задавая SQL запрос в виде строки, но для получения результата обращаясь к базе данных и запуская этот запрос в явном виде.

    То есть пока мы только задали запрос, но не запустили его. Вот почему значение переменной summResult осталось без изменений. А запустить запрос можно, например, с помощью методов ToArray, ToList или ToDictionary:

    int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };
    
    int summResult = 0;
    
    // определяем запрос и сохраняем его в переменной selectedData
    IEnumerable<int> selectedData = dataArray.Select(
        x =>
        {
            summResult += x;
            return x;
        });
    
    // запускаем запрос selectedData
    selectedData.ToArray();
    
    // печатаем значение переменной summResult
    Console.WriteLine(summResult);
    

    Этот код уже выведет на экран значение переменной summResult, равное сумме всех элементов массива dataArray, равное 15.

    С этим разобрались. А что тогда выведет на экран эта программа?

    int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; //1
    var summResult = dataArray.Sum() + dataArray.Skip(3).Take(2).Sum(); //2
    var groupedData = dataArray.GroupBy(x => x).Select(  //3
        x =>
        {
            summResult += x.Key;
            return x.Key;
        });
    
    Console.WriteLine(summResult); //4
    

    Переменная groupedData (строка 3) на самом деле реализует интерфейс IEnumerable и по сути определяет запрос к источнику данных dataArray. А это значит, что для работы анонимного делегата, который изменяет значение переменной summResult, этот запрос должен быть запущен явно. Но такого запуска в нашей программе нет. Поэтому значение переменной summResult будет изменено только в строке 2, а все остальное мы можем не рассматривать в наших вычислениях.

    Тогда нетрудно посчитать значение переменной summResult, которое равно, соответственно, 15 + 7, т.е. 22.

    Пример 5


    Скажем сразу — этот пример мы не рассматриваем на наших лекциях в Академии, но иногда обсуждаем во время кофе-брейков скорее как анекдот.

    Несмотря на то, что он вряд ли является показательным с точки зрения определения уровня разработчика, этот пример встречался нам в нескольких различных тестах. Возможно, его используют за универсальность, потому что он отрабатывает одинаково как на C и C++, так и на C# и Java.

    Итак, пусть есть строка кода:

    int i = (int)+(char)-(int)+(long)-1;
    

    Чему будет равно значение переменной i?
    Ответ: 1

    Можно подумать, что здесь используется численная арифметика над размерами каждого типа в байтах, поскольку для преобразования типов тут довольно неожиданно встречаются знаки «+» и «-».

    Известно, что в C# тип integer имеет размер 4 байта, long – 8, char – 2.

    Тогда легко подумать, что наша строка кода будет равносильна следующему арифметическому выражению:

    int i = (4)+(2)-(4)+(8)-1;
    

    Однако это не так. А чтобы сбить с толку и направить по такому ложному рассуждению, пример может быть изменен, например, так:

    int i = (int)+(char)-(int)+(long)-sizeof(int);
    

    Знаки «+» и «-» используются в этом примере не как бинарные арифметические операции, а как унарные операторы. Тогда наша строка кода является лишь последовательностью явных преобразований типов, перемешанных с вызовами унарных операций, которую можно записать так:

            int i =
            (int)( // call explicit operator int(char), i.e. char to int
              +( // call unary operator +
                (char)( // call explicit operator char(int), i.e. int to char
                  -( // call unary operator -
                    (int)( // call explicit operator int(long), i.e. long to int
                      +( // call unary operator +
                        (long)( // call explicit operator long(int), i.e. int to long
                          -1
                        )
                      )
                    )
                  )
                )
              )
            );
    


    image

    Заинтересовало Обучение в Veeam Academy?


    Сейчас идет набор на весенний интенсив по C# в Санкт-Петербурге, и мы приглашаем всех желающих пройти онлайн-тестирование на сайте Veeam Academy.

    Курс стартует 18 февраля 2019 г., продлится до середины мая и будет, как всегда, совершенно бесплатным. Регистрация для всех, кто хочет пройти входное тестирование, уже доступна на сайте Академии: academy.veeam.ru

    image
    Veeam Software
    161,00
    Продукты для резервного копирования информации
    Поделиться публикацией

    Комментарии 46

      +9
      В чем польза от этих примеров?
        +9
        Все примеры, кроме может быть Linq — какие то бесполезные.
        Особенно растроила структура с IDisposable — структуры и так непростые к применению, так надо было добавить ещё.
          –1
          Про Linq вообще чисто на внимательность(=
          +25

          Объяснение к первому примеру — неверное. Дело не в том, что в блоке finally происходит упаковка, а в том, что компилятор создает "защитную" копию переменной, поскольку переменные в блоке 'using' очень похожи на readonly-поля (ее нельзя переприсвоить) и компилятор пытается гарантировать "неизменяемость" состояния.


          Посмотрите во что компилятор разворачивает код:


          SDummy sDummy = default(SDummy);
                  SDummy sDummy2 = sDummy;
                  try
                  {
                      Console.WriteLine(sDummy.GetDispose());
                  }
                  finally
                  {
                      ((IDisposable)sDummy2).Dispose();
                  }
                  Console.WriteLine(sDummy.GetDispose());

          И упаковки — не просиходит в этом коде вообще! И это очень, повторюсь, очень важно. Если бы она была, то енумераторы в коллекциях не были бы структурами (какой смысл делать их структурами, если блок 'using', который генерирует компилятор для foreach все равно приведет к упаковке?


          Ну и главное, эти примеры нужно всегда давать в контексте: структуры — это оптимизация и к их примненеию нужно подходить соответственно — использовать тогда, когда профилирование показало их необхоидмость.


          Потом нужно объяснять семантическую разницу между классами и структурами и говорить о том, что структуры должны быть неизменяемыми (должны быть readonly struct). Если структура изменяемая, то компилятор генерирует огромную кучу защитных компий, которые совершенно не очевидны никому в этом мире, кроме пары десятков человек. А эти копии могут а) негативно повлиять на производительность (тут идет напоминание, что структуры — это оптимизация) и б) вы можете очень легко отстрелить себе ногу, поскольку изменение состояние произойдет на компии.


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


          Это очень advanced знания, которые нужны либо а) энтузиастам-задротам или б) людям, которые разрабатываеют очень высоконагруженные приложения на .net.

            +13
            Да, и хочется добавить.

            Вообще, меня очень смущает первый вопрос. Он показывает, что автор этого вопроса сам не совсем понимает, что происходит у дот-нетика под капотом.

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

            Защитные же копии — это другой зверь. Их и было много, а с появлением 'in' модфикаторов и ref-return-ов стало еще больше. И я бы рассчитывал, что в голове у автора подобных вопросов эти два кейса лежат на разных полках, поскольку они приводят к разным проблемам, по разному проявляются, по разному ищутся и по разному решаются.
              0
              А разве в случае «in» компилятор не просто проверяет попытки прямых изменений полей, из-за чего появляется возможность передавать структуры не по значению, а по ссылке? Тесты показывают хорошее ускорение в случае сложных структур, если бы создавались защитные копии — было бы наоборот.
                0

                Там не всё так просто. Да, вы можете передавать структуры по ссылке, но если у компилятора не будет 100% уверенности, что операция не приведёт к изменению структуры, то будет создана её копия, причём без всяких предупреждений.


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


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

                  0
                  Ну генерики в структах вообще зло и должны быть запрещены из-за непредсказуемых аллокаций на ровном месте. Вложенные структуры — да, не использую, надо будет обновить тесты.
                    –1

                    Идите нафик с подобными предложениями и не мешайте писать высокопроизводительный код. Генерики в сочетании со структурами хороши тем, что JIT-компилятор для каждой комбинации параметров генерирует свой код. Если что-то позволяет выстрелить в ногу, то это не означает, что от этого нужно отказываться. Я ещё, о ужас, иногда использую System.Runtime.CompilerServices.Unsafe.

                      –2
                      Ансейф я и сам использую. Проблема как раз в неконтролируемых аллокациях памяти в довольно неожиданных местах:
                      jacksondunstan.com/articles/3453
                      jacksondunstan.com/articles/3468

                      Идите нафик с подобными предложениями и не мешайте писать высокопроизводительный код.

                      Надеюсь, что никогда не придется пользоваться подобным «высокопроизводительным» кодом.
                        –2

                        Это проблема не с генерик-структурами, а с конкретной кривой реализацией JIT-компилятора или среды выполнения. Конкретно в том случае использовался Mono довольно старой версии.


                        Я много времени потратил на сравнение производительности в совершенно различных сценариях и не заметил абсолютно никаких проблем что в MS .NET, что в Mono последних версий.


                        Хотя не, немного вру: у меня есть пример кода, где в случае использования структуры T выражения new T() и default(T) оказывались неэквивалетными. Но там совсем непотребство творилось. Пост, что ли, запилить...

                          0
                          Это проблема не с генерик-структурами, а с конкретной кривой реализацией JIT-компилятора или среды выполнения.

                          «У меня все работает» ©. Игнорирование потенциально возможных проблем на разных рантаймах — это не решение. Раньше подобное приходилось слышать про mono.
                          не заметил абсолютно никаких проблем

                          Отличный аргумент. Но это не значит, что их там нет.
                            –1
                            То есть вместо того, чтобы поправить баг в Mono или поспособствовать решению проблемы (завести тикет и подробно описать в нём баг), вы предлагаете запретить криво работающий в нём функционал, сделав плохо всем. Отличная идея!
                              –2
                              Я предлагаю крайне осторожно использовать генерики для структур или вообще запретить их к использованию. Разумеется, в командах и административными мерами, а не на уровне языка. Пусть кто хочет, тот себе стреляет во все конечности.
                                –2

                                Отлично. Запретите тогда тот же KeyValuePair<,>.

                                  –2
                                  Штатная правильная реализация != «ухты, мы теперь можем и так!» По ссылкам выше как раз показаны особенности, обходя которые, можно использовать данный функционал. Другое дело — кто будет разбираться в этих тонкостях в общем случае? Работает и ладно.
                                    –2

                                    И как же вы их обойдёте для KeyValuePair<,>, где его нельзя сконстрировать напрямую через поля, а можно только через конструктор?

                                      –2
                                      По идее нельзя отдавать такой конструктор в паблик и нужно правильно собирать его в энумераторе.
                                        –2
                                        То есть отрезать ногу, когда она болит?

                                        Представьте себе, этот тип ещё и в пользовательском коде можно конструировать и использовать. И это далеко не единственный тип генерик-структуры, который используется. Разработчики ещё всякие ValueTuple, ValueTask выкатывают.
                                          0
                                          У меня специализация — мобильный геймдев, процессинг данных в рантайме без фризов на сборку мусора, т.е нулевые аллокации в основном цикле апдейта, причем на несравнимо более дохлом железе, чем десктоп. Кейсы с выделением памяти всегда выносятся на этап инициализации и не допускаются в процессинге (и не потому, что рантайм такое не позволяет). Где общая производительность и непрерывность по времени исполнения не важна — там допускаю использование всего, чего душа пожелает.
                  +2
                  Нет, все сложнее. Полное описание можно найти здесь: The ‘in’-modifier and the readonly structs in C#.

                  Если коротко, то in-модификатор очень похож на readonly поля: такие параметры нельзя переприсвоить и для структур компилятор генерирует защитные копии для обращений к свофствам и методам, чтобы обеспечить неизменяемость. А вот эти защитные копии и будут ломать нам ноги, поскольку они не явные.
                    0

                    С доступом к полю на чтение проблем никаких нет, так как компилятор в этом случае не порождает защитной копии, в этом нет никакой необходимости. Другое дело — это вызов метода или обращение к свойству на чтение/запись для аргументов с модификатор 'in' и для readonly-полей в случае структур не объявленных как readonly struct, здесь компилятор вынужден создавать защитную копию, так как не знает, приведет ли вызов метода к изменению значения или нет. Для структур объявленных как readonly struct при вызове методов/свойств компилятор уже не будет создавать защитной копии, так как гарантируется, что вызов не приведет к изменениям.

                      0

                      Нет, не гарантируется. Пример:


                      struct NonReadonlyStruct
                      {
                          public int f;
                      
                          public void SetField(int v) => f = v;
                      }
                      
                      readonly struct ReadonlyStruct
                      {
                          readonly NonReadonlyStruct s;
                          readonly int c;
                      
                          // Тут будет ошибка компиляции
                          public void SetField1(int v) => c = v;
                      
                          // А тут - копирование структуры
                          public void SetField2(int v) => s.SetField(v);
                      }
                        0

                        Для readonly-структур именно что гарантируется, потому как такая структура не может содержать не readonly-поля, компилятор просто не даст объявить такую структуру. Поэтому в случае вызова метода у readonly-структуры C# не создает защитную копию.


                        В вашем же примере, в методе SetField вы вызываете метод SetField2 у не readonly-структуры, инстанс которого записан в readonly-поле, именно поэтому и создается защитная копия, потому что C# не знает, изменит вызов метода внутреннее состояние или нет. Но сам вызов SetField у ReadonlyStruct не приводит к созданию копии инстанса ReadonlyStruct .


                        Другими словами:


                        public void Test_0(in SomeReadonyStruct a, in SomeMutableStruct b) {
                            // Защитная копия не создается
                            a.SomeMethod();
                        
                            // Здесь C# создает защитную копию
                            b.SomeMethod();
                        }
                        
                        public void Test_1(SomeReadonyStruct a, SomeMutableStruct b) {
                            // Защитная копия не создается
                            a.SomeMethod();
                        
                            // Защитная копия не создается
                            b.SomeMethod();
                        }

                        ЗЫ. Я в предыдущем комментарии оговорился на счет доступа к полю/свойству на запись для структур у readonly-полей и аргументов с модификатором 'in', так как C# запрещает их изменять, но, к сожалению, исправить возможности уже не было, ждал, когда комментарий модерацию пройдет.

                          0
                          Для readonly-структур именно что гарантируется, потому как такая структура не может содержать не readonly-поля, компилятор просто не даст объявить такую структуру.

                          Всё, что может гарантировать компилятор — это запретить прямое изменение readonly полей. А вызывать методы у readonly полей он не запрещает.


                          Но сам вызов SetField у ReadonlyStruct не приводит к созданию копии инстанса ReadonlyStruct

                          Тем не менее, защитная копия дочерней структуры создаётся. Это вам не const-методы в C++.

                            0
                            Тем не менее, защитная копия дочерней структуры создаётся.

                            Еще раз, защитная копия создается для инстанса, метод которого ты вызываешь, если это не readonly-структура, и инстанс которого записан в readonly-поле или это аргумент объявленный с модификатор 'in'.


                            А то, что для дочерней структуры создается, так как раз строго так как я и написал. Дочерняя мутабельная структура инстанс которой записан в readonly-поле, метод которой вызывается. Так как структура мутабельная и записана в readonly-поле, то компилятор создает для нее защитную копию, и уже у копии вызывает метод.


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

                              0
                              Никто никогда не говорил и не обещал, что раз структура иммутабельная, то для внутренних полей никто не будет создавать защитных копии внутри методов этой структуры. Смотреть надо в конкретной точке вызова.

                              Так в этом и заключается коварство in. Многие почему-то считают, что этого происходить не будет.

                                0
                                Так в этом и заключается коварство in. Многие почему-то считают, что этого происходить не будет.

                                'in' здесь совершенно не причем. Защитная копия при вызове s.SetField(v) из твоего примера будет создаваться в любом случае, что с 'in' что без 'in', или будь это даже локальной переменной. Как говорится, записал мутабельную структуру в readonly-поле, сам себе злобный буратино, будь готов к копиям при вызове его методов.


                                Для информации: C# не делает межпроцедурный анализ, и решать делать ли защитную копию или нет, решает в точке вызова метода структуры. Если программист указал, что память, занятая моей структурой должна остаться неизменной (readonly-поле, аргумент с модификатором 'in'), то C# это и будет обеспечивать. Дальше точки вызова он не заглядывает. Программист там хоть диск пусть форматирует.


                                И readonly-поле и аргумент с модификатором 'in' как раз и служат для обеспечения гарантии неизменности памяти, ничего более, и штатными средствами, не обманывая компилятор, никак не удастся изменить память, занятую этой структурой. Причем, до появления readonly struct не было никакой возможности сказать, что не стоит делать побитную копию, перед тем как вызвать метод структуры, потому что раньше хоть по факту структура могла быть иммутабельной, для компилятора она все равно оставалась мутабельной, но теперь, в случае readonly struct, компилятор знает, что структура иммутабельная, и вызов метода не приведет к изменению памяти занятую этой структурой.


                                Правило ведь простое как 5 копеек: вызов метода мутабельной структуры в readonly-семантике приводит к созданию защитной копий памяти, занимаемую этой самой структурой для гарантии ее неизменности.

                              0
                              Всё, что может гарантировать компилятор — это запретить прямое изменение readonly полей. А вызывать методы у readonly полей он не запрещает.

                              А кто говорил, что запрещено вызывать методы?

                      0
                      Вообще, меня очень смущает первый вопрос. Он показывает, что автор этого вопроса сам не совсем понимает, что происходит у дот-нетика под капотом.

                      Это нормально.
                      Я когда придумываю задания, часто ошибаюсь с ответом. Тут главное, проверить ответ. И поблагодарить человека, если он дал более полное объяснение.
                        +3
                        То есть вы собеседуете людей, сами не зная верных ответов на свои задания?
                        Это как минимум странно…
                          +3
                          Тривиальный вопрос "можно ли получить доступ к приватному полю" хакается 5 способами. "Чем отличается абстрактный класс от интерфейса" может обернуться обстоятельным рассказом про особенности сериализаторов. Однажды мне заявили, что в интерфейсе можно объявить константу — язык С# такого не позволяет, но если сгенерировать интерфейс не шарпом, .NET его подцепит…

                          И задавая себе любой нетривиальный вопрос, ты понимаешь: «по идее, X работает так, но если задаться целью...». На базовом уровне один ответ, копнешь глубже — уже другой.
                        –2
                        SergeyT Спасибо за комментарий и полезные замечания.
                        Действительно, в объяснении первого примера допущена ошибка.
                        ̶И̶ ̶м̶ы̶ ̶у̶ж̶е̶ ̶у̶в̶о̶л̶и̶л̶и̶ ̶е̶г̶о̶ ̶а̶в̶т̶о̶р̶а̶ :)

                        Вы правы. Действительно, компилятор разворачивает блок using для значимых типов в другую конструкцию, нарушая спецификацию языка C# ради оптимизации (согласно комментариям Э. Липперта https://stackoverflow.com/questions/2412981/if-my-struct-implements-idisposable-will-it-be-boxed-when-used-in-a-using-statem).
                        Таким образом, упаковки (boxing) не происходит, однако создается скрытая копия переменной.
                        Подробности с описанием очень похожего примера можно найти у Эрика Липперта (https://ericlippert.com/2011/03/14/to-box-or-not-to-box/)
                        Общий вывод такой, что изменяемые значимые типы (mutable value types) — это зло, которое лучше избегать.

                        Соответствующие изменения в статью постараемся внести в ближайшее время.

                        Мы особенно рады, что этот пост не оставил равнодушным автора одной из наиболее популярных книг нашей библиотеки (https://habr.com/ru/company/veeam/blog/417691/).
                          +3
                          Вы правы. Действительно, компилятор разворачивает блок using для значимых типов в другую конструкцию, нарушая спецификацию языка C# ради оптимизации


                          И снова я с этим не совсем согласен. Эрик, ведь, — жук, да он пишет вот что: «You’d be perfectly justified in thinking that there is a boxing performed in the finally because that’s what the spec says.». Но ведь он пишет лишь то, что «да, в спеке есть пример, который говорит, что using блок выворачивается в каст», но спека не говорит, что каст там должен быть.

                          Спека (вот здесь, например) вообще хитро написана, там есть вот что:

                          An implementation is permitted to implement a given using-statement differently, e.g. for performance reasons, as long as the behavior is consistent with the above expansion.


                          Так что никто никого не нарушает;), не стоит спешить с выводами.
                            0
                            Эрик сам признается, что это техническое нарушение:
                            «This is technically a violation of the C# specification. The spec states that the finally block should have the semantics of ((IDisposable)resource).Dispose(); which is clearly a boxing conversion. We do not actually generate the boxing conversion»

                            С чем трудно не согласиться, читая спеку C# о using statement:

                            When ResourceType is a non-nullable value type, the expansion is

                            {
                                ResourceType resource = expression;
                                try {
                                    statement;
                                }
                                finally {
                                    ((IDisposable)resource).Dispose();
                                }
                            }
                            


                            Но спека написана ветиевато. И, видимо, неспроста :)

                              +2
                              Комментарий Эрика датирован 2011-м, и после этого, скорее всего, спека была изменена, чтобы убрать эту неоднозначность. Я же привел кусок спеки:

                              An implementation is permitted to implement a given using-statement differently, e.g. for performance reasons, as long as the behavior is consistent with the above expansion.


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

                              Так что *сегодня* никакого нарушения спеки нет. Не стоит вводить себя и читателей в заблуждение;)
                        0
                        Посмотрите во что компилятор разворачивает код

                        Если взять развернутый код и получить из него IL, то какая-то упаковка в блоке finally есть. Но при этом ее нет, если получить IL из оригинального кода.
                        Не могли бы вы пояснить, почему не происходит упаковки копии sDummy2 в случае оригинального кода? И почему развернутый и оригинальный код дают разный IL?
                          0
                          Проблема в том, что нет никакого смысла в том, чтобы «взять развернутый код и получить из него IL». Есть IL-код, генерируемый компилятором в котором упаоквки нет, а есть 'constrained' вызов метода Dispose.

                          «Развернутый код» же — это лишь пример (псевдокод, по сути) того, что делает компилятор. На самом же деле, поведение несколько иное — более эффективное.

                            0
                            Добавил бы, вызов причем идет через callvirt, предваренный опкодом constrained, так при этом, в случае если метод определен в struct, то упаковки не будет, а если метод не определен(вернее переопределен), т.е. это методы ValueType — ToString, Equals, то будет неявная упаковка и вызов виртуального метода.

                            Код
                            using System;
                            struct First{
                                public override string ToString()
                                {
                                    return "It is First";
                                }
                            }
                            struct Second {
                            }
                            
                            public class C {
                                public void M() {
                                    var f=new First();
                                    var s=new Second();
                                    
                                    Console.WriteLine(f.ToString());
                                    Console.WriteLine(s.ToString());
                                }
                            }
                            


                            в первом случае упаковки не будет вообще, а вот во втором случае, даже отсутствие явного опкода box, будет неявная упаковка (это видно по коду после JIT, вызов конструктора, копирование и вызов виртуального метода )
                        0

                        А во втором примере если поле вынести в другой класс, то последовательность так же отработает?

                          +1
                          Тоже недавно искал разные забавные штуки, например:
                          1) Как поменять переменные в замыкании
                          2) Как подменить сам метод у замыкания
                          3) Каст объектов через Unsafe

                          Если кому интересно:
                          gist.github.com/dbr176/5e8768ead9b9c8311b24c56d694876df
                            0

                            Зачем же вы так дезинформируете общественность объяснением первого примера? Сами же даёте ссылку на объяснения Эрика Липперта, где он последовательно объясняет, что никакого боксинга нет. А в статье говорите "потому что боксинг". А имеет место быть "embedded statement", когда значение структуры копируется/захватывается блоком using и манипуляции над копией не видны снаружи этого юзинга. Это огромная разница с боксингом.


                            Сам постоянно использую disposable struct (не mutable, а именно disposable), потому что это удобное средство простого профилирования участков кода аля


                            using (new Perf("Считаем интеграл методом №7"))
                            {
                                // some code
                            }
                            
                            ....
                            
                            public readonly struct Perf
                            {
                                private readonly string _text;
                                private readonly long _startTime;
                                // etc
                            
                                public Perf(string text)
                                {
                                    _text = text;
                                    _startTime = ... // засечь текущий момент времени и сохранить в поле
                                }
                            
                                public void Dispose()
                                {
                                    // засечь текущий момент времени, вычислить дельту, сбросить дельту в лог вместе с текстовкой _text (консоль/etc.)
                                }
                            }
                            

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


                            Даже пришлось перепроверять это после прочтения, напугали. Но все хорошо — 0 bytes allocated )).

                              0
                              sungam3r в разборе первого примера у нас действительно была ошибка. Теперь исправились.
                              Спасибо, что тоже заметили неточность. Зато теперь все знают про disposable struct ))
                                0
                                Ждал 2 дня одобрения комментария. Впервые решил отписать что-нибудь.
                              –1
                              Касательно примера 4 — объяснение внутренней работы хорошее, но правильный вариант все таки такой(вдруг кто-то из новичков будет читать данную статью):
                              int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };
                              
                              int summResult = dataArray.Sum();
                              
                              Console.WriteLine(summResult);
                              

                              Также расширение .Sum() позволяет задать селектор, если нужно(например .Sum(item => item.Value1)). Тип будет выведен из типа перечисления(в данном случае int[] — это
                              IEnumerable<int>
                              и будет выведен тип int) или типа, который получает селектор.
                                +1
                                мы решили приоткрыть немного внутренней кухни и предлагаем вашему вниманию несколько примеров на C#, которые мы разбираем с нашими студентами.
                                Не забивайте студентам голову мусором, учите писать простой и понятный код, в большистве случаев не сложно понять что делает программа, бывает очень сложно понять почему она делает именно это.
                                В глубинах языка разобраться значительно проще: доступны спеки, отладчики, дампы ит.д.
                                Куда сложнее писать концептуально целостный софт, где выбранные абстрации использются по назначению и в точности реализуют заданные кейсы из бизнеса.
                                Потом получается, что на собеседовании человек знает за сколько тактов GC собирает кучу, а код на выходе по прежнему плохо читаемый и не поддерживаемый.

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

                                Самое читаемое