Про новинки в .NET 5 и C# 9.0

    Добрый день.

    В нашей компании .NET используется с самого его рождения. У нас в продуктиве работают решения, написанные на всех версиях фреймворка: от самой первой и до последней на сегодняшний день .NET Core 3.1.

    История .NET, за которой мы пристально следим всё это время, творится на глазах: версия .NET 5, которую планируют релизить в ноябре, только что вышла в виде Release Candidate 2. Нас давно предупреждали, что пятая версия будет эпохальной: с нею закончится .NET-шизофрения, когда существовали две ветки фреймворка: классический и Core. Теперь они сольются в экстазе, и будет один сплошной .NET.

    Вышедший RC2 уже можно начинать полноценно использовать – никаких новых изменений перед релизом больше не ожидается, будет только фикс найденных багов. Более того: на RC2 уже работает официальный сайт, посвящённый .NET.

    А мы представляем вам обзор новшеств в .NET 5 и C# 9. Вся информация с примерами кода взята из официального блога разработчиков платформы .NET (а также ещё из массы источников) и проверена лично.

    Новые нативные и просто новые типы


    В C# и .NET одновременно добавили нативные типы:

    • nint и nuint для C#
    • соответствующие им System.IntPtr и System.UIntPtr в BCL

    Смысл для добавления этих типов — операции с низкоуровневыми API. А фишка в том, что реальный размер этих типов определяется уже во время выполнения и зависит от разрядности системы: на 32-разрядных их размер будет 4 байта, а на 64-разрядных, соответственно, 8 байт.

    С большой вероятностью вы не столкнётесь с этими типами в реальной работе. Как, впрочем, и с ещё одним новым типом: Half. Этот тип существует только в BCL, аналога в C# для него пока нет. Это 16-битный тип для значений с плавающей точкой. Он может пригодиться для тех случаев, когда адская точность не требуется, и можно выиграть немного памяти для хранения значений, ведь типы float и double занимают 4 и 8 байт. Самое интересное, что для этого типа вообще пока не определены арифметические операции, и вы не сможете даже сложить две переменные типа Half без явного приведения их к float или double. То есть, назначение этого типа сейчас чисто утилитарное — экономия места. Впрочем, арифметику к нему планируют добавить в следующем релизе .NET и C#. Через год.

    Атрибуты у локальных функций


    Раньше они были запрещены, и это создавало некоторые неудобства. В частности, нельзя было обвешать атрибутами параметры локальных функций. Теперь у них можно задавать атрибуты, причём, как для самой функции, так и для её параметров. Например, вот так:

    #nullable enable
    private static void Process(string?[] lines, string mark)
    {
        foreach (var line in lines)
        {
            if (IsValid(line))
            {
                // Processing logic...
            }
        }
    
        bool IsValid([NotNullWhen(true)] string? line)
        {
            return !string.IsNullOrEmpty(line) && line.Length >= mark.Length;
        }
    }
    

    Статические лямбда-выражения


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

    Чтобы избежать таких ошибок, лямбда-выражения теперь можно помечать ключевым словом static. И в этом случае они теряют доступ к любому локальному контексту: от локальных переменных до this и base.

    Вот довольно исчерпывающий пример использования:

    static void SomeFunc(Func<int, int> f)
    {
        Console.WriteLine(f(5));
    }
    
    static void Main(string[] args)
    {
        int y1 = 10;
        const int y2 = 10;
        SomeFunc(i => i + y1);          // выведет 15
        SomeFunc(static i => i + y1);   // ошибка компиляции: y1 не видна в лямбде
        SomeFunc(static i => i + y2);   // выведет 15
    }

    Обратите внимание на то, что константы статические лямбды захватывают прекрасно.

    GetEnumerator как метод расширения


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

    Вот пример, когда через foreach становится возможно перебрать ValueTuple с помощью написанного для него метода расширения:

    static class Program
    {
        public static IEnumerator<T> GetEnumerator<T>(this ValueTuple<T, T, T, T, T> source)
        {
            yield return source.Item1;
            yield return source.Item2;
            yield return source.Item3;
            yield return source.Item4;
            yield return source.Item5;
        }
    
        static void Main(string[] args)
        {
            foreach(var item in (1,2,3,4,5))
            {
                System.Console.WriteLine(item);
            }
        }
    }

    Этот код выводит в консоль числа от 1 до 5.

    Discard pattern в параметрах лямбда-выражений и анонимных функций


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

    Func<int, int, int> someFunc1 = (_, _) => {return 5;};
    Func<int, int, int> someFunc2 = (int _, int _) => {return 5;};
    Func<int, int, int> someFunc3 = delegate (int _, int _) {return 5;};
    

    Инструкции верхнего уровня в C#


    Речь идёт об упрощённой структуре кода на C#. Теперь написание простейшего кода действительно выглядит просто:

    using System;
    
    Console.WriteLine("Hello World!");
    

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

    Кстати, в будущем разработчики C# думают развить тему с упрощением синтаксиса и попробовать избавиться от конструкции using System; в очевидных случаях. А пока можете избавиться от неё, просто написав вот так:

    System.Console.WriteLine("Hello World!");
    

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

    Можно использовать более сложные варианты:

    using System;
    using System.Runtime.InteropServices;
    
    Console.WriteLine("Hello World!");
    FromWhom();
    Show.Excitement("Top-level programs can be brief, and can grow as slowly or quickly in complexity as you'd like", 8);
    
    void FromWhom()
    {
        Console.WriteLine($"From {RuntimeInformation.FrameworkDescription}");
    }
    
    internal class Show
    {
        internal static void Excitement(string message, int levelOf)
        {
            Console.Write(message);
    
            for (int i = 0; i < levelOf; i++)
            {
                Console.Write("!");
            }
    
            Console.WriteLine();
        }
    }
    

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

    Разумеется, у этой фичи есть ограничения. Главное из них — так можно делать только в одном файле проекта. Как правило, это имеет смысл делать в том файле, где вы раньше создавали точку входа в программу в виде функции Main(string[] args). При этом саму функцию Main определять там же нельзя — это второе ограничение. Фактически, сам такой файл с упрощённым синтаксисом и есть функция Main, и в нём даже уже присутствует неявно переменная args, являющаяся массивом с параметрами. То есть, вот такой код тоже скомпилируется и выведет длину массива:

    System.Console.WriteLine(args.Length);
    

    В общем, фича не самая важная, но для демонстрационных и обучающих целей вполне себе подходит. Детали тут.

    Pattern matching в операторе if


    Представьте, что вам надо проверить переменную-объект на то, что она не принадлежит определённому типу. До сих пор надо было писать вот так:

    if (!(vehicle is Car)) { ... }
    

    Но с C# 9.0 можно писать по-человечески:

    if (vehicle is not Car) { ... }
    

    Также появилась возможность компактной записи некоторых проверок:

    if (context is {IsReachable: true, Length: > 1 })
    {
        Console.WriteLine(context.Name);
    }
    

    Эта новая форма записи эквивалентна старой доброй вот такого вида:

    if (context is object && context.IsReachable && context.Length > 1 )
    {
        Console.WriteLine(context.Name);
    }
    

    Или ещё можно записать то же самое относительно по-новому (но это уже вчерашний день):

    if (context?.IsReachable == true && context?.Length > 1 )
    {
        Console.WriteLine(context.Name);
    }
    

    В новом синтаксисе можно также использовать логические операторы and, or и not, плюс, скобки для расстановки приоритетов:

    if (context is {Length: > 0 and (< 10 or 25) })
    {
        Console.WriteLine(context.Name);
    }
    

    И это только улучшения pattern matching в обычном if. Что добавили в pattern matching для switch expression – читайте далее.

    Улучшенный pattern matching в switch expression


    В switch expression (не путать с оператором switch) добавили огромные улучшения в плане pattern matching. Рассмотрим на примерах из официальной документации. Примеры посвящены расчёту платы за проезд какого-то транспорта в некоторое время. Вот первый пример:

    public decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car c           => 2.00m,
        Taxi t          => 3.50m,
        Bus b           => 5.00m,
        DeliveryTruck t => 10.00m,
        { }             => throw new ArgumentException("Unknown vehicle type", nameof(vehicle)),
        null            => throw new ArgumentNullException(nameof(vehicle))
    };
    

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

    Это не всё. Обратите внимание, что для каждого сопоставления с объектом вы вынуждены создавать переменную: c для Car, t для Taxi и так далее. Но эти переменные не используют. В таких случаях уже сейчас в C# 8.0 можно использовать discard pattern:

    public decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car _           => 2.00m,
        Taxi _          => 3.50m,
        Bus _           => 5.00m,
        DeliveryTruck _ => 10.00m,
        // ...
    };
    

    Но начиная с девятой версии C# можно вообще ничего не писать в таких случаях:

    public decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car             => 2.00m,
        Taxi            => 3.50m,
        Bus             => 5.00m,
        DeliveryTruck   => 10.00m,
        // ...
    };
    

    Улучшения в switch expression на этом далеко не заканчиваются. Теперь легче стало писать более сложные выражения. Например, часто возвращаемый результат должен зависеть от значений свойств переданного объекта. Теперь это можно записать короче и удобнее, чем комбинацией if'ов:

    public static decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car { Passengers: 0 } => 2.00m + 0.50m,
        Car { Passengers: 1 } => 2.0m,
        Car { Passengers: 2 } => 2.0m - 0.50m,
        Car => 2.00m - 1.0m,
        // ...
    };
    

    Обратите внимание на первые три строки в switch: по факту проверяется значение свойства Passengers и в случае равенства возвращается соответствующий результат. Если же не будет ни одного совпадения, то будет возвращено значение для общего варианта (четвёртая строка внутри switch). Кстати, значения свойств проверяются только в том случае, если переданный объект vehicle не равен null и является экземпляром класса Car. То есть, бояться Null Reference Exception при проверках не стоит.

    Но и это ещё не всё. Теперь в switch expression можно даже писать выражения для более удобного сопоставления:

    public static decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
        Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
        Bus => 5.00m,
    
        DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
        DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
        DeliveryTruck => 8.00m,
        // ...
    };
    

    И это тоже ещё не всё. Синтаксис switch expression расширили до вложенных switch expression, чтобы нам было ещё проще описывать сложные условия:

    public static decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car c => c.Passengers switch
        {
            0 => 2.00m + 0.5m,
            1 => 2.0m,
            2 => 2.0m - 0.5m,
            _ => 2.00m - 1.0m
        },
        // ...
    };
    

    В итоге если полностью склеить все уже приведённые примеры кода, то получится вот такая картина со всеми описанными нововведениями сразу:

    public static decimal CalculateToll(object vehicle) =>
        vehicle switch
    {
        Car c => c.Passengers switch
        {
            0 => 2.00m + 0.5m,
            1 => 2.0m,
            2 => 2.0m - 0.5m,
            _ => 2.00m - 1.0m
        },
    
        Taxi t => t.Fares switch
        {
            0 => 3.50m + 1.00m,
            1 => 3.50m,
            2 => 3.50m - 0.50m,
            _ => 3.50m - 1.00m
        },
    
        Bus b when ((double)b.Riders / (double)b.Capacity) < 0.50 => 5.00m + 2.00m,
        Bus b when ((double)b.Riders / (double)b.Capacity) > 0.90 => 5.00m - 1.00m,
        Bus => 5.00m,
    
        DeliveryTruck t when (t.GrossWeightClass >= 5000) => 10.00m + 5.00m,
        DeliveryTruck t when (t.GrossWeightClass >= 3000 && t.GrossWeightClass < 5000) => 10.00m,
        DeliveryTruck => 8.00m,
    
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException(nameof(vehicle))
    };
    

    Но и это тоже ещё не всё. Вот ещё один пример: обычная функция, которая с помощью механизма switch expression на основании переданного времени определяет нагрузку: утренний/вечерний час пик, дневной и ночной периоды:

    private enum TimeBand
    {
        MorningRush,
        Daytime,
        EveningRush,
        Overnight
    }
    
    private static TimeBand GetTimeBand(DateTime timeOfToll) =>
        timeOfToll.Hour switch
        {
            < 6 or > 19 => TimeBand.Overnight,
            < 10 => TimeBand.MorningRush,
            < 16 => TimeBand.Daytime,
            _ => TimeBand.EveningRush,
        };
    

    Как видите, в C# 9.0 ещё появилась возможность использовать при сопоставлении операторы сравнения <, >, <=, >=, а также логические операторы and, or и not.

    Но и это, чёрт побери, ещё не конец. В switch expression теперь можно использовать… кортежи. Вот полный пример кода, который вычисляет некий коэффициент к плате за проезд, в зависимости от дня недели, времени суток и направления движения (в город/из города):

    private enum TimeBand
    {
        MorningRush,
        Daytime,
        EveningRush,
        Overnight
    }
    
    private static bool IsWeekDay(DateTime timeOfToll) =>
        timeOfToll.DayOfWeek switch
    {
        DayOfWeek.Saturday => false,
        DayOfWeek.Sunday => false,
        _ => true
    };
    
    private static TimeBand GetTimeBand(DateTime timeOfToll) =>
        timeOfToll.Hour switch
    {
        < 6 or > 19 => TimeBand.Overnight,
        < 10 => TimeBand.MorningRush,
        < 16 => TimeBand.Daytime,
        _ => TimeBand.EveningRush,
    };
    
    public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
        (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true) => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime, true) => 1.50m,
        (true, TimeBand.Daytime, false) => 1.50m,
        (true, TimeBand.EveningRush, true) => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight, true) => 0.75m,
        (true, TimeBand.Overnight, false) => 0.75m,
        (false, TimeBand.MorningRush, true) => 1.00m,
        (false, TimeBand.MorningRush, false) => 1.00m,
        (false, TimeBand.Daytime, true) => 1.00m,
        (false, TimeBand.Daytime, false) => 1.00m,
        (false, TimeBand.EveningRush, true) => 1.00m,
        (false, TimeBand.EveningRush, false) => 1.00m,
        (false, TimeBand.Overnight, true) => 1.00m,
        (false, TimeBand.Overnight, false) => 1.00m,
    };
    

    В методе PeakTimePremiumFull как раз для сопоставления используются кортежи, и это стало возможным в новой версии C# 9.0. Кстати, если посмотреть внимательно на код, то напрашиваются две оптимизации:

    • последние восемь строк возвращают одно и то же значение;
    • дневной и ночной трафик имеют один и тот же коэффициент.

    В итоге код метода можно сильно сократить с помощью discard pattern:

    public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
        (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.MorningRush, true)  => 2.00m,
        (true, TimeBand.MorningRush, false) => 1.00m,
        (true, TimeBand.Daytime,     _)     => 1.50m,
        (true, TimeBand.EveningRush, true)  => 1.00m,
        (true, TimeBand.EveningRush, false) => 2.00m,
        (true, TimeBand.Overnight,   _)     => 0.75m,
        (false, _,                   _)     => 1.00m,
    };
    

    Ну а если ещё более внимательно присмотреться, то можно сократить и этот вариант, вынеся в общий случай коэффициент 1.0:

    public static decimal PeakTimePremiumFull(DateTime timeOfToll, bool inbound) =>
        (IsWeekDay(timeOfToll), GetTimeBand(timeOfToll), inbound) switch
    {
        (true, TimeBand.Overnight, _) => 0.75m,
        (true, TimeBand.Daytime, _) => 1.5m,
        (true, TimeBand.MorningRush, true) => 2.0m,
        (true, TimeBand.EveningRush, false) => 2.0m,
        _ => 1.0m,
    };
    

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

    Кортежи в switch expression можно использовать и в C# 8.0. Никчемный разработчик, который писал эту статью, стал немножечко умнее.


    Ну и напоследок вот ещё один сумасшедший пример, демонстрирующий новый синтакcис сопоставлений и с кортежами, и по свойствам объектов:

    public static bool IsAccessOkOfficial(Person user, Content content, int season) => 
        (user, content, season) switch 
    {
        ({Type: Child}, {Type: ChildsPlay}, _)          => true,
        ({Type: Child}, _, _)                           => false,
        (_ , {Type: Public}, _)                         => true,
        ({Type: Monarch}, {Type: ForHerEyesOnly}, _)    => true,
        (OpenCaseFile f, {Type: ChildsPlay}, 4) when f.Name == "Sherlock Holmes"  => true,
        {Item1: OpenCaseFile {Type: var type}, Item2: {Name: var name}} 
            when type == PoorlyDefined && name.Contains("Sherrinford") && season >= 3 => true,
        (OpenCaseFile, var c, 4) when c.Name.Contains("Sherrinford")              => true,
        (OpenCaseFile {RiskLevel: >50 and <100 }, {Type: StateSecret}, 3) => true,
        _                                               => false,
    };
    

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

    Новый new, а также в принципе улучшенный target typing


    Давным-давно в C# появилась возможность писать var вместо названия типа, ибо сам тип можно было определить из контекста (собственно, это и называется target typing). То есть, вместо вот такой записи:

    SomeLongNamedType variable = new SomeLongNamedType();
    

    стало возможно писать более компактно:

    var variable = new SomeLongNamedType()
    

    И о типе переменной variable компилятор догадается сам. Спустя годы реализовали обратный синтаксис:

    SomeLongNamedType variable = new ();
    

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

    var result = SomeMethod(new (2020,10,01));
    
    //...
    
    public Car SomeMethod(DateTime p)
    {
        //...
    
        return new() { Passengers = 2 };
    }
    

    В данном примере при вызове SomeMethod параметр типа DateTime создаётся сокращённым синтаксисом. Возвращаемое из метода значение создаётся таким же способом.

    Где действительно будет выгода от такого синтаксиса, так это при определении коллекций:

    List<DateTime> datesList = new()
    {
        new(2020, 10, 01),
        new(2020, 10, 02),
        new(2020, 10, 03),
        new(2020, 10, 04),
        new(2020, 10, 05)
    };
    
    Car[] cars = 
    {
        new() {Passengers = 2},
        new() {Passengers = 3},
        new() {Passengers = 4}
    };
    

    Отсутствие необходимости писать полное название типа при перечислении элементов коллекции делает код чуть-чуть чище.

    Target typed операторы ?? и ?:


    В C# 9.0 прокачали тернарный оператор ?:. Раньше он требовал полного соответствия типов возвращаемых значений, а сейчас он более сообразительный. Вот пример выражения, недопустимого в ранних версиях языка, но вполне легального в девятой:

    int? result = b ? 0 : null; // nullable value type
    

    Раньше требовалось явное приведение типов у нуля к int?.. Теперь это не является необходимым.

    Также в новой версии языка допустимо использовать вот такую конструкцию:

    Person person = student ?? customer; // Shared base type
    

    Типы customer и student, хоть и являются производными от Person, но формально являются разными. Предыдущая версия языка не разрешала вам использовать такую конструкцию без явного приведения типа. Теперь компилятор прекрасно понимает, что имеется в виду.

    Переопределение возвращаемого типа методов


    В C# 9.0 разрешили переопределять возвращаемый тип у перекрываемых методов. Требование одно: новый тип должен быть наследуемым от оригинального (ковариантным). Вот пример:

    abstract class Animal
    {
        public abstract Food GetFood();
        ...
    }
    class Tiger : Animal
    {
        public override Meat GetFood() => ...;
    }
    

    В классе Tiger возвращаемое значение у метода GetFood переопределено с Food на Meat. Теперь это нормально если Meat является производным типом от Food.

    init-свойства — это не совсем readonly-члены


    В новой версии языка появилась интересная фишка: init-свойства. Это свойства, значение которых можно установить только при начальной инициализации объекта. Казалось бы, что для этого существуют readonly-члены класса, но на самом деле это разные вещи, позволяющие решать разные задачи. Чтобы понять, в чём разница, и в чём прелесть init-свойств, вот пример:

    Person employee = new () {
        Name = "Paul McCartney",
        Company = "High Technologies Center",
        CompanyAddress = new () {
            Country = "Russia",
            City = "Izhevsk",
            Line1 = "246, Karl Marx St."
        }
    }

    Подобный синтаксис объявления экземпляра класса очень удобен, особенно когда среди свойств класса встречаются ещё объекты. Но у такого синтаксиса есть ограничения: соответствующие свойства класса должны быть изменяемыми. Всё потому, что инициализация этих свойств происходит после вызова конструктора. То есть, класс Person из примера должен быть объявлен вот таким образом:

    class Person {
        //...
        public string Name {get; set;}
        public string Company {get; set;}
        public Address CompanyAddress {get; set;}
        //...
    }
    

    Однако, по факту свойство Name является неизменяемым. В настоящее время есть только единственный способ сделать это свойство read-only – объявить приватный сеттер:

    class Person {
        //...
        public string Name {get; private set;}
        //...
    }
    

    Но в этом случае мы сразу же лишаемся возможности пользоваться удобным синтаксисом объявления экземпляра класса через присваивание значений свойствам внутри фигурных скобок. И мы можем установить значение свойства Name только передав его в параметры конструктору класса. Теперь представьте, что свойство CompanyAddress, вообще-то, тоже неизменяемое по смыслу. В общем, в такую ситуацию я попадал очень много раз, и всегда приходилось выбирать из двух зол:

    • навороченные конструкторы с кучей параметров, зато все свойства класса read-only;
    • удобный синтаксис создания объекта, но зато все свойства класса read-write, а я должен об этом помнить и случайно не поменять их где-то.

    В этом месте кто-то может вспомнить про readonly-члены класса и предложить оформить класс Person так:

    class Person {
        //...
        public readonly string Name;
        public readonly string Company;
        public readonly string CompanyAddress;
        //...
    }
    

    На что я отвечу, что этот способ мало того, что не по фен-шую, так он ещё и не решает проблему удобной инициализации: readonly-члены тоже могут быть заданы только в конструкторе, как и свойства с приватным сеттером.

    Но в C# 9.0 эта проблема решена: если вы определите свойство, как init-свойство, вы получите и удобный синтаксис создания объекта, и реально неизменяемое в дальнейшем свойство:

    class Person {
        public string Name { get; init; }
        public string Company { get; init; }
        public Address CompanyAddress { get; init; }
    }
    

    Кстати, в init-свойствах, как и в конструкторе, можно инициализировать readonly-члены класса, и вы можете писать вот так:

    public class Person
    {
        private readonly string name;
           
        public string Name
        { 
            get => name; 
            init => name = (value ?? throw new ArgumentNullException(nameof(Name)));
        }
    }
    

    Record — это узаконенная DTO'шка


    Продолжая тему неизменяемых свойств, мы подошли к главному, на мой взгляд, нововведению языка: типу record. Этот тип предназначен для удобного создания целых неизменяемых структур, а не только свойств. Причина для появления отдельного типа проста: работая по всем канонам, мы постоянно создаём DTO'шки для изоляции разных уровней приложения. DTO'шки обычно представляют собой просто набор полей, без всякой бизнес-логики. И, как правило, значения этих полей не изменяются в течение времени жизни этой DTO'шки.

    Лирическое отступление.

    DTO – Data Transfer Object. Такой объект служит только для передачи данных между различными слоями внутри приложения (DAL, BL, PL) или подсистемами внутри какой-то системы приложений. Ещё мы их часто называем «модельки». Обычно мы возвращаем такие модельки-DTO'шки из уровня DAL в уровень BL, там обогащаем полученные данные, оборачиваем их в новую DTO-модельку, выкидываем на уровень представления, а там уже вполне можем преобразовать ещё раз в очередную DTO-модельку, чтобы выкинуть клиенту в каком-то виде (в HTML-вьюшке или JSON-строке).

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

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

    В общем, с DTO-модельками дело имеют все.

    Так, спустя много-много лет, разработчики C# добрались-таки до реально нужного улучшения: они легализовали DTO-модельки в виде отдельного типа record.

    До сих пор все DTO-модельки, что мы создавали (а мы создавали их массово) представляли собой обычные классы. Для компилятора и для рантайма они ничем не отличались от всех прочих классов, хотя не являлись таковыми в классическом смысле. Мало кто использовал для DTO-моделек структуры (struct) — это не всегда было приемлемо по различным причинам.

    Теперь же мы можем определить record (далее — запись) — особую структуру, которая предназначена для создания неизменяемых DTO-моделек. Запись занимает промежуточное место между структурам и классами в их обычном понимании. Это и недокласс и сверхструктура. Запись — всё ещё ссылочный тип со всеми вытекающими последствиями. Записи почти всегда ведут себя как обычный класс, могут содержать методы, допускают наследование (но только от других записей, не от объектов, хотя если запись явно не наследуется ни от чего, то она так же неявно наследуется от object, как и всё в C#), могут реализовывать интерфейсы. Более того, вы вообще не обязаны делать записи полностью неизменяемыми. А где же тогда смысл и в чём разница?

    Давайте просто создадим запись:

    public record Person 
    {
        public string LastName { get; }
        public string FirstName { get; }
    
        public Person(string first, string last) => (FirstName, LastName) = (first, last);
    }
    

    А теперь вот вам пример использования:

    Person p1 = new ("Paul", "McCartney");
    Person p2 = new ("Paul", "McCartney");
    
    System.Console.WriteLine(p1 == p2);
    

    Этот пример выведет в консоль true. Если бы Person был классом, то в консоль было бы выведено false, поскольку объекты сравниваются по ссылке: две ссылочные переменные равны только если ссылаются на один и тот же объект. Но это не так с записями. Записи сравниваются по значению всех их полей, включая приватные.

    Продолжая предыдущий пример, посмотрим на этот код:

    System.Console.WriteLine(p1);
    

    В случае с классом мы бы получили в консоль полное название класса. Но в случае с записями мы увидим в консоли вот что:

    Person { LastName = McCartney, FirstName = Paul}
    

    Дело в том, что для записей метод ToString() неявно переопределяется и выводит не название типа, а полный список публичных полей со значениями. Точно так же для записей неявно переопределены операторы == и !=, что даёт возможность менять логику сравнения.

    Поиграем с наследованием записей:

    public record Teacher : Person
    {
        public string Subject { get; }
    
        public Teacher(string first, string last, string sub)
            : base(first, last) => Subject = sub;
    }
    

    Теперь создадим две записи разных типов и сравним их:

    Person p = new("Paul", "McCartney");
    Teacher t = new("Paul", "McCartney", "Programming");
    
    System.Console.WriteLine(p == t);
    

    Хотя запись Teacher и унаследована от Person, переменные p и t не будут равны, в консоль выведется false. Это потому что сравнение производится не только по всем полям записей, но и по типам, а типы тут явно разные.

    И хотя сравнение наследуемых типов записей разрешено (но бессмысленно), сравнение вообще разных типов записей недопустимо в принципе:

    public record Person
    {
        public string LastName { get; }
        public string FirstName { get; }
    
        public Person(string first, string last) => (FirstName, LastName) = (first, last);
    }
    
    public record Person2
    {
        public string LastName { get; }
        public string FirstName { get; }
    
        public Person2(string first, string last) => (FirstName, LastName) = (first, last);
    }
    
    // ...
    
    Person p = new("Paul", "McCartney");
    Person2 p2 = new("Paul", "McCartney");
    System.Console.WriteLine(p == p2);    // ошибка компиляции
    

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

    Ещё одна приятная особенность записей — ключевое слово with, с помощью которого легко создавать модификации ваших DTO-моделек. Посмотрите на пример:

    Person me = new("Steve", "Brown");
    Person brother = me with { FirstName = "Paul" };
    

    В этом примере у записи brother значения всех полей будут заполнены из записи me, кроме поля FirstName – оно будет изменено на Paul.

    До сих пор вы видели классический способ создания записей — с полным определением конструкторов, свойств и так далее. Но теперь появился ещё и лаконичный способ:

    public record Person(string FirstName, string LastName);
    
    public record Teacher(string FirstName, string LastName,
        string Subject)
        : Person(FirstName, LastName);
    
    public sealed record Student(string FirstName,
        string LastName, int Level)
        : Person(FirstName, LastName);
    

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

    public record Pet(string Name)
    {
        public void ShredTheFurniture() =>
            Console.WriteLine("Shredding furniture");
    }
    
    public record Dog(string Name) : Pet(Name)
    {
        public void WagTail() =>
            Console.WriteLine("It's tail wagging time");
    
        public override string ToString()
        {
            StringBuilder s = new();
            base.PrintMembers(s);
            return $"{s.ToString()} is a dog";
        }
    }
    

    Свойства и конструктор у записей в этом случае также будут созданы автоматически. Всё меньше и меньше boilerplate-кода, но только применимо к записям. Для классов и структур это не работает.

    Кроме всего уже сказанного, компилятор также умеет автоматически создавать деконструктор для записей:

    var person = new Person("Bill", "Wagner");
    
    var (first, last) = person; // Этот деконструктор создан автоматически
    Console.WriteLine(first);
    Console.WriteLine(last);
    

    Как бы там ни было, но на уровне IL записи всё ещё представляют собой класс. Однако, есть одно подозрение, которому пока не найдено никакого подтверждения: наверняка на уровне рантайма записи где-то будут дико оптимизироваться. Скорее всего, за счёт того, что заведомо будет известно, что какая-то конкретная запись — неизменяемая. Это открывает возможности для оптимизации, как минимум, в многопоточной среде, причём, разработчику даже не надо прикладывать особенных усилий для этого.

    А пока — все переписываем DTO-модельки с классов на записи.

    .NET Source Generators


    Source Generator (далее — просто генератор) — это довольно интересная фишка. Генератор является куском кода, который выполняется на этапе компиляции, имеет возможность проанализировать уже скомпилированный код, и может сгенерировать дополнительный код, который так же будет скомпилирован. Если не совсем понятно, то вот один довольно актуальный пример, когда генератор может быть востребован.

    Представьте себе веб-приложение на C#/.NET, которое вы пишете на ASP.NET Core. При запуске этого приложения происходит огромное количество инициализационной фоновой работы по анализу того, из чего это приложение состоит и что вообще должно делать. При этом неистово используется рефлексия. В результате время от запуска приложения до начала обработки первого запроса может быть неприлично долгим, что неприемлемо в высоконагруженных сервисах. Генератор может помочь сократить это время: ещё на этапе компиляции он может проанализировать ваше уже скомпилированное приложение и дополнительно сгенерировать нужный код, который проинициализирует его при запуске гораздо быстрее.

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

    Генераторы кода — тема новая и слишком необычная, чтобы уместить её в рамках этого поста. Дополнительно можно ознакомиться с примером простейшего «Hello, world!» генератора в этом обзоре.

    С генераторами кода связаны две новые фичи, про которые написано далее.

    Частичные методы (partial method)


    Частичные классы в C# есть уже давно, их изначальная цель — отделять код, сгенерированный неким дизайнером от кода, написанного программистом. В C# 9.0 подогнали частичные методы. Они выглядят примерно так:

    public partial class MyClass
    {
        public partial int DoSomeWork(out string p);
    }
    public partial class MyClass
    {
        public partial int DoSomeWork(out string p)
        {
            p = "test";
            System.Console.WriteLine("Partial method");
            return 5;
        }
    }
    

    Этот суррогатный пример демонстрирует то, что частичные методы ничем, по сути, не отличаются от обычных: могут возвращать значения, могут принимать out-переменные, могут иметь модификаторы доступа.

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

    Методы-инициализаторы (module initializers)


    Есть три повода для внедрения этого функционала:

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

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

    using System.Runtime.CompilerServices;
    class C
    {
        [ModuleInitializer]
        internal static void M1()
        {
            // ...
        }
    }
    

    На метод накладываются некоторые ограничения:

    • он должен быть статическим;
    • он не должен иметь параметров;
    • он не должен ничего возвращать;
    • он не должен работать с обобщениями (generics);
    • он должен быть доступен из содержащего его модуля, то есть:
      • он должен быть internal или public
      • он не должен быть локальным методом

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

    Заключение


    Уже опубликовав пост, мы заметили, что он больше посвящён новинкам в языке C# 9.0, чем новинкам самого .NET. Но и так неплохо получилось.
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      +10
      В C# и .NET одновременно добавили нативные типы

      Добавили?! Они же там с рождения были! Максимум, что могли добавить в язык — так это короткие псевдонимы и арифметические операции (которые и раньше были, но не языковые, а перегруженные).


      Частичные классы в C# есть уже давно, их изначальная цель — отделять код, сгенерированный неким дизайнером от кода, написанного программистом. В C# 9.0 подогнали частичные методы.

      [...] По имеющейся информации частичные методы будут тесно связаны с генераторами кода, где и предполагается их использование.

      Частичные методы точно так же есть уже давно, и их именно для генераторов кода и делали. Их ещё дизайнер Linq2Sql использовал как возможные точки расширения...


      Новые тут разве что out-параметры.

        0
        Новое ещё как минимум то, что они могут быть public и возвращать значение.
          0
          В C# и .NET одновременно добавили нативные типы
          Добавили?! Они же там с рождения были!

          Они были в IL/CLR, но C# и BCL ничего про них не знали.

          +5
          В нашей компании .NET используется с самого его рождения

          Как вы относитесь к перенагромождению C# фичами? Помните, каким простым был C# 2? Ощущение, что фичи добавляются ради фич, чтобы имитировать деятельность. Напр., не вижу никакого смысла в новом обратном синтаксисе MyType t = new() Или, например, кому было худо от отсутствия маппинга System.IntPtr как nint?


          Сейчас мне больше импонирует подход Go, где каждое новое изменение языка/стандартной библиотеки тщательно выверяется ("а стоит ли оно того?")

            +27

            Лично я отношусь с осторожным интересом.


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


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


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

              +3

              Ну с C# 2.0 разница может будет большая, а вот с 5.0 уже не особо. Как раз с тех пор глобально ничего супер-полезного не добавили. Хотя полезные мелочи тут и там, конечно, используются. Но прям удобств всё не добавляют и не добавляют. разве что генераторы чуть более решительные шаг вперед, но про них ниже.

              +9
              Напр., не вижу никакого смысла в новом обратном синтаксисе MyType t = new()

              А так:


              var x = new List<Point>{ new (10, 11), new (4, 66)};
                +3
                Кстати, а объявление полей работает аналогично? Типа:
                private List<T> items = new();
                Ну и в довесок — а свойства с означенным значением, или как оно по русски называется:
                public List<T> Items {get;set;} = new();
                  +2

                  Да, попробуйте сами на https://sharplab.io/

                    +14

                    Чёрт. Какая прекрасная фича. В этом релизе они добавили почти всё, что мне так не хватает в повседневной работе с СиШарп. Особенно рекорды и инит

                      +3

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

                        +6

                        Мне в повседневной жизни не хватает АДТ (чтобы описать тип ИЛИ то ИЛИ это, реализация на свитче по типам выглядит ужасно неудобно), не хватает хотя бы базовых пруфов (хочу получить массив объектов, отсортированных именно вот по этому полю), тайпклассов (хочу вешать ограничения на типы, а не на инстансы типов. Например


                        void PrintMaxValueTwice<T>() where T : HasMaxValue 
                        {
                          Console.WriteLine(T.MaxValue);
                          Console.WriteLine(T.MaxValue);
                        }
                        
                        PrintMaxValueTwice<int>();
                        PrintMaxValueTwice<TimeSpan>();

                        Не хватает ХКТ, чтобы я мог абстрагироваться от конкретных типов в ненужных местах:


                        async T<int> DoubleValueAsync<T>(T<int> asyncFunc) where T<X> : IAwaitable<X>
                        {
                          var result = await asyncFunc();
                          return result * 2;
                        }
                        var t1 = DoubleValueAsync(Task.FromResult(10));
                        var t2 = DoubleValueAsync(ValueTask.FromResult(10));

                        Ну и рекорды, да, немного экономят на необходимости жать ctrl + alt + enter -> generate equality members.

                          +2

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

                    0

                    Хороший аргумент. Но большое число вариантов сделать одно и то же, конечно, вгоняют в грусть.

                    +5

                    До крестов шарпу пока далековато, хотя, признаться, новый синтаксис паттерн-матчинга инопланетный: использовать в шарпе "неполные" куски кода типа (< 0 or > 10) — это не так же естественно, как в функциональных языках, где эти куски по отдельности реально существуют.


                    Все остальные фичи идут на упрощение восприятия того, что и так существовало: если у записи нет оператора равенства, то не надо думать о его семантике; если нет функции Main, то не надо думать, какой из вариантов функции использован; большинство будет использовать генераторы, а не писать их; target typing чисто убирает копипасту в нескольких местах, где она ещё оставалась.

                    0

                    Прямо какой-то пролог получается

                      +18
                      F# получается
                        +1
                        Ну F# как бы изначально был полигоном по обкатке фич, потому не странно.
                          –4

                          А где тут F#? Паттерн матчинг например был в хаскеле в 91м году, в скалакотлинах чуть позже, почему именно F#?

                            +1
                            Ближайший в области видимости сишарпистов язык.
                              0

                              Я бы назвал это скорее параллельной эволюцей, ну да ладно.

                                0

                                Языки первой степени родства (знакомые с теорией музыки и гармонией поймут).

                          +6

                          Из всего перечисленного конечно радуют ковариантные типы у виртуальных методов, адекватный вывод типа у ? : тернарного оператора, ну и паттерн not.


                          Недавно смотрел обсуждение с, если не изменяет память, Мадсом Торгерсеном (ведущий дизайнер языка), разговор был кажется с ЮАР-коммъюнити. Там самый популярный вопрос был — почему with не завезли для кастомных типов. И ответ был очень интересный — они планируют ввести некий то ли аттрибут то ли еще какой-то флаг для методов, который будет указывать, что метод конструирует объект. Работает например для фабрик. Благодаря этому можно избавиться от внутренней магии, которая сейчас существует в record, и реализовать адекватную поддержку with для типов, ну и добавить немного оптимизаций. Я конечно разделяю энтузиазм полноценных DTO на классах с наследованием и вот этим всем, но условный struct record, или даже ручной readonly struct с get/init свойствами и поддержкой with очень сильно пойдет на пользу при работе с immutable типами, особенно сейчас, когда везде Span, ref, stackalloc и вот это вот все. Можно будет заводить var otherRectangle = thisRectangle with {Width = 10}. Придется ждать 10 версии языка.

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

                              Я бы сказал — в некий мультитул. К примеру, у меня есть такие физические устройства: от швейцарского (на самом деле китайского) ножа до мультиметра. И у них есть такие функции, которыми я ни разу не пользовался и даже не знаю, что это такое. Но это не мешает мне с их помощью решать свой круг задач: всегда иметь под рукой мини-отвёртку с мини-пассатижами и прозванивать электрические цепи. А это даже не половина того, на что они способны.


                              С языком то же самое. Это ни однозначно плохо, ни однозначно хорошо. Это потребность — иметь гибкий инструмент ценой некоторых компромиссов. Как было сказано мною выше, диапазон задач для C#/.NET огромен, а самое главное — они меняются. Соответственно обогащается и меняется и язык.


                              Рано или поздно развитие C# и .NET может дойти до какого-то критического порога, за которым появится… например, C# Lite и очередная инкарнация .NET Compact. А может мы пересядем все на другие компьютерные архитектуры, и C# с .NET плавно перекатятся туда, по пути соответственно изменившись и стряхнув с себя ненужные артефакты.


                              Так и развивается всё в природе. Это нормально.

                                +7

                                Тут может повторится история с Экселем:
                                По статистике обычный пользователь Экселя использует не более 20% его функционала.
                                Но убрать остальные 80% нельзя, потому что у каждого пользователя свои 20%.

                                0

                                Все языки превращаются в кресты со временем. Кресты превратились в кресты, шарп превращается в кресты, даже хаскель превращается в этакие ФП-кресты (100500 нагромождений расширений и лёгкий хейт в сообществах, но альтернативы неюзабельны).

                                –1
                                Когда наконец-то продолжать допиливать F# и перестанут таскать функционал в C# по кусочкам?
                                  +2

                                  F# теперь занят переписыванием своих фич для совместимости с C#.

                                  +5
                                  Отсутствие необходимости писать полное название типа при перечислении элементов коллекции делает код чуть-чуть чище.

                                  Здорово (как и всё остальное). Но немножко жалко что всё ещё приходится писать полное имя класса при определении конструкторов и деструкторов, вместо лаконичного this (как в D):


                                  class SomeClass {
                                    this() {...}
                                    ~this() {...}
                                  }

                                  Вот совершенно непонятно зачем нужны для них полные имена. Хотя современные IDE позволяют на лету всё это рефакторить, но всё равно это кажется избыточным.

                                    +2

                                    Или так:


                                    class SomeClass {
                                      new() {...}
                                    }
                                      –1

                                      в C# уже есть зарезервированное слово ctor.
                                      И cctor для статического конструктора.

                                        0
                                        Толку то с того, что оно есть в IL, если я не могу его использовать в своём коде типа такого:

                                        class SomeClass {
                                            ctor() {...}
                                        }
                                          –1

                                          Пишем ctor, и нажимаем tab два раза, получаем конструктор класса.
                                          А с resharper — на один tab меньше.
                                          Вообще, даже ctor полностью печатать не надо, его тоже tab-ом можно добить.


                                          Скриншот с Visual Studio

                                          image

                                            +2
                                            Не, ну это ж не ключевое слово языка, получается, а просто фича редактора.
                                          +1

                                          Насколько я помню, зарезервировано оно в IL, а не в C#. И в начале имени метода добавлена точка, как раз чтобы не на стороне языка не пришлось ничего резервировать.

                                      +7
                                      Помню когда выходил .Net Core 1.0 ставший кстати core в последний момент, а до этого носящий гордое имя NET 5.0, тоже мамой клялись что это RC и никаких фундаментальных изменений не будет, а потом они внесли в последний момент изменение и отвалились все DI контейнеры, так что в этот раз пожалуй дождусь релиза
                                        0

                                        Не знаю у кого отвалились контейнеры, а у нас отвалилась вся подсветка в студии и половину сборки приходилось менять чтобы заработало, и после каждого обновлении RC-версии студии (каждую неделю) чинилось что-то одно и отваливалось в трёх разных местах. А обновы ставить приходилось, потому что весь тулинг устаревал на глазах. Человек ушел в отпуск, пришел через 2 недели — уже csproj вместо project.json, bower/npm депрекейтед, теперь в моде снова нугет, ну и так далее… Собсна да, в какой-то момент для управления зависимостями использовалось сразу три пакетника: bower, npm и json. У нас решили проект как раз на нем делать, так что я прекрасно помню дату — весна 2016го, там как раз все три использовалось, потому что часть пакетов была только в одном, часть во втором, часть в третьем. И только великолепный jquery был подключен сразу во всех трех пакетниках, причем емнип несовместимых версий. Сидели писали код в блокноте, потому что через консоль все компилируется, но в ИДЕ всё красное, включая юзинги и любые типы включая String. Все ошибки компиляции вычленять нужно было из консоли, ошибки во вьюхах MVC — протыкивать руками все страницы и смотреть, рендерится ли правильно или нет. Да, весело было.


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

                                          0
                                          епрекейтят окончательно настольный фреймворк
                                          Всмысле?
                                            0

                                            Ну есть настольный, а есть кор. Пятый — это на самом деле кор, но с брендом от настольного фулл фреймворка. Такие вот чудеса маркетинга.


                                            А ещё интересно что .Net Standard будет

                                              +1
                                              Останется для поддержки всего зоопарка старых рантаймов начиная с .net fw 4.6 заканчивая mono и .net core 3.1, а для всего остальноего — net 5.0, но при этом .net 5 будет обратно совместим с netstandard
                                              Цитата из документации
                                              New application development can specify the net5.0 target framework moniker (TFM) for all project types, including class libraries. Sharing code between .NET 5 workloads is simplified in that all you need is the net5.0 TFM.

                                              The net5.0 TFM combines and replaces the netcoreapp and netstandard names. This TFM will generally only include technologies that work cross-platform, like was done with .NET Standard. However, if you plan to share code between .NET Framework, .NET Core, and .NET 5 workloads, you can do so by specifying netstandard2.0

                                              Ссылка
                                                0

                                                Супер, спасибо

                                        +24

                                        Как обычно, решарпер подскажет что там нового подвезли

                                          0

                                          Ну или Райдер :)

                                          +1

                                          Это все отлично, а что с долгожданной кроссплатформенностью? Что с UI приложениями под никсы, винду и мак? На сколько мне не изменяет память коре 5 был заявлен как мультиплатформа из коробки.

                                            0
                                            Для GUI .NET использует библиотеки Windows. Для никсов и мака весь гуй надо пилить с нуля. Вряд ли они этим заморочатся когда-либо — на разработку и поддержку потребуется уйма ресурсов, а выгоды практически никакой. Кроссплатформенность Майкрософту нужна для того, чтобы создать конкуренцию Java в энтерпрайз-сегменте. Гуй для этого не нужен.
                                              +2
                                              Ну во первых есть Avalonia UI, во вторых Uno Platform, но это всё сделало комьюнити. Как сам недавно узнал, Майки тоже активно пилят свой кроссплатформенный UI — MAUI, который (если глянуть в коммиты или почитать о нём статейки) является дальнейшим развитием Xamarin и да, он будет поддерживать Win, IOS, Andorid, Mac и даже Linux. Обещают релизнуть вместе с .Net 6, т.е. в ноябре 2к21.
                                                0
                                                Кажется на счёт MAUI я ошибся. На сколько я помню, читал об этом в какой-то статье на хабре, но на днях посмотрел небольшой доклад об этом фреймворке и докладчице задали прямой вопрос на счёт поддержки Linux, на который она ответила примерно так «Microsoft сейчас делает свои продукты open source, так что это возможно. Никакой официальной информации из пресс релизов об этом нет.». Так что хз как оно будет.
                                                  +1
                                                  github.com/dotnet/maui лучше инфу о фреймворке смотреть тут. Судя по табличке, поддержка Linux возможна, но только если сообщество само будет этим заниматься.
                                              +1

                                              А что нужно? с 2016 пишу под линух, полет нормальный.

                                                +3
                                                Каким образом? Используешь библиотеки Mono? Или обёртки над Tk? Или как? Расскажи, пожалуйста, интересно очень. Давным-давно, я пробовал использовать Mono, но мой простенький GUI-проект крешился в разных непредсказуемых местах. Я это дело пофиксил и он стал работать, но выглядел довольно убого, по сравнению с теми же винформами, но под виндой.
                                                  –3

                                                  Просто перестал писать UI и переключился исключительно на написание приложений в докере. Если вдруг нужен гуй — то просто пишу его на реакте и он из шарпового кода таскает данные по хттп.

                                                    +1
                                                    Если вдруг нужен гуй — то просто пишу его на реакте и он из шарпового кода таскает данные по хттп.

                                                    Может, это и удобно, но ресурсозатратно. В итоге приложения, которые раньше ели 10-20 мегабайт, теперь жрут больше гига из-за того, что тащат с собой браузер.

                                                      0

                                                      Ну я не предлагаю делать так какой-нибудь плеер или игру. А какой-нибудь постгрес или монго я и локально в докере запускаю — так удобнее, чем устанавливать. Оверхед там совсем небольшой.


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

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

                                                        И по моему опыту производительность и размер обычно особой роли не играют. Вон у нас стоимость проектов идёт на десятки-сотни тысяч евро и там добавить пару сотен евро на железo помощнее никого не напрягает. Ну или если совсем грубо сказать, то «оптимизация» стоит намного дороже железа…
                                                          0
                                                          Ну я бы не назвал это прямо страдать, но да гуй, особенно десктоп, это скорее вещь прикладная и как минимум в B2B она обычно делается постольку поскольку.

                                                          Ну я прям страдал. Когда хотел асинк-авейт, а мне такие "а у нас .net 3.5, у клиентов ХР". Никакого Ci/CD конечно, никакого докера, то есть в половине случаев разбираешься, что за мусор на конечной системе стоит и как оно все интерферирует с твоим приложением. Возиться с MSI, реестром… Брр, не, спасибо.

                                                            0
                                                            Это конечно боль. У нас сейчас аналогичная ситуация с не поддерживаемой семёркой, которая всё ещё стоит у 99% клиентов. Но как минимум превьюшка 5.0 даже на семёрке бегает если поставить net5.0-windows7 как таргет. Посмотрим что будет в релизе.
                                                +7
                                                WPF и WinForms никогда не станут кроссплатформенными. WPF опирается на DirectX, а WinForms сильно привязано к Windows API.
                                                От комьюнити есть кроссплатформенные GUI-фреймворки Avalonia, Eto, GtkSharp, SpaceVIL.

                                                Со стороны Microsoftв следующем году выйдет .NET Multi-platform App UI, что является эволюцией Xamarin.Forms. Multi-platform App UI (MAUI) будут кроссплатформенные для Android, iOS, macOS, Windows. Для этих платформ поддержку будет осуществлять сам Майкрософт. Для Линукса поддержка будет от комьюнити. Начиная с .NET 5, будут объединяться .NET Core и Mono /Xamarin в одну библиотеку базовых классов (BCL) и набор инструментов (SDK). Плюс еще другие улучшение по сравнению с Xamarin.Forms, так что код должен быть максимум унифицирован. В репозитории MAUI можно почитать подробнее и увидеть разницу между текущими Xamarin.Forms и будущим MAUI.

                                                Также для любителей всяких Электронов, встречал проекты, которые делают что-то подобное на Блейзоре. Пока что это очень сырое и не подходит для нормально использования, но возможно через 1-2 года (с приходом AOT-компилятора) это будет юзабельное на уровне того же Электрона.
                                                  0
                                                  а WinForms сильно привязано к Windows API.

                                                  Тем не менее, приложения с WinForms вполне себе нормально работают под Linux в Mono. Просто Microsoft это не нужно. А вот WPF действительно не портировать, можно только запускать под wine.

                                                    0
                                                    А, точно. Вспомнил как сам когда-то пытался что-то сделать на WinForms на Linux.
                                                    Но в итоге моё приложение постоянно падало с MissingMethodException, так как много контролов и методов WinForms не было имплементировано. В общем, мне тогда этот порт WinForms показался достаточно ограниченным и со всякими приколами.
                                                    Возможно сейчас это лучше работает, но я не проверял и не слышал, чтобы кто-то это использовал.
                                                      0

                                                      Раньше весь Mono был такой, пока MS не открыла исходные коды. В любом случае, принципиального барьера для использования WinForms под Linux нет, всё упирается исключительно в человеко-часы. А так как MS считает WinForms устаревшим продуктов, то и расширять его поддержку не планирует.

                                                    0
                                                    WPF опирается на DirectX

                                                    Никогда не понимал, почему этот аргумент — аргумент. Ну и пусть оно под виндой работает над DirectX. Никто не мешает на других системах сделать его на базе OpenGL или вулкана. Было бы желание.
                                                      +2
                                                      Никто не мешает на других системах сделать его на базе OpenGL или вулкана.

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


                                                      Было бы желание

                                                      А его тоже нет. Большинство софта который сейчас пишется — это серверный b2b/b2c.

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

                                                        А его тоже нет.
                                                        Ну вот разве что.
                                                        0
                                                        Никто не говорит, что это в теории невозможно. В теории ядро Windows можно заменить ядром Linux. Вопрос насколько это сложно и целесообразно сделать.

                                                        Вот здесь люди уже два года обсуждают кроссплатформенный WPF. Представители Microdsfot дали понять, что вкладывать в это ресурсы не собираются. А комьюнити так и не смогло особо определиться, какие прослойки/замены использовать для D3D использовать.
                                                        Так что никакого прогресса в этом плане вроде нет. Но я могу конечно ошибаться, может кто-то и работает над такой задачей.
                                                    +1

                                                    Я прям удивлён: завезли всё то, что я так давно хотел видеть в языке.
                                                    Больше не надо писать Dictionary<SomeLongKeyType, SomeComplexType<ParamName1, ParamName1>> dictionaryName = new Dictionary<SomeLongKeyType, SomeComplexType<ParamName1, ParamName1>>();
                                                    Больше не надо писать if (!(someVariable is null)) ...
                                                    Больше не надо самому писать рекорды, костыли вместо нормальных инициализаторов, явное приведение к типу в тернарных операторах.


                                                    Единственное, чего не хватает — ну сделайте уже типы-перечисления реализующими IEquatable, наконец. Надоело EqualityComparer<>.Default.Equals использовать.

                                                      +1

                                                      вместо if (!(someVariable is null)) можно (и нужно) писать if (someVariable is {}) особенно полезно для if (someVariable is {} notNullVariable), потому что он например nullable распаковывает. Лично у меня все время происходит идиосинкразия когда мне нужно писать nullable.Value, даже если я только что проверил на то что оно не нулл, а GetValueOrDefault() вызывать опасно — мало ли кто проверку на нулл уберёт. А такая запись решает все проблемы без минусов.

                                                        +4

                                                        Я стараюсь писать понятный код. А для человека, не особо знакомого с pattern matching, выражение if (someVariable is {}) ... — абсолютно нелогичная и противоестественная конструкция. А вот if (someVariable is not null) ... вполне очевидна и понятна даже тем, кто на C#2.0 пишет.

                                                          0

                                                          Вопрос настолько базовой привычки, что я даж не знаю. Плюс такой вариант короче. Плюс позволяет выделять ненулл переменную в новвую переменную, как я выше написал. Ну да, надо запомнить что is {} проверяет, что переменная не нулл, как по мне никаких сверхспособностей для этого не нужно. Ну то есть


                                                          Я стараюсь писать понятный код.

                                                          А код someVariable is {} надо полагать непонятный?

                                                            +7
                                                            А код someVariable is {} надо полагать непонятный?

                                                            Да, неочевидный. JavaScript-ом уже попахивает.

                                                              0

                                                              А стрелочные лямбды не попахивают? А то вдруг надо писать delegate {...} и вот это всё. Чтобы пахло сишарпом.


                                                              и вы так и не ответили что будем делать с нуллейблами:


                                                              Guid? x = GetGuid();
                                                              if (x is not null) {
                                                                 ???
                                                              }
                                                                +2

                                                                Нет, стрелочные лямбды не попахивают. Вроде очевидный синтаксис.


                                                                и вы так и не ответили что будем делать с нуллейблами:

                                                                Не знаю, я их вообще почти не использую. А если и использую, то явно со свойствами HasValue и Value.


                                                                И вообще, здесь положено страдать, потому что использование одного и того же синтаксиса для Nullable и для nullable reference types — вселенское зло.

                                                                  +1
                                                                  Нет, стрелочные лямбды не попахивают. Вроде очевидный синтаксис.

                                                                  Не очень последовательно. Выглядит как скорее "стрелки давно, они попривычнее будут. А вот is {} поновее, поэтому как жс". Хотя в ЖС никакого is нет в принципе.


                                                                  Не знаю, я их вообще почти не использую. А если и использую, то явно со свойствами HasValue и Value.

                                                                  Ну то есть получаете проблемы "забыл проверить на HasValue — получил эксепшн"? Зачем, если можно получить гарантии от компилятора?


                                                                  И вообще, здесь положено страдать, потому что использование одного и того же синтаксиса для Nullable и для nullable reference types — вселенское зло.

                                                                  Зачем страдать больше если можно страдать меньше? Ну да, синтаксис такой, сделали хреново. Придется или с этим жить, или не писать на сишарпе. Я в процессе накапливания внутреннго настроя на второй вариант, но пока что следую первому.

                                                                    0

                                                                    Я бы просто сделал так:


                                                                    bool GetGuid(out Guid guid)

                                                                    вместо


                                                                    Guid? GetGuid()
                                                                      +2

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

                                                                        +2

                                                                        Во-первых как выше сказали, значение может быть проигнорированно.
                                                                        Во-вторых вы теряете все возможности использовать результат как экспрешн:


                                                                        var guidHashString = GetGuid()?.GetHashCode?.ToString() ?? "No hash";

                                                                        Нет, с типом Guid? всё в порядке, информация о том что значение может отсутствовать должна быть в типе, а не в соглашении "ну если там бул и аут параметр ну значит быть он означает, было ли проинициализированно значение". Компилятор >> соглашения.

                                                                      +1
                                                                      Не знаю, я их вообще почти не использую. А если и использую, то явно со свойствами HasValue и Value.

                                                                      То есть нивелируя всю пользу, которые они могут принести?


                                                                      И вообще, здесь положено страдать, потому что использование одного и того же синтаксиса для Nullable и для nullable reference types — вселенское зло.

                                                                      А это ещё почему?

                                                                        –1
                                                                        А это ещё почему?

                                                                        Семантика разная.

                                                                          +1

                                                                          С чего бы это? Семантика одна: значение может отсутствовать.

                                                                            0

                                                                            Вот только в одном случае это алиас для Nullable<>, а в другом — атрибут переменной или поля.

                                                                              +1

                                                                              Да, это ужасно, но что поделать.

                                                                      +7
                                                                      if (x is not null but y)
                                                                      


                                                                      Шутка.
                                                                +3
                                                                if (!(someVariable is null)) можно (и нужно) писать if (someVariable is {})

                                                                В данном случае вообще можно написать if (someVariable != null).

                                                                  +2
                                                                  1. они по разному работают. Самый простой пример


                                                                    void Main()
                                                                    {
                                                                        Console.WriteLine(IsNullA(new Weird()));
                                                                        Console.WriteLine(IsNullB(new Weird()));
                                                                    }
                                                                    
                                                                    static bool IsNullA(Weird obj) => obj == null;
                                                                    static bool IsNullB(Weird obj) => obj is null;
                                                                    
                                                                    class Weird : IEquatable<Weird>
                                                                    {
                                                                        public bool Equals(Weird other) => true;
                                                                        public override bool Equals(object obj) => true;
                                                                        public override int GetHashCode() => 0;
                                                                        public static bool operator ==(Weird left, Weird right) => left.Equals(right);
                                                                        public static bool operator !=(Weird left, Weird right) => !left.Equals(right);
                                                                    }

                                                                  2. вы все ещё не можете надежно написать код с нуллейблами:


                                                                    if (someVariable != null) {
                                                                      DoSomething(someVariable.Value);
                                                                    }

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


                                                                    При этом


                                                                    if (someVariable is {} some) {
                                                                      DoSomething(some);
                                                                    }

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


                                                                    if (someVariable is Guid some) {
                                                                      DoSomething(some);
                                                                    }


                                                                  Просто я не люблю писать лишнее, особенно для длинных типов вроде IReadOnlyDictionary<Something, SomethingElse>. Никаких проблем запомнить что {} означает не нулл я не вижу. Тем более, что в паттерн матчинге в свитч экспрешне все равно пригождается иногда, то есть запомнить все равно нужно.

                                                                    +1
                                                                    они по разному работают. Самый простой пример

                                                                    В этом случае да, но такая перегрузка операторов — редкость. Кстати, а компилятор оптимизирует someVariable != null в простых случаях?

                                                                      0

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


                                                                      Кстати, а компилятор оптимизирует someVariable != null в простых случаях?

                                                                      Не совсем понял, что имеется в виду. Вызов != будет вызывать оператор, а вызов на is null/ is {} будет одной инструкцией. Для примера выше (рекламирую LinqPad):


                                                                      IsNullA:
                                                                      IL_0000:  ldarg.0     
                                                                      IL_0001:  ldnull      
                                                                      IL_0002:  call        UserQuery+Weird.op_Equality
                                                                      IL_0007:  ret         
                                                                      
                                                                      IsNullB:
                                                                      IL_0000:  ldarg.0     
                                                                      IL_0001:  ldnull      
                                                                      IL_0002:  ceq         
                                                                      IL_0004:  ret    

                                                                      Если речь про ассемблер то так


                                                                      C.IsNullA(Weird)
                                                                          L0000: mov eax, [rcx]
                                                                          L0002: mov eax, 1
                                                                          L0007: ret
                                                                      
                                                                      C.IsNullB(Weird)
                                                                          L0000: test rcx, rcx
                                                                          L0003: sete al
                                                                          L0006: movzx eax, al
                                                                          L0009: ret
                                                                        0
                                                                        Не совсем понял, что имеется в виду.

                                                                        Имеется в виду: можно ли безболезненно заменять !(someVariable is null) на someVariable != null, если не используется перегрузка операторов?


                                                                        Как оказалось — да. Компилятор генерирует абсолютно идентичный asm.

                                                                          +2

                                                                          Можно, только принцип открытости/закрытости никто не отменял. Сегодня у класса не переопределен оператор, а завтра уже да. И код по-другому начинает работать. Это надо учитывать.


                                                                          А так, основная аргументация вторая. Чтобы неправильное разыменования нулла давало ошибку компиляции, а не наллреф в рантайме при вызове .Value

                                                                            0

                                                                            Можно безопасно заменить на !object.ReferenceEquals(someVariable, null)

                                                                              0
                                                                              А станет someVariable is not null, что, согласитесь, более лаконично.
                                                                          0

                                                                          Нет, компилятор просто подставит вызов оператора !=.
                                                                          И в любом случае, вам придётся сравнить объект по ссылке в реализации Equals:


                                                                          public bool Equals(Weird other) => other is not null && ... ;
                                                                          public bool Equals(Weird other) => other is {} && ... ;
                                                                          public bool Equals(Weird other) => !ReferenceEquals(other, null) && ... ;
                                                                        +2
                                                                        В данном случае вообще можно написать if (someVariable != null).

                                                                        И это будет вызов оператора !=, а не проверка значения на null.

                                                                      0

                                                                      Можно писать


                                                                      if (xxx is object)
                                                                        0

                                                                        Это то же что и is {}, только длиннее и приходится делать каст в object, велью типов это означает боксинг, и результат успешного каста нельзя использовать дальше нормально (оно будет типа object). В общем, менее удобно.

                                                                      +5
                                                                      Ну поехали ребят:
                                                                      Кто напишет верные результаты, получает рогалик

                                                                      public Car SomeMethod(DateTime p)
                                                                      {
                                                                      return new() { Passengers = 2 };
                                                                      }
                                                                      public Car SomeMethod(DateTime p)
                                                                      {
                                                                      return new { Passengers = 2 };
                                                                      }
                                                                      public T SomeMethod(DateTime p)
                                                                      {
                                                                      return new() { Passengers = 2 };
                                                                      }
                                                                      public T SomeMethod(DateTime p)
                                                                      {
                                                                      return new { Passengers = 2 };
                                                                      }
                                                                        +2
                                                                        По-идее только первый вариант должен работать. Второй — это анонимный тип. 3 без указания конкретных типов не будет работать. 4 — без указания явно object тоже не будет работать
                                                                          0

                                                                          В третьем случае будет работать если явно ограничить T:
                                                                          public T SomeMethod() where T : Car, new() => new() {Passengers = 2};
                                                                          Альтернативно, валиден код вида
                                                                          public T SomeMethod() where T : new() => new();

                                                                            0
                                                                            Можно, наверно, но зачем
                                                                              0

                                                                              Очевидно же, для случая:


                                                                              class Car
                                                                              {
                                                                                  public int Passengers { get; init; }
                                                                              }
                                                                              0

                                                                              Кстати, new() с агрументами в дженериках не собираются подогнать?

                                                                                0

                                                                                Тогда уж сразу статические виртуальные классы.

                                                                          +17

                                                                          Спасибо за статью, громадное количество текста)


                                                                          Теперь что касается по фичам: увы, сишарп продолжает идти по принципу "накидаем сахара, а там видно будет". 99% новых фичей дешугарится в старые версии, в итоге просто чутка облегчают написание кода. При этом кардинально новых вещей, чтобы не копипастить по 100 раз одно и то же в язык не добавляют.


                                                                          По пунктам


                                                                          • Новые нативные и просто новые типы — ок, пусть будут
                                                                          • Атрибуты у локальных функций — ок, пусть будут
                                                                          • GetEnumerator как метод расширения — шаг в правильном направлении, но как всегда видин "сахарозный" подход. форыч вызвать на таких можно, а вот написать какой-нибудь where T : IStaticEnumerable не выйдет. Решилось бы тайпклассами
                                                                          • Discard pattern — вот это очень хорошо, давно пора. Надеюсь, они везде работают, а не как обычно, в частности в ду нотац… простите, linq-синтаксисе.
                                                                          • Инструкции верхнего уровня в C# — идея ок, реализация конеш такая. Сколько они костылей воткнули ради этих глобальных Args и остального — с ума сойти. Могли бы просто поти по принципу раста/linqpad — потребовать просто void Main() написать, от 8 буковок разработчики бы не обломались, зато вся магия исчезла бы.
                                                                          • if (vehicle is not Car car) — если за пределами if можно будет воспользоваться переменной car то фича годная, иначе — не заменяет того варианта с if (!(vehicle is Car car))
                                                                          • if (context is {Length: > 0 and (< 10 or 25) }) — а вот это уже пушка. Мне просто жаль ребят из команды парсера и компилятора, которые впиливали вот этот новый dsl. Плюс вот лично мне тут ваще непонятно, в какой момент мы работаем с контекстом, а в какой момент начинаем считать его элементы. То что в языке появляются and и or которые как && и ||, но не совсем. В итоге эта фича для меня идет просто со знаком минус.
                                                                          • Улучшенный pattern matching в swtich expression — как выше уже вроде писали, это уже в текущей версии присутствует. В целом, полезная штука. Давно пора было вместо бесполезного свитча сделать паттерн матчер.
                                                                          • Новый new, а также в принципе улучшенный target typing — опять же, фича с триллионом ограничений, и в реальности почти всегда ненужная потому что используется var. Поможет только в инициализаторах полей и тестах без AutoData. В целом ничег вредного, но и ничего особо полезного
                                                                          • Target typed операторы ?? и ?: — ну, тут ничего не скажешь, наконец-то научили их не тупить в очевидных сценариях. Фича со знаком плюс, хотя и сахар, и маленькая
                                                                          • Переопределение возвращаемого типа методов — тоже полезная штука, ценна в частности для реализация какого-нибудь Clone() который должен возвращать This — то есть для абстрактной фигуры — фигуру, но для треугольника — треугольник. Одна из двух несахарных фичей, однозначно плюс
                                                                          • init-свойства — это не совсем readonly-члены — отлично пункт назван. У нас уже есть ридонли поля, ридонли свойства, свойства с приватным сеттером, теперь добавляются еще инит-свойства. Зачем они нужны для меня до сих пор загадка, особенно если учитывать что добавили нормальные рекорды. Предлагаю шараду: угадайте, какой из вариантов инициализации что означает:
                                                                            public class Person(string FirstName, string LastName)
                                                                            public class Person { string FirstName; string LastName; }
                                                                            public data class Person { string FirstName; string LastName; }
                                                                            public class Person { string FirstName {get;set;} string LastName {get;set;} }
                                                                            public class Person { string FirstName {get;init;} string LastName {get;init;} }
                                                                            public class Person(string firstName, string LastName)
                                                                          • Record — это узаконенная DTO'шка — ну, это хорошая штука. Хотя с полноценными генераторами делать целую языковую фичу под это было бы необязательно. В целом, плюсовая фича. Но опять сахарная
                                                                          • .NET Source Generators — а вот это вторая несахарная фича, но как всегда, майкрософт пошел на кучу компромиссов. Один из важных кейсв — сурс генераторы не могут быть вложенными. то есть сгенерировать код из которого будет сгенерирован код нельзя. В частности, в это точно упираются ребята из asp.net которые планировали рейзор перевести на сурс генераторы (а пока останутся на рефлекшне как и раньше), ну и в целом, это кажется блажью только пока не попишешь немного макросов, а там сразу становится понятно, что без этого фича так же ограничена, как ООП с наследованием только одного интерфейса. Во-вторых сурс генераторы не могут менять сигнатуры. Что это означает? Ну, что например атрибут "добавить неявный CancellationToken и прокинуть во все вложенные методы" (этакий АОП) сделать не выйдет. Но что важнее, не выйдет сделать юзерские рекорды. Возьмем раст, там рекорд это обычная структурка на которую навешана пара сорсгенераторов:


                                                                            #[derive(Clone, Eq, PartialEq, Hash)]
                                                                            struct Person {
                                                                                first_name: String,
                                                                                last_name: String
                                                                            }

                                                                            которое раскрывается примерно в Person : Clone, Eq, PartialEq, Hash { ... }. То есть меняется сигнатура (добавляются реализации интерфейсов). В шарповых сорсгенераторах такого не будет.


                                                                          • Частичные методы (partial method) — сахар, особо не нужен (кроме как в контексте предыдущей фичи)
                                                                          • Методы-инициализаторы (module initializers) — вредная магия, но без которой генераторы видимо совсем плохие.



                                                                          Ну и какой итог отсюда можно сделать? Добавили ли в язык полезных фичей? Ну, добавили. Стал ли он удобнее в использовании и т.п.? — Да, стал. Стал ли он удобнее пропорционально добавленной сложности? — однозначнно нет. Сахар, который плох именно тем, что не добавляет ортогональности и не позволяет комбинировать его, фичи с кучей оговорок, и прочее — язык очень сильно усложняется, а вот полезного к сожалению не очень. Нужные вещи вроде полноценных макросов, шейпов, типов высших порядков — откладываются из релиза в релиз. Простейшие рекорды которые наверное единственное стоящее нововведение новой версии языка обещали с 6(!) версии больше пяти лет.


                                                                          Всегда очень нравился сишарп. Но чем дальше, тем больше понимаю, что для продуктивной разработки он мне все меньше и меньше подходит. И не потому что он становится хуже, нет, конечно же нет. Но как говорила Алиса, тут нужно бежать, чтобы оставаться на месте, а чтобы куда-то попасть нужно бежать ещё сильнее. Другие языки бегута куда лучше, даже Java на которую мне всегда было больно смотреть без слез с каждым релизом выглядит все более и более достойно. Увы, на мой взгляд сишарп бежит едва достаточно чтобы просто оставаться на этом самом месте. А жаль.

                                                                            +1
                                                                            .NET Source Generators — а вот это вторая несахарная фича, но как всегда, майкрософт пошел на кучу компромиссов.

                                                                            Огромное разочарование, согласен. Я так подозреваю, разрабы IDE отказались делать поддержку чего-то нормального. В результате этот кривой бесполезный костыль.


                                                                            Другие языки бегута куда лучше, даже Java на которую мне всегда было больно смотреть без слез с каждым релизом выглядит все более и более достойно.

                                                                            Я не очень активно слежу за Java, но у них проблем ещё больше, по-моему. Перетаскивания обещаний много версий подряд вовсе не меньше. Сколько там лет Project Valhalla? Царского наследия, портящего современный дизайн, больше.


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


                                                                            Ну да, шарп не движется так быстро, как хотелось бы. Но где альтернатива-то?

                                                                              +1
                                                                              у меня пока не складывается ощущение, что котлин ощутимо лучше шарпа хоть в каком-то измерении, будь то мощь или популярность

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

                                                                                0

                                                                                У нас есть один микросервис, в котором уже ушедший из компании разработчик решил поюзать резалты. Ну, шобы надежно и все такое. В итоге, весь код обернут во все эти OnSuccess(() => ...)/OnError(() => ...). И мне вчера человек жаловался, что у него и студия, и райдер тупо лагают от такого количества лямбд :) Так что вопрос не только в эстетике.


                                                                                А проект мелкий, пара десятков тысяч sloc.

                                                                                  0

                                                                                  Сейчас можно делать switch с деконструкцией, но без нормальной поддержки DU в C# он всё равно не до конца удобен:


                                                                                  • нужно всегда выписывать полный тип: не OK(var value), а Result.OK<Foo, Bar>(var value)
                                                                                  • компилятор не догадывается, что других вариантов кроме OK и Err быть не может, и жалуется, что не проверен _
                                                                                    0

                                                                                    Ну свитч с деконструкцией все же не всегда выручает, например Есть у нас некий Either<int, Exception>, и задача сделать "если не ошибка то умножь значение на два иначе ничего не делай". Со свитчем будет очень длинно и неудобно. Про отсутствие exhaustive и общее количества бойлера говорить не приходится, вы и сами про это написали.

                                                                                +2
                                                                                Ну да, шарп не движется так быстро, как хотелось бы. Но где альтернатива-то?

                                                                                Джава вполне себе альтернатива. Если работать в ЖБ то есть котлин, смысл которого правда становитя все меньше с тех пор, как комитет по джаве перестал топтаться на месте из версии в версию. Алтернативы: самая близкая мне по духу — скала, есть все нужные фичи, распространенность высокая, ЗП одни из лучших на рынке, но синтаксис такой себе, имплиситы вместо тайпклассов тоже на любителя. Хаскель — язык почти во всем лучше всех альтернатив, но распространен мало, поэтому если на него делать ставку, то придется ограничиться не таким большим числом вакансий, и искать/менятть работу может быть проблемой. Ну и если байтики нравится перекладывать, то раст отличная альтернатива. Почти все нужные фичи есть, писать удобно, из минусов опять же меньшая популярность чем у шарпа.




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


                                                                                P.S. знакомый из шарп чатика как раз месяц назад на хаскеле работу нашел, сидит пилит. Вот он — большой молодец)

                                                                                  0
                                                                                  Ну не знаю. Мы параллельно работаем с Java и с С# и я бы не сказал что Java приятнее. Скорее наоборот Java у нас берётся только если нужен кросплатформенный/линукcовый десктоп. Если этого не надо, то .Net гораздо удобнее и приятнее.

                                                                                  Но это как бы на вкус и цвет конечно…
                                                                                    0

                                                                                    А какая версия джавы у вас?

                                                                                      0
                                                                                      10 на данный момент. В некоторых проектах даже вынуждены пока на 8 сидеть.
                                                                                        +2

                                                                                        Ну так жабу 10 надо сравнивать с C# 4.0 :) Это как раз версия конца эпохи застоя. На 14 имхо сильно лучше стало

                                                                                          +4
                                                                                          Может быть. Но тут кстати у нас проблема с апгрейдом версий в этой самой джаве. То есть работает это всё в джаве на мой взгляд похуже чем в шарпе. Ну опять же по крайней мере у нас. Поэтому и сидим на джаве всё ещё на 8-10, а на шарпе уже с нетерпением ждём этот самый Net 5.0 :)
                                                                                            0

                                                                                            +1. Как скальщик (кстати, в скале большая часть вышеперечисленного уже есть, и очень радует, что добавляют в шарп) могу сказать, что тоже есть препятствия для перехода на новые версии JVM (например, Spark и Flink только недавно научились работать с 11 версией (когда вышла 14)).

                                                                                              0

                                                                                              В C# версия языка и версия фреймворка — вещи, не связанные тесно.
                                                                                              Вы можете спокойно писать на C# 9.0 под целевую платформу .NET Framework 4.0, и даже под 3.5, и даже с async-await из-за совместимости IL-кода. Главное, чтобы библиотеки нужные были.

                                                                                                0

                                                                                                Ну не так совсем. Асинк-авейт под 2.0 не заведется (без очень грязных хаков), дефолтные методы интерфейсов тоже, и так далее. Если покопаться, то там не все так просто. Но "сахарные" фичи, коих большинство — да, заведутся

                                                                                                  0

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

                                                                                                  0

                                                                                                  Писать можно, но поддержки от MS таких комбинаций, начиная с С# 8.0, не будет

                                                                                                    0

                                                                                                    Поддержка есть, просто не все возможности C# 8.0 будут работать, например, default interface methods.

                                                                                                      0

                                                                                                      https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/configure-language-version


                                                                                                      C# 8.0 is supported only on .NET Core 3.x and newer versions.

                                                                                                      C# 9.0 is supported only on .NET 5 and newer versions.

                                                                                                      Понятно, что никто не запрещает руками поменять версию языка в csproj, но Microsoft не гарантирует, что это будет работать вообще или будет работать корректно. "Choosing a language version newer than the default can cause hard to diagnose compile-time and runtime errors."

                                                                                                        +1

                                                                                                        Вот прямо сейчас я пользуюсь C# 8.0 под .NET Framework 4.7.2 без каких-либо правок. Что я делаю не так?


                                                                                                        Вышенаписанное правильно читать: все нововведения C# 8.0 доступны на .NET Core 3.x и выше.

                                                                                +3
                                                                                На метод накладываются некоторые ограничения:
                                                                                он не должен работать с обобщениями (generics);

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

                                                                                +1

                                                                                Статья супер, спасибо! Кажется, самая подробная из всех, что я видел — на английском в том числе.

                                                                                  0
                                                                                  Слабенько, я думал в след за реализацией методов в интерфейсах и множественное наследование добавят)))
                                                                                    0
                                                                                    у нас сейчас будет основная задача деконстукторы ограничений обобщенных типов. как видите очень схожее мы ввели при if матчинга объектов.
                                                                                    множественное наследование даже не обсуждаем.
                                                                                    из интересностей пока только обсуждаем наследование enum.
                                                                                      0
                                                                                      Я же с юмором, имел ввиду что этот релиз гораздо интереснее и полезнее прошлого.
                                                                                    0

                                                                                    спасибо! очень люблю C# а теперь люблю ещё больше <3

                                                                                      0
                                                                                      А фичу что-то вроде «монады» так и не завезли? Вроде сначала говорили, а потом замолчали…
                                                                                        0

                                                                                        Монада — это не фича, а просто интерфейс определенного вида. Точно так же как IEnumerable — не фича. Так что вопрос неясен.

                                                                                          0
                                                                                          Сори, перепутал термины. Я имел ввиду Discriminated Unions- когда для разных типов мы можем одинаковые операции объявлять.
                                                                                            0

                                                                                            Discriminated unions — это один из двух компонент для ADT, вместе с тип-произведениями (то есть просто классами). И да, их очень не хватает. Эмуляция на объектах дорогостоящая и неудобная. Не сказал бы что это позволяет определять одинаковые операции для разных типов, это скорее энумы на стероидах, в которых кроме самого значения могут быть дополнительные данные:


                                                                                            enum Command {
                                                                                               Login { string UserName, string Password },
                                                                                               ChangePassword { string NewPassword },
                                                                                               Logout
                                                                                            }

                                                                                            Данные-то у нас тут везде примерно одинаковые, строки, но пользы полно

                                                                                        +1

                                                                                        https://weblogs.asp.net/dixin/category-theory-via-csharp-7-monad-and-linq-to-monads


                                                                                        As Brian Beckman said in this Channel 9 video:


                                                                                        LINQ is monad. It is very carefully designed by Erik Meijer so that it is monad.

                                                                                        Eric Lippert also mentioned:


                                                                                        The LINQ syntax is designed specifically to make operations on the sequence monad feel natural, but in fact the implementation is more general; what C# calls "SelectMany" is a slightly modified form of the "Bind" operation on an arbitrary monad.
                                                                                          0

                                                                                          LINQ это не монада, это ду-нотация. Разница такая же, как возможность написать цикл foreach и возможность реализовать IEnumerable — связанные вещи, но совсем не одно и то же.

                                                                                        0
                                                                                        Из статьи непонятно, как компилятор определяет, как компилятор понимает, какой из частичных методов «DoSomeWork» будет вызываться?
                                                                                        Или реализация разрешена только 1 раз и в остальных случаях нужна ";" после объявления метода?
                                                                                          +2
                                                                                          Или реализация разрешена только 1 раз и в остальных случаях нужна ";" после объявления метода?

                                                                                          Да.

                                                                                          0
                                                                                          Что вы еще хотите видеть? пишите.
                                                                                          Как я писал выше сейчас у нас в работе под C#10 из интересностей «деконстукторы ограничений обобщенных типов». И обсуждаем наследование enum
                                                                                            0
                                                                                            partial methods.
                                                                                            я уверен, что еще в примерно во времена visual studio 2013 эта фича уже была в C#
                                                                                              0
                                                                                              Если написанное в статье соответствует действительности, то у них изменилась семантика.
                                                                                              Раньше метод был частичным, если он не возвращает значения и не имеет out-параметров для того, чтобы вызов этого метода ни на что не влиял, если у него нет реализации и, соответственно, вызова. Поэтому он не мог быть частью публичного интерфейса.
                                                                                              Судя по описанному выше (я не проверял), публичный частичный метод или частичный метод, возвращающий значение, обязан быть реализован где-то в другой части класса.

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

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