Новинки C# 7.2. Span<T> и Memory<T>



    Доброго времени суток, %habrauser%! Столкнувшись с проблемой замедления работы системы при передачи использовании переменных хранимых в стеке я отправился в гугл и нашел отличное решение использование коллекции Span<T>, которая была добавлена в версии языка C# 7.2. Но я заметил, что в рунете почти нет статей посвященных этому обновлению. Поэтому я решил набросать небольшую статью, которая может быть полезна начинающим.

    Добавлен новый модификатор доступа private protected


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

    Неконечные именованные аргументы


    В C# могут использоваться так называемые именованные аргументы. Они позволяют разработчику не запоминать необходимый порядок следования параметров метода, если вручную указывается имя параметра. Давайте рассмотрим сигнатуру данного метода:

    public void Print(string text, int size, string title)

    Мы можем обратиться к этому методу как обычно

    Print("Hello, World!", 24, "Hi!");

    А можем воспользоваться именованными аргументами и задавать параметры в произвольном порядке. Или задать часть аргументов по порядку (такие аргументы называются позиционные), а оставшиеся задать с помощью имени.

    Print(size: 24, title: "Hi!", text: "Hello, World!");
    Print("Hello, World!", title: "Hi!", size: 24);

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

    Print(text: "Hello, World!", 24, "Hi!");

    Но при этом, если позиционный порядок был нарушен, то будет выведено сообщение об ошибке. Данные код работать не будет, так как не смотря на то, что size находится на месте, title и text поменяны местами.

    Print(title: "Hi!", 24, text: "Hello, World!"); // Ошибка.

    Начальные символы подчеркивания в числовых литералах


    В языке C# уже была реализована возможность использовать двоичные и шестнадцатеричные литералы с использованием разделителя подчеркивание «_», поэтому ранее использование его в качестве первого символа литерала было запрещено. Теперь это ограничение было снято.

    
    int i = 0b1101_0111; // Раньше можно было только так.
    int i = 0b_1101_0111; // Теперь стало немного красивее.
    

    Использование ссылок для типов данных хранимых по значению


    Для начала стоит вспомнить, что существуют два типа данных:

    • Хранимые по значению — простые типы данных, такие как int или bool, которые хранятся напрямую в стеке, за счет чего к ним осуществляется быстрый доступ
    • Хранимые по ссылке — более сложные структуры, такие как string или классы, которые хранятся в куче, а в стеке хранится только указатель на область памяти кучи.

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

    Здесь заключается одна не очевидная проблема, так как при передаче значимого типа в метод каждый раз создается его новая копия, то возникает замедление работы системы и чрезмерное использование памяти. Особенно это актуально для объемных структур, которые хранятся в стеке. Представьте себе рекурсивный вызов метода, с передачей в качестве аргумента структуры с 20 полями… Поэтому в новой версии языка были предприняты шаги для оптимизации работы с переменными хранимыми по значению.

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

    private int Sum(in int value1, in int value2);

    Так как структура struct тоже хранится в стеке, для нее добавлено использование модификатор readonly, который по сути является аналогом in

    readonly public struct ReadonlyPosition
    {
        public ReadonlyPosition(int x, int y)
        {
            X = x;
            Y = y;
        }
      
        public int X { get; }
        public int Y { get; }
    
        private static readonly ReadonlyPosition _position = new ReadonlyPosition();
        public static ref ReadonlyPosition Position => ref _position;
    }

    Добавлен модификатор ref struct, который указывает, что структура передается по ссылке, и должен обрабатываться с выделением стека.

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

    Ну и наконец был добавлен тип Span<T>, который позволяет создать коллекцию данных, хранимую в стеке, но доступ к которой осуществляется по ссылке. Тип Memory<T> является расширением типа Span<T> и используется для потокобезопасного доступа по ссылке к коллекции хранимой в стеке.

    На этом я завершу свой небольшой обзор обновления. Надеюсь кому-нибудь пригодится данная информация. Я постарался описать изменения и нововведение языка наиболее простым языком. Спасибо за уделенное время.

    Источник: Новые возможности C# 7.2 | Microsoft Docs
    Поделиться публикацией

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

      –7
      Почему 7.2 (а не скажем 8)? Где ссылки на первоисточник инфы? Как по мне — не до конца разжевано, многое остается «за кадром», не уверен, что мое «додумывание» верно.

      Например, именование параметров при вызове возможно необходимо, чтобы при рефакторинге получить ошибки по всех вызовах где позиция не совпадет? А зачем?

      Про подчеркивание не понял вообще, что за проблему решили.
        0
        Чуть-чуть изменил про подчеркивание, надеюсь стало понятнее. И добавил источник. Спасибо за замечания )
          0
          Не нашел в официальной документации упоминания о типах Span T, Memory T — подскажите пожалуйста откуда взята информация?
        0
        Например, именование параметров при вызове возможно необходимо, чтобы при рефакторинге получить ошибки по всех вызовах где позиция не совпадет?

        Рефакторинг не может приводить к ошибкам, иначе это не рефакторинг по определению рефакторинга. :)


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


        ShowMessage(true, null, "Hello, world!");
        ShowMessage(modal: true, parentWindow: null, "Hello, world!");

        Про подчеркивание не понял вообще, что за проблему решили.

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


        0xfedcba09
        0xfedc_ba09
        0x_fedcba09
        0x_fedc_ba09

        Странно, что сразу так не сделали, когда вводили разделители в 7.0.

          0
          Спасибо за ответ. Действительно литералы так «красивше».

          А про сигнатуру метода все равно не понял, но я и раньше не особо-то использовал именованные параметры, скажем, исключительно если много из них со значенями по умолчанию (в конце списка параметров) и мне хочется только один передать:
          MyMessageBox("Bla", "Error", icon: Icon.Error);
          Уж получше чем указывать значения, равные тем, что по-умолчанию, для всех предыдущих параметров:
          MyMessageBox("Bla", "Error", Buttons.Ok, Sounds.Beep, Parent.Center, Icon.Error);
          В чем именно «соль» использования параметров без имени после именованых — не понятно. В вашем примере первый вариант самый правильный. Посмотреть сигнатуру метода (если нужно) можно простым мышконаведением в нормальной IDE, а каждый раз писать второй вариант — перебор.
            +1
            Посмотреть сигнатуру метода (если нужно) можно простым мышконаведением в нормальной IDE, а каждый раз писать второй вариант — перебор.

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


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


            MyMessageBox(caption: "Bla", message: "Error", Buttons.Ok, Sounds.Beep, Parent.Center, Icon.Error);

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

      • НЛО прилетело и опубликовало эту надпись здесь
          +22
          Честно говоря, когда видишь в заголовке Span и Memory, хочется увидеть в статье несколько больше информации о них, чем 1 абзац на двоих. Предыдущие новинки разжеваны и с примерами, а с ними скомкано, словно вам под конец надоело писать. Прям кликбейтом запахло.
            0
            Особенно это актуально для объемных структур хранимых в стеке, например массивов. Представьте себе рекурсивный вызов метода, с передачей в качестве аргумента массива из 1 000 000 элементов…


            Исходя из: msdn.microsoft.com/en-us/library/bb985948.aspx массив не хранится в стеке. На мой взгляд здесь есть неточность.
            Arrays are mechanisms that allow you to treat several items as a single collection. The Microsoft® .NET Common Language Runtime (CLR) supports single-dimensional arrays, multidimensional arrays, and jagged arrays (arrays of arrays). All array types are implicitly derived from System.Array, which itself is derived from System.Object. This means that all arrays are always reference types which are allocated on the managed heap, and your app's variable contains a reference to the array and not the array itself.

              0
              Да, моя ошибка. Уже исправил. Спасибо )
              +1
              Хранимые по значению — простые типы данных, такие как int или bool, которые хранятся напрямую в стеке

              Но ведь это миф, значимые типы хранятся по месту создания, стало быть где угодно.
              Про Span уже сказали, тоже ожидал больше подробностей.
                0
                В данном контексте имеется ввиду хранение параметров методов и локальных переменных.
                0
                Зачем разделили понятия const и readonly? Ведь можно было const ref и все в таком духе, а не городить ключевые слова за так.
                  0
                  Понятия const и readonly в языке C# разделены начиная с самой первой версии. И описанное поведение — явный readonly.
                    0
                    В этом и заключается вопрос)
                      0

                      Ключевое слово in в языке уже было. Использовалось в паре с out для обозначения ковариантности и контрвариантности. И внутри foreach ещё.


                      Для передачи параметров по ссылке использовались ключевые слова ref (значение параметра может быть изменено) и out (значение параметра должно быть изменено). Теперь есть третий вариант передачи по ссылке — in (значение параметра не может быть изменено). Между двумя вариантами, использовать 'in' или 'readonly ref', выбрали 'in' для краткости и смысловой симметрии с 'out'.

                        +1
                        Да-да-да, но я задавал вопрос о дизайне: почему именно in и readonly, вместо const и const ref. Интересно было бы услышать версии, почему создатели Java и C# стараются избегать const в широком смысле, как это было в C.
                          0

                          Константа в C# отличается от readonly переменной тем, что вычисляется во время компиляции и вычисленное значение подставляется вместо имени константы везде, где это имя упоминается. Readonly-переменнная — это хоть и readonly, но всё-таки переменная, т.е. именованное место в памяти, где лежит какое-то значение.


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


                          Пример константы — число пи. Пример readonly-переменной — id объекта, он у всех разный, присваивается один раз во время создания объекта и никогда не меняется.

                            +1

                            Ну и если подобную логику разделения на const и readonly в C# применить к параметрам (без оглядки на другие языки), то в варианте с использованием ключевого слова const получается глупость:


                            void Foo(const float x)

                            Выглядит как передача константы в метод, но у констант значения никогда не меняются. Этот параметр просто лишний.


                            void Foo(ref const float x)

                            Ссылка на значение константы? Смотри предыдущий пункт.


                            void Foo(const ref float x)

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

                              0
                              Какая польза для программиста в отличиях const и readonly? И те, и другие менять нельзя, вне зависимости от этапа создания значения. Есть ли какие-то кейсы (кроме создания самих const-переменных, конечно), при которых эта разница играла бы роль на уровне языка?

                              Бонус-вопрос: почему интерполированные строки не могут быть const, а конкатенированные — могут?
                                0
                                почему интерполированные строки не могут быть const, а конкатенированные — могут?

                                Это следует из той самой разницы между readonly и const на уровне языка, о которой уже было написано выше. const — определяется во время компиляции, а readonly — во время исполнения. Интерполяция строк трансформируется компилятором в вызов String.Format, что по определению уже не compile time. И да, не все конкатенированные строки могут быть const.
                                  0
                                  const — определяется во время компиляции, а readonly — во время исполнения
                                  Мне нужен кейс, где это полезно (для чего так сделали?), а не констатация, что это так.

                                  Интерполяция строк трансформируется компилятором в вызов String.Format, что по определению уже не compile time
                                  В дополнение к предыдущему пункту: компилятор прекрасно справляется с конкатенацией const-значений, а интерполяцию (тех же const-значений) в компайл-тайме он почему-то не осиливает.
                                    0

                                    Ну-ну, интерполируйте-ка вот это: $"{1000.67}"

                                      0
                                      В чем вы видите проблему для разработчиков компилятора?
                                        0

                                        В том, что в зависимости от значения Thread.CurrentThread.CurrentCulture это может быть одно из следующих значений: "1000.67", "1000,67", "1,000.67", "1.000,67", "1 000.67", "1 000,67" (список может быть неполным).

                                          0
                                          Хорошо, этот случай не работает и для конкатенации. Как насчет строк-констант?
                                          const string a = "Hello";
                                          const string b = " World!";
                                          const string c = a + b; // OK
                                          // const string c = $"{a}{b}"; // error
                                            0
                                            Попробую начать сначала.
                                            const — псевдоним литерала, т.е. используя псевдоним вы реально используете литерал. Например,
                                            const a = "Hello";
                                            string b = a;
                                            

                                            является аналогом
                                            string b = "Hello";
                                            

                                            readonly — объявлет переменную и в этом принципиальная разница с const. Из-за этой разницы не рекомендуется использовать публичные (public) константы, т.к. при изменении значения публичной константы, необходимо перекомпилировать все сборки, которые эту константу использовали. Если этого не сделать, то часть модулей будет использовать старое значение, а перекомпилированные модули будут использовать уже новое. Отличный способ нарваться на проблемы. readonly поля такой особенностью не обладают, т.к. для них выделяется необходимая область памяти и уже туда кладется значение или ссылка.
                                            Что касается интерполяции строк, то это синтаксический сахар для замены не самого удобного в использовании string.Format. Как я уже написал ранее const это псевдоним литерала, а вызов string.Format не является литералом, т.к. результат можно получить только во время выполнения и он зависит, например, от региональных настроек системы.
                                              0
                                              Что касается интерполяции строк, то это синтаксический сахар для замены не самого удобного в использовании string.Format
                                              Скажите просто, что вы не знаете объективных причин реализовать интерполяцию строк такого рода (где никакие региональные настройки, фаза луны и прочие вещи, которые почему-то не мешают конкатенации) в компайл-тайме и мы торжественно закроем этот тредик!
                                                0
                                                не реализовывать*
                                0
                                void Foo(const float x)
                                Выглядит как передача константы в метод, но у констант значения никогда не меняются. Этот параметр просто лишний.
                                const — как признак неизменности объекта, вне зависимости от его природы. Но даже по вашей (в смысле текущей сишарпной) логике, ведь мы можем передавать различные константы, почему же нет?
                                void Foo(const ref float x)
                                Если это константная ссылка (грубо, адрес в памяти), то значит, адрес там всегда один и тот же, всегда указывает на один и тот же объект. Тоже хрень какая-то.
                                Тогда как насчет readonly ref?
                                  +1
                                  const — как признак неизменности объекта, вне зависимости от его природы. Но даже по вашей (в смысле текущей сишарпной) логике, ведь мы можем передавать различные константы, почему же нет?

                                  По сишарпной логике, если мы передаём в метод разные значения, то x не константа. Значения-то разные, а у константы значение всегда одно. В сишарпной логике этот x не const, а readonly — может быть инициализирован разными значениями, но менять полученное значение нельзя.


                                  Тогда как насчет readonly ref?

                                  Ссылка на объект, который менять можно, а саму ссылку перенаправить на другой объект нельзя. Разница с обычным модификатором ref мне неочевидна. Может и есть, но надо вдумываться.


                                  По идее, ссылка на объект, который нельзя изменять, должна обозначаться как ref readonly. Для результатов и локальных ссылок оно ровно так и пишется:


                                  ref readonly float Foo() { }
                                  
                                  ref readonly float x = ref Foo();

                                  Для параметров выбирали из вариантов ref readonly и in, выбрали in. Это такое же старое ключевое слово, оно намного короче, дополняет out и описывает смысл, а не реализацию:
                                  in — ссылка для чтения входных данных
                                  out — ссылка для записи выходных данных
                                  ref — просто ссылка, делай что хочешь

                    0

                    Span и Memory не добавлены в язык. Они добавлены отдельным nuget пакетом и могут использоваться с любой версией C#. Другое дело, что в CLR для них добавили специальную поддержку.

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

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