August 2, 2020

Мои любимые фичи в C#

Мое знакомство с C# началось больше 3.5 лет назад во время прохождения курсов. После этого я перешел на самостоятельное изучение, читая и конспектируя справочник по 6 версии языка.

Помимо небольших консольных приложений я работал с компонентами и созданием пользовательских интерфейсов, используя Windows Forms и WPF. Дополнительной мотивацией для продолжения использования и изучения языка стал игровой движок Unity, поэтому многие юзкейсы я буду приводить именно в контексте его использования. К моменту написания уже вышел C# 9. Интересных моментов и своеобразных фишек в моей памяти накопилось много, поэтому решил вынести их в отдельную статью.

Интерфейсы IENumerable и IENumerator

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

public interface IEnumerable 
{ 
    IEnumerator GetEnumerator();
}

public interface IEnumerator
{
    bool MoveNext(); // перемещение на одну позицию вперед 
    object Current {get;}  // текущий элемент в контейнере
    void Reset(); // перемещение в начало контейнера
}

Интерфейс IENumerator также используется в Unity для реализации корутин, выполняющихся "параллельно" методу Update (но по факту вызывающихся каждый фрейм, а не работающих в отдельном потоке) и использующихся для процедурной анимации и выполнения любых других задач на некотором временном промежутке.

Операторы объединения с NULL ?? и ??=

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

double SumNumbers(List<double[]> setsOfNumbers, int indexOfSetToSum)
{
    return setsOfNumbers?[indexOfSetToSum]?.Sum() ?? double.NaN;
}

var sum = SumNumbers(null, 0);
Console.WriteLine(sum);

В таком случае будет выведено NaN. В данном примере еще используется null-безопасность, речь о которой пойдет ниже.

В качестве правого операнда также может быть использовано выражение throw.

А оператор ??= используется в ситуациях, когда нужно присвоить объекту значение только в случае, если он равен null. Это позволяет упростить такой код:

if (variable is null)
{
    variable = expression;
}

До вида:

variable ??= expression;

Методы расширения

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

Например для класса Transform в Unity мне потребовалось написать метод-расширение, удаляющий всех его объектов-детей:

public static class TransformExtension 
{
    public static Transform Clear(this Transform transform) 
    {
        foreach (Transform child in transform) 
        {
            Object.Destroy(child.gameObject);
        } 
        return transform;
    }
}

В таком случае для удаления всех дочерних объектов у GameObject player будет использоваться такой код:

player.transform.Clear();

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

Делегаты 🔥

Начал замечать, насколько часто используются делегаты и во встроенных библиотеках языка (callback-и в классе Timer в статическом делегате Elapsed) и внутри Unity (в классе Button определен делегат onClick).

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

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

Хороший и понятный пример с metanit:

class Program
{
    delegate int Operation(int x, int y);
     
    static void Main(string[] args)
    {
        // присваивание адреса метода через конструктор
        Operation del = Add; // делегат указывает на метод Add
        int result = del(4,5); // фактически Add(4, 5)
        Console.WriteLine(result);
 
        del = Multiply; // теперь делегат указывает на метод Multiply
        result = del(4, 5); // фактически Multiply(4, 5)
        Console.WriteLine(result);
 
        Console.Read();
    }
    private static int Add(int x, int y)
    {
        return x+y;
    }
    private static int Multiply (int x, int y)
    {
        return x * y;
    }
}

Выражения с with

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

Реализовано это с очень простым синтаксисом:

var otherPerson = person with { LastName = "Hanselman" };

Выражение с with вызывает конструктор копирования, после чего применяет инициализатор измененного поля.

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

Data классы

Возвращаясь к работе с иммутабельными данными, для создания класса, наполненного init-only свойствами, в C# были добавлены специальные data-классы, благодаря чему код вида:

public data class Person
{
    public string FirstName { get; init; }
    public string LastName { get; init; }
}

Удалось упростить до вида:

public data class Person { string FirstName; string LastName; }

Более того, упрощены определения конструктора и деконструктора и код вида:

public data class Person 
{ 
    string FirstName; 
    string LastName; 
    public Person(string firstName, string lastName) 
      => (FirstName, LastName) = (firstName, lastName);
    public void Deconstruct(out string firstName, out string lastName) 
      => (firstName, lastName) = (FirstName, LastName);
}

Упрощается до:

public data class Person(string FirstName, string LastName);

После чего можно писать такой код:

var person = new Person("Scott", "Hunter");
var (f, l) = person; 

Написание простых программ, начиная с версии 9.0 🔥🔥

Большим улучшением в работе с языком стало упрощение синтаксиса, позволяющее писать в более простом стиле, убрав все лишнее, что может сильно усложнить жизнь новичку (у меня так было в 8 классе, когда очень сильно путался со всеми этими ООП-шными понятиями, пока не понял, что к чему и для чего существуют class Program и static void Main, а других новичков это вполне могло отпугнуть, когда тот же Python является куда более доброжелательным).

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

using System;
class Program
{
    static void Main()
    {
        Console.WriteLine("Hello World!");
    }
}

До такого:

using System;

Console.WriteLine("Hello World!");

Объявления using для переменной

Статические локальные функции 🔥

Nullability, null-безопасность, nullable типы

Индексы и диапазоны (Range, слайсы)

Интерполяция строк 🔥🔥

Асинхронные потоки и типы 🔥

Рефлексия

Type Class

Деревья выражений

Чего языку не хватает?

Неудобный перенос скобки на новую строку

Морально устаревшие точка с запятой в конце строки

Ключевое слово new, которое давно пора опустить

Ключевое слово foreach

Заключение