Страницы

Поиск по вопросам

четверг, 2 января 2020 г.

C# 8.0 Records. Зачем нужно?

#c_sharp #net #c_sharp_80


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

Наткнулся там на Records.

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


Ответы

Ответ 1



record class - это не "кастрированный класс", а иммутабельный класс, у которого часть важных методов уже реализована компилятором. Слово "иммутабельный" тут - не недостаток, а важная фича. Такие классы просто необходимы для следующих задач: использование в качестве ключа в словаре: каждый раз, когда вам нужен словарь по двум параметрам, приходится или использовать кортежи, или использовать анонимные типы, или писать свой класс с методами Equals и GetHashCode, причем использовать кортежи в публичном API - дурной тон, а анонимные типы так и вовсе приводят к нетипизированному ключу; кеширование данных: данные в кеше обязаны быть неизменяемыми, чего можно достигнуть или защитным копированием - или иммутабельностью; многопоточный доступ: к иммутабельным данным можно обращаться из любых потоков без блокировок.

Ответ 2



С каждой новой версией C# в него добавляют все больше фишек из функционального программирования, и Records - одна из таких фишек. Из парадигмы функционального программирования: Основной особенностью функционального программирования, определяющей как преимущества, так и недостатки данной парадигмы, является то, что в ней реализуется модель вычислений без состояний. Если императивная программа на любом этапе исполнения имеет состояние, то есть совокупность значений всех переменных, и производит побочные эффекты, то чисто функциональная программа ни целиком, ни частями состояния не имеет и побочных эффектов не производит. То, что в императивных языках делается путём присваивания значений переменным, в функциональных достигается путём передачи выражений в параметры функций. Непосредственным следствием становится то, что чисто функциональная программа не может изменять уже имеющиеся у неё данные, а может лишь порождать новые путём копирования и/или расширения старых. Records является просто иммутабельным классом, состояние которого невозможно изменить. В некоторых случаях такой подход уменьшает количество возможных ошибок. Возьмем простой код: MyClass something = new MyClass(arg1, arg2, arg3); MyMethod(something); Что произошло в MyMethod? Никто не знает. Изменилось ли состояние something? Находиться ли он валидном состоянии или нужно после вызова метода сразу проверить что-то и вбросить Exception? Самый известный и частоиспользуемый иммутабельный класс в C# это String. Я думаю все согласятся, что допустить ошибку при работе со строками очень сложно. Этот класс только выиграл от иммутабельности. Конечно, это не совсем хорошая аналогия с Records, потому что в строк есть свои методы, но если очень нужно мы можем добавлять методы расширения для Records. Несмотря на то, что это не совсем удобно, все равно кода в итоге получиться меньше, чем при имлементации иммутабельного класса вручную.

Ответ 3



Это синтаксический сахар для более короткого объявления класса, который просто содержит набор полей. В примере из статьи показано, что следующий класс: public class Sword : IEquatable { public int Damage { get; } public int Durability { get; } public Sword(int Damage, int Durability) { this.Damage = Damage; this.Durability = Durability; } public bool Equals(Sword other) { return Equals(Damage, other.Damage) && Equals(Durability, other.Durability); } public override bool Equals(object other) { return (other as Sword)?.Equals(this) == true; } public override int GetHashCode() { return (Damage.GetHashCode() * 17 + Durability.GetHashCode()); } public void Deconstruct(out int Damage, out int Durability) { Damage = this.Damage; Durability = this.Durability; } public Sword With(int Damage = this.Damage, int Durability = this.Durability) => new Sword(Damage, Durability); } Можно будет объявить так: public class Sword(int Damage, int Durability); И компилятор сам додумает все остальное: конструкторы, свойства, сравнение. Если нужен класс с функционалом не по-умолчанию, то всегда можно воспользоваться полным синтаксисом объявления класса. Это нововведение позволит сэкономить код в тех случаях, когда нужен именно класс с набором полей и ничего больше. Экономия кода указывается как основное обоснование в предложении на Github: Motivation A significant number of type declarations in C# are little more than aggregate collections of typed data. Unfortunately, declaring such types requires a great deal of boilerplate code. Records provide a mechanism for declaring a datatype by describing the members of the aggregate along with additional code or deviations from the usual boilerplate, if any. Обоснование Значительная часть объявленных типов в C# немногим отличается от набора типизированных данных. К сожалению, объявление таких типов требует большого количества шаблонного кода. Записи предоставляют механизм для объявления типа данных путем перечисления членов набора либо отличий от стандартного шаблона, если таковые имеются.

Ответ 4



Ниже представлен некоторый псведокод, в котором опущены многие детали, включая спецификаторы доступа и прочую шелуху, которая не важна для понимания. Так же используется минимальное количество полей, чтобы сократить код. Представьте, что у Вас есть такой класс: class Person { string name; } И вот такой класс для хранения экземпляров предыдущего: class CompanyRegister { List persons; } CompanyRegister — главное хранилище всех персон в программе, поэтому, если кому-то нужно добавить персону, или получить по ней данные, он обращается к этому классу. Для получения персоны по какому-то признаку нам понадобится функция GetPerson(), но что она будет возвращать? Пусть она возвращает Person, является ли это проблемой? Безусловно. Если мы возвращаем Person то любая сущность может изменить этот объект вне CompanyRegister! Но это противоречит основам нашего класса: только он отвечает за изменения (он может оповещать другие сущности, либо же заниматься каким-то слежением за изменениями — не важно). Как нам решить проблему? Если бы мы писали на C++, то мы бы сделали возвращаемый тип const Person&, и проблема была бы решена. Но у нас C#, который при всех своих достоинствах, не может похвастаться наличием таких средства как C++ в части «оконстанивания» и предотвращения лишних копий. Кстати о копиях, мы могли быть сделать Person структурой, но это повлекло бы излишнее копирование, которого по возможности все стараются избегать. Исходя из того, что язык в C++ превращать никто не собирается, а в C# [почти] всё есть ссылка, то разработчики обратили свой взор в сторону функционального программирования и взяли на вооружение неизменяемые типы. Реализация таких типов в C# это куча кода, который нужно писать для каждого класса, либо же использовать всяческие кодогенераторы. Вот как, к примеру, мы могли бы сделать наш Person неизменяемым: class Person { string name; Person(string name) { name = name; } Builder GetBuilder() { return new Builder(this); } class Builder { string name; Builder(Person person) { name = person.name; } Person ToImmutable() { return new Person(name: name); } } } Выглядит чудесно, не правда ли? А теперь представьте, что у нас не одно поле, а с десяток полей. Как Вам? Так вот Records делает то же самое, только писать всё это не нужно, а синтаксис создания новых объектов на основании старых становится куда удобнее. Как часто всё это нужно? Очень. Ведь у нас очень часто есть данные, за которые кто-то отвечает, и этот кто-то не хочет, чтобы их меняли все кому не лень за пределами этого класса. Это очень часто встречается в GUI-приложениях, когда из модели данные передаются в представление (VM и прочие).

Комментариев нет:

Отправить комментарий