Пожалуйста, опишите ошибку

Нашли баг? Помогите нам его исправить, заполнив эту форму

Что нового в C#7?

Андрей Гришин
web/desktop - разработчик

С официальным релизом Visual Studio 2017 стала доступна и новая версия языка C# 7.

Нововведения в C#7

Основные новшества новой версии языка призваны облегчить работу с данными, добавить ещё больше «синтаксического сахара» и улучшить производительность. Каким образом, рассмотрим далее в статье.

Список изменений:

Binary Literals

Бинарные литералы.

Ранее


        int hex = 0x1EA144FE;

        int dec = 280;
        byte decByte = 128;
        

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

В C# 7


        int hex = 0x1EA144FE;

        int bin = 0b000100011000;
        byte binByte = 0b10000000;
        

Digit separators

Разделители в литералах. Числа, особенно представленные в двоичной системе, могут стать очень большими, и чтобы их удобнее было читать, добавили разделители в литералы.

Ранее


        uint hexColor = 0xFF_D4_1A_1A;
        int interval = 60000;
        double real = 3.2556;
        

Длина разделителя не ограничена.

В C# 7


        uint hexColor = 0xFF_D4_1A_1A;
        int interval = 60_000;
        double real = 3.25____________56;
        

Expression-bodied members

Больше лямбда выражений.

В новой версии языка пополнился список объектов, к которым можно применять Expression-bodied members.
В C# 6 использование лямбда оператора было применимо к get свойствам и методам.

    • Methods — C# 6
    • Property Get — C# 6

        public static class FileHelper
        {
            public static string TempPath => Path.GetTempPath();

            public static bool CheckExistFile(string filePath) => File.Exists(filePath);
        }
        
    • В новой версии C# 7 стало возможно использовать лямбда оператор в: Деструкторах, Свойствах

set

    • и Индексаторах.

 

    • Constructor — C# 7
    • Finalizer — C# 7
    • Property Set — C# 7

            public class Book
            {
                public Book(string title) => Title = title;

                ~Book() => Console.WriteLine("Destructor is executing");

                private string _title;
                public string Title
                {
                    get => _title;
                    set => _title = value ?? "Unknown book";
                }

                private string _content;
                public string Content
                {
                    get => _content;
                    set => _content = value ?? "No content";
                }
            }
            
    • Indexer — C# 7

    internal class SampleCollection
    {
        private readonly T[] _array = new T[10_000];

        public T this[int i]
        {
            get => _array[i];
            set => _array[i] = value;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            SampleCollection stringCollection = new SampleCollection
                {
                    [0] = "Microsoft",
                    [1] = "Apple",
                    [2] = "Google",
                    [3] = "Facebook"
                };

                Console.WriteLine(stringCollection[0]);
                Console.WriteLine(stringCollection[2]);
        }
    }
            

Теперь на тех местах, где должен был быть return или тело конструктора/метода { }, теперь можно использовать лямда оператор =>. Главное потом не запутаться в коде после всех упрощений. Если провести код через декомпилятор, то можно увидеть, что лямбда операторы меняются обратно на методы с телами, так же и аксессор с мутатором.

Декомпилированный код в Sharplab


        public class Book
        {
            private string _title;

            private string _content;

            public string Title
            {
                get
                {
                    return this._title;
                }
                set
                {
                    Book arg_0B_0 = this;
                    string arg_0B_1;
                    if ((arg_0B_1 = value) == null)
                        {
                            arg_0B_1 = "Unknown book";
                        }
                    arg_0B_0._title = arg_0B_1;
                }
        }

        public string Content
        {
            get
            {
                return this._content;
            }
            set
            {
                Book arg_0B_0 = this;
                string arg_0B_1;
                if ((arg_0B_1 = value) == null)
                {
                    arg_0B_1 = "No content";
                }
                arg_0B_0._content = arg_0B_1;
            }
        }

        public Book(string title)
        {
            this.Title = title;
        }

        ~Book()
        {
            Console.WriteLine("Destructor is executing");
        }
    }
        

Out variables

out переменные.

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

Ранее


        static void Main(string[] args)
        {
            string processName = "chrome";
            string countProcesses;
            Process process = GetProcess(processName, out countProcesses);

            int count;
            int.TryParse(countProcesses, out count);

            process.Kill();
            Console.WriteLine($"{count} processes of {processName} was kill");

        }

        static Process GetProcess(string a, out string countProcesses)
        {
            Process[] processes = Process.GetProcessesByName(a);
            countProcesses = processes.Length.ToString();
            return processes.FirstOrDefault();
        } 
        



В C# 7


        static void Main(string[] args)
        {
            string processName = "chrome";
            Process process = GetProcess(processName, out string countProcesses);

            int.TryParse(countProcesses, out int count);

            process.Kill();
            Console.WriteLine($"{count} {processName} processes were killed");

        }

        static Process GetProcess(string a, out string processFileLength)
        {
            Process[] processes = Process.GetProcessesByName(a);
            processFileLength = processes.Length.ToString();
            return processes.FirstOrDefault();
        }
        

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

Discards

Дискарды.

Бывает, что при работе с кортежами или методами, содержащими out параметры, вам нет необходимости использовать какие-либо элементы кортежа или out переменную в дальнейшем. Для этого теперь можно использовать дискарды.
К примеру, мы просто хотим узнать, возможен ли парсинг строки в переменную int, но сам результат парсинга нас не интересует, для этого можно result переменную объявить как discard. Обозначается он как знак подчёркивания _. Таким образом, не нужно будет придумывать имя переменной и загромождать код лишними именами.

Ранее


        static void Main(string[] args)
        {
            int result;
            if(int.TryParse("100", out result))
            {
                Console.WriteLine("Parsing successful");
            }
            else
            {
                Console.WriteLine("Parsing impossible");
            }
        }
        



В C# 7


        static void Main(string[] args)
        {
            if(int.TryParse("100", out int _))
            {
                Console.WriteLine("Parsing successful");
            }
            else
            {
                Console.WriteLine("Parsing impossible");
            }
        }
        

Tuples / ValueTuple

Основная проблема старых кортежей в том, что ими было не удобно пользоваться. Зачастую необходимо вернуть больше одного значения из метода, и варианта было два:
• Использовать out/ref, но тогда помеченные переменные нельзя использовать в асинхронных методах. Излишнее использование out/ref переменных может привести к путанице в коде;
• Использовать Tuple, но они так же не удобны в использовании, разбираться в переменных аля item1, item2,item3… мало кому понравится
На смену в C# 7 спешит новая структура, ValueTuple.
ValueTuple нельзя привести к Tuple.

Данная структура хорошо упрощает работу во многих случаях, например:

  • Больше не нужно вспоминать, что значит тот или иной item*, просто сразу даём имя переменой и работаем, используя его.
  • Переменные из ValueTupe доступны в локальной области видимости.
  • Один из часто используемых типов коллекций Dictionary. Но у него имеется ограничение на уникальность Key значения. Благодаря ValueTuple теперь проще задавать ключи для словаря.
  • ValueTuple можно использовать как тип возвращаемого значения в методах, и, что очень важно, можно возвращать Task<ValueTuple>.

    static void Main(string[] args)
    {
        // old Tuple ////////////////////////////////////////////////
        Tuple<int[], string> tuple = new Tuple<int[], string>(new[] { 4, 2, 6, 6 }, "Hello World");

        // new ValueTuple ////////////////////////////////////////////////
        ValueTuple<int[], string> valueTuple = new ValueTuple<int[], string>(new[] { 4, 2, 6, 6 }, "Hello World");

        //tuple = valueTuple; //! Different structs

        (int[], string) valueTupleShort = (new[] { 4, 2, 6, 6 }, "Hello World");

        (string firstName, string SecondName) = ("Robert", "Martin");

        var (x, _, y) = (0.5, 1, Math.PI);
        
        var (A, b) = GetMaxValues();

        Console.WriteLine($"{firstName} {SecondName}");

        Dictionary<(int,int),DateTime> dictionary = new Dictionary<(int, int), DateTime>
        {
            {(2,7),DateTime.Now },
            {(3,9),DateTime.UtcNow }
        };

            Console.WriteLine(GetMaxValues());
            Console.WriteLine(GetMaxValuesAsync().Result);
        }

        static (int a, int b) GetMaxValues()
        {
            return (int.MaxValue, int.MinValue);
        }

        static async Task<(int a, int b)> GetMaxValuesAsync()
        {
            return await Task.Factory.StartNew(() => (int.MaxValue,int.MinValue));
        }
        

Примечание: для использования структуры ValueTuple необходимо как целевую платформу выбирать .NET Framework 4.7 или соответствующие .NET Core/.NET Standart 2.0.
Либо можно установить ValueTuple как отдельный компонент через NuGet. Актуально при внедрении в существующие проекты или когда необходима минимальная версия .NET Framework для работы приложения.

Deconstruction

Распаковка кортежей.

Один из способов использования новых кортежей — их распаковка. Некоторые из примеров были описаны выше, когда мы разделяли кортеж на части, как отдельные новые переменные
(string firstName, string SecondName) = («Robert», «Martin»);
var (A, b) = GetMaxValues();
Распаковку можно применять не только к кортежам, но и к любым объектам. Необходимо только реализовать метод Deconstruct в классе.


    class Program
    {
        static void Main(string[] args)
        {
            Person person = new Person("Don", "Mattrick", 53);

            var (firstName, secondName, age) = person;

            Console.WriteLine($"{firstName} {secondName} {age}");
        }
    }

    public class Person
    {
        public string FirstName { get; set; }
        public string SecondName { get; set; }

        public int Age { get; set; }

        public Person(string firstName, string secondName, int age)
        {
            FirstName = firstName;
            SecondName = secondName;
            Age = age;
        }

        public void Deconstruct(out string firstName, out string secondName, out int age)
        {
            firstName = FirstName;
            secondName = SecondName;
            age = Age;
        }
    }

Local functions

Локальные функции.

Зачастую, открывая класс мы видим в нём слишком много методов, и некоторые из них используются только в контексте других методов. При большом количестве таких функций чтение кода может осложниться. В C# 7 появились локальные функции (методы). Использовать их можно тогда, когда в теле метода накопилось слишком много кода, и нужно его структурировать. Или же если в теле метода вызываются функции, которые используются только в контексте данного метода. Переменные метода будут так же доступны и в локальных функциях, что очень удобно.

Ранее


        static void Main(string[] args)
        {
            SearchFilesInTemp("*.tmp", 5000);
        }

        private static void SearchFilesInTemp(string fileName, long size)
        {
            List<string> files = new List<string>();
            foreach (var file in Directory.GetFiles(Path.GetTempPath(), fileName))
            {
                if (CheckFileSize(file, size))
                    files.Add(file);
            }
            PrintFilesPaths(files);                     
        }

        private static bool CheckFileSize(string file, long size)
        {
            return new FileInfo(file).Length > size;
        }
        private static void PrintFilesPaths(List<string> files)
        {
            if (files.Count == 0)
            {
                Console.WriteLine("Files not found");
                return;
            }

            foreach (var file in files)
            {
                Console.WriteLine(file);
            }
        }
        

В C# 7


        static void Main(string[] args)
        {
            SearchFiles("*.tmp", 5000);
        }
        private static void SearchFiles(string fileName, long size)
        {
            List<string> files = new List<string>();
            foreach (var file in Directory.GetFiles(Path.GetTempPath(), fileName))
            {
                if (CheckFileSize(file))
                    files.Add(file);
            }
            PrintFilesPaths();

            bool CheckFileSize(string file) => new FileInfo(file).Length > size;

            void PrintFilesPaths()
            {
                if (files.Count == 0)
                {
                    Console.WriteLine("Files not found");
                    return;
                }
                    
                foreach (var file in files)
                {
                    Console.WriteLine(file);
                }
            }
        }
        

В предыдущем примере у нас имеется единственный метод, который ищет файлы по определённым критериям во временной папке. Сам метод SearchFilesInTemp внутри себя разбит на две локальные функции.
CheckFileSize — производит фильтрацию найденных файлов по размеру.
PrintFilesPaths — выводит найденные пути к файлам.
Во многих случаях деконструкция метода на локальные функции может повысить читаемость и структурированность кода. Потому что для человека не знакомым с кодом сразу станет понятнее, что должы делать несколько строчек кода в методе. При условии, что все методы именуются правильно.

Generalized async return types / ValueTask

Обобщенные типы асинхронных возвратов

Раньше асинхронные методы могли возвращать void, Task и Task<T>. Так как Task является ссылочным типом, то при его частом использовании требуется выделение немалого количества памяти.
В C# 7 можно улучшить производительность путём использования ValueTask и кеширования результата. Например, если результат ожидаемый от Task задачи уже доступен, то вернуть его используя ValueTask<T>.


        static void Main(string[] args)
        {
            RunExampleAsync().Wait();
        }

        private static bool _cache;
        private static int _cacheResult;


        public static async Task RunExampleAsync()
        {
            var resultA = await AsyncCall();
            Console.WriteLine(resultA);
            var resultB = await AsyncCall();
            Console.WriteLine(resultB);
        }

        private static ValueTask AsyncCall()
        {
            return _cache ? new ValueTask(_cacheResult) : new ValueTask(LoadCache());
        }

        private static async Task LoadCache()
        {
            await Task.Delay(100);
            _cache = true;
            _cacheResult = int.MaxValue;
            return _cacheResult;
        }

throw expressions

throw выражения.

В новой версии C# стало больше мест, где можно использовать throw выражения.
Например, раньше необходимо было сначала произвести проверку на null и, если объект пустой, выкинуть исключение.
Теперь можно произвести проверку на null и выбросить исключение, используя оператор ??. Если значение получено, то присвоить его, если значение null, то выкинуть исключение.


    public partial class MainWindow : Window
    {
        private Brush BackgroundBrush { get; set; }

        public MainWindow()
        {
            InitializeComponent();

            ModalWindow modalWindow = GetModalWindow() ?? throw new InvalidOperationException("Could not find window");
            modalWindow.Show();
        }
        private string id;
        public string Id
        {
            get => id;
            set => id = value ?? throw new ArgumentNullException(paramName: nameof(value), message: "Id must not be null");
        }

        private void SetColor(Brush brush)
        {
            BackgroundBrush = brush ?? throw new ArgumentNullException(nameof(brush));

            _ = brush ?? throw new ArgumentNullException(nameof(brush));
        }

        private static ModalWindow GetModalWindow()
        {
            return Application.Current.Windows.OfType<ModalWindow>().FirstOrDefault();
        }
    }

Pattern matching

Сопоставление с шаблоном.
В C# 7 появляется такое понятие как Pattern matching. Сопоставление с шаблоном представляет собой синтаксическую конструкцию, которая позволяет проверить соответствие переменной определенному шаблону и извлечь из нее информацию, если соответствие имеется.
Имеются шаблоны следующих типов:

  • Constant matching — проверяет, равна ли переменная конкретно заданной константе или нет.
  • Type matching — проверяет, имеет ли переменная тип T, и если да, то извлекает ее значение в новую переменную x типа T.
  • Var matching — используется для создания новой переменной того же типа с тем же значением.

Constant matching


        static void Main(string[] args)
        {
            var a = 100;
            var s = "Hello";

            if (a is 100)
            {
                Console.WriteLine($"variable {a} == 100");
            }

            if (s is "Hello")
            {
                Console.WriteLine("variable s is a welcome message");
            }
        }
         

В данном примере мы сравниваем var переменную с константным значением. Эквивалентно оператору ==.

Type matching


Ранее


         public ICommand PasswordChangedCommand => passwordChangedCommand ??
                 (passwordChangedCommand = new RelayCommand<object>(object =>
                 {
                     PasswordBox passwordBox = object as PasswordBox;
                     if(passwordBox != null)
                        passwordBox.Tag = new NetworkCredential("", Password).Password;
                 }));

        

В C# 7


         public ICommand PasswordChangedCommand => passwordChangedCommand ??
                 (passwordChangedCommand = new RelayCommand<object>(object =>
                 {
                     if (object is PasswordBox box)
                        passwordBox.Tag = new NetworkCredential("", Password).Password;
                 }));
        

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

Примечание: если вы используете pattern matching для сравнения с типом, и в локальной области видимости имеется одноимённая к этим классом константа, то для сравнения с константой необходимо взять её в скобки.


    class Program
    {
        static void Main(string[] args)
        {
            const int Point = 12;

            bool CheckObject(object obj)
            {
                return obj is Point;
            }

            bool CheckObject_Const(object obj)
            {
                return obj is (Point);
            }
        }
    }

    public class Point
    {
        private int X { get; set; }
        private int Y { get; set; }
        public Point(int x, int y)
        {
            X = x;
            y = Y;
        }
    }

Switch-case


С введением pattern patching изменилось и поведение условного оператора switch.

  • Ранее через switch удобно было сравнивать только с константами и enum перечислениями. Сейчас же в switch можно делать сравнения: с константами, var переменными и типами.
  • Порядок выражений case теперь имеет значение. Так же, из-за того, что в case можно добавлять дополнительные условия к объектам, изменился и порядок их задания. Если кейсы с условиями выставлены в неверном порядке, то IDE выведет ошибку уровня компиляции — «The switch case has already been handled by a previous case».
    Условие по умолчанию как и раньше вычисляется последним.
  • В case можно добавлять дополнительные условия проверки, используя ключевое слово when.

        static void Main(string[] args)
        {
            var cat = new Cat("Lozly", 8, Colors.Black);
            var dog = new Dog("Borbosa", 20, Colors.Brown);
            var variable = "Hello";
            string empty = string.Empty;
            var list = new List<string>();

            ShowObjectInfo(cat);
            ShowObjectInfo(dog);
            ShowObjectInfo(variable);
            ShowObjectInfo(empty);
            ShowObjectInfo(list);
        }

        static void ShowObjectInfo(object @class)
        {
            switch (@class)
            {
                case Cat cat:
                    Console.WriteLine($"Cat: {cat.Name} Color: {cat.Color}");
                    break;
                case Dog dog when dog.Weight > 10:
                    Console.WriteLine($"{dog.Name} is a big dog");
                    break;
                case Dog d:
                    Console.WriteLine($"Dog: {d.Name} Weight: {d.Weight}");
                    break;
                case string s when string.IsNullOrEmpty(s):
                    Console.WriteLine("The empty string");
                    break;
                case string s:
                    Console.WriteLine(s);
                    break;
                case DateTime _:
                    Console.WriteLine("Object is DateTime");
                    break;
                case var o:
                    Console.WriteLine(o.GetType());
                    break;
                case null:
                    throw new ArgumentNullException(nameof(@class));
        
            }
        }
    }

    abstract class Animal
    {
        public abstract string Name { get; set; }
        public abstract int Weight { get; set; }
        public abstract Colors Color { get; set; }
    }

    internal class Cat : Animal
    {
        public Cat(string name, int weight, Colors color)
        {
            Name = name;
            Weight = weight;
            Color = color;
        }
        public override string Name { get; set; }
        public override int Weight { get; set; }
        public override Colors Color { get; set; }
    }

    internal class Dog : Animal
    {
        public Dog(string name, int weight, Colors color)
        {
            Name = name;
            Weight = weight;
            Color = color;
        }

        public override string Name { get; set; }
        public override int Weight { get; set; }
        public override Colors Color { get; set; }
    }

    public enum Colors
    {
        Brown,
        Black,
        White
    }
    

Ref locals and returns

Локальные переменные и возвращаемые значения по ссылке


Если ранее в safe коде можно было передавать только переменные в метод и возвращать их же (с помощью ключевого слова ref), то теперь можно объявлять и возвращаемый тип метода как ref, а также сохранять в локальную переменную по ссылке.
Т.е. теперь можно работать с unsafe кодом в safe манере. Из примера ниже можно увидеть, насколько это упрощает работу. Это может повысить производительность, когда необходимо передать ссылку на место в большой структуре данных.
Ранее


        static unsafe void Main(string[] args)
        {
            int[] array = {2, 4, 9, 7, 6, 2, 4, 12, 5, 6};
            int* maxElement = GetMaxElement(ref array);
            Console.WriteLine(*maxElement);
            Console.ReadKey();
        }

        static unsafe int* GetMaxElement(ref int[] array)
        {
            int maxElement = int.MinValue;
            int index = -1;
            for (int i = 0; i < array.Length - 1; i++)
            {
                if (maxElement < array[i])
                {
                    maxElement = array[i];
                    index = i;
                }                              
            }
            fixed(int* element = &array[index])
            return element;
        }

В C# 7


        static void Main(string[] args)
        {
            int[] array = {2, 4, 9, 7, 6, 2, 4, 12, 5, 6};
            int maxElement = GetMaxElement(ref array);
            Console.WriteLine(maxElement);
            Console.ReadKey();
        }

        static ref int GetMaxElement(ref int[] array)
        {
            int maxElement = int.MinValue;
            int index = -1;
            for (int i = 0; i < array.Length - 1; i++)
            {
                if (maxElement < array[i])
                {
                    maxElement = array[i];
                    index = i;
                }                              
            }
            return ref array[index];
        }


Примечание: По ссылке нельзя возвращать локальные переменные. Вернуть можно только те переменные, которые были переданы в метод, а также массивы.
Переменные инициализируются определенной ссылкой и в будущем не меняются.

Заключение

Очередная версия языка привнесла не мало новшеств в написание кода. C# 7 продолжает движение языка по упрощению написания кода, делая его ещё более понятным и компактным. Изменения коснулись не только синтаксиса, за счет добавления ещё большего «syntactic sugar» в язык, но и производительности. Используя ValueTask структуру и возврат значений по ссылке, появляется возможность улучшить производительность в коде, где это возможно.
Во время прошлого обновления языка, .NET языки C# и VB перенесены на компилятор Roslyn, который является open source проектом, появились новые структуры ValueTask и ValueTuple. Благодаря этому не нужно ждать большого обновления, а также переносить проект на новый .NET Framework, чтобы пользоваться новыми фитчами. Теперь достаточно загрузить необходимые компоненты через NuGet.
В целом видно, что Microsoft старается хоть и понемногу, но улучшать свой основной язык программирования в лучшую сторону, и, я думаю, у них это неплохо получается.

Читать и комментировать

Краснодар

Коммунаров, 268,
3 эт, офисы 705, 707

+7 (861) 200 27 34

Хьюстон

3523 Brinton trails Ln Katy

+1 833 933 0204

Москва

+7 (495) 145-01-05